From 1e921f54c7e6e24167b4745eaa5c42f28cb1e562 Mon Sep 17 00:00:00 2001 From: "m.krostelev" Date: Wed, 25 May 2022 12:53:03 +0300 Subject: [PATCH 1/8] add extra_fields to OIDCUser, attributes to create_user method --- fastapi_keycloak/api.py | 20 +++++++++++++++++--- fastapi_keycloak/model.py | 2 ++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/fastapi_keycloak/api.py b/fastapi_keycloak/api.py index 833d463..5cbbf46 100644 --- a/fastapi_keycloak/api.py +++ b/fastapi_keycloak/api.py @@ -7,7 +7,7 @@ from urllib.parse import urlencode import requests -from fastapi import Depends, FastAPI, HTTPException +from fastapi import Depends, FastAPI, HTTPException, status from fastapi.security import OAuth2PasswordBearer from jose import ExpiredSignatureError, JWTError, jwt from jose.exceptions import JWTClaimsError @@ -221,12 +221,13 @@ def user_auth_scheme(self) -> OAuth2PasswordBearer: """ return OAuth2PasswordBearer(tokenUrl=self.token_uri) - def get_current_user(self, required_roles: List[str] = None) -> OIDCUser: + def get_current_user(self, required_roles: List[str] = None, extra_fields: List[str] = None,) -> OIDCUser: """Returns the current user based on an access token in the HTTP-header. Optionally verifies roles are possessed by the user Args: required_roles List[str]: List of role names required for this endpoint + extra_fields List[str]: The names of the additional fields you need that are encoded in JWT Returns: OIDCUser: Decoded JWT content @@ -258,12 +259,22 @@ def current_user( decoded_token = self._decode_token(token=token, audience="account") user = OIDCUser.parse_obj(decoded_token) if required_roles: + if not user.realm_access: # in cases where there are no roles in realm accessing + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=f"Role(s) {', '.join(required_roles)} is required to perform this action", + ) for role in required_roles: if role not in user.roles: raise HTTPException( - status_code=403, + status_code=status.HTTP_403_FORBIDDEN, detail=f'Role "{role}" is required to perform this action', ) + + if extra_fields: + for field in extra_fields: + user.extra_fields[field] = decoded_token.get(field, None) + return user return current_user @@ -715,6 +726,7 @@ def create_user( enabled: bool = True, initial_roles: List[str] = None, send_email_verification: bool = True, + attributes: dict[str, Any] = None, ) -> KeycloakUser: """ @@ -729,6 +741,7 @@ def create_user( send_email_verification (bool): If true, the email verification will be added as an required action and the email triggered - if the user was created successfully. Defaults to `True` + attributes (dict): attributes of new user Returns: KeycloakUser: If the creation succeeded @@ -749,6 +762,7 @@ def create_user( {"temporary": False, "type": "password", "value": password} ], "requiredActions": ["VERIFY_EMAIL" if send_email_verification else None], + "attributes": attributes } response = self._admin_request( url=self.users_uri, data=data, method=HTTPMethod.POST diff --git a/fastapi_keycloak/model.py b/fastapi_keycloak/model.py index 870a644..f8d5504 100644 --- a/fastapi_keycloak/model.py +++ b/fastapi_keycloak/model.py @@ -91,6 +91,7 @@ class OIDCUser(BaseModel): preferred_username (Optional[str]): realm_access (dict): resource_access (dict): + extra_fields (dict): Notes: Check the Keycloak documentation at https://www.keycloak.org/docs-api/15.0/rest-api/index.html for details. This is a mere proxy object. @@ -108,6 +109,7 @@ class OIDCUser(BaseModel): preferred_username: Optional[str] realm_access: Optional[dict] resource_access: Optional[dict] + extra_fields: Optional[dict] @property def roles(self) -> List[str]: From 7bdfc0e81cb2260532274db00de034773c4a7900 Mon Sep 17 00:00:00 2001 From: "m.krostelev" Date: Wed, 25 May 2022 12:53:03 +0300 Subject: [PATCH 2/8] add extra_fields to OIDCUser, attributes to create_user method --- fastapi_keycloak/api.py | 20 +++++++++++++++++--- fastapi_keycloak/model.py | 2 ++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/fastapi_keycloak/api.py b/fastapi_keycloak/api.py index 833d463..0340340 100644 --- a/fastapi_keycloak/api.py +++ b/fastapi_keycloak/api.py @@ -7,7 +7,7 @@ from urllib.parse import urlencode import requests -from fastapi import Depends, FastAPI, HTTPException +from fastapi import Depends, FastAPI, HTTPException, status from fastapi.security import OAuth2PasswordBearer from jose import ExpiredSignatureError, JWTError, jwt from jose.exceptions import JWTClaimsError @@ -221,12 +221,13 @@ def user_auth_scheme(self) -> OAuth2PasswordBearer: """ return OAuth2PasswordBearer(tokenUrl=self.token_uri) - def get_current_user(self, required_roles: List[str] = None) -> OIDCUser: + def get_current_user(self, required_roles: List[str] = None, extra_fields: List[str] = None) -> OIDCUser: """Returns the current user based on an access token in the HTTP-header. Optionally verifies roles are possessed by the user Args: required_roles List[str]: List of role names required for this endpoint + extra_fields List[str]: The names of the additional fields you need that are encoded in JWT Returns: OIDCUser: Decoded JWT content @@ -258,12 +259,22 @@ def current_user( decoded_token = self._decode_token(token=token, audience="account") user = OIDCUser.parse_obj(decoded_token) if required_roles: + if not user.realm_access: # in cases where there are no roles in realm accessing + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=f"Role(s) {', '.join(required_roles)} is required to perform this action", + ) for role in required_roles: if role not in user.roles: raise HTTPException( - status_code=403, + status_code=status.HTTP_403_FORBIDDEN, detail=f'Role "{role}" is required to perform this action', ) + + if extra_fields: + for field in extra_fields: + user.extra_fields[field] = decoded_token.get(field, None) + return user return current_user @@ -715,6 +726,7 @@ def create_user( enabled: bool = True, initial_roles: List[str] = None, send_email_verification: bool = True, + attributes: dict[str, Any] = None, ) -> KeycloakUser: """ @@ -729,6 +741,7 @@ def create_user( send_email_verification (bool): If true, the email verification will be added as an required action and the email triggered - if the user was created successfully. Defaults to `True` + attributes (dict): attributes of new user Returns: KeycloakUser: If the creation succeeded @@ -749,6 +762,7 @@ def create_user( {"temporary": False, "type": "password", "value": password} ], "requiredActions": ["VERIFY_EMAIL" if send_email_verification else None], + "attributes": attributes } response = self._admin_request( url=self.users_uri, data=data, method=HTTPMethod.POST diff --git a/fastapi_keycloak/model.py b/fastapi_keycloak/model.py index 870a644..f8d5504 100644 --- a/fastapi_keycloak/model.py +++ b/fastapi_keycloak/model.py @@ -91,6 +91,7 @@ class OIDCUser(BaseModel): preferred_username (Optional[str]): realm_access (dict): resource_access (dict): + extra_fields (dict): Notes: Check the Keycloak documentation at https://www.keycloak.org/docs-api/15.0/rest-api/index.html for details. This is a mere proxy object. @@ -108,6 +109,7 @@ class OIDCUser(BaseModel): preferred_username: Optional[str] realm_access: Optional[dict] resource_access: Optional[dict] + extra_fields: Optional[dict] @property def roles(self) -> List[str]: From 3378429db5d0589b26adbca603deee4139f5df9f Mon Sep 17 00:00:00 2001 From: "m.krostelev" Date: Wed, 25 May 2022 13:07:33 +0300 Subject: [PATCH 3/8] add coma --- fastapi_keycloak/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastapi_keycloak/api.py b/fastapi_keycloak/api.py index 0340340..b3a8970 100644 --- a/fastapi_keycloak/api.py +++ b/fastapi_keycloak/api.py @@ -762,7 +762,7 @@ def create_user( {"temporary": False, "type": "password", "value": password} ], "requiredActions": ["VERIFY_EMAIL" if send_email_verification else None], - "attributes": attributes + "attributes": attributes, } response = self._admin_request( url=self.users_uri, data=data, method=HTTPMethod.POST From 9fdedb4431b24f82b3877f385186bafaa6a89208 Mon Sep 17 00:00:00 2001 From: twistfire92 <48478011+twistfire92@users.noreply.github.com> Date: Fri, 17 Jun 2022 10:44:25 +0300 Subject: [PATCH 4/8] Update model.py --- fastapi_keycloak/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastapi_keycloak/model.py b/fastapi_keycloak/model.py index f8d5504..e7bbbec 100644 --- a/fastapi_keycloak/model.py +++ b/fastapi_keycloak/model.py @@ -109,7 +109,7 @@ class OIDCUser(BaseModel): preferred_username: Optional[str] realm_access: Optional[dict] resource_access: Optional[dict] - extra_fields: Optional[dict] + extra_fields: Optional[dict] = {} @property def roles(self) -> List[str]: From 368fce894c5176bc9644a1b47091ae2e43c10b09 Mon Sep 17 00:00:00 2001 From: "m.krostelev" Date: Fri, 17 Jun 2022 11:01:19 +0300 Subject: [PATCH 5/8] Revert "Update model.py" This reverts commit 9fdedb4431b24f82b3877f385186bafaa6a89208. --- fastapi_keycloak/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastapi_keycloak/model.py b/fastapi_keycloak/model.py index e7bbbec..f8d5504 100644 --- a/fastapi_keycloak/model.py +++ b/fastapi_keycloak/model.py @@ -109,7 +109,7 @@ class OIDCUser(BaseModel): preferred_username: Optional[str] realm_access: Optional[dict] resource_access: Optional[dict] - extra_fields: Optional[dict] = {} + extra_fields: Optional[dict] @property def roles(self) -> List[str]: From 90cec1f901c490ba110b0fae61f851c1e3108939 Mon Sep 17 00:00:00 2001 From: "m.krostelev" Date: Fri, 17 Jun 2022 11:03:21 +0300 Subject: [PATCH 6/8] OIDCUser model fix --- fastapi_keycloak/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastapi_keycloak/model.py b/fastapi_keycloak/model.py index f8d5504..e7bbbec 100644 --- a/fastapi_keycloak/model.py +++ b/fastapi_keycloak/model.py @@ -109,7 +109,7 @@ class OIDCUser(BaseModel): preferred_username: Optional[str] realm_access: Optional[dict] resource_access: Optional[dict] - extra_fields: Optional[dict] + extra_fields: Optional[dict] = {} @property def roles(self) -> List[str]: From 95731818ed4ea27a6f452dc623b9f9a6b15a2cce Mon Sep 17 00:00:00 2001 From: "m.krostelev" Date: Tue, 21 Jun 2022 13:44:30 +0300 Subject: [PATCH 7/8] add default factory for extra_fields in OIDCUser --- fastapi_keycloak/model.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fastapi_keycloak/model.py b/fastapi_keycloak/model.py index e7bbbec..0830fa2 100644 --- a/fastapi_keycloak/model.py +++ b/fastapi_keycloak/model.py @@ -1,7 +1,7 @@ from enum import Enum from typing import List, Optional -from pydantic import BaseModel, SecretStr +from pydantic import BaseModel, SecretStr, Field from fastapi_keycloak.exceptions import KeycloakError @@ -109,7 +109,7 @@ class OIDCUser(BaseModel): preferred_username: Optional[str] realm_access: Optional[dict] resource_access: Optional[dict] - extra_fields: Optional[dict] = {} + extra_fields: dict = Field(default_factory=lambda: dict()) @property def roles(self) -> List[str]: From 6d3a55314bf3e5e2274fd56f0be44cf5dd2e6d25 Mon Sep 17 00:00:00 2001 From: "m.krostelev" Date: Tue, 21 Jun 2022 14:44:51 +0300 Subject: [PATCH 8/8] remove lambda --- fastapi_keycloak/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastapi_keycloak/model.py b/fastapi_keycloak/model.py index 0830fa2..f249757 100644 --- a/fastapi_keycloak/model.py +++ b/fastapi_keycloak/model.py @@ -109,7 +109,7 @@ class OIDCUser(BaseModel): preferred_username: Optional[str] realm_access: Optional[dict] resource_access: Optional[dict] - extra_fields: dict = Field(default_factory=lambda: dict()) + extra_fields: dict = Field(default_factory=dict) @property def roles(self) -> List[str]: