From 010c2f7328f7f149a630e071eb98acd258da8d91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robin=20Hru=C5=A1ka?= Date: Thu, 25 May 2023 18:18:30 +0200 Subject: [PATCH 1/8] Add a PUT endpoint for impersonation --- seacatauth/authn/handler.py | 123 ++++++++++++++++++++++++++---------- 1 file changed, 88 insertions(+), 35 deletions(-) diff --git a/seacatauth/authn/handler.py b/seacatauth/authn/handler.py index d2932453..5a19c0a7 100644 --- a/seacatauth/authn/handler.py +++ b/seacatauth/authn/handler.py @@ -45,7 +45,8 @@ def __init__(self, app, authn_svc): web_app.router.add_put(r'/public/login/{lsid}/smslogin', self.smslogin) web_app.router.add_put(r'/public/login/{lsid}/webauthn', self.webauthn_login) web_app.router.add_put(r'/public/logout', self.logout) - web_app.router.add_post("/impersonate", self.impersonate) + web_app.router.add_put("/impersonate", self.impersonate) + web_app.router.add_post("/impersonate", self.impersonate_and_redirect) # Public endpoints web_app_public = app.PublicWebContainer.WebApp @@ -54,7 +55,7 @@ def __init__(self, app, authn_svc): web_app_public.router.add_put(r'/public/login/{lsid}/smslogin', self.smslogin) web_app_public.router.add_put(r'/public/login/{lsid}/webauthn', self.webauthn_login) web_app_public.router.add_put(r'/public/logout', self.logout) - web_app_public.router.add_post("/impersonate", self.impersonate) + web_app_public.router.add_post("/impersonate", self.impersonate_and_redirect) @asab.web.rest.json_schema_handler({ "type": "object", @@ -385,13 +386,57 @@ async def _get_client_login_key(self, client_id): login_key = None return login_key + @asab.web.rest.json_schema_handler({ + "type": "object", + "required": ["credentials_id"], + "properties": { + "credentials_id": { + "type": "string", + "description": "Credentials ID of the impersonation target."}, + "expiration": { + "oneOf": [{"type": "string"}, {"type": "string"}], + "description": + "Expiration of the impersonated session. The value can be either the number of seconds " + "or a time-unit string such as '4 h' or '3 d'."}}, + "example": { + "credentials_id": "mongodb:default:abc123def456", + "expiration": "5m"} + }) + @access_control("authz:impersonate") + async def impersonate(self, request): + """ + Open a root session impersonated as a different user. + Response contains a Set-Cookie header with the new root session cookie. + + Requires `authz:impersonate`. + """ + from_info = [request.remote] + ff = request.headers.get("X-Forwarded-For") + if ff is not None: + from_info.extend(ff.split(", ")) + + request_data = await request.post() + target_cid = request_data["credentials_id"] + if request.Session.Session.ParentSessionId is None: + impersonator_root_session = request.Session + else: + impersonator_root_session = await self.SessionService.get(request.Session.Session.ParentSessionId) + + assert impersonator_root_session.Cookie.Id is not None + + session = await self._impersonate(impersonator_root_session, from_info, target_cid) + response = asab.web.rest.json_response(request, {"result": "OK"}) + set_cookie(self.App, response, session, cookie_domain=self.CookieService.RootCookieDomain) + return response + @access_control("authz:impersonate") - async def impersonate(self, request, *, credentials_id): + async def impersonate_and_redirect(self, request): """ - Open a root session as a different user. Responds contains a Set-Cookie header with the new root session cookie. - This effectively overwrites user's current root cookie. Reference to current root session is kept in the - impersonated session. On logout, the original root cookie is set again. + Open a root session impersonated as a different user. Response contains a Set-Cookie header with the new + root session cookie and redirection to the authorize endpoint. This effectively overwrites user's current + root cookie. Reference to current root session is kept in the impersonated session. + On logout, the original root cookie is set again. Requires `authz:impersonate`. --- @@ -427,7 +472,6 @@ async def impersonate(self, request, *, credentials_id): oidc_service = self.App.get_service("seacatauth.OpenIdConnectService") client_service = self.App.get_service("seacatauth.ClientService") - # TODO: Restrict impersonation based on agent X target resource intersection from_info = [request.remote] ff = request.headers.get("X-Forwarded-For") if ff is not None: @@ -435,22 +479,50 @@ async def impersonate(self, request, *, credentials_id): request_data = await request.post() target_cid = request_data["credentials_id"] - if request.Session.Session.Type == "root": impersonator_root_session = request.Session else: impersonator_root_session = await self.SessionService.get(request.Session.Session.ParentSessionId) + session = await self._impersonate(impersonator_root_session, from_info, target_cid) + + client_dict = await client_service.get(request_data["client_id"]) + query = { + k: v for k, v in request_data.items() + if k in frozenset([ + "redirect_uri", "response_type", "scope", "prompt", "code_challenge", "code_challenge_method"]) + } + authorize_uri = oidc_service.build_authorize_uri(client_dict, client_id=request_data["client_id"], **query) + + response = aiohttp.web.HTTPFound( + authorize_uri, + headers={ + "Location": authorize_uri, + "Refresh": "0;url={}".format(authorize_uri), + }, + content_type="text/html", + text="""\n\n...\n\n""" + ) + set_cookie(self.App, response, session, cookie_domain=self.CookieService.RootCookieDomain) + return response + + async def _impersonate(self, impersonator_root_session, impersonator_from_info, target_cid): + """ + Create a new impersonated session and log the event. + """ + # TODO: Restrict impersonation based on agent X target resource intersection + impersonator_cid = impersonator_root_session.Credentials.Id try: - session = await self.AuthenticationService.create_impersonated_session(impersonator_root_session, target_cid) + session = await self.AuthenticationService.create_impersonated_session( + impersonator_root_session, target_cid) except Exception as e: await self.AuditService.append( AuditCode.IMPERSONATION_FAILED, { - "impersonator_cid": credentials_id, + "impersonator_cid": impersonator_cid, "impersonator_sid": impersonator_root_session.SessionId, "target_cid": target_cid, - "fi": from_info, + "fi": impersonator_from_info, } ) raise e @@ -459,40 +531,21 @@ async def impersonate(self, request, *, credentials_id): asab.LOG_NOTICE, "Impersonation successful.", struct_data={ - "impersonator_cid": credentials_id, + "impersonator_cid": impersonator_cid, "impersonator_sid": impersonator_root_session.SessionId, "target_cid": target_cid, "target_sid": str(session.Session.Id), - "from_info": from_info, + "from_info": impersonator_from_info, } ) await self.AuditService.append( AuditCode.IMPERSONATION_SUCCESSFUL, { - "impersonator_cid": credentials_id, + "impersonator_cid": impersonator_cid, "impersonator_sid": impersonator_root_session.SessionId, "target_cid": target_cid, "target_sid": session.Session.Id, - "fi": from_info, + "fi": impersonator_from_info, } ) - - client_dict = await client_service.get(request_data["client_id"]) - query = { - k: v for k, v in request_data.items() - if k in frozenset([ - "redirect_uri", "response_type", "scope", "prompt", "code_challenge", "code_challenge_method"]) - } - authorize_uri = oidc_service.build_authorize_uri(client_dict, client_id=request_data["client_id"], **query) - - response = aiohttp.web.HTTPFound( - authorize_uri, - headers={ - "Location": authorize_uri, - "Refresh": "0;url={}".format(authorize_uri), - }, - content_type="text/html", - text="""\n\n...\n\n""" - ) - set_cookie(self.App, response, session, cookie_domain=self.CookieService.RootCookieDomain) - return response + return session From 9464810fb31a564b6ba00abb647d79f81779e3c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robin=20Hru=C5=A1ka?= Date: Thu, 25 May 2023 18:22:15 +0200 Subject: [PATCH 2/8] Use double quotes --- seacatauth/authn/handler.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/seacatauth/authn/handler.py b/seacatauth/authn/handler.py index 5a19c0a7..e19edef9 100644 --- a/seacatauth/authn/handler.py +++ b/seacatauth/authn/handler.py @@ -32,29 +32,29 @@ class AuthenticationHandler(object): def __init__(self, app, authn_svc): self.App = app self.AuthenticationService = authn_svc - self.CredentialsService = app.get_service('seacatauth.CredentialsService') - self.SessionService = app.get_service('seacatauth.SessionService') - self.CookieService = app.get_service('seacatauth.CookieService') - self.AuditService = app.get_service('seacatauth.AuditService') + self.CredentialsService = app.get_service("seacatauth.CredentialsService") + self.SessionService = app.get_service("seacatauth.SessionService") + self.CookieService = app.get_service("seacatauth.CookieService") + self.AuditService = app.get_service("seacatauth.AuditService") self.BatmanService = app.BatmanService - self.CommunicationService = app.get_service('seacatauth.CommunicationService') + self.CommunicationService = app.get_service("seacatauth.CommunicationService") web_app = app.WebContainer.WebApp - web_app.router.add_put(r'/public/login.prologue', self.login_prologue) - web_app.router.add_put(r'/public/login/{lsid}', self.login) - web_app.router.add_put(r'/public/login/{lsid}/smslogin', self.smslogin) - web_app.router.add_put(r'/public/login/{lsid}/webauthn', self.webauthn_login) - web_app.router.add_put(r'/public/logout', self.logout) + web_app.router.add_put(r"/public/login.prologue", self.login_prologue) + web_app.router.add_put(r"/public/login/{lsid}", self.login) + web_app.router.add_put(r"/public/login/{lsid}/smslogin", self.smslogin) + web_app.router.add_put(r"/public/login/{lsid}/webauthn", self.webauthn_login) + web_app.router.add_put(r"/public/logout", self.logout) web_app.router.add_put("/impersonate", self.impersonate) web_app.router.add_post("/impersonate", self.impersonate_and_redirect) # Public endpoints web_app_public = app.PublicWebContainer.WebApp - web_app_public.router.add_put(r'/public/login.prologue', self.login_prologue) - web_app_public.router.add_put(r'/public/login/{lsid}', self.login) - web_app_public.router.add_put(r'/public/login/{lsid}/smslogin', self.smslogin) - web_app_public.router.add_put(r'/public/login/{lsid}/webauthn', self.webauthn_login) - web_app_public.router.add_put(r'/public/logout', self.logout) + web_app_public.router.add_put(r"/public/login.prologue", self.login_prologue) + web_app_public.router.add_put(r"/public/login/{lsid}", self.login) + web_app_public.router.add_put(r"/public/login/{lsid}/smslogin", self.smslogin) + web_app_public.router.add_put(r"/public/login/{lsid}/webauthn", self.webauthn_login) + web_app_public.router.add_put(r"/public/logout", self.logout) web_app_public.router.add_post("/impersonate", self.impersonate_and_redirect) @asab.web.rest.json_schema_handler({ From 2016905fc54f242bc4e554b887d348221cd6545a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robin=20Hru=C5=A1ka?= Date: Mon, 29 May 2023 17:17:05 +0200 Subject: [PATCH 3/8] Debugging --- seacatauth/authn/handler.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/seacatauth/authn/handler.py b/seacatauth/authn/handler.py index e19edef9..ef1509c2 100644 --- a/seacatauth/authn/handler.py +++ b/seacatauth/authn/handler.py @@ -55,6 +55,7 @@ def __init__(self, app, authn_svc): web_app_public.router.add_put(r"/public/login/{lsid}/smslogin", self.smslogin) web_app_public.router.add_put(r"/public/login/{lsid}/webauthn", self.webauthn_login) web_app_public.router.add_put(r"/public/logout", self.logout) + web_app_public.router.add_put("/impersonate", self.impersonate) web_app_public.router.add_post("/impersonate", self.impersonate_and_redirect) @asab.web.rest.json_schema_handler({ @@ -298,15 +299,18 @@ async def logout(self, request): delete_cookie(self.App, response) - # If the current session is impersonated, try to restore the original session + # If the current session is impersonated and the original session has a + # root cookie, try to restore the original session if session.Authentication.ImpersonatorSessionId is not None: try: impersonator_session = await self.SessionService.get(session.Authentication.ImpersonatorSessionId) - print(impersonator_session) - set_cookie(self.App, response, impersonator_session) + except KeyError: L.log(asab.LOG_NOTICE, "Impersonator session not found.", struct_data={ "sid": session.Authentication.ImpersonatorSessionId}) + else: + if impersonator_session.Cookie is not None: + set_cookie(self.App, response, impersonator_session) if self.BatmanService is not None: response.del_cookie(self.BatmanService.CookieName) @@ -394,7 +398,7 @@ async def _get_client_login_key(self, client_id): "type": "string", "description": "Credentials ID of the impersonation target."}, "expiration": { - "oneOf": [{"type": "string"}, {"type": "string"}], + "oneOf": [{"type": "string"}, {"type": "number"}], "description": "Expiration of the impersonated session. The value can be either the number of seconds " "or a time-unit string such as '4 h' or '3 d'."}}, @@ -403,7 +407,7 @@ async def _get_client_login_key(self, client_id): "expiration": "5m"} }) @access_control("authz:impersonate") - async def impersonate(self, request): + async def impersonate(self, request, *, json_data): """ Open a root session impersonated as a different user. Response contains a Set-Cookie header with the new root session cookie. @@ -415,15 +419,12 @@ async def impersonate(self, request): if ff is not None: from_info.extend(ff.split(", ")) - request_data = await request.post() - target_cid = request_data["credentials_id"] + target_cid = json_data["credentials_id"] if request.Session.Session.ParentSessionId is None: impersonator_root_session = request.Session else: impersonator_root_session = await self.SessionService.get(request.Session.Session.ParentSessionId) - assert impersonator_root_session.Cookie.Id is not None - session = await self._impersonate(impersonator_root_session, from_info, target_cid) response = asab.web.rest.json_response(request, {"result": "OK"}) set_cookie(self.App, response, session, cookie_domain=self.CookieService.RootCookieDomain) From 4e1248b6f677cc59a7c35955549ef07ad80e3f6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robin=20Hru=C5=A1ka?= Date: Tue, 30 May 2023 12:40:03 +0200 Subject: [PATCH 4/8] AccessDeniedError surfaces as HTTPForbidden --- seacatauth/authn/handler.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/seacatauth/authn/handler.py b/seacatauth/authn/handler.py index ef1509c2..858a114c 100644 --- a/seacatauth/authn/handler.py +++ b/seacatauth/authn/handler.py @@ -10,6 +10,7 @@ import urllib.parse import jwcrypto.jwk +import seacatauth.exceptions from ..audit import AuditCode from ..cookie import set_cookie, delete_cookie from ..decorators import access_control @@ -304,12 +305,14 @@ async def logout(self, request): if session.Authentication.ImpersonatorSessionId is not None: try: impersonator_session = await self.SessionService.get(session.Authentication.ImpersonatorSessionId) - except KeyError: L.log(asab.LOG_NOTICE, "Impersonator session not found.", struct_data={ "sid": session.Authentication.ImpersonatorSessionId}) else: - if impersonator_session.Cookie is not None: + if impersonator_session.Cookie is None: + # Case when the impersonation was started by an M2M session, which has no cookie + pass + else: set_cookie(self.App, response, impersonator_session) if self.BatmanService is not None: @@ -516,6 +519,17 @@ async def _impersonate(self, impersonator_root_session, impersonator_from_info, try: session = await self.AuthenticationService.create_impersonated_session( impersonator_root_session, target_cid) + except seacatauth.exceptions.AccessDeniedError as e: + await self.AuditService.append( + AuditCode.IMPERSONATION_FAILED, + { + "impersonator_cid": impersonator_cid, + "impersonator_sid": impersonator_root_session.SessionId, + "target_cid": target_cid, + "fi": impersonator_from_info, + } + ) + raise aiohttp.web.HTTPForbidden() from e except Exception as e: await self.AuditService.append( AuditCode.IMPERSONATION_FAILED, From c5979f17646aec29d5fe586c106b9178dc7cbd77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robin=20Hru=C5=A1ka?= Date: Mon, 5 Jun 2023 14:52:16 +0200 Subject: [PATCH 5/8] Allow impersonation of superuser credentials but never authorize the authz:superuser resource. --- seacatauth/authn/service.py | 11 ++++++----- seacatauth/authz/utils.py | 18 ++++++++++++++---- seacatauth/session/builders.py | 7 +++++-- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/seacatauth/authn/service.py b/seacatauth/authn/service.py index e99e8df7..093163a1 100644 --- a/seacatauth/authn/service.py +++ b/seacatauth/authn/service.py @@ -438,10 +438,10 @@ async def create_impersonated_session(self, impersonator_session, target_cid: st target_authz = await build_credentials_authz( self.TenantService, self.RoleService, target_cid, tenants=None) if self.RBACService.is_superuser(target_authz): - L.warning("Cannot impersonate a superuser.", struct_data={ - "impersonator_cid": impersonator_cid, "target_cid": target_cid}) - raise exceptions.AccessDeniedError( - subject=impersonator_cid, resource="impersonate:{}".format(target_cid)) + L.warning( + "Impersonation target is a superuser. Resource 'authz:superuser' will be excluded " + "from the impersonated session.", + struct_data={"impersonator_cid": impersonator_cid, "target_cid": target_cid}) scope = frozenset(["profile", "email", "phone"]) session_builders = [ @@ -450,7 +450,8 @@ async def create_impersonated_session(self, impersonator_session, target_cid: st tenant_service=self.TenantService, role_service=self.RoleService, credentials_id=target_cid, - tenants=None # Root session is tenant-agnostic + tenants=None, # Root session is tenant-agnostic + exclude_resources={"authz:superuser", "authz:impersonate"}, ), cookie_session_builder(), await available_factors_session_builder(self, target_cid), diff --git a/seacatauth/authz/utils.py b/seacatauth/authz/utils.py index 8233bc39..05154271 100644 --- a/seacatauth/authz/utils.py +++ b/seacatauth/authz/utils.py @@ -1,4 +1,7 @@ -async def build_credentials_authz(tenant_service, role_service, credentials_id, tenants=None): +async def build_credentials_authz( + tenant_service, role_service, credentials_id, + tenants=None, exclude_resources=frozenset() +): """ Creates a nested 'authz' dict with tenant:resource structure: { @@ -7,13 +10,16 @@ async def build_credentials_authz(tenant_service, role_service, credentials_id, 'tenantB': ['resourceA', 'resourceB', 'resourceE', 'resourceD'], } """ - # Add global roles and resources under "*" authz = {} tenant = "*" authz[tenant] = set() for role in await role_service.get_roles_by_credentials(credentials_id, [tenant]): - authz[tenant].update(await role_service.get_role_resources(role)) + authz[tenant].update( + res + for res in await role_service.get_role_resources(role) + if res not in exclude_resources + ) authz[tenant] = list(authz[tenant]) # Add tenant-specific roles and resources @@ -21,7 +27,11 @@ async def build_credentials_authz(tenant_service, role_service, credentials_id, for tenant in tenants: authz[tenant] = set() for role in await role_service.get_roles_by_credentials(credentials_id, [tenant]): - authz[tenant].update(await role_service.get_role_resources(role)) + authz[tenant].update( + res + for res in await role_service.get_role_resources(role) + if res not in exclude_resources + ) authz[tenant] = list(authz[tenant]) return authz diff --git a/seacatauth/session/builders.py b/seacatauth/session/builders.py index bc277d55..9311de97 100644 --- a/seacatauth/session/builders.py +++ b/seacatauth/session/builders.py @@ -43,13 +43,16 @@ async def external_login_session_builder(external_login_service, credentials_id) return ((SessionAdapter.FN.Authentication.ExternalLoginOptions, external_logins),) -async def authz_session_builder(tenant_service, role_service, credentials_id, tenants=None): +async def authz_session_builder( + tenant_service, role_service, credentials_id, + tenants=None, exclude_resources=frozenset() +): """ Add 'authz' dict with currently authorized tenants and their resources Add 'tenants' list with complete list of credential's tenants """ tenants = tenants or [] - authz = await build_credentials_authz(tenant_service, role_service, credentials_id, tenants) + authz = await build_credentials_authz(tenant_service, role_service, credentials_id, tenants, exclude_resources) user_tenants = list(set(await tenant_service.get_tenants(credentials_id)).union(tenants)) return ( (SessionAdapter.FN.Authorization.Authz, authz), From d6b94c56e3113a7c0f5243c2c3fa2f4bafcf2df3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robin=20Hru=C5=A1ka?= Date: Mon, 5 Jun 2023 15:29:53 +0200 Subject: [PATCH 6/8] Also remove authz:superuser resource from impersonated client sessions. --- seacatauth/authz/utils.py | 4 +++- seacatauth/cookie/service.py | 7 +++++++ seacatauth/openidconnect/service.py | 7 +++++++ seacatauth/session/builders.py | 2 +- 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/seacatauth/authz/utils.py b/seacatauth/authz/utils.py index 05154271..e58158f3 100644 --- a/seacatauth/authz/utils.py +++ b/seacatauth/authz/utils.py @@ -1,6 +1,6 @@ async def build_credentials_authz( tenant_service, role_service, credentials_id, - tenants=None, exclude_resources=frozenset() + tenants=None, exclude_resources=None ): """ Creates a nested 'authz' dict with tenant:resource structure: @@ -10,6 +10,8 @@ async def build_credentials_authz( 'tenantB': ['resourceA', 'resourceB', 'resourceE', 'resourceD'], } """ + exclude_resources = exclude_resources or frozenset() + # Add global roles and resources under "*" authz = {} tenant = "*" diff --git a/seacatauth/cookie/service.py b/seacatauth/cookie/service.py index 9823270e..e3c306c2 100644 --- a/seacatauth/cookie/service.py +++ b/seacatauth/cookie/service.py @@ -159,6 +159,12 @@ async def create_cookie_client_session(self, root_session, client_id, scope, ten except KeyError: raise KeyError("Client '{}' not found".format(client_id)) + # Make sure dangerous resources are removed from impersonated sessions + if root_session.Authentication.ImpersonatorSessionId is not None: + exclude_resources = {"authz:superuser", "authz:impersonate"} + else: + exclude_resources = None + # Build the session session_builders = [ await credentials_session_builder(self.CredentialsService, root_session.Credentials.Id, scope), @@ -167,6 +173,7 @@ async def create_cookie_client_session(self, root_session, client_id, scope, ten role_service=self.RoleService, credentials_id=root_session.Credentials.Id, tenants=tenants, + exclude_resources=exclude_resources, ), cookie_session_builder(), ] diff --git a/seacatauth/openidconnect/service.py b/seacatauth/openidconnect/service.py index 062ede09..be58a6cc 100644 --- a/seacatauth/openidconnect/service.py +++ b/seacatauth/openidconnect/service.py @@ -258,6 +258,12 @@ async def create_oidc_session( code_challenge_method: str = None ): # TODO: Choose builders based on scope + # Make sure dangerous resources are removed from impersonated sessions + if root_session.Authentication.ImpersonatorSessionId is not None: + exclude_resources = {"authz:superuser", "authz:impersonate"} + else: + exclude_resources = set() + session_builders = [ await credentials_session_builder(self.CredentialsService, root_session.Credentials.Id, scope), await authz_session_builder( @@ -265,6 +271,7 @@ async def create_oidc_session( role_service=self.RoleService, credentials_id=root_session.Credentials.Id, tenants=tenants, + exclude_resources=exclude_resources, ) ] diff --git a/seacatauth/session/builders.py b/seacatauth/session/builders.py index 9311de97..19b0aba8 100644 --- a/seacatauth/session/builders.py +++ b/seacatauth/session/builders.py @@ -45,7 +45,7 @@ async def external_login_session_builder(external_login_service, credentials_id) async def authz_session_builder( tenant_service, role_service, credentials_id, - tenants=None, exclude_resources=frozenset() + tenants=None, exclude_resources=None ): """ Add 'authz' dict with currently authorized tenants and their resources From 913ac506c897559b6304d1fe8a9f8e03110ebcef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robin=20Hru=C5=A1ka?= Date: Mon, 5 Jun 2023 15:45:14 +0200 Subject: [PATCH 7/8] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc8ace42..d8946b0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ - User impersonation (#188, PLUM Sprint 230509) - Configurable client session expiration (#204, PLUM Sprint 230509) - Additional TLS options in LDAP provider (#208, PLUM Sprint 230519) +- Impersonation REST API (#210, PLUM Sprint 230602) + +### Refactoring +- Allow impersonating superuser credentials, but exclude the superuser resource (#210, PLUM Sprint 230602) --- From f17d3a7087bc3dc10cafb044357af079f1477d7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robin=20Hru=C5=A1ka?= Date: Mon, 5 Jun 2023 15:46:33 +0200 Subject: [PATCH 8/8] Update CHANGELOG.md --- CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5716431..93a91a32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,8 +14,6 @@ ### Refactoring - Allow impersonating superuser credentials, but exclude the superuser resource (#210, PLUM Sprint 230602) - -### Refactoring - Session performance improvement (#211) ---