Merle Nerger
3 years ago
committed by
Jackson Kwok
No known key found for this signature in database
GPG Key ID: BB88FE9481A0273D
5 changed files with 381 additions and 0 deletions
-
9README.md
-
8src/keycloak/exceptions.py
-
76src/keycloak/keycloak_openid.py
-
145src/keycloak/uma_permissions.py
-
143tests/test_uma_permissions.py
@ -0,0 +1,145 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# |
|||
# The MIT License (MIT) |
|||
# |
|||
# Copyright (C) 2017 Marcos Pereira <marcospereira.mpj@gmail.com> |
|||
# |
|||
# 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 KeycloakPermissionFormatError, PermissionDefinitionError |
|||
|
|||
|
|||
class UMAPermission: |
|||
"""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, UMAPermission): |
|||
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 UMAPermission(resource=result_resource, scope=result_scope) |
|||
|
|||
|
|||
class Resource(UMAPermission): |
|||
def __init__(self, resource): |
|||
super().__init__(resource=resource) |
|||
|
|||
|
|||
class Scope(UMAPermission): |
|||
def __init__(self, scope): |
|||
super().__init__(scope=scope) |
|||
|
|||
|
|||
class AuthStatus: |
|||
"""A class that represents the authorization/login status of a user associated with a token. |
|||
This has to evaluate to True if and only if the user is properly authorized for the requested resource.""" |
|||
|
|||
def __init__(self, is_logged_in, is_authorized, missing_permissions): |
|||
self.is_logged_in = is_logged_in |
|||
self.is_authorized = is_authorized |
|||
self.missing_permissions = missing_permissions |
|||
|
|||
def __bool__(self): |
|||
return self.is_authorized |
|||
|
|||
def __repr__(self): |
|||
return f"AuthStatus(is_authorized={self.is_authorized}, is_logged_in={self.is_logged_in}, missing_permissions={self.missing_permissions})" |
|||
|
|||
|
|||
def build_permission_param(permissions): |
|||
""" |
|||
Transform permissions to a set, so they are usable for requests |
|||
|
|||
:param permissions: either str (resource#scope), |
|||
iterable[str] (resource#scope), |
|||
dict[str,str] (resource: scope), |
|||
dict[str,iterable[str]] (resource: scopes) |
|||
:return: result bool |
|||
""" |
|||
if permissions is None or permissions == "": |
|||
return set() |
|||
if isinstance(permissions, (str, UMAPermission)): |
|||
return set((permissions,)) |
|||
|
|||
try: # treat as dictionary of permissions |
|||
result = set() |
|||
for resource, scopes in permissions.items(): |
|||
if scopes is None: |
|||
result.add(resource) |
|||
elif isinstance(scopes, (str, UMAPermission)): |
|||
result.add("{}#{}".format(resource, scopes)) |
|||
else: |
|||
for scope in scopes: |
|||
if not isinstance(scope, (str, UMAPermission)): |
|||
raise KeycloakPermissionFormatError( |
|||
"misbuilt permission {}".format(permissions) |
|||
) |
|||
result.add("{}#{}".format(resource, scope)) |
|||
return result |
|||
except AttributeError: |
|||
pass |
|||
|
|||
try: # treat as any other iterable of permissions |
|||
return set(permissions) |
|||
except TypeError: |
|||
pass |
|||
raise KeycloakPermissionFormatError("misbuilt permission {}".format(permissions)) |
@ -0,0 +1,143 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# |
|||
# Copyright (C) 2017 Marcos Pereira <marcospereira.mpj@gmail.com> |
|||
# |
|||
# This program is free software: you can redistribute it and/or modify |
|||
# it under the terms of the GNU Lesser General Public License as published by |
|||
# the Free Software Foundation, either version 3 of the License, or |
|||
# (at your option) any later version. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU Lesser General Public License for more details. |
|||
# |
|||
# You should have received a copy of the GNU Lesser General Public License |
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|||
import pytest |
|||
import re |
|||
from keycloak.exceptions import KeycloakPermissionFormatError, PermissionDefinitionError |
|||
from keycloak.uma_permissions import build_permission_param, Resource, Scope |
|||
|
|||
|
|||
|
|||
def test_resource_with_scope_obj(): |
|||
r = Resource("Resource1") |
|||
s = Scope("Scope1") |
|||
assert r(s) == "Resource1#Scope1" |
|||
|
|||
|
|||
def test_scope_with_resource_obj(): |
|||
r = Resource("Resource1") |
|||
s = Scope("Scope1") |
|||
assert s(r) == "Resource1#Scope1" |
|||
|
|||
|
|||
def test_resource_scope_str(): |
|||
r = Resource("Resource1") |
|||
s = "Scope1" |
|||
assert r(scope=s) == "Resource1#Scope1" |
|||
|
|||
|
|||
def test_scope_resource_str(): |
|||
r = "Resource1" |
|||
s = Scope("Scope1") |
|||
assert s(resource=r) == "Resource1#Scope1" |
|||
|
|||
|
|||
def test_resource_scope_dict(): |
|||
r = Resource("Resource1") |
|||
s = {"scope": "Scope1"} |
|||
assert r(**s) == "Resource1#Scope1" |
|||
|
|||
|
|||
def test_scope_resource_dict(): |
|||
r = {"resource": "Resource1"} |
|||
s = Scope("Scope1") |
|||
assert s(**r) == "Resource1#Scope1" |
|||
|
|||
|
|||
def test_resource_scope_list(): |
|||
r = Resource("Resource1") |
|||
s = ["Scope1"] |
|||
with pytest.raises(PermissionDefinitionError) as err: |
|||
r(*s) |
|||
assert err.match("can't determine if 'Scope1' is a resource or scope") |
|||
|
|||
|
|||
def test_build_permission_none(): |
|||
assert build_permission_param(None) == set() |
|||
|
|||
|
|||
def test_build_permission_empty_str(): |
|||
assert build_permission_param("") == set() |
|||
|
|||
|
|||
def test_build_permission_empty_list(): |
|||
assert build_permission_param([]) == set() |
|||
|
|||
|
|||
def test_build_permission_empty_tuple(): |
|||
assert build_permission_param(()) == set() |
|||
|
|||
|
|||
def test_build_permission_empty_set(): |
|||
assert build_permission_param(set()) == set() |
|||
|
|||
|
|||
def test_build_permission_empty_dict(): |
|||
assert build_permission_param({}) == set() |
|||
|
|||
|
|||
def test_build_permission_str(): |
|||
assert build_permission_param("resource1") == {"resource1"} |
|||
|
|||
|
|||
def test_build_permission_list_str(): |
|||
assert build_permission_param(["res1#scope1", "res1#scope2"]) == {"res1#scope1", "res1#scope2"} |
|||
|
|||
|
|||
def test_build_permission_tuple_str(): |
|||
assert build_permission_param(("res1#scope1", "res1#scope2")) == {"res1#scope1", "res1#scope2"} |
|||
|
|||
|
|||
def test_build_permission_set_str(): |
|||
assert build_permission_param({"res1#scope1", "res1#scope2"}) == {"res1#scope1", "res1#scope2"} |
|||
|
|||
|
|||
def test_build_permission_tuple_dict_str_str(): |
|||
assert build_permission_param({"res1": "scope1"}) == {"res1#scope1"} |
|||
|
|||
|
|||
def test_build_permission_tuple_dict_str_list_str(): |
|||
assert build_permission_param({"res1": ["scope1", "scope2"]}) == {"res1#scope1", "res1#scope2"} |
|||
|
|||
|
|||
def test_build_permission_tuple_dict_str_list_str2(): |
|||
assert build_permission_param( |
|||
{"res1": ["scope1", "scope2"], "res2": ["scope2", "scope3"]} |
|||
) == {"res1#scope1", "res1#scope2", "res2#scope2", "res2#scope3"} |
|||
|
|||
|
|||
def test_build_permission_misbuilt_dict_str_list_list_str(): |
|||
with pytest.raises(KeycloakPermissionFormatError) as err: |
|||
build_permission_param({"res1": [["scope1", "scope2"]]}) |
|||
assert err.match(re.escape("misbuilt permission {'res1': [['scope1', 'scope2']]}")) |
|||
|
|||
|
|||
def test_build_permission_misbuilt_list_list_str(): |
|||
with pytest.raises(KeycloakPermissionFormatError) as err: |
|||
build_permission_param([["scope1", "scope2"]]) |
|||
assert err.match(re.escape("misbuilt permission [['scope1', 'scope2']]")) |
|||
|
|||
|
|||
def test_build_permission_misbuilt_list_set_str(): |
|||
with pytest.raises(KeycloakPermissionFormatError) as err: |
|||
build_permission_param([{"scope1", "scope2"}]) |
|||
assert err.match(re.escape("misbuilt permission [{'scope1', 'scope2'}]")) |
|||
|
|||
|
|||
def test_build_permission_misbuilt_set_set_str(): |
|||
with pytest.raises(KeycloakPermissionFormatError) as err: |
|||
build_permission_param([{"scope1"}]) |
|||
assert err.match(re.escape("misbuilt permission [{'scope1'}]")) |
Write
Preview
Loading…
Cancel
Save
Reference in new issue