From 4e3fe9c7e1a1270c0c6ccb5838803efbc79c122a Mon Sep 17 00:00:00 2001 From: Marcos Pereira Date: Wed, 23 Aug 2017 08:37:16 -0300 Subject: [PATCH] Added authorization services. --- .gitignore | 1 + .travis.yml | 8 --- keycloak/__init__.py | 79 +++++++++++++++++++++----- keycloak/authorization/__init__.py | 80 ++++++++++++++++++++++++++ keycloak/authorization/permission.py | 82 +++++++++++++++++++++++++++ keycloak/authorization/policy.py | 84 ++++++++++++++++++++++++++++ keycloak/authorization/role.py | 27 +++++++++ keycloak/connection.py | 35 ++++++++---- keycloak/exceptions.py | 4 ++ 9 files changed, 368 insertions(+), 32 deletions(-) delete mode 100644 .travis.yml create mode 100644 keycloak/authorization/__init__.py create mode 100644 keycloak/authorization/permission.py create mode 100644 keycloak/authorization/policy.py create mode 100644 keycloak/authorization/role.py diff --git a/.gitignore b/.gitignore index 374f13c..3f4c507 100644 --- a/.gitignore +++ b/.gitignore @@ -102,3 +102,4 @@ ENV/ .idea/ main.py +s3air-authz-config.json \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 18d1e36..0000000 --- a/.travis.yml +++ /dev/null @@ -1,8 +0,0 @@ -language: python -python: - - "3.6" - - "pypy" -install: - - pip3 install -r requirements.txt -script: - python3 -m unittest discover diff --git a/keycloak/__init__.py b/keycloak/__init__.py index 722a666..79e3761 100644 --- a/keycloak/__init__.py +++ b/keycloak/__init__.py @@ -14,8 +14,8 @@ # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . - - +from keycloak.authorization import Authorization +from keycloak.exceptions import KeycloakAuthorizationConfigError from .exceptions import raise_error_from_response, KeycloakGetError, KeycloakSecretNotFound, \ KeycloakRPTNotFound from .urls_patterns import ( @@ -30,34 +30,61 @@ from .urls_patterns import ( ) from .connection import ConnectionManager from jose import jwt +import json class Keycloak: def __init__(self, server_url, client_id, realm_name, client_secret_key=None): - self.client_id = client_id - self.client_secret_key = client_secret_key - self.realm_name = realm_name + self._client_id = client_id + self._client_secret_key = client_secret_key + self._realm_name = realm_name - self.connection = ConnectionManager(base_url=server_url, + self._connection = ConnectionManager(base_url=server_url, headers={}, timeout=60) + self._authorization = Authorization() + @property - def get_client_id(self): - return self.client_id + def client_id(self): + return self._client_id + + @client_id.setter + def client_id(self, value): + self._client_id = value @property - def get_client_secret_key(self): - return self.client_secret_key + def client_secret_key(self): + return self._client_secret_key + + @client_secret_key.setter + def client_secret_key(self, value): + self._client_secret_key = value @property - def get_realm_name(self): - return self.realm_name + def realm_name(self): + return self._realm_name + + @realm_name.setter + def realm_name(self, value): + self._realm_name = value @property - def get_connection(self): - return self.connection + def connection(self): + return self._connection + + @connection.setter + def connection(self, value): + self._connection = value + + @property + def authorization(self): + return self._authorization + + @authorization.setter + def authorization(self, value): + self._authorization = value def _add_secret_key(self, payload): """ @@ -229,3 +256,27 @@ class Keycloak: return jwt.decode(token, key, algorithms=algorithms, audience=self.client_id, **kwargs) + + def load_authorization_config(self, path): + """ + Load Keycloak settings (authorization) + + :param path: settings file (json) + :return: + """ + authorization_file = open(path, 'r') + authorization_json = json.loads(authorization_file.read()) + self.authorization.load_config(authorization_json) + authorization_file.close() + + def get_permissions(self): + + if not self.authorization.policies: + raise KeycloakAuthorizationConfigError( + "Keycloak settings not found. Load Authorization Keycloak settings." + ) + + return + + + diff --git a/keycloak/authorization/__init__.py b/keycloak/authorization/__init__.py new file mode 100644 index 0000000..e5ab8bd --- /dev/null +++ b/keycloak/authorization/__init__.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2017 Marcos Pereira +# +# 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 . + +import ast +import json + +from keycloak.authorization.permission import Permission +from keycloak.authorization.policy import Policy +from keycloak.authorization.role import Role + + +class Authorization: + + def __init__(self): + self._policies = {} + + @property + def policies(self): + return self._policies + + @policies.setter + def policies(self, value): + self._policies = value + + def load_config(self, data): + """ + + :param data: + :return: + """ + for pol in data['policies']: + if pol['type'] == 'role': + policy = Policy(name=pol['name'], + type=pol['type'], + logic=pol['logic'], + decision_strategy=pol['decisionStrategy']) + + config_roles = json.loads(pol['config']['roles']) + for role in config_roles: + policy.add_role(Role(name=role['id'], + required=role['required'])) + + self.policies[policy.name] = policy + + if pol['type'] == 'scope': + permission = Permission(name=pol['name'], + type=pol['type'], + logic=pol['logic'], + decision_strategy=pol['decisionStrategy']) + + permission.scopes = ast.literal_eval(pol['config']['scopes']) + + for policy_name in ast.literal_eval(pol['config']['applyPolicies']): + self.policies[policy_name].add_permission(permission) + + if pol['type'] == 'resource': + permission = Permission(name=pol['name'], + type=pol['type'], + logic=pol['logic'], + decision_strategy=pol['decisionStrategy']) + + permission.resources = ast.literal_eval(pol['config']['resources']) + + for policy_name in ast.literal_eval(pol['config']['applyPolicies']): + self.policies[policy_name].add_permission(permission) + diff --git a/keycloak/authorization/permission.py b/keycloak/authorization/permission.py new file mode 100644 index 0000000..d67e396 --- /dev/null +++ b/keycloak/authorization/permission.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2017 Marcos Pereira +# +# 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 . + + +class Permission: + + def __init__(self, name, type, logic, decision_strategy): + self._name = name + self._type = type + self._logic = logic + self._decision_strategy = decision_strategy + self._resources = [] + self._scopes = [] + + def __repr__(self): + return "" % (self.name, self.type) + + def __str__(self): + return "Permission: %s (%s)" % (self.name, self.type) + + @property + def name(self): + return self._name + + @name.setter + def name(self, value): + self._name = value + + @property + def type(self): + return self._type + + @type.setter + def type(self, value): + self._type = value + + @property + def logic(self): + return self._logic + + @logic.setter + def logic(self, value): + self._logic = value + + @property + def decision_strategy(self): + return self._decision_strategy + + @decision_strategy.setter + def decision_strategy(self, value): + self._decision_strategy = value + + @property + def resources(self): + return self._resources + + @resources.setter + def resources(self, value): + self._resources = value + + @property + def scopes(self): + return self._scopes + + @scopes.setter + def scopes(self, value): + self._scopes = value + diff --git a/keycloak/authorization/policy.py b/keycloak/authorization/policy.py new file mode 100644 index 0000000..2abdf5e --- /dev/null +++ b/keycloak/authorization/policy.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2017 Marcos Pereira +# +# 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 . + +from keycloak.exceptions import KeycloakAuthorizationConfigError + + +class Policy: + + def __init__(self, name, type, logic, decision_strategy): + self._name = name + self._type = type + self._logic = logic + self._decision_strategy = decision_strategy + self._roles = [] + self._permissions = [] + + def __repr__(self): + return "" % (self.name, self.type) + + def __str__(self): + return "Policy: %s (%s)" % (self.name, self.type) + + @property + def name(self): + return self._name + + @name.setter + def name(self, value): + self._name = value + + @property + def type(self): + return self._type + + @type.setter + def type(self, value): + self._type = value + + @property + def logic(self): + return self._logic + + @logic.setter + def logic(self, value): + self._logic = value + + @property + def decision_strategy(self): + return self._decision_strategy + + @decision_strategy.setter + def decision_strategy(self, value): + self._decision_strategy = value + + @property + def roles(self): + return self._roles + + @property + def permissions(self): + return self._permissions + + def add_role(self, role): + if self.type != 'role': + raise KeycloakAuthorizationConfigError( + "Can't add role. Policy type is different of role") + self._roles.append(role) + + def add_permission(self, permission): + self._permissions.append(permission) diff --git a/keycloak/authorization/role.py b/keycloak/authorization/role.py new file mode 100644 index 0000000..ff7efde --- /dev/null +++ b/keycloak/authorization/role.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2017 Marcos Pereira +# +# 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 . + + +class Role: + + def __init__(self, name, required=False): + self.name = name + self.required = required + + @property + def get_name(self): + return self.name diff --git a/keycloak/connection.py b/keycloak/connection.py index 43781e4..8a1bd55 100644 --- a/keycloak/connection.py +++ b/keycloak/connection.py @@ -33,26 +33,41 @@ class ConnectionManager(object): """ def __init__(self, base_url, headers={}, timeout=60): - self.base_url = base_url - self.headers = headers - self.timeout = timeout + self._base_url = base_url + self._headers = headers + self._timeout = timeout @property - def get_base_url(self): + def base_url(self): """ Return base url in use for requests to the server. """ - return self.base_url + return self._base_url + + @base_url.setter + def base_url(self, value): + """ """ + self._base_url = value @property - def get_timeout(self): + def timeout(self): """ Return timeout in use for request to the server. """ - return self.timeout + return self._timeout + + @timeout.setter + def timeout(self, value): + """ """ + self._timeout = value @property - def get_headers(self): + def headers(self): """ Return header request to the server. """ - return self.headers + return self._headers + + @headers.setter + def headers(self, value): + """ """ + self._headers = value - def get_param_headers(self, key): + def param_headers(self, key): """ Return a specific header parameter. :arg key (str): Header parameters key. diff --git a/keycloak/exceptions.py b/keycloak/exceptions.py index a9a7313..0ce69bb 100644 --- a/keycloak/exceptions.py +++ b/keycloak/exceptions.py @@ -76,6 +76,10 @@ class KeycloakRPTNotFound(KeycloakOperationError): pass +class KeycloakAuthorizationConfigError(KeycloakOperationError): + pass + + def raise_error_from_response(response, error, expected_code=200): if expected_code == response.status_code: