From a190a8753c7f9209a8141cd4ce5dba31e1261d3c Mon Sep 17 00:00:00 2001 From: Merle Nerger Date: Wed, 13 Apr 2022 16:59:01 +0200 Subject: [PATCH] UMA: convenient resource and scope assembly --- keycloak/exceptions.py | 4 ++ keycloak/keycloak_openid.py | 8 ++-- keycloak/uma_permissions.py | 88 +++++++++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+), 4 deletions(-) create mode 100644 keycloak/uma_permissions.py diff --git a/keycloak/exceptions.py b/keycloak/exceptions.py index 025dd0d..e5f49dd 100644 --- a/keycloak/exceptions.py +++ b/keycloak/exceptions.py @@ -81,6 +81,10 @@ class KeycloakPermissionFormatError(KeycloakOperationError): pass +class PermissionDefinitionError(Exception): + pass + + def raise_error_from_response(response, error, expected_codes=None, skip_exists=False): if expected_codes is None: expected_codes = [200, 201, 204] diff --git a/keycloak/keycloak_openid.py b/keycloak/keycloak_openid.py index 2beeb12..ad25c1d 100644 --- a/keycloak/keycloak_openid.py +++ b/keycloak/keycloak_openid.py @@ -25,6 +25,7 @@ import json from jose import jwt +from .uma_permissions import UMA_Permission from .authorization import Authorization from .connection import ConnectionManager from .exceptions import KeycloakPermissionFormatError, raise_error_from_response, KeycloakGetError, \ @@ -297,7 +298,6 @@ class KeycloakOpenID: data_raw = self.connection.raw_get(URL_REALM.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError)['public_key'] - def entitlement(self, token, resource_server_id): """ Client applications can use a specific endpoint to obtain a special security token @@ -523,7 +523,7 @@ def build_permission_param(permissions): """ if permissions is None or permissions == "": return set() - if isinstance(permissions, str): + if isinstance(permissions, (str, UMA_Permission)): return set((permissions,)) try: # treat as dictionary of permissions @@ -531,11 +531,11 @@ def build_permission_param(permissions): for resource, scopes in permissions.items(): if scopes is None: result.add(resource) - elif isinstance(scopes, str): + elif isinstance(scopes, (str, UMA_Permission)): result.add("{}#{}".format(resource, scopes)) else: for scope in scopes: - if not isinstance(scope, str): + if not isinstance(scope, (str, UMA_Permission)): raise KeycloakPermissionFormatError( 'misbuilt permission {}'.format(permissions)) result.add("{}#{}".format(resource, scope)) diff --git a/keycloak/uma_permissions.py b/keycloak/uma_permissions.py new file mode 100644 index 0000000..ca29737 --- /dev/null +++ b/keycloak/uma_permissions.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +# +# The MIT License (MIT) +# +# Copyright (C) 2017 Marcos Pereira +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +# the Software, and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +from keycloak.exceptions import PermissionDefinitionError + + +class UMA_Permission(): + """A class to conveniently assembly permissions. + The class itself is callable, and will return the assembled permission. + + Usage example: + + >>> r = Resource("Users") + >>> s = Scope("delete") + >>> permission = r(s) + >>> print(permission) + 'Users#delete' + + """ + + def __init__(self, *, resource="", scope=""): + self.resource = resource + self.scope = scope + + def __str__(self): + scope = self.scope + if scope: + scope = "#"+scope + return "{}{}".format(self.resource, scope) + + def __eq__(self, __o: object) -> bool: + return str(self) == str(__o) + + def __repr__(self) -> str: + return self.__str__() + + def __hash__(self) -> int: + return hash(str(self)) + + def __call__(self, *args, resource="", scope="") -> object: + result_resource = self.resource + result_scope = self.scope + + for arg in args: + if not isinstance(arg, UMA_Permission): + raise PermissionDefinitionError( + "can't determine if '{}' is a resource or scope".format(arg)) + if arg.resource: + result_resource = str(arg.resource) + if arg.scope: + result_scope = str(arg.scope) + + if resource: + result_resource = str(resource) + if scope: + result_scope = str(scope) + + return UMA_Permission(resource=result_resource, scope=result_scope) + + +class Resource(UMA_Permission): + def __init__(self, resource): + super().__init__(resource=resource) + + +class Scope(UMA_Permission): + def __init__(self, scope): + super().__init__(scope=scope)