Skip to content

Commit

Permalink
Fix password update for users with jwt
Browse files Browse the repository at this point in the history
  • Loading branch information
tomach committed Aug 30, 2024
1 parent 1319f49 commit 900b89a
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 36 deletions.
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ Changelog
Unreleased
----------

* Fixed password update for users who have ``jwt`` configured.

2.41.0 (2024-08-22)
-------------------

Expand Down
125 changes: 90 additions & 35 deletions crate/operator/update_user_password.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@

from crate.operator.config import config
from crate.operator.utils.formatting import b64decode
from crate.operator.utils.jwt import crate_version_supports_jwt
from crate.operator.utils.kubeapi import get_cratedb_resource
from crate.operator.utils.notifications import send_operation_progress_notification
from crate.operator.webhooks import WebhookAction, WebhookOperation, WebhookStatus

Expand Down Expand Up @@ -62,6 +64,8 @@ async def update_user_password(
"""
scheme = "https" if has_ssl else "http"
password = b64decode(new_password)
cratedb = await get_cratedb_resource(namespace, cluster_id)
crate_version = cratedb["spec"]["cluster"]["version"]

def get_curl_command(payload: dict) -> List[str]:
return [
Expand All @@ -78,6 +82,18 @@ def get_curl_command(payload: dict) -> List[str]:
"\\n",
]

async def pod_exec(cmd):
return await core_ws.connect_get_namespaced_pod_exec(
namespace=namespace,
name=pod_name,
command=cmd,
container="crate",
stderr=True,
stdin=False,
stdout=True,
tty=False,
)

command_alter_user = get_curl_command(
{
"stmt": 'ALTER USER "{}" SET (password = $1)'.format(username),
Expand All @@ -88,51 +104,90 @@ def get_curl_command(payload: dict) -> List[str]:

async with WsApiClient() as ws_api_client:
core_ws = CoreV1Api(ws_api_client)
try:
logger.info("Trying to update user password ...")
result = await core_ws.connect_get_namespaced_pod_exec(
namespace=namespace,
name=pod_name,
command=command_alter_user,
container="crate",
stderr=True,
stdin=False,
stdout=True,
tty=False,
if crate_version_supports_jwt(crate_version):
# For users with `jwt` and `password` set, we need to reset
# `jwt` config first to be able to update the password.
iss = cratedb["spec"].get("grandCentral", {}).get("jwkUrl")

command_reset_user_jwt = get_curl_command(
{
"stmt": 'ALTER USER "{}" SET (jwt = NULL)'.format(username),
"args": [],
}
)
if "rowcount" in result:
logger.info("... success")
try:
logger.info("Trying to reset user jwt config ...")
result = await pod_exec(command_reset_user_jwt)
except ApiException as e:
exception_logger(
"... failed. Status: %s Reason: %s", e.status, e.reason
)
raise _temporary_error()
except TemporaryError:
raise
except WSServerHandshakeError as e:
exception_logger(
"... failed. Status: %s Message: %s", e.status, e.message
)
raise _temporary_error()
except Exception as e:
exception_logger(
"... failed. Unexpected exception. Class: %s. Message: %s",
type(e).__name__,
str(e),
)
raise _temporary_error()
else:
logger.info("... error. %s", result)
raise TemporaryError(delay=config.BOOTSTRAP_RETRY_DELAY)
if "rowcount" in result:
logger.info("... success")
command_alter_user = get_curl_command(
{
"stmt": (
'ALTER USER "{}" SET (password = $1, jwt = '
'{{"iss" = $2, "username" = $3, "aud" = $4}})'
).format(username),
"args": [password, iss, username, cluster_id],
}
)
else:
logger.info("... error. %s", result)
raise _temporary_error()

await send_operation_progress_notification(
namespace=namespace,
name=cluster_id,
message="Password updated successfully.",
logger=logger,
status=WebhookStatus.SUCCESS,
operation=WebhookOperation.UPDATE,
action=WebhookAction.PASSWORD_UPDATE,
)
try:
logger.info("Trying to update user password ...")
result = await pod_exec(command_alter_user)
except ApiException as e:
# We don't use `logger.exception()` to not accidentally include the
# password in the log messages which might be part of the string
# representation of the exception.
exception_logger("... failed. Status: %s Reason: %s", e.status, e.reason)
raise TemporaryError(delay=config.BOOTSTRAP_RETRY_DELAY)
except WSServerHandshakeError as e:
# We don't use `logger.exception()` to not accidentally include the
# password in the log messages which might be part of the string
# representation of the exception.
exception_logger("... failed. Status: %s Message: %s", e.status, e.message)
raise TemporaryError(delay=config.BOOTSTRAP_RETRY_DELAY)
raise _temporary_error()
except TemporaryError:
raise
except WSServerHandshakeError as e:
exception_logger("... failed. Status: %s Message: %s", e.status, e.message)
raise _temporary_error()
except Exception as e:
exception_logger(
"... failed. Unexpected exception was raised. Class: %s. Message: %s",
type(e).__name__,
str(e),
)
raise TemporaryError(delay=config.BOOTSTRAP_RETRY_DELAY)
raise _temporary_error()
else:
if "rowcount" in result:
logger.info("... success")
else:
logger.info("... error. %s", result)
raise _temporary_error()

await send_operation_progress_notification(
namespace=namespace,
name=cluster_id,
message="Password updated successfully.",
logger=logger,
status=WebhookStatus.SUCCESS,
operation=WebhookOperation.UPDATE,
action=WebhookAction.PASSWORD_UPDATE,
)


def _temporary_error():
return TemporaryError(delay=config.BOOTSTRAP_RETRY_DELAY)
9 changes: 8 additions & 1 deletion tests/test_update_password.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
)

from .utils import (
CRATE_VERSION,
DEFAULT_TIMEOUT,
assert_wait_for,
start_cluster,
Expand Down Expand Up @@ -182,12 +183,18 @@ async def test_update_cluster_password(
)


@mock.patch("crate.operator.update_user_password.get_cratedb_resource")
@mock.patch("crate.operator.webhooks.webhook_client.send_notification")
@mock.patch("kubernetes_asyncio.client.CoreV1Api.connect_get_namespaced_pod_exec")
async def test_update_cluster_password_errors(
mock_pod_exec, mock_send_notification: mock.AsyncMock
mock_pod_exec,
mock_send_notification: mock.AsyncMock,
mock_get_cratedb_resource: mock.AsyncMock,
):
mock_pod_exec.side_effect = Exception("test-exception")
mock_get_cratedb_resource.return_value = {
"spec": {"cluster": {"version": CRATE_VERSION}}
}
with pytest.raises(Exception) as e:
await update_user_password(
namespace="a-namespace",
Expand Down

0 comments on commit 900b89a

Please sign in to comment.