From 4e6b9eb085256e2bc8a9c3afeea473e2b5c9ca41 Mon Sep 17 00:00:00 2001 From: Daniel Pimenta Date: Wed, 5 Jul 2023 11:13:18 +0100 Subject: [PATCH 01/64] Fix develop dockerfile --- Dockerfile.develop | 2 +- README.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile.develop b/Dockerfile.develop index 6fd5056..3e548ba 100644 --- a/Dockerfile.develop +++ b/Dockerfile.develop @@ -6,6 +6,6 @@ COPY . . ENV FLASK_APP "src/app:create_app()" ENV FLASK_ENV develop ENV FLASK_DEBUG 1 -RUN pip install -r requirements.develop.txt +RUN pip install -r requirements.txt EXPOSE 5566 CMD [ "python", "-m" , "flask", "run", "--host=0.0.0.0", "--port=5566"] diff --git a/README.md b/README.md index b6d7721..b2dbc01 100644 --- a/README.md +++ b/README.md @@ -113,8 +113,8 @@ cd um-identity-api ``` 5.2 Run locally with Docker ```sh - docker build . --progress=plain -t um-identity-api:latest - docker run --rm -dp 5566:5566 --name um-identity-api --network eoepcanetwork um-identity-api:latest + docker build . --progress=plain -t um-identity-api:develop + docker run --rm -dp 5566:5566 --name um-identity-api --network eoepcanetwork um-identity-api:develop ``` 5.3 Run develop branch with Docker ```sh From 918d4c6746ef77bd13f169ad5e7b8f677bfe41db Mon Sep 17 00:00:00 2001 From: Daniel Pimenta Date: Wed, 5 Jul 2023 11:18:10 +0100 Subject: [PATCH 02/64] Change keycloak urls --- conf/config.demo.ini | 2 +- conf/config.develop.ini | 2 +- conf/config.production.ini | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/conf/config.demo.ini b/conf/config.demo.ini index e48db3d..3df0b99 100644 --- a/conf/config.demo.ini +++ b/conf/config.demo.ini @@ -1,5 +1,5 @@ [Keycloak] -auth_server_url = https://keycloak.demo.eoepca.org/ +auth_server_url = https://identity.keycloak.demo.eoepca.org/ admin_username = admin admin_password = admin realm = demo diff --git a/conf/config.develop.ini b/conf/config.develop.ini index c285100..8499a2c 100644 --- a/conf/config.develop.ini +++ b/conf/config.develop.ini @@ -1,5 +1,5 @@ [Keycloak] -auth_server_url = https://keycloak.develop.eoepca.org/ +auth_server_url = https://identity.keycloak.develop.eoepca.org/ admin_username = admin admin_password = admin realm = demo diff --git a/conf/config.production.ini b/conf/config.production.ini index 70b1159..c81abc8 100644 --- a/conf/config.production.ini +++ b/conf/config.production.ini @@ -1,5 +1,5 @@ [Keycloak] -auth_server_url = https://keycloak.eoepca.org/ +auth_server_url = https://identity.keycloak.eoepca.org/ admin_username = admin admin_password = admin realm = demo From ba4b8b6be73dcb6e37b7db9509beac417fdf71ed Mon Sep 17 00:00:00 2001 From: Daniel Pimenta Date: Wed, 5 Jul 2023 11:52:25 +0100 Subject: [PATCH 03/64] Fix develop workflow tag --- .github/workflows/develop-docker-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/develop-docker-publish.yml b/.github/workflows/develop-docker-publish.yml index 6af5f08..41e2382 100644 --- a/.github/workflows/develop-docker-publish.yml +++ b/.github/workflows/develop-docker-publish.yml @@ -66,7 +66,7 @@ jobs: with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | - type=raw,value=development + type=raw,value=develop type=ref,event=tag # Build and push Docker image with Buildx (don't push on PR) From 5796045e41177abe4d3ab3e5167afa90880aeb50 Mon Sep 17 00:00:00 2001 From: Daniel Pimenta Date: Wed, 5 Jul 2023 11:54:18 +0100 Subject: [PATCH 04/64] Fix production workflow --- .github/workflows/production-docker-publish.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/production-docker-publish.yml b/.github/workflows/production-docker-publish.yml index 051ca30..30056f1 100644 --- a/.github/workflows/production-docker-publish.yml +++ b/.github/workflows/production-docker-publish.yml @@ -9,11 +9,11 @@ on: schedule: - cron: '29 9 * * *' push: - branches: [ "master", "develop" ] + branches: [ "master" ] # Publish semver tags as releases. tags: [ 'v*.*.*' ] pull_request: - branches: [ "master", "develop" ] + branches: [ "master" ] env: # Use docker.io for Docker Hub if empty From 9f4d9c2131721d6e0a9b91b17e0e95ba8e886aa4 Mon Sep 17 00:00:00 2001 From: Daniel Pimenta Date: Wed, 5 Jul 2023 12:46:08 +0100 Subject: [PATCH 05/64] Change log message --- src/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.py b/src/app.py index 8c418b6..c051d0e 100644 --- a/src/app.py +++ b/src/app.py @@ -59,7 +59,7 @@ def identity_api(config, keycloak): def keycloak_client(config): auth_server_url = config.get("Keycloak", "auth_server_url") realm = config.get("Keycloak", "realm") - logger.info("Starting Keycloak client for: " + str(auth_server_url) + ", realm: " + str(realm)) + logger.info("Starting Keycloak client for: " + str(auth_server_url) + "/" + str(realm)) return KeycloakClient(server_url=auth_server_url, realm=realm, resource_server_endpoint=config.get("Keycloak", "resource_server_endpoint"), From 45704b4f2db7d814476d47cefb054acbbc633633 Mon Sep 17 00:00:00 2001 From: Daniel Pimenta Date: Wed, 5 Jul 2023 12:57:11 +0100 Subject: [PATCH 06/64] Change config --- conf/config.demo.ini | 2 +- conf/config.develop.ini | 2 +- conf/config.ini | 2 +- conf/config.production.ini | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/conf/config.demo.ini b/conf/config.demo.ini index 3df0b99..5933101 100644 --- a/conf/config.demo.ini +++ b/conf/config.demo.ini @@ -1,7 +1,7 @@ [Keycloak] auth_server_url = https://identity.keycloak.demo.eoepca.org/ admin_username = admin -admin_password = admin +admin_password = admin_Abcd1234# realm = demo resource_server_endpoint = https://dummy-service.develop.eoepca.org [Swagger] diff --git a/conf/config.develop.ini b/conf/config.develop.ini index 8499a2c..4d2ddce 100644 --- a/conf/config.develop.ini +++ b/conf/config.develop.ini @@ -1,7 +1,7 @@ [Keycloak] auth_server_url = https://identity.keycloak.develop.eoepca.org/ admin_username = admin -admin_password = admin +admin_password = admin_Abcd1234# realm = demo resource_server_endpoint = https://dummy-service.develop.eoepca.org [Swagger] diff --git a/conf/config.ini b/conf/config.ini index dae557b..0c7dfc1 100644 --- a/conf/config.ini +++ b/conf/config.ini @@ -1,7 +1,7 @@ [Keycloak] auth_server_url = http://localhost:8080 admin_username = admin -admin_password = admin +admin_password = admin_Abcd1234# realm = demo resource_server_endpoint = https://dummy-service.develop.eoepca.org [Swagger] diff --git a/conf/config.production.ini b/conf/config.production.ini index c81abc8..850eaa1 100644 --- a/conf/config.production.ini +++ b/conf/config.production.ini @@ -1,7 +1,7 @@ [Keycloak] auth_server_url = https://identity.keycloak.eoepca.org/ admin_username = admin -admin_password = admin +admin_password = admin_Abcd1234# realm = demo resource_server_endpoint = https://dummy-service.develop.eoepca.org [Swagger] From 8fefb8070d405b90fb485f624e8723eb04dc5563 Mon Sep 17 00:00:00 2001 From: Daniel Pimenta Date: Wed, 5 Jul 2023 16:13:52 +0100 Subject: [PATCH 07/64] Add health check --- requirements.local.txt | 1 + requirements.production.txt | 1 + requirements.txt | 1 + src/app.py | 39 ++++++++++++++++++++++++------------- 4 files changed, 29 insertions(+), 13 deletions(-) diff --git a/requirements.local.txt b/requirements.local.txt index 0190cfa..c888228 100644 --- a/requirements.local.txt +++ b/requirements.local.txt @@ -11,4 +11,5 @@ configparser==5.3.0 waitress==2.1.2 python-dotenv==1.0.0 retry==0.9.2 +flask-healthz==0.0.3 file:../um-identity-service diff --git a/requirements.production.txt b/requirements.production.txt index 990b946..c49e133 100644 --- a/requirements.production.txt +++ b/requirements.production.txt @@ -11,4 +11,5 @@ configparser==5.3.0 waitress==2.1.2 python-dotenv==1.0.0 retry==0.9.2 +flask-healthz==0.0.3 identityutils @ git+https://github.com/eoepca/um-identity-service@master diff --git a/requirements.txt b/requirements.txt index e1d55cb..dba2487 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,4 +11,5 @@ configparser==5.3.0 waitress==2.1.2 python-dotenv==1.0.0 retry==0.9.2 +flask-healthz==0.0.3 identityutils @ git+https://github.com/eoepca/um-identity-service@develop diff --git a/src/app.py b/src/app.py index c051d0e..a70e70d 100644 --- a/src/app.py +++ b/src/app.py @@ -18,12 +18,13 @@ from identityutils.configuration import load_configuration from identityutils.keycloak_client import KeycloakClient from retry.api import retry_call +from flask_healthz import healthz, HealthError logger.Logger.get_instance().load_configuration(os.path.join(os.path.dirname(__file__), "../conf/logging.yaml")) logger = logging.getLogger("IDENTITY_API") mode = os.environ.get('FLASK_ENV') -logger.info("mode " + str(mode)) +logger.info("Starting app in mode: " + str(mode)) if mode == 'develop': config_file = "config.develop.ini" elif mode == 'demo': @@ -34,14 +35,26 @@ config_file = "config.ini" config_path = os.path.join(os.path.dirname(__file__), "../conf/", config_file) +keycloak = None -def identity_api(config, keycloak): - api = Flask(__name__) - api.secret_key = ''.join(choice(ascii_lowercase) for _ in range(30)) # Random key - api.register_blueprint(resources.construct_blueprint(keycloak_client=keycloak)) - api.register_blueprint(policies.construct_blueprint(keycloak_client=keycloak)) - api.register_blueprint(permissions.construct_blueprint(keycloak_client=keycloak)) +def readiness(): + if keycloak is None: + raise HealthError("Keycloak client is not ready") + +def identity_api(config): + app = Flask(__name__) + app.secret_key = ''.join(choice(ascii_lowercase) for _ in range(30)) # Random key + + app.config['HEALTHZ'] = { + "live": lambda: None, + "ready": "src.app.readiness" + } + + app.register_blueprint(resources.construct_blueprint(keycloak_client=keycloak)) + app.register_blueprint(policies.construct_blueprint(keycloak_client=keycloak)) + app.register_blueprint(permissions.construct_blueprint(keycloak_client=keycloak)) + app.register_blueprint(healthz, url_prefix="/health") swagger_spec_resources = json.load(open(os.path.join(os.path.dirname(__file__), "../conf/swagger.json"))) swaggerui_resources_blueprint = get_swaggerui_blueprint( config.get('Swagger', 'swagger_url'), @@ -51,15 +64,15 @@ def identity_api(config, keycloak): 'spec': swagger_spec_resources }, ) - api.register_blueprint(swaggerui_resources_blueprint) + app.register_blueprint(swaggerui_resources_blueprint) - return api + return app def keycloak_client(config): auth_server_url = config.get("Keycloak", "auth_server_url") realm = config.get("Keycloak", "realm") - logger.info("Starting Keycloak client for: " + str(auth_server_url) + "/" + str(realm)) + logger.info("Starting Keycloak client for: " + str(auth_server_url) + " realm: " + str(realm)) return KeycloakClient(server_url=auth_server_url, realm=realm, resource_server_endpoint=config.get("Keycloak", "resource_server_endpoint"), @@ -71,6 +84,6 @@ def keycloak_client(config): def create_app(): """Create a Flask application using the app factory pattern.""" config = load_configuration(config_path) - keycloak = retry_call(keycloak_client, fargs=[config], exceptions=(KeycloakConnectionError, NewConnectionError), - delay=0.5, backoff=1.2, jitter=(1, 2), logger=logger) - return identity_api(config, keycloak) + # keycloak = retry_call(keycloak_client, fargs=[config], exceptions=(KeycloakConnectionError, NewConnectionError), + # delay=0.5, backoff=1.2, jitter=(1, 2), logger=logger) + return identity_api(config) From e2b6f3f061724d698e8d8003015a700c52e401ae Mon Sep 17 00:00:00 2001 From: Daniel Pimenta Date: Wed, 5 Jul 2023 16:27:55 +0100 Subject: [PATCH 08/64] Fix health check --- src/app.py | 30 ++++++++++-------------------- 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/src/app.py b/src/app.py index a70e70d..5667645 100644 --- a/src/app.py +++ b/src/app.py @@ -35,22 +35,13 @@ config_file = "config.ini" config_path = os.path.join(os.path.dirname(__file__), "../conf/", config_file) -keycloak = None - - -def readiness(): - if keycloak is None: - raise HealthError("Keycloak client is not ready") - -def identity_api(config): - app = Flask(__name__) - app.secret_key = ''.join(choice(ascii_lowercase) for _ in range(30)) # Random key - - app.config['HEALTHZ'] = { - "live": lambda: None, - "ready": "src.app.readiness" - } +app = Flask(__name__) +app.secret_key = ''.join(choice(ascii_lowercase) for _ in range(30)) # Random key +app.config['HEALTHZ'] = { + "live": lambda: None +} +def register_endpoints(config, keycloak): app.register_blueprint(resources.construct_blueprint(keycloak_client=keycloak)) app.register_blueprint(policies.construct_blueprint(keycloak_client=keycloak)) app.register_blueprint(permissions.construct_blueprint(keycloak_client=keycloak)) @@ -66,8 +57,6 @@ def identity_api(config): ) app.register_blueprint(swaggerui_resources_blueprint) - return app - def keycloak_client(config): auth_server_url = config.get("Keycloak", "auth_server_url") @@ -84,6 +73,7 @@ def keycloak_client(config): def create_app(): """Create a Flask application using the app factory pattern.""" config = load_configuration(config_path) - # keycloak = retry_call(keycloak_client, fargs=[config], exceptions=(KeycloakConnectionError, NewConnectionError), - # delay=0.5, backoff=1.2, jitter=(1, 2), logger=logger) - return identity_api(config) + keycloak = retry_call(keycloak_client, fargs=[config], exceptions=(KeycloakConnectionError, NewConnectionError), + delay=0.5, backoff=1.2, jitter=(1, 2), logger=logger) + register_endpoints(config, None) + return app From e9b702d0987104df6ae08a4d81a17c84ae6dab54 Mon Sep 17 00:00:00 2001 From: Daniel Pimenta Date: Wed, 5 Jul 2023 16:31:27 +0100 Subject: [PATCH 09/64] Add ready health endpoint --- src/app.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app.py b/src/app.py index 5667645..647ec0d 100644 --- a/src/app.py +++ b/src/app.py @@ -38,7 +38,8 @@ app = Flask(__name__) app.secret_key = ''.join(choice(ascii_lowercase) for _ in range(30)) # Random key app.config['HEALTHZ'] = { - "live": lambda: None + "live": lambda: None, + "ready": lambda: None } def register_endpoints(config, keycloak): From 7ad7d2070933ebacdc97d8dd4661e1c031ba1fc9 Mon Sep 17 00:00:00 2001 From: Daniel Pimenta Date: Wed, 5 Jul 2023 16:37:48 +0100 Subject: [PATCH 10/64] Fix issue --- src/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.py b/src/app.py index 647ec0d..a4333fb 100644 --- a/src/app.py +++ b/src/app.py @@ -76,5 +76,5 @@ def create_app(): config = load_configuration(config_path) keycloak = retry_call(keycloak_client, fargs=[config], exceptions=(KeycloakConnectionError, NewConnectionError), delay=0.5, backoff=1.2, jitter=(1, 2), logger=logger) - register_endpoints(config, None) + register_endpoints(config, keycloak) return app From 94c2d7e235aad219c8e111542a45092cf8a150c9 Mon Sep 17 00:00:00 2001 From: Daniel Pimenta Date: Wed, 5 Jul 2023 17:32:24 +0100 Subject: [PATCH 11/64] Change workflow filenames --- .../{develop-docker-publish.yml => docker-publish-develop.yml} | 0 .../{production-docker-publish.yml => docker-publish-master.yml} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename .github/workflows/{develop-docker-publish.yml => docker-publish-develop.yml} (100%) rename .github/workflows/{production-docker-publish.yml => docker-publish-master.yml} (100%) diff --git a/.github/workflows/develop-docker-publish.yml b/.github/workflows/docker-publish-develop.yml similarity index 100% rename from .github/workflows/develop-docker-publish.yml rename to .github/workflows/docker-publish-develop.yml diff --git a/.github/workflows/production-docker-publish.yml b/.github/workflows/docker-publish-master.yml similarity index 100% rename from .github/workflows/production-docker-publish.yml rename to .github/workflows/docker-publish-master.yml From 4667b27146121d3ca6e09988645daa7aa1c33241 Mon Sep 17 00:00:00 2001 From: flaviorosadme <82375986+flaviorosadme@users.noreply.github.com> Date: Tue, 25 Jul 2023 15:07:23 +0100 Subject: [PATCH 12/64] Eoepca 910 um keycloak develop an identity api based on keycloak api (#17) * feat: policies endpoints added, not completely * feat: working on update policies * feat: all remaining added, still policy update not working, create and update scope based permission not working * feat: last resource permissions endpoints added and working * fix: changed pyyaml version from 5.4.1 to 5.3.1 * feat: endpoints changed --- requirements.txt | 2 +- src/blueprints/permissions.py | 43 +++++++++++- src/blueprints/policies.py | 119 +++++++++++++++++++++++++++++++++- 3 files changed, 161 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index dba2487..d0f716d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ requests==2.25.1 flask-swagger-ui==4.11.1 python-keycloak==3.2.0 mock==5.0.2 -pyyaml==5.4.1 +pyyaml==5.3.1 elasticsearch==8.8.0 lxml==4.9.2 configparser==5.3.0 diff --git a/src/blueprints/permissions.py b/src/blueprints/permissions.py index 8aa7906..60cd05c 100644 --- a/src/blueprints/permissions.py +++ b/src/blueprints/permissions.py @@ -1,8 +1,49 @@ -from flask import Blueprint +from flask import Blueprint, request def construct_blueprint(keycloak_client): keycloak_client = keycloak_client permissions = Blueprint('permissions', __name__) + @permissions.route("/permissions/", methods=["GET"]) + def get_client_authz_permissions(client_id: str): + return keycloak_client.get_client_authz_permissions(client_id) + + @permissions.route("/permissions//management", methods=["GET"]) + def get_client_management_permissions(client_id: str): + return keycloak_client.get_client_management_permissions(client_id) + + @permissions.route("/permissions//resources", methods=["GET"]) + def get_client_resource_permissions(client_id: str): + return keycloak_client.get_client_resource_permissions(client_id) + + #@permissions.route("/client_authz_scope_permissions//", methods=["GET"]) + #def get_client_authz_scope_permissions(client_id: str, scope_id: str): + # return keycloak_client.get_client_authz_scope_permissions(client_id, scope_id) + + #@permissions.route("/client_authz_scope_permissions/", methods=["POST"]) + #def create_client_authz_scope_based_permissions(client_id: str): + # payload = request.get_json() + # return keycloak_client.create_client_authz_scope_based_permission(client_id, payload) + + @permissions.route("/permissions//resources", methods=["POST"]) + def create_client_authz_resource_based_permission(client_id: str): + payload = request.get_json() + return keycloak_client.create_client_authz_resource_based_permission(client_id, payload) + + @permissions.route("/permissions//management", methods=["PUT"]) + def update_client_management_permissions(client_id: str): + payload = request.get_json() + return keycloak_client.update_client_management_permissions(client_id, payload) + + @permissions.route("/permissions//resources/", methods=["PUT"]) + def update_client_authz_resource_permission(client_id: str, permission_id): + payload = request.get_json() + return keycloak_client.update_client_authz_resource_permission(client_id, payload, permission_id) + + #@permissions.route("/permissions//scopes/", methods=["PUT"]) + #def update_client_authz_scope_permissions(client_id: str, scope_id): + # payload = request.get_json() + # return keycloak_client.update_client_authz_scope_permission(client_id, payload, scope_id) + return permissions diff --git a/src/blueprints/policies.py b/src/blueprints/policies.py index cc0fe3d..4fe974b 100644 --- a/src/blueprints/policies.py +++ b/src/blueprints/policies.py @@ -1,8 +1,125 @@ -from flask import Blueprint +from flask import Blueprint, request def construct_blueprint(keycloak_client): keycloak_client = keycloak_client policies = Blueprint('policies', __name__) + + @policies.route("/policies", methods=["GET"]) + def get_policies(): + resource = request.args.get('resource', "") + name = request.args.get('name', "") + scope = request.args.get('uri', "") + first = int(request.args.get('first', 0)) + maximum = int(request.args.get('maximum', -1)) + return keycloak_client.get_policies(resource, name, scope, first, maximum) + # --------------- GET ----------------- + @policies.route("/policies/", methods=["GET"]) + def get_client_authz_policies(client_id: str): + return keycloak_client.get_client_authz_policies(client_id) + + # --------------- POST ----------------- + + @policies.route("/policies/client", methods=["POST"]) + def create_client_policy(): + policy = request.get_json() + return keycloak_client.register_client_policy(policy) + + + @policies.route("/policies/aggregated", methods = ["POST"]) + def create_aggregated_policy(): + payload = request.get_json() + name = payload["name"] + policies = payload["policies"] + strategy = payload["strategy"] + return keycloak_client.register_aggregated_policy(name, policies, strategy) + + @policies.route("/policies/scope", methods = ["POST"]) + def create_client_scope_policy(): + policy = request.get_json() + return keycloak_client.register_client_scope_policy(policy) + + @policies.route("/policies/group", methods = ["POST"]) + def create_group_policy(): + name = request.get_json()["name"] + groups = request.get_json()["groups"] + groups_claim = request.get_json()["groups_claim"] + return keycloak_client.register_group_policy(name, groups, groups_claim) + + @policies.route("/policies/regex", methods = ["POST"]) + def create_regex_policy(): + payload = request.get_json() + name = payload["name"] + regex = payload["regex"] + target_claim = payload["target_claim"] + return keycloak_client.register_regex_policy(name, regex, target_claim) + + @policies.route("/policies/role", methods = ["POST"]) + def create_role_policy(): + payload = request.get_json() + name = payload["name"] + roles = payload["roles"] + return keycloak_client.register_role_policy(name, roles) + + @policies.route("/policies/time", methods = ["POST"]) + def create_time_policy(): + # time can be one of: + # "notAfter":"1970-01-01 00:00:00" + # "notBefore":"1970-01-01 00:00:00" + # "dayMonth": + # "dayMonthEnd": + # "month": + # "monthEnd": + # "year": + # "yearEnd": + # "hour": + # "hourEnd": + # "minute": + # "minuteEnd": + possible_times = [ + "notAfter", + "notBefore", + "dayMonth", + "dayMonthEnd", + "month", + "monthEnd", + "year", + "yearEnd", + "hour", + "hourEnd", + "minute", + "minuteEnd" + ] + payload = request.get_json() + name = payload["name"] + time = {} + for key, value in payload.items(): + if key in possible_times: + time[key] = value + return keycloak_client.register_time_policy(name, time) + + @policies.route("/policies/user", methods = ["POST"]) + def create_user_policy(): + payload = request.get_json() + name = payload["name"] + users = payload["users"] + return keycloak_client.register_user_policy(name, users) + + + + # --------------- UPDATE ----------------- + + @policies.route("/policies/", methods=["PUT"]) + def update_policy(policy_id: str): + policy = request.get_json() + return keycloak_client.update_policy(policy_id, policy) + + # --------------- DELETE ----------------- + + @policies.route("/policies/", methods=["DELETE"]) + def delete_policy(policy_id: str): + return keycloak_client.delete_policy(policy_id) + + return policies From e8f2581ed1486d897c0859c6d4d88fa0b4f8c599 Mon Sep 17 00:00:00 2001 From: Daniel Pimenta Date: Mon, 21 Aug 2023 11:21:03 +0100 Subject: [PATCH 13/64] Update README --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b2dbc01..9fdadc6 100644 --- a/README.md +++ b/README.md @@ -108,21 +108,21 @@ cd um-identity-api 5.1 Run locally with Python ```sh - pip install -r requirements.txt - python src/app.py + pip install -r requirements.local.txt + python -m "flask" run --host=0.0.0.0 --port=5566 ``` 5.2 Run locally with Docker ```sh docker build . --progress=plain -t um-identity-api:develop - docker run --rm -dp 5566:5566 --name um-identity-api --network eoepcanetwork um-identity-api:develop + docker run --rm -dp 5566:5566 --name um-identity-api um-identity-api:develop ``` 5.3 Run develop branch with Docker ```sh - docker run --rm -dp 5566:5566 --name um-identity-api --network eoepcanetwork ghcr.io/eoepca/um-identity-api:develop + docker run --rm -dp 5566:5566 --name um-identity-api ghcr.io/eoepca/um-identity-api:develop ``` 5.4 Run master branch with Docker ```sh - docker run --rm -dp 5566:5566 --name um-identity-api --network eoepcanetwork ghcr.io/eoepca/um-identity-api:production + docker run --rm -dp 5566:5566 --name um-identity-api ghcr.io/eoepca/um-identity-api:production ``` ## Documentation From 6743f64206ee5e685c829c76b4c86ae7fff09e16 Mon Sep 17 00:00:00 2001 From: Daniel Pimenta Date: Tue, 26 Sep 2023 11:30:00 +0100 Subject: [PATCH 14/64] Update config --- conf/config.demo.ini | 4 ++-- conf/config.develop.ini | 4 ++-- conf/config.ini | 4 ++-- conf/config.production.ini | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/conf/config.demo.ini b/conf/config.demo.ini index 5933101..a5cfcf2 100644 --- a/conf/config.demo.ini +++ b/conf/config.demo.ini @@ -1,10 +1,10 @@ [Keycloak] auth_server_url = https://identity.keycloak.demo.eoepca.org/ admin_username = admin -admin_password = admin_Abcd1234# +admin_password = admin realm = demo resource_server_endpoint = https://dummy-service.develop.eoepca.org [Swagger] swagger_url = /swagger-ui swagger_api_url = /swagger-ui-api -swagger_app_name = Identity API +swagger_app_name = Identity API \ No newline at end of file diff --git a/conf/config.develop.ini b/conf/config.develop.ini index 4d2ddce..93a4b73 100644 --- a/conf/config.develop.ini +++ b/conf/config.develop.ini @@ -1,10 +1,10 @@ [Keycloak] auth_server_url = https://identity.keycloak.develop.eoepca.org/ admin_username = admin -admin_password = admin_Abcd1234# +admin_password = admin realm = demo resource_server_endpoint = https://dummy-service.develop.eoepca.org [Swagger] swagger_url = /swagger-ui swagger_api_url = /swagger-ui-api -swagger_app_name = Identity API +swagger_app_name = Identity API \ No newline at end of file diff --git a/conf/config.ini b/conf/config.ini index 0c7dfc1..29a7f88 100644 --- a/conf/config.ini +++ b/conf/config.ini @@ -1,10 +1,10 @@ [Keycloak] auth_server_url = http://localhost:8080 admin_username = admin -admin_password = admin_Abcd1234# +admin_password = admin realm = demo resource_server_endpoint = https://dummy-service.develop.eoepca.org [Swagger] swagger_url = /swagger-ui swagger_api_url = /swagger-ui-api -swagger_app_name = Identity API +swagger_app_name = Identity API \ No newline at end of file diff --git a/conf/config.production.ini b/conf/config.production.ini index 850eaa1..b62f7b6 100644 --- a/conf/config.production.ini +++ b/conf/config.production.ini @@ -1,10 +1,10 @@ [Keycloak] auth_server_url = https://identity.keycloak.eoepca.org/ admin_username = admin -admin_password = admin_Abcd1234# +admin_password = admin realm = demo resource_server_endpoint = https://dummy-service.develop.eoepca.org [Swagger] swagger_url = /swagger-ui swagger_api_url = /swagger-ui-api -swagger_app_name = Identity API +swagger_app_name = Identity API \ No newline at end of file From f9c6a848ed5604c29d50188f5500b09047d01931 Mon Sep 17 00:00:00 2001 From: Daniel Pimenta Date: Tue, 26 Sep 2023 11:34:07 +0100 Subject: [PATCH 15/64] Update config --- conf/config.demo.ini | 4 ++-- conf/config.develop.ini | 4 ++-- conf/config.ini | 6 +++--- conf/config.production.ini | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/conf/config.demo.ini b/conf/config.demo.ini index a5cfcf2..dd1e4a0 100644 --- a/conf/config.demo.ini +++ b/conf/config.demo.ini @@ -1,8 +1,8 @@ [Keycloak] auth_server_url = https://identity.keycloak.demo.eoepca.org/ admin_username = admin -admin_password = admin -realm = demo +admin_password = REPLACEME +realm = master resource_server_endpoint = https://dummy-service.develop.eoepca.org [Swagger] swagger_url = /swagger-ui diff --git a/conf/config.develop.ini b/conf/config.develop.ini index 93a4b73..14657ee 100644 --- a/conf/config.develop.ini +++ b/conf/config.develop.ini @@ -1,8 +1,8 @@ [Keycloak] auth_server_url = https://identity.keycloak.develop.eoepca.org/ admin_username = admin -admin_password = admin -realm = demo +admin_password = REPLACEME +realm = master resource_server_endpoint = https://dummy-service.develop.eoepca.org [Swagger] swagger_url = /swagger-ui diff --git a/conf/config.ini b/conf/config.ini index 29a7f88..bfa4624 100644 --- a/conf/config.ini +++ b/conf/config.ini @@ -1,8 +1,8 @@ [Keycloak] -auth_server_url = http://localhost:8080 +auth_server_url = http://localhost:8080/ admin_username = admin -admin_password = admin -realm = demo +admin_password = REPLACEME +realm = master resource_server_endpoint = https://dummy-service.develop.eoepca.org [Swagger] swagger_url = /swagger-ui diff --git a/conf/config.production.ini b/conf/config.production.ini index b62f7b6..829d728 100644 --- a/conf/config.production.ini +++ b/conf/config.production.ini @@ -1,8 +1,8 @@ [Keycloak] auth_server_url = https://identity.keycloak.eoepca.org/ admin_username = admin -admin_password = admin -realm = demo +admin_password = REPLACEME +realm = master resource_server_endpoint = https://dummy-service.develop.eoepca.org [Swagger] swagger_url = /swagger-ui From 8468ce0cf494154274d88f060a223e88041fcf47 Mon Sep 17 00:00:00 2001 From: Daniel Pimenta Date: Tue, 26 Sep 2023 11:34:39 +0100 Subject: [PATCH 16/64] Update config --- conf/config.demo.ini | 2 +- conf/config.develop.ini | 2 +- conf/config.ini | 2 +- conf/config.production.ini | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/conf/config.demo.ini b/conf/config.demo.ini index dd1e4a0..26904f7 100644 --- a/conf/config.demo.ini +++ b/conf/config.demo.ini @@ -1,7 +1,7 @@ [Keycloak] auth_server_url = https://identity.keycloak.demo.eoepca.org/ admin_username = admin -admin_password = REPLACEME +admin_password = CHANGE ME realm = master resource_server_endpoint = https://dummy-service.develop.eoepca.org [Swagger] diff --git a/conf/config.develop.ini b/conf/config.develop.ini index 14657ee..bd07365 100644 --- a/conf/config.develop.ini +++ b/conf/config.develop.ini @@ -1,7 +1,7 @@ [Keycloak] auth_server_url = https://identity.keycloak.develop.eoepca.org/ admin_username = admin -admin_password = REPLACEME +admin_password = CHANGE ME realm = master resource_server_endpoint = https://dummy-service.develop.eoepca.org [Swagger] diff --git a/conf/config.ini b/conf/config.ini index bfa4624..e236e2f 100644 --- a/conf/config.ini +++ b/conf/config.ini @@ -1,7 +1,7 @@ [Keycloak] auth_server_url = http://localhost:8080/ admin_username = admin -admin_password = REPLACEME +admin_password = CHANGE ME realm = master resource_server_endpoint = https://dummy-service.develop.eoepca.org [Swagger] diff --git a/conf/config.production.ini b/conf/config.production.ini index 829d728..d32e0ec 100644 --- a/conf/config.production.ini +++ b/conf/config.production.ini @@ -1,7 +1,7 @@ [Keycloak] auth_server_url = https://identity.keycloak.eoepca.org/ admin_username = admin -admin_password = REPLACEME +admin_password = CHANGE ME realm = master resource_server_endpoint = https://dummy-service.develop.eoepca.org [Swagger] From c170db2c8d41f0425124a338a45d78b00a089926 Mon Sep 17 00:00:00 2001 From: flaviorosadme <82375986+flaviorosadme@users.noreply.github.com> Date: Mon, 9 Oct 2023 11:32:33 +0100 Subject: [PATCH 17/64] Api testing (#18) * feat: added client_id as param to enpoints and other fixes * added changes for permissions endpoints --- src/blueprints/permissions.py | 14 ++--- src/blueprints/policies.py | 110 +++++++++++++++------------------- src/blueprints/resources.py | 32 ++++------ 3 files changed, 66 insertions(+), 90 deletions(-) diff --git a/src/blueprints/permissions.py b/src/blueprints/permissions.py index 60cd05c..1f6bc86 100644 --- a/src/blueprints/permissions.py +++ b/src/blueprints/permissions.py @@ -5,15 +5,15 @@ def construct_blueprint(keycloak_client): keycloak_client = keycloak_client permissions = Blueprint('permissions', __name__) - @permissions.route("/permissions/", methods=["GET"]) + @permissions.route("//permissions", methods=["GET"]) def get_client_authz_permissions(client_id: str): return keycloak_client.get_client_authz_permissions(client_id) - @permissions.route("/permissions//management", methods=["GET"]) + @permissions.route("//permissions/management", methods=["GET"]) def get_client_management_permissions(client_id: str): return keycloak_client.get_client_management_permissions(client_id) - @permissions.route("/permissions//resources", methods=["GET"]) + @permissions.route("//permissions/resources", methods=["GET"]) def get_client_resource_permissions(client_id: str): return keycloak_client.get_client_resource_permissions(client_id) @@ -26,22 +26,22 @@ def get_client_resource_permissions(client_id: str): # payload = request.get_json() # return keycloak_client.create_client_authz_scope_based_permission(client_id, payload) - @permissions.route("/permissions//resources", methods=["POST"]) + @permissions.route("//permissions/resources", methods=["POST"]) def create_client_authz_resource_based_permission(client_id: str): payload = request.get_json() return keycloak_client.create_client_authz_resource_based_permission(client_id, payload) - @permissions.route("/permissions//management", methods=["PUT"]) + @permissions.route("//permissions/management", methods=["PUT"]) def update_client_management_permissions(client_id: str): payload = request.get_json() return keycloak_client.update_client_management_permissions(client_id, payload) - @permissions.route("/permissions//resources/", methods=["PUT"]) + @permissions.route("//permissions/resources/", methods=["PUT"]) def update_client_authz_resource_permission(client_id: str, permission_id): payload = request.get_json() return keycloak_client.update_client_authz_resource_permission(client_id, payload, permission_id) - #@permissions.route("/permissions//scopes/", methods=["PUT"]) + #@permissions.route("//permissions/scopes/", methods=["PUT"]) #def update_client_authz_scope_permissions(client_id: str, scope_id): # payload = request.get_json() # return keycloak_client.update_client_authz_scope_permission(client_id, payload, scope_id) diff --git a/src/blueprints/policies.py b/src/blueprints/policies.py index 4fe974b..71d5670 100644 --- a/src/blueprints/policies.py +++ b/src/blueprints/policies.py @@ -5,65 +5,56 @@ def construct_blueprint(keycloak_client): keycloak_client = keycloak_client policies = Blueprint('policies', __name__) - - @policies.route("/policies", methods=["GET"]) - def get_policies(): - resource = request.args.get('resource', "") - name = request.args.get('name', "") - scope = request.args.get('uri', "") - first = int(request.args.get('first', 0)) - maximum = int(request.args.get('maximum', -1)) - return keycloak_client.get_policies(resource, name, scope, first, maximum) + # -------- Always returns empty ------- + #@policies.route("/policies", methods=["GET"]) + #def get_policies(): + # resource = request.args.get('resource', "") + # name = request.args.get('name', "") + # scope = request.args.get('uri', "") + # first = int(request.args.get('first', 0)) + # maximum = int(request.args.get('maximum', -1)) + # return keycloak_client.get_policies(resource, name, scope, first, maximum) # --------------- GET ----------------- - @policies.route("/policies/", methods=["GET"]) + + @policies.route("//policies", methods=["GET"]) def get_client_authz_policies(client_id: str): return keycloak_client.get_client_authz_policies(client_id) # --------------- POST ----------------- - @policies.route("/policies/client", methods=["POST"]) - def create_client_policy(): + @policies.route("//policies/client", methods=["POST"]) + def create_client_policy(client_id: str): policy = request.get_json() - return keycloak_client.register_client_policy(policy) + return keycloak_client.register_client_policy(policy, client_id) - @policies.route("/policies/aggregated", methods = ["POST"]) - def create_aggregated_policy(): - payload = request.get_json() - name = payload["name"] - policies = payload["policies"] - strategy = payload["strategy"] - return keycloak_client.register_aggregated_policy(name, policies, strategy) + @policies.route("//policies/aggregated", methods = ["POST"]) + def create_aggregated_policy(client_id: str): + policy = request.get_json() + return keycloak_client.register_aggregated_policy(policy, client_id) - @policies.route("/policies/scope", methods = ["POST"]) - def create_client_scope_policy(): + @policies.route("//policies/scope", methods = ["POST"]) + def create_client_scope_policy(client_id: str): policy = request.get_json() - return keycloak_client.register_client_scope_policy(policy) + return keycloak_client.register_client_scope_policy(policy, client_id) - @policies.route("/policies/group", methods = ["POST"]) - def create_group_policy(): - name = request.get_json()["name"] - groups = request.get_json()["groups"] - groups_claim = request.get_json()["groups_claim"] - return keycloak_client.register_group_policy(name, groups, groups_claim) + @policies.route("//policies/group", methods = ["POST"]) + def create_group_policy(client_id: str): + policy = request.get_json() + return keycloak_client.register_group_policy(policy, client_id) - @policies.route("/policies/regex", methods = ["POST"]) - def create_regex_policy(): - payload = request.get_json() - name = payload["name"] - regex = payload["regex"] - target_claim = payload["target_claim"] - return keycloak_client.register_regex_policy(name, regex, target_claim) + @policies.route("//policies/regex", methods = ["POST"]) + def create_regex_policy(client_id: str): + policy = request.get_json() + return keycloak_client.register_regex_policy(policy, client_id) - @policies.route("/policies/role", methods = ["POST"]) - def create_role_policy(): - payload = request.get_json() - name = payload["name"] - roles = payload["roles"] - return keycloak_client.register_role_policy(name, roles) + @policies.route("//policies/role", methods = ["POST"]) + def create_role_policy(client_id: str): + policy = request.get_json() + return keycloak_client.register_role_policy(policy, client_id) - @policies.route("/policies/time", methods = ["POST"]) - def create_time_policy(): + @policies.route("//policies/time", methods = ["POST"]) + def create_time_policy(client_id: str): # time can be one of: # "notAfter":"1970-01-01 00:00:00" # "notBefore":"1970-01-01 00:00:00" @@ -91,35 +82,28 @@ def create_time_policy(): "minute", "minuteEnd" ] - payload = request.get_json() - name = payload["name"] - time = {} - for key, value in payload.items(): - if key in possible_times: - time[key] = value - return keycloak_client.register_time_policy(name, time) + policy = request.get_json() + return keycloak_client.register_time_policy(policy, client_id) - @policies.route("/policies/user", methods = ["POST"]) - def create_user_policy(): - payload = request.get_json() - name = payload["name"] - users = payload["users"] - return keycloak_client.register_user_policy(name, users) + @policies.route("//policies/user", methods = ["POST"]) + def create_user_policy(client_id: str): + policy = request.get_json() + return keycloak_client.register_user_policy(policy, client_id) # --------------- UPDATE ----------------- - @policies.route("/policies/", methods=["PUT"]) - def update_policy(policy_id: str): + @policies.route("//policies/", methods=["PUT"]) + def update_policy(client_id: str, policy_id: str): policy = request.get_json() - return keycloak_client.update_policy(policy_id, policy) + return keycloak_client.update_policy(policy_id, policy, client_id) # --------------- DELETE ----------------- - @policies.route("/policies/", methods=["DELETE"]) - def delete_policy(policy_id: str): - return keycloak_client.delete_policy(policy_id) + @policies.route("//policies/", methods=["DELETE"]) + def delete_policy(client_id: str ,policy_id: str): + return keycloak_client.delete_policy(policy_id, client_id) return policies diff --git a/src/blueprints/resources.py b/src/blueprints/resources.py index a35d1a6..680df89 100644 --- a/src/blueprints/resources.py +++ b/src/blueprints/resources.py @@ -5,34 +5,26 @@ def construct_blueprint(keycloak_client): keycloak_client = keycloak_client resources = Blueprint('resources', __name__) - @resources.route("/resources", methods=["GET"]) - def get_resources(): - name = request.args.get('name', "") - exact_name = request.args.get('exact_name', False) - uri = request.args.get('uri', "") - owner = request.args.get('owner', "") - resource_type = request.args.get('resource_type', "") - scope = request.args.get('scope', "") - first = int(request.args.get('first', 0)) - maximum = int(request.args.get('maximum', -1)) - return keycloak_client.get_resources(name, exact_name, uri, owner, resource_type, scope, first, maximum) + @resources.route("//resources", methods=["GET"]) + def get_resources(client_id: str): + return keycloak_client.get_resources(client_id) @resources.route("/resources/", methods=["GET"]) def get_resource(resource_id: str): return keycloak_client.get_resource(resource_id) - @resources.route("/resources", methods=["POST"]) - def register_resource(): + @resources.route("//resources", methods=["POST"]) + def register_resource(client_id: str ): resource = request.get_json() - return keycloak_client.register_resource(resource) + return keycloak_client.register_resource(resource, client_id) - @resources.route("/resources/", methods=["PUT"]) - def update_resource(resource_id: str): + @resources.route("//resources/", methods=["PUT"]) + def update_resource(client_id: str, resource_id: str): resource = request.get_json() - return keycloak_client.update_resource(resource_id, resource) + return keycloak_client.update_resource(resource_id, resource, client_id) - @resources.route("/resources/", methods=["DELETE"]) - def delete_resource(resource_id: str): - return keycloak_client.delete_resource(resource_id) + @resources.route("//resources/", methods=["DELETE"]) + def delete_resource(client_id: str, resource_id: str): + return keycloak_client.delete_resource(resource_id, client_id) return resources From cf2d35ae4cb75bad275fc0b6aee077fbbae0b0de Mon Sep 17 00:00:00 2001 From: Daniel Pimenta Date: Tue, 10 Oct 2023 10:44:23 +0100 Subject: [PATCH 18/64] Update ci --- .github/workflows/docker-publish-develop.yml | 4 +--- .github/workflows/docker-publish-master.yml | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/docker-publish-develop.yml b/.github/workflows/docker-publish-develop.yml index 41e2382..fe5d26e 100644 --- a/.github/workflows/docker-publish-develop.yml +++ b/.github/workflows/docker-publish-develop.yml @@ -6,8 +6,6 @@ name: Docker # documentation. on: - schedule: - - cron: '29 9 * * *' push: branches: [ "develop" ] # Publish semver tags as releases. @@ -95,4 +93,4 @@ jobs: COSIGN_EXPERIMENTAL: "true" # This step uses the identity token to provision an ephemeral certificate # against the sigstore community Fulcio instance. - run: echo "${{ steps.meta.outputs.tags }}" | xargs -I {} cosign sign {}@${{ steps.build-and-push.outputs.digest }} + run: echo "${{ steps.meta.outputs.tags }}" | xargs -I {} cosign sign {}@${{ steps.build-and-push.outputs.digest }} \ No newline at end of file diff --git a/.github/workflows/docker-publish-master.yml b/.github/workflows/docker-publish-master.yml index 30056f1..4c6f2ea 100644 --- a/.github/workflows/docker-publish-master.yml +++ b/.github/workflows/docker-publish-master.yml @@ -6,8 +6,6 @@ name: Docker # documentation. on: - schedule: - - cron: '29 9 * * *' push: branches: [ "master" ] # Publish semver tags as releases. @@ -95,4 +93,4 @@ jobs: COSIGN_EXPERIMENTAL: "true" # This step uses the identity token to provision an ephemeral certificate # against the sigstore community Fulcio instance. - run: echo "${{ steps.meta.outputs.tags }}" | xargs -I {} cosign sign {}@${{ steps.build-and-push.outputs.digest }} + run: echo "${{ steps.meta.outputs.tags }}" | xargs -I {} cosign sign {}@${{ steps.build-and-push.outputs.digest }} \ No newline at end of file From 5eecb1ee05838224b127cd0a8dbefce0f2f045eb Mon Sep 17 00:00:00 2001 From: Daniel Pimenta Date: Tue, 10 Oct 2023 10:47:18 +0100 Subject: [PATCH 19/64] Update ci --- .github/workflows/docker-publish-develop.yml | 22 +++++++++++++++++++- .github/workflows/docker-publish-master.yml | 22 +++++++++++++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker-publish-develop.yml b/.github/workflows/docker-publish-develop.yml index fe5d26e..36434ce 100644 --- a/.github/workflows/docker-publish-develop.yml +++ b/.github/workflows/docker-publish-develop.yml @@ -93,4 +93,24 @@ jobs: COSIGN_EXPERIMENTAL: "true" # This step uses the identity token to provision an ephemeral certificate # against the sigstore community Fulcio instance. - run: echo "${{ steps.meta.outputs.tags }}" | xargs -I {} cosign sign {}@${{ steps.build-and-push.outputs.digest }} \ No newline at end of file + run: echo "${{ steps.meta.outputs.tags }}" | xargs -I {} cosign sign {}@${{ steps.build-and-push.outputs.digest }} + + - name: Log into registry ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Extract metadata (tags, labels) for Docker + id: docker_meta + uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 + with: + images: ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }} + + - name: Build and push Docker image + uses: docker/build-push-action@v2 + with: + context: "{{defaultContext}}" + push: true + tags: ${{ steps.docker_meta.outputs.tags }} + labels: ${{ steps.docker_meta.outputs.labels }} \ No newline at end of file diff --git a/.github/workflows/docker-publish-master.yml b/.github/workflows/docker-publish-master.yml index 4c6f2ea..29c9e10 100644 --- a/.github/workflows/docker-publish-master.yml +++ b/.github/workflows/docker-publish-master.yml @@ -93,4 +93,24 @@ jobs: COSIGN_EXPERIMENTAL: "true" # This step uses the identity token to provision an ephemeral certificate # against the sigstore community Fulcio instance. - run: echo "${{ steps.meta.outputs.tags }}" | xargs -I {} cosign sign {}@${{ steps.build-and-push.outputs.digest }} \ No newline at end of file + run: echo "${{ steps.meta.outputs.tags }}" | xargs -I {} cosign sign {}@${{ steps.build-and-push.outputs.digest }} + + - name: Log into registry ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Extract metadata (tags, labels) for Docker + id: docker_meta + uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 + with: + images: ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }} + + - name: Build and push Docker image + uses: docker/build-push-action@v2 + with: + context: "{{defaultContext}}" + push: true + tags: ${{ steps.docker_meta.outputs.tags }} + labels: ${{ steps.docker_meta.outputs.labels }} \ No newline at end of file From b6a0828c007a404fbe609d9362a51ac06ad33960 Mon Sep 17 00:00:00 2001 From: Daniel Pimenta Date: Sun, 15 Oct 2023 21:34:46 +0100 Subject: [PATCH 20/64] Release v1.0.0 --- .github/workflows/docker-publish-develop.yml | 26 +++++++++---------- .github/workflows/docker-publish-master.yml | 26 +++++++++---------- Dockerfile.demo | 11 -------- Dockerfile.develop | 11 -------- Dockerfile.production | 11 -------- conf/config.demo.ini | 10 ------- conf/config.develop.ini | 10 ------- conf/config.ini | 1 - conf/config.production.ini | 10 ------- images/logo.png | Bin 2312 -> 0 bytes images/screenshot.png | Bin 6347 -> 0 bytes requirements.local.txt | 15 ----------- requirements.production.txt | 15 ----------- requirements.txt | 2 +- src/app.py | 4 +-- 15 files changed, 29 insertions(+), 123 deletions(-) delete mode 100644 Dockerfile.demo delete mode 100644 Dockerfile.develop delete mode 100644 Dockerfile.production delete mode 100644 conf/config.demo.ini delete mode 100644 conf/config.develop.ini delete mode 100644 conf/config.production.ini delete mode 100644 images/logo.png delete mode 100644 images/screenshot.png delete mode 100644 requirements.local.txt delete mode 100644 requirements.production.txt diff --git a/.github/workflows/docker-publish-develop.yml b/.github/workflows/docker-publish-develop.yml index 36434ce..fd58d66 100644 --- a/.github/workflows/docker-publish-develop.yml +++ b/.github/workflows/docker-publish-develop.yml @@ -101,16 +101,16 @@ jobs: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - - name: Extract metadata (tags, labels) for Docker - id: docker_meta - uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 - with: - images: ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }} - - - name: Build and push Docker image - uses: docker/build-push-action@v2 - with: - context: "{{defaultContext}}" - push: true - tags: ${{ steps.docker_meta.outputs.tags }} - labels: ${{ steps.docker_meta.outputs.labels }} \ No newline at end of file +# - name: Extract metadata (tags, labels) for Docker +# id: docker_meta +# uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 +# with: +# images: ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }} +# +# - name: Build and push Docker image +# uses: docker/build-push-action@v2 +# with: +# context: "{{defaultContext}}" +# push: true +# tags: ${{ steps.docker_meta.outputs.tags }} +# labels: ${{ steps.docker_meta.outputs.labels }} \ No newline at end of file diff --git a/.github/workflows/docker-publish-master.yml b/.github/workflows/docker-publish-master.yml index 29c9e10..3062134 100644 --- a/.github/workflows/docker-publish-master.yml +++ b/.github/workflows/docker-publish-master.yml @@ -101,16 +101,16 @@ jobs: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - - name: Extract metadata (tags, labels) for Docker - id: docker_meta - uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 - with: - images: ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }} - - - name: Build and push Docker image - uses: docker/build-push-action@v2 - with: - context: "{{defaultContext}}" - push: true - tags: ${{ steps.docker_meta.outputs.tags }} - labels: ${{ steps.docker_meta.outputs.labels }} \ No newline at end of file +# - name: Extract metadata (tags, labels) for Docker +# id: docker_meta +# uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 +# with: +# images: ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }} +# +# - name: Build and push Docker image +# uses: docker/build-push-action@v2 +# with: +# context: "{{defaultContext}}" +# push: true +# tags: ${{ steps.docker_meta.outputs.tags }} +# labels: ${{ steps.docker_meta.outputs.labels }} \ No newline at end of file diff --git a/Dockerfile.demo b/Dockerfile.demo deleted file mode 100644 index 79c6a5c..0000000 --- a/Dockerfile.demo +++ /dev/null @@ -1,11 +0,0 @@ -FROM python:alpine -RUN apk add --no-cache git -RUN mkdir /app -WORKDIR /app -COPY . . -ENV FLASK_APP "src/app:create_app()" -ENV FLASK_ENV demo -ENV FLASK_DEBUG 1 -RUN pip install -r requirements.txt -EXPOSE 5566 -CMD [ "python", "-m" , "flask", "run", "--host=0.0.0.0", "--port=5566"] diff --git a/Dockerfile.develop b/Dockerfile.develop deleted file mode 100644 index 3e548ba..0000000 --- a/Dockerfile.develop +++ /dev/null @@ -1,11 +0,0 @@ -FROM python:alpine -RUN apk add --no-cache git -RUN mkdir /app -WORKDIR /app -COPY . . -ENV FLASK_APP "src/app:create_app()" -ENV FLASK_ENV develop -ENV FLASK_DEBUG 1 -RUN pip install -r requirements.txt -EXPOSE 5566 -CMD [ "python", "-m" , "flask", "run", "--host=0.0.0.0", "--port=5566"] diff --git a/Dockerfile.production b/Dockerfile.production deleted file mode 100644 index f13e03b..0000000 --- a/Dockerfile.production +++ /dev/null @@ -1,11 +0,0 @@ -FROM python:alpine -RUN apk add --no-cache git -RUN mkdir /app -WORKDIR /app -COPY . . -ENV FLASK_APP "src/app:create_app()" -ENV FLASK_ENV production -ENV FLASK_DEBUG 0 -RUN pip install -r requirements.production.txt -EXPOSE 5566 -CMD [ "python", "-m" , "flask", "run", "--host=0.0.0.0", "--port=5566"] diff --git a/conf/config.demo.ini b/conf/config.demo.ini deleted file mode 100644 index 26904f7..0000000 --- a/conf/config.demo.ini +++ /dev/null @@ -1,10 +0,0 @@ -[Keycloak] -auth_server_url = https://identity.keycloak.demo.eoepca.org/ -admin_username = admin -admin_password = CHANGE ME -realm = master -resource_server_endpoint = https://dummy-service.develop.eoepca.org -[Swagger] -swagger_url = /swagger-ui -swagger_api_url = /swagger-ui-api -swagger_app_name = Identity API \ No newline at end of file diff --git a/conf/config.develop.ini b/conf/config.develop.ini deleted file mode 100644 index bd07365..0000000 --- a/conf/config.develop.ini +++ /dev/null @@ -1,10 +0,0 @@ -[Keycloak] -auth_server_url = https://identity.keycloak.develop.eoepca.org/ -admin_username = admin -admin_password = CHANGE ME -realm = master -resource_server_endpoint = https://dummy-service.develop.eoepca.org -[Swagger] -swagger_url = /swagger-ui -swagger_api_url = /swagger-ui-api -swagger_app_name = Identity API \ No newline at end of file diff --git a/conf/config.ini b/conf/config.ini index e236e2f..cbb9eea 100644 --- a/conf/config.ini +++ b/conf/config.ini @@ -3,7 +3,6 @@ auth_server_url = http://localhost:8080/ admin_username = admin admin_password = CHANGE ME realm = master -resource_server_endpoint = https://dummy-service.develop.eoepca.org [Swagger] swagger_url = /swagger-ui swagger_api_url = /swagger-ui-api diff --git a/conf/config.production.ini b/conf/config.production.ini deleted file mode 100644 index d32e0ec..0000000 --- a/conf/config.production.ini +++ /dev/null @@ -1,10 +0,0 @@ -[Keycloak] -auth_server_url = https://identity.keycloak.eoepca.org/ -admin_username = admin -admin_password = CHANGE ME -realm = master -resource_server_endpoint = https://dummy-service.develop.eoepca.org -[Swagger] -swagger_url = /swagger-ui -swagger_api_url = /swagger-ui-api -swagger_app_name = Identity API \ No newline at end of file diff --git a/images/logo.png b/images/logo.png deleted file mode 100644 index 0f38ba9369899ff527b5b81b77c7518185f8a18a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2312 zcmV+j3HSDiP)CLJYzcIwAce4upp!L< zZqZCM{$n=U!4S<136S8A#Hbe|VuFcr3W-jnE3gfRQ6R!_Dt8NPQA|d4?bgKzjHP8k zfo<(r_M^u?t~Bkvx9>gg%{lKm_rA|3Ie+xt^FHT!pZnx|&yTm~Ra$G}a>c1%ECAL3 ztAQmz7H9z)V;f5kCxPMrm%jr~0s97;zdG*IDHSIHt{D5}HNbtqDxkrYQz_yY@MGZT z10VRxX&(+4B|yjM=DENoU>%@bJC#C?0Cx?}y#04cH(W6RI!1rh20R0_CE`(f(ZKhC zdk1HJIleAeNn`?aoP2m0unV{_DTf3e1=bJFx-F3;XqxVe%TGL%4fv*D}HN30^xE^@Pm0wfF088KQn*+QBWDKr73Vd>CUe94i zzSSj5-|lOmu$uZU!%H3*n%DCUSN=@xUbN~&s|;FUP;QqWKR!eAddAV}+i11dFwRbA zVnzC*vWD8!KQzC4r%T7`nW6dJ;v=TtPMFPCyKi(n%9tk1dG7IoY0Pz+H z@QVL<7jFj(2@*oQol3xx*PrSFx~v*CBme3pH{~l;a^>Hvm)w*ys#yOLpt=BGkC^8x zS8?WF&a7Hs31E9v8p0?y3g729BJ5@0t*3paEDE35RT6s=6! zbVq0jpe;{JV%W>S(e~+FrAikc{$mb{32>xsRUWg%bAs)7S3@X0fFFCl3E+NeYb7v* zQIky!@8{w}yK-f&{4a%LXO9B=-)R5T*=k$}MFPCo^G%HVskC8re9@slJqPRnet)$6 zM&t8`i~l>=yqFNWJ>SH+pXf3Nt-gd-zeTJ4iw^$zGo~)c7{DiP?&m7t4~qu>vK6@P zXvZfakC~g&189_SF(Gt&zKL@`Rg|?FwR_><3m=N|p-2K^6tg=F+n#UY+)s67IhyAe z4!kgPIu~RN;1hdefT(g6T0Oq-K)&pMf`>8-=*k}0ovU0;bye?8DcJ+NyKwMK;P90z zb0+l?l&a4G8-WLlzX)XzxDuO7yryW+&jcphQ++r4&pmy^AGzV=0F_KpS8a7(wo3<3@=*^Oe8=s3qiJv zPybA@WpCu_kL}Q?XASKwnS5<+?FQP$w|_R<*X#sL_5h|FJu4DWFYRm9Z&r2;snjG+{H| z)!LFHV!UEA-T_L0P=U}SKze|r39JW@F+j3}HwF+1NS5FvKoTHXLQ4SKD`OY!du1{4 z_N(iH|6G4{-lU%8mB6a5bG;IH3;vFY^NHbhWMDl&>UINtV|O>ODpI`}QYWNSJH}CQATgK-mrSN@zWRBtXK1mH=scbEt1@ z#;00aDSvmcZvy-0P#Fa#OaP-mkpPc~1f-teBtT{Xi4xu{z=|sqjIC+SVS09c3JLFk z2IqH)BmojdW&!CY1PLhn zK3O3>hy?gVBp}^{AOSK9@QVLgK+(&i)d|CQ(r2f!C&WRkw@l(a(j=X4_bsl{lmotp*HZy#lR%kl8j?k_4G; zW3$kz7p>+S+WPC{>y<8gl%#3vsa?R0hPUt@a6hmU=s&l%+4Sv|0?@Se)W?B6F)rBy z`~=tw%m!`%zJvKY8O{Ln&#i49egA{f1bF<^*MJ9OI3?5$&z@V?{MpGLl+hK{2kMxi zo=B;!@Y&5A3MDfc&OwHp_X~>{LL4Xyk5SkTurpv3~T_FId)1&hU37U<-RD} z0{}EWdL|3J49KESZKHwDpYNXWbh$65O#py~zB9{!UBHEId=ng_fxE_gW^9_SLSzB} zG;BHB20R1EoWHg>4fKq!&+Le*R51Yn8n&F93v2?`Q5a4NVY3(5IKIB=AC)Q^CBQ`G z-t*T0_W`Srhe<+YKk!qaPv6pL^uxwVfQgG98D9Xb0agP`C=3)^fX3Lyglaht{0kTa i`hh3)tqlWljr}A7KtO4N(v*%;FX4g#B=n*b=^!19LO?pwBE?7%DN>{5gYCegM%(vSE(X0X_U4V6&gd|uA+Jsb>=Rj;|$DQ5gp+rJ_nuP(#?5jmn+v{#(3A$MHurBciZ=;}e^;SkEUim9%kpKl9LpmY z^(>>+F3Kcej>enKybH=Rp2|D(h;@zSo*r{@!sTgmo+2FCZ$p3I!55U;F@sEQ6yRV`3Kwj2;8}M0SMc8B)YRVC+*9K#KL&`^351U zYsO~#Jh2_;8uR;R5#*$Bit%vV}KCdf;$wT8_P&&WXwOcFkMsHYtR=SaNm(Up(Ts`!0*w~y-BO* zO=4jQjZ+@J*^k^lKfUJ{k?s9`*a8H}P@7*rG&p-?GyJ?lW{5|RO&)ZoNIaJ91$bsi z9+^!KI^&^OL2~dJ5*#2ykEx>^h-i6tB(gA3{=s3IHz95N*PhFlqI=G)mopQZv1eX@ zKPF7d$U+(-Mo(r0Vpa#kk#uF(XCc5#|vGs&fPp!BySq8a?>=^QYmN0k%m1f zf+qjv1KYDH=%ImN{V2zjwN$OXkR8MlS}6lw+#IU%8szdVM0pOQ)i~Q zrW4E8bj0~Oea4{7n8GP;AHP$f7zkQ!NdNpJ+?jO6Md?5H!4X}I49%9SXOD-duVEHLExC7?B-e7%{JqtKj#V@e1%<_e}NTSd90| z@HAc;TpI6fS`gpR-`H9j`-IwjvyrfIcY}h)n&d7C`h8I7HCRQ~8*XgLxZCg}`x)D( z4*kwjceEw9AXmhG6AQA*hM#dM>&qRQNhnB!7s5kvB$DSOt;)gWSIQ^4H@i*eewnKh zX=l!4Ub-xM`IZ@*iMt4$$#`Y937LsY>Ehen+b(w9Rfy8evd3x`IcY5)(HcA%7pjKm-{_R>D z1OG4l`3CiUXPs!B-g@MG4~xpwD(e(aWaVLD&a1XjRM3~?6 zA!`IRfNH@USNhCa%?BAM!GT~OGWk=zRAv`8?8NwF1giPA;g3w!gVbFGc%`H_ZZD*X z4V9FYP;`1FQz!eNv>vvFLBg>r)oM@GqLovX(U0pm@>p9QFGjbjqSPR&SoEq%seg-a zw4t_MS4N_&u!qqt%e-nKuYqSK9VU*;9WtI4%A=pF>ADmhp&ruX{6SXxG@XwtQJd*2 zU1;kY_s+}Arbm#X!q&q&lu^x=5e6F9H4-)YEFg$q2o7aF?k?*Vn${nuo`&~7>K~M{ zFtZpD%%l+wA4)*SXC}y}*u?5R6)zh>Z2mSN8K4kk$M^J|M7e9zF2Yi+4!=x&jy zg+jyoyG?PA!yXH=2MdW^77+EV7qm4n&T^l6_@T7>R$5xrrO0iSCiCU0euRq4EMjHC zvD5|s_D0L{^j+S3-b7wbP1C%g2JyZ8#kBAhs;_&UMV+UY8}P}^KtID^}6*^ zOHl2I@B&i{Q?XCmZw+Q}Q`1^hiCVQ&?ng|aNAcYF1%HK`{Du?gdvD-lFDqQXHoQAn zK7diA(^d2f4JGwAD6~U;t9o9e>u=RItkciw4raZiouaLukZ5qZbJJM_zVDmj<2$3? z9+%0NmY<)asn(e~kf`Of~B>_u9njxy3Y4CfA!0w_=!B>WT>dsvKJ2O(=zoD_uAX|hNYdwQZ*v5 zz8+gxyu$zb`N-~@y{{C`g|1T@=SsJN5@w;58N!DC~+#=P~w& z>o#?(f;;fputnv*+?^SmAhv)VYjl-8ICCu^sAjo*cVylyJKY~_O`@0Jfr*m4X)Z@>BQ^&*}Xe}6))?s*FK5P6`xsfV{4;>X;eW9emP zJ|rVa`M%ExQIR9|l3aG``Z@q`pAP_#VF0j8bRic2z)K7O7Oeq5HU$7!+|q0Gt`aB6 zZMD>tiC5y-fz<{RFH~;orXB!bp?drSF+PP6+fsnSQ$t6EVw#4UmLGzrI;1CdvXdIh z*KYcbtc=~bdmeS6Z3@4rJB}!JMPg&4PZWIR?P8S5iQ43UvW)*XF zzhi;zopKk>ip?Ae)r8T7>#GL{2=X;2T~ryfgnQrDT~>}W8Xi+fQpI|dLOWTpu8z4o z%RTK>#|Zx;N8=)6vdzV+s;Xi>KmHDEyhY_#5Fqo?JH9S$24Ze*E(e~OZf$G(Kms7C zXPPb*L&TNLlC9_KFUkWzwe>BX^-ey-umfdm)ddWZ5k&5z*V z{?9LIEFwfvL(-YIGs=Quy<;+|*;quOpJl(DpX*!Z)o4hZ?=@e&a=X&Z)XEBLEmmSM zuWM+?As>+P`L;bP@m0kB0g3Ob>EPi8s$$EVQ&es^K)hCAml%|=5w#!fey~cwU02v$ z45_S6%Gg?wgw2HR3kP>%IFH^Xd*AAOyw3$;c?&g0TQG1^>n1s%tuUGXgx~TCg-Aev zNvu{Nwb4@4SP|xEymVVVMxNF*_^AHblFRx1Et7ytYR?~9$ZcDG+3HnEPx|Iag8SZpJ4v&P$?@hRysFYNAg)gC-{wjka*|yg@Tg*}h3m^e zx!PIwXLGN8TGiedufykA+L8jtg#EZ(XN?9iTf6sn{e-L5>#r&h-g-KPF-AoZRXJxY z6-Yv?wWWMlJJX~uqobZuqMvU|eLUDB(1HPzf;Vs8v|1L|7UzQM znxavTh?f630O+BrvL-G+j(;M^PtVC9$7>oT`=!CwO!w<=eJs<~V|)lw%<1>aC@hrV zctQ(9iV*eS#;uZriTw?a7u~=+SDL{q&7NS{=ji~Q?Vz27AvKH|Qbflz>ESVWd#}1s zEdxIm3c!S!E{o)!L-GR~Ddq2(o-nu+iP4WeCk+tzch3oflDOHxvFFc?9}FB%oizRi zPEJ7nV>TzT|IFqD_8)8hChR}*`)5kwkC1IwE22e zsvGCC0gy>qnMfaEFP@}Fg$U^?iHaU8_-c3!IwR`Sq{o`X8qGiI?cc`GU)AUj8h)Mu ze_m#P14AbTalFrySMRceRz>=~Tqq|kNJ%Lh-ESH?P`aJ&;h*Q(9_Ld;YS~CUV+G5w zfUc5&|7EiNQL87@{om#N@0Zt~llMQIv_Hl4N8@iG@1(qbisYy9SJd+V1OO+Q_-B;= ze~o~Ke(8`SE6bt(elYJ87HaA-l0T>>Cw2y%RTzvb@pM~#`Y+0s4UY!irO10OgMmp| zBV^IX+89e>8{T#*Hj(u-$|rvG-aZ&`doW7ujyNe}qu99>Zmcg|l8YgJu&j>)>yZBfr)3Dk diff --git a/requirements.local.txt b/requirements.local.txt deleted file mode 100644 index c888228..0000000 --- a/requirements.local.txt +++ /dev/null @@ -1,15 +0,0 @@ -Flask==2.2.2 -WellKnownHandler==0.2.0 -requests==2.25.1 -flask-swagger-ui==4.11.1 -python-keycloak==3.2.0 -mock==5.0.2 -pyyaml==5.4.1 -elasticsearch==8.8.0 -lxml==4.9.2 -configparser==5.3.0 -waitress==2.1.2 -python-dotenv==1.0.0 -retry==0.9.2 -flask-healthz==0.0.3 -file:../um-identity-service diff --git a/requirements.production.txt b/requirements.production.txt deleted file mode 100644 index c49e133..0000000 --- a/requirements.production.txt +++ /dev/null @@ -1,15 +0,0 @@ -Flask==2.2.2 -WellKnownHandler==0.2.0 -requests==2.25.1 -flask-swagger-ui==4.11.1 -python-keycloak==3.2.0 -mock==5.0.2 -pyyaml==5.4.1 -elasticsearch==8.8.0 -lxml==4.9.2 -configparser==5.3.0 -waitress==2.1.2 -python-dotenv==1.0.0 -retry==0.9.2 -flask-healthz==0.0.3 -identityutils @ git+https://github.com/eoepca/um-identity-service@master diff --git a/requirements.txt b/requirements.txt index d0f716d..b4c7fcd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,4 +12,4 @@ waitress==2.1.2 python-dotenv==1.0.0 retry==0.9.2 flask-healthz==0.0.3 -identityutils @ git+https://github.com/eoepca/um-identity-service@develop +identityutils @ git+https://github.com/eoepca/um-identity-service@master \ No newline at end of file diff --git a/src/app.py b/src/app.py index a4333fb..484bf89 100644 --- a/src/app.py +++ b/src/app.py @@ -60,12 +60,12 @@ def register_endpoints(config, keycloak): def keycloak_client(config): + logger.info("config: " + str(config)) auth_server_url = config.get("Keycloak", "auth_server_url") realm = config.get("Keycloak", "realm") logger.info("Starting Keycloak client for: " + str(auth_server_url) + " realm: " + str(realm)) return KeycloakClient(server_url=auth_server_url, realm=realm, - resource_server_endpoint=config.get("Keycloak", "resource_server_endpoint"), username=config.get("Keycloak", "admin_username"), password=config.get("Keycloak", "admin_password") ) @@ -77,4 +77,4 @@ def create_app(): keycloak = retry_call(keycloak_client, fargs=[config], exceptions=(KeycloakConnectionError, NewConnectionError), delay=0.5, backoff=1.2, jitter=(1, 2), logger=logger) register_endpoints(config, keycloak) - return app + return app \ No newline at end of file From 197b1c0d2bce5d5a7da57ebe2bae49f6c70fc44b Mon Sep 17 00:00:00 2001 From: Daniel Pimenta Date: Sun, 15 Oct 2023 21:56:30 +0100 Subject: [PATCH 21/64] Fix ci --- .github/workflows/docker-publish-develop.yml | 1 - .github/workflows/docker-publish-master.yml | 1 - 2 files changed, 2 deletions(-) diff --git a/.github/workflows/docker-publish-develop.yml b/.github/workflows/docker-publish-develop.yml index fd58d66..2c011ce 100644 --- a/.github/workflows/docker-publish-develop.yml +++ b/.github/workflows/docker-publish-develop.yml @@ -74,7 +74,6 @@ jobs: uses: docker/build-push-action@ac9327eae2b366085ac7f6a2d02df8aa8ead720a with: context: . - file: ./Dockerfile.develop push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/docker-publish-master.yml b/.github/workflows/docker-publish-master.yml index 3062134..31becf9 100644 --- a/.github/workflows/docker-publish-master.yml +++ b/.github/workflows/docker-publish-master.yml @@ -74,7 +74,6 @@ jobs: uses: docker/build-push-action@ac9327eae2b366085ac7f6a2d02df8aa8ead720a with: context: . - file: ./Dockerfile.production push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} From 85b8f271609ea5b885f5b8535a2ef17727f77a8b Mon Sep 17 00:00:00 2001 From: Daniel Pimenta Date: Sun, 15 Oct 2023 22:00:40 +0100 Subject: [PATCH 22/64] Fix requirements --- Dockerfile | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 97711b4..bde3033 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,4 +8,4 @@ ENV FLASK_ENV local ENV FLASK_DEBUG 1 RUN pip install -r requirements.txt EXPOSE 5566 -CMD [ "python", "-m" , "flask", "run", "--host=0.0.0.0", "--port=5566"] +CMD [ "python", "-m" , "flask", "run", "--host=0.0.0.0", "--port=5566"] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index b4c7fcd..a5d18fa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ python-keycloak==3.2.0 mock==5.0.2 pyyaml==5.3.1 elasticsearch==8.8.0 -lxml==4.9.2 +lxml==4.9.3 configparser==5.3.0 waitress==2.1.2 python-dotenv==1.0.0 From 5aace574fe85eaaf7ef02dd78c5eada6e3b4c2ed Mon Sep 17 00:00:00 2001 From: Daniel Pimenta Date: Sun, 15 Oct 2023 22:04:38 +0100 Subject: [PATCH 23/64] Fix ci --- .github/workflows/docker-publish-develop.yml | 38 ++++++++++---------- .github/workflows/docker-publish-master.yml | 36 +++++++++---------- 2 files changed, 37 insertions(+), 37 deletions(-) diff --git a/.github/workflows/docker-publish-develop.yml b/.github/workflows/docker-publish-develop.yml index 2c011ce..d167db5 100644 --- a/.github/workflows/docker-publish-develop.yml +++ b/.github/workflows/docker-publish-develop.yml @@ -81,25 +81,25 @@ jobs: cache-to: type=gha,mode=max - # Sign the resulting Docker image digest except on PRs. - # This will only write to the public Rekor transparency log when the Docker - # repository is public to avoid leaking data. If you would like to publish - # transparency data even for private images, pass --force to cosign below. - # https://github.com/sigstore/cosign - - name: Sign the published Docker image - if: ${{ github.event_name != 'pull_request' }} - env: - COSIGN_EXPERIMENTAL: "true" - # This step uses the identity token to provision an ephemeral certificate - # against the sigstore community Fulcio instance. - run: echo "${{ steps.meta.outputs.tags }}" | xargs -I {} cosign sign {}@${{ steps.build-and-push.outputs.digest }} - - - name: Log into registry ${{ env.DOCKER_REGISTRY }} - uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - +# # Sign the resulting Docker image digest except on PRs. +# # This will only write to the public Rekor transparency log when the Docker +# # repository is public to avoid leaking data. If you would like to publish +# # transparency data even for private images, pass --force to cosign below. +# # https://github.com/sigstore/cosign +# - name: Sign the published Docker image +# if: ${{ github.event_name != 'pull_request' }} +# env: +# COSIGN_EXPERIMENTAL: "true" +# # This step uses the identity token to provision an ephemeral certificate +# # against the sigstore community Fulcio instance. +# run: echo "${{ steps.meta.outputs.tags }}" | xargs -I {} cosign sign {}@${{ steps.build-and-push.outputs.digest }} +# +# - name: Log into registry ${{ env.DOCKER_REGISTRY }} +# uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 +# with: +# username: ${{ secrets.DOCKER_USERNAME }} +# password: ${{ secrets.DOCKER_PASSWORD }} +# # - name: Extract metadata (tags, labels) for Docker # id: docker_meta # uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 diff --git a/.github/workflows/docker-publish-master.yml b/.github/workflows/docker-publish-master.yml index 31becf9..1be0e89 100644 --- a/.github/workflows/docker-publish-master.yml +++ b/.github/workflows/docker-publish-master.yml @@ -81,24 +81,24 @@ jobs: cache-to: type=gha,mode=max - # Sign the resulting Docker image digest except on PRs. - # This will only write to the public Rekor transparency log when the Docker - # repository is public to avoid leaking data. If you would like to publish - # transparency data even for private images, pass --force to cosign below. - # https://github.com/sigstore/cosign - - name: Sign the published Docker image - if: ${{ github.event_name != 'pull_request' }} - env: - COSIGN_EXPERIMENTAL: "true" - # This step uses the identity token to provision an ephemeral certificate - # against the sigstore community Fulcio instance. - run: echo "${{ steps.meta.outputs.tags }}" | xargs -I {} cosign sign {}@${{ steps.build-and-push.outputs.digest }} - - - name: Log into registry ${{ env.DOCKER_REGISTRY }} - uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} +# # Sign the resulting Docker image digest except on PRs. +# # This will only write to the public Rekor transparency log when the Docker +# # repository is public to avoid leaking data. If you would like to publish +# # transparency data even for private images, pass --force to cosign below. +# # https://github.com/sigstore/cosign +# - name: Sign the published Docker image +# if: ${{ github.event_name != 'pull_request' }} +# env: +# COSIGN_EXPERIMENTAL: "true" +# # This step uses the identity token to provision an ephemeral certificate +# # against the sigstore community Fulcio instance. +# run: echo "${{ steps.meta.outputs.tags }}" | xargs -I {} cosign sign {}@${{ steps.build-and-push.outputs.digest }} +# +# - name: Log into registry ${{ env.DOCKER_REGISTRY }} +# uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 +# with: +# username: ${{ secrets.DOCKER_USERNAME }} +# password: ${{ secrets.DOCKER_PASSWORD }} # - name: Extract metadata (tags, labels) for Docker # id: docker_meta From e5559d5ee59da87ca7ca90a6916349f8503fa0dc Mon Sep 17 00:00:00 2001 From: Daniel Pimenta Date: Sun, 15 Oct 2023 22:26:58 +0100 Subject: [PATCH 24/64] Upgrade flask version --- requirements.txt | 2 +- src/app.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index a5d18fa..fb5a4fd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -Flask==2.2.2 +Flask==3.0.0 WellKnownHandler==0.2.0 requests==2.25.1 flask-swagger-ui==4.11.1 diff --git a/src/app.py b/src/app.py index 484bf89..b96c9f2 100644 --- a/src/app.py +++ b/src/app.py @@ -18,7 +18,7 @@ from identityutils.configuration import load_configuration from identityutils.keycloak_client import KeycloakClient from retry.api import retry_call -from flask_healthz import healthz, HealthError +from flask_healthz import healthz logger.Logger.get_instance().load_configuration(os.path.join(os.path.dirname(__file__), "../conf/logging.yaml")) logger = logging.getLogger("IDENTITY_API") From df2ec57ca1b5a5f6efdbdbdc5ca5b7a2852977d2 Mon Sep 17 00:00:00 2001 From: Daniel Pimenta Date: Sun, 15 Oct 2023 23:17:56 +0100 Subject: [PATCH 25/64] Update requirements --- README.md | 6 +++--- requirements.txt | 12 ++++++------ src/app.py | 13 +------------ 3 files changed, 10 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 9fdadc6..f8cc1f5 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ vagrant ssh 3. Clone the repo ```sh -git clone https://github.com/EOEPCA/um-identity-apigit +git clone https://github.com/EOEPCA/um-identity-api ``` 4. Change local directory @@ -108,7 +108,7 @@ cd um-identity-api 5.1 Run locally with Python ```sh - pip install -r requirements.local.txt + pip install -r requirements.txt python -m "flask" run --host=0.0.0.0 --port=5566 ``` 5.2 Run locally with Docker @@ -182,4 +182,4 @@ Project Link: [https://github.com/EOEPCA/um-identity-api](https://github.com/EOE [issues-url]: https://github.com/EOEPCA/um-identity-api/issues [license-shield]: https://img.shields.io/github/license/EOEPCA/um-identity-apisvg?style=flat-square [license-url]: https://github.com/EOEPCA/um-identity-api/blob/master/LICENSE -[build-shield]: https://www.travis-ci.com/EOEPCA/um-identity-apisvg?branch=master +[build-shield]: https://www.travis-ci.com/EOEPCA/um-identity-apisvg?branch=master \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index fb5a4fd..464652f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,15 +1,15 @@ Flask==3.0.0 WellKnownHandler==0.2.0 -requests==2.25.1 +requests==2.31.0 flask-swagger-ui==4.11.1 -python-keycloak==3.2.0 -mock==5.0.2 -pyyaml==5.3.1 +python-keycloak==3.3.0 +mock==5.1.0 +pyyaml==6.0.1 elasticsearch==8.8.0 lxml==4.9.3 -configparser==5.3.0 +configparser==6.0.0 waitress==2.1.2 python-dotenv==1.0.0 retry==0.9.2 -flask-healthz==0.0.3 +flask-healthz==1.0.0 identityutils @ git+https://github.com/eoepca/um-identity-service@master \ No newline at end of file diff --git a/src/app.py b/src/app.py index b96c9f2..5978c57 100644 --- a/src/app.py +++ b/src/app.py @@ -23,17 +23,7 @@ logger.Logger.get_instance().load_configuration(os.path.join(os.path.dirname(__file__), "../conf/logging.yaml")) logger = logging.getLogger("IDENTITY_API") -mode = os.environ.get('FLASK_ENV') -logger.info("Starting app in mode: " + str(mode)) -if mode == 'develop': - config_file = "config.develop.ini" -elif mode == 'demo': - config_file = "config.demo.ini" -elif mode == 'production': - config_file = "config.production.ini" -else: - config_file = "config.ini" -config_path = os.path.join(os.path.dirname(__file__), "../conf/", config_file) +config_path = os.path.join(os.path.dirname(__file__), "../conf/config.ini") app = Flask(__name__) app.secret_key = ''.join(choice(ascii_lowercase) for _ in range(30)) # Random key @@ -60,7 +50,6 @@ def register_endpoints(config, keycloak): def keycloak_client(config): - logger.info("config: " + str(config)) auth_server_url = config.get("Keycloak", "auth_server_url") realm = config.get("Keycloak", "realm") logger.info("Starting Keycloak client for: " + str(auth_server_url) + " realm: " + str(realm)) From c19db101e1878b9dec90a1b3cba24e0052d854b5 Mon Sep 17 00:00:00 2001 From: flaviorosadme <82375986+flaviorosadme@users.noreply.github.com> Date: Mon, 16 Oct 2023 17:59:54 +0100 Subject: [PATCH 26/64] feat: added error handling (#23) --- src/blueprints/permissions.py | 52 +++++++++++++++++--- src/blueprints/policies.py | 93 ++++++++++++++++++++++++++++++----- src/blueprints/resources.py | 44 +++++++++++++++-- 3 files changed, 166 insertions(+), 23 deletions(-) diff --git a/src/blueprints/permissions.py b/src/blueprints/permissions.py index 1f6bc86..4f0b27e 100644 --- a/src/blueprints/permissions.py +++ b/src/blueprints/permissions.py @@ -1,4 +1,5 @@ from flask import Blueprint, request +from keycloak import KeycloakGetError, KeycloakPostError, KeycloakPutError def construct_blueprint(keycloak_client): @@ -7,15 +8,33 @@ def construct_blueprint(keycloak_client): @permissions.route("//permissions", methods=["GET"]) def get_client_authz_permissions(client_id: str): - return keycloak_client.get_client_authz_permissions(client_id) + try: + response = keycloak_client.get_client_authz_permissions(client_id) + return response + except KeycloakGetError as error: + return custom_error(error.error_message, error.response_code) + except: + return custom_error("Unknown server error", 500) @permissions.route("//permissions/management", methods=["GET"]) def get_client_management_permissions(client_id: str): - return keycloak_client.get_client_management_permissions(client_id) + try: + response = keycloak_client.get_client_management_permissions(client_id) + return response + except KeycloakGetError as error: + return custom_error(error.error_message, error.response_code) + except: + return custom_error("Unknown server error", 500) @permissions.route("//permissions/resources", methods=["GET"]) def get_client_resource_permissions(client_id: str): - return keycloak_client.get_client_resource_permissions(client_id) + try: + response = keycloak_client.get_client_resource_permissions(client_id) + return response + except KeycloakGetError as error: + return custom_error(error.error_message, error.response_code) + except: + return custom_error("Unknown server error", 500) #@permissions.route("/client_authz_scope_permissions//", methods=["GET"]) #def get_client_authz_scope_permissions(client_id: str, scope_id: str): @@ -29,21 +48,42 @@ def get_client_resource_permissions(client_id: str): @permissions.route("//permissions/resources", methods=["POST"]) def create_client_authz_resource_based_permission(client_id: str): payload = request.get_json() - return keycloak_client.create_client_authz_resource_based_permission(client_id, payload) + try: + response = keycloak_client.create_client_authz_resource_based_permission(client_id, payload) + return response + except KeycloakPostError as error: + return custom_error(error.error_message, error.response_code) + except: + return custom_error("Unknown server error", 500) @permissions.route("//permissions/management", methods=["PUT"]) def update_client_management_permissions(client_id: str): payload = request.get_json() - return keycloak_client.update_client_management_permissions(client_id, payload) + try: + response = keycloak_client.update_client_management_permissions(client_id, payload) + return response + except KeycloakPostError as error: + return custom_error(error.error_message, error.response_code) + except: + return custom_error("Unknown server error", 500) @permissions.route("//permissions/resources/", methods=["PUT"]) def update_client_authz_resource_permission(client_id: str, permission_id): payload = request.get_json() - return keycloak_client.update_client_authz_resource_permission(client_id, payload, permission_id) + try: + response = keycloak_client.update_client_authz_resource_permission(client_id, payload, permission_id) + return response + except KeycloakPutError as error: + return custom_error(error.error_message, error.response_code) + except: + return custom_error("Unknown server error", 500) #@permissions.route("//permissions/scopes/", methods=["PUT"]) #def update_client_authz_scope_permissions(client_id: str, scope_id): # payload = request.get_json() # return keycloak_client.update_client_authz_scope_permission(client_id, payload, scope_id) + def custom_error(message, status_code): + return message, status_code + return permissions diff --git a/src/blueprints/policies.py b/src/blueprints/policies.py index 71d5670..d0d8f0e 100644 --- a/src/blueprints/policies.py +++ b/src/blueprints/policies.py @@ -1,4 +1,5 @@ from flask import Blueprint, request +from keycloak import KeycloakDeleteError, KeycloakGetError, KeycloakPostError, KeycloakPutError def construct_blueprint(keycloak_client): @@ -18,40 +19,82 @@ def construct_blueprint(keycloak_client): @policies.route("//policies", methods=["GET"]) def get_client_authz_policies(client_id: str): - return keycloak_client.get_client_authz_policies(client_id) + try: + response = keycloak_client.get_client_authz_policies(client_id) + return response + except KeycloakGetError as error: + return custom_error(error.error_message, error.response_code) + except: + return custom_error("Unknown server error", 500) # --------------- POST ----------------- @policies.route("//policies/client", methods=["POST"]) def create_client_policy(client_id: str): policy = request.get_json() - return keycloak_client.register_client_policy(policy, client_id) + try: + response = keycloak_client.register_client_policy(policy, client_id) + return response + except KeycloakPostError as error: + return custom_error(error.error_message, error.response_code) + except: + return custom_error("Unknown server error", 500) @policies.route("//policies/aggregated", methods = ["POST"]) def create_aggregated_policy(client_id: str): policy = request.get_json() - return keycloak_client.register_aggregated_policy(policy, client_id) + try: + response = keycloak_client.register_aggregated_policy(policy, client_id) + return response + except KeycloakPostError as error: + return custom_error(error.error_message, error.response_code) + except: + return custom_error("Unknown server error", 500) @policies.route("//policies/scope", methods = ["POST"]) def create_client_scope_policy(client_id: str): policy = request.get_json() - return keycloak_client.register_client_scope_policy(policy, client_id) + try: + response = keycloak_client.register_client_scope_policy(policy, client_id) + return response + except KeycloakPostError as error: + return custom_error(error.error_message, error.response_code) + except: + return custom_error("Unknown server error", 500) @policies.route("//policies/group", methods = ["POST"]) def create_group_policy(client_id: str): policy = request.get_json() - return keycloak_client.register_group_policy(policy, client_id) + try: + response = keycloak_client.register_group_policy(policy, client_id) + return response + except KeycloakPostError as error: + return custom_error(error.error_message, error.response_code) + except: + return custom_error("Unknown server error", 500) @policies.route("//policies/regex", methods = ["POST"]) def create_regex_policy(client_id: str): policy = request.get_json() - return keycloak_client.register_regex_policy(policy, client_id) + try: + response = keycloak_client.register_regex_policy(policy, client_id) + return response + except KeycloakPostError as error: + return custom_error(error.error_message, error.response_code) + except: + return custom_error("Unknown server error", 500) @policies.route("//policies/role", methods = ["POST"]) def create_role_policy(client_id: str): policy = request.get_json() - return keycloak_client.register_role_policy(policy, client_id) + try: + response = keycloak_client.register_role_policy(policy, client_id) + return response + except KeycloakPostError as error: + return custom_error(error.error_message, error.response_code) + except: + return custom_error("Unknown server error", 500) @policies.route("//policies/time", methods = ["POST"]) def create_time_policy(client_id: str): @@ -83,12 +126,24 @@ def create_time_policy(client_id: str): "minuteEnd" ] policy = request.get_json() - return keycloak_client.register_time_policy(policy, client_id) + try: + response = keycloak_client.register_time_policy(policy, client_id) + return response + except KeycloakPostError as error: + return custom_error(error.error_message, error.response_code) + except: + return custom_error("Unknown server error", 500) @policies.route("//policies/user", methods = ["POST"]) def create_user_policy(client_id: str): policy = request.get_json() - return keycloak_client.register_user_policy(policy, client_id) + try: + response = keycloak_client.register_user_policy(policy, client_id) + return response + except KeycloakPostError as error: + return custom_error(error.error_message, error.response_code) + except: + return custom_error("Unknown server error", 500) @@ -97,13 +152,27 @@ def create_user_policy(client_id: str): @policies.route("//policies/", methods=["PUT"]) def update_policy(client_id: str, policy_id: str): policy = request.get_json() - return keycloak_client.update_policy(policy_id, policy, client_id) + try: + response = keycloak_client.update_policy(policy_id, policy, client_id) + return response + except KeycloakPutError as error: + return custom_error(error.error_message, error.response_code) + except: + return custom_error("Unknown server error", 500) # --------------- DELETE ----------------- @policies.route("//policies/", methods=["DELETE"]) def delete_policy(client_id: str ,policy_id: str): - return keycloak_client.delete_policy(policy_id, client_id) - + try: + response = keycloak_client.delete_policy(policy_id, client_id) + return response + except KeycloakDeleteError as error: + return custom_error(error.error_message, error.response_code) + except: + return custom_error("Unknown server error", 500) + + def custom_error(message, status_code): + return message, status_code return policies diff --git a/src/blueprints/resources.py b/src/blueprints/resources.py index 680df89..6d80c6c 100644 --- a/src/blueprints/resources.py +++ b/src/blueprints/resources.py @@ -1,4 +1,5 @@ from flask import Blueprint, request +from keycloak import KeycloakDeleteError, KeycloakGetError, KeycloakPostError, KeycloakPutError def construct_blueprint(keycloak_client): @@ -7,24 +8,57 @@ def construct_blueprint(keycloak_client): @resources.route("//resources", methods=["GET"]) def get_resources(client_id: str): - return keycloak_client.get_resources(client_id) + try: + response = keycloak_client.get_resources(client_id) + return response + except KeycloakGetError as error: + return custom_error(error.error_message, error.response_code) + except: + return custom_error("Unknown server error", 500) @resources.route("/resources/", methods=["GET"]) def get_resource(resource_id: str): - return keycloak_client.get_resource(resource_id) + try: + response = keycloak_client.get_resource(resource_id) + return response + except KeycloakGetError as error: + return custom_error(error.error_message, error.response_code) + except: + return custom_error("Unknown server error", 500) @resources.route("//resources", methods=["POST"]) def register_resource(client_id: str ): resource = request.get_json() - return keycloak_client.register_resource(resource, client_id) + try: + response = keycloak_client.register_resource(resource, client_id) + return response + except KeycloakPostError as error: + return custom_error(error.error_message, error.response_code) + except: + return custom_error("Unknown server error", 500) @resources.route("//resources/", methods=["PUT"]) def update_resource(client_id: str, resource_id: str): resource = request.get_json() - return keycloak_client.update_resource(resource_id, resource, client_id) + try: + response = keycloak_client.update_resource(resource_id, resource, client_id) + return response + except KeycloakPutError as error: + return custom_error(error.error_message, error.response_code) + except: + return custom_error("Unknown server error", 500) @resources.route("//resources/", methods=["DELETE"]) def delete_resource(client_id: str, resource_id: str): - return keycloak_client.delete_resource(resource_id, client_id) + try: + response = keycloak_client.delete_resource(resource_id, client_id) + return response + except KeycloakDeleteError as error: + return custom_error(error.error_message, error.response_code) + except: + return custom_error("Unknown server error", 500) + + def custom_error(message, status_code): + return message, status_code return resources From 7fda50631b2e0117d4d233c714d36c3f7c52b834 Mon Sep 17 00:00:00 2001 From: flaviorosadme Date: Wed, 18 Oct 2023 15:03:02 +0100 Subject: [PATCH 27/64] feat: added validator of register and protect resource enpoint to test --- src/blueprints/resources.py | 199 +++++++++++++++++++++++++++++++++++- 1 file changed, 197 insertions(+), 2 deletions(-) diff --git a/src/blueprints/resources.py b/src/blueprints/resources.py index 680df89..d486012 100644 --- a/src/blueprints/resources.py +++ b/src/blueprints/resources.py @@ -16,7 +16,86 @@ def get_resource(resource_id: str): @resources.route("//resources", methods=["POST"]) def register_resource(client_id: str ): resource = request.get_json() - return keycloak_client.register_resource(resource, client_id) + try: + response = keycloak_client.register_resource(resource, client_id) + return response + except KeycloakPostError as error: + return custom_error(error.error_message, error.response_code) + except: + return custom_error("Unknown server error", 500) + + + @resources.route("//register-resources", methods=["POST"]) + def register_and_protect_resources(client_id: str ): + payload = [{ + "resource":{ + "name": "resource1", + "uris": ["/resource1/", "/resource2/"], + 'attributes': {}, + 'scopes': ['view'], + 'ownerManagedAccess': False, + }, + "permissions": { + "user": { + "users":["user1","user2"], + "logic":"NEGATIVE" + }, + "role": { + "roles":["role1","role2"], + "logic":"POSITIVE" + }, + }, + "decisionStrategy": "UNANIMOUS" + }] + payload = request.get_json() + policy_list = [] + + for item in payload: + # validate item fields + error = _validate_register_resource(item) + if error: + return custom_error(error, 400) + else: + return item + """resource = item["resource"] + policies = item["permissions"] + decisionStrategy = item['decisionStrategy'] if 'decisionStrategy' in item else "UNANIMOUS" + type = 'urn:' + client_id + ':resources:default' + scopes = resource['scopes'] if 'scopes' in resource and resource['scopes'] != [] else ['access'] + + try: + # reconstruct resource object so it works when user sends unknown fields and to change field names to match what keycloak api expects + response_resource = keycloak_client.register_resource({ + "name": resource["name"] + "uris": resource["uris"] + "attributes": resource["attributes"] + "resource_scopes": resource["scopes"] + "ownerManagedAccess": resource["ownerManagedAccess"] + }, client_id) + for policy_type in policies: + policy = {"name": resource["name"].trim().replace(" ", "") + "" + policy_type + "_policy"} + for _key in policies[policy_type]: + policy[_key] = policies[policy_type][_key] + policy_list.append(policy["name"]) + response_policy = keycloak_client.register_general_policy(policy, client_id, policy_type) + + permission_payload = { + "type": "resource", + "name": resource["name"] + "permission", + "decisionStrategy": decisionStrategy, + "resources": [ + resource["name"] + ], + "policies": policy_list + } + + permission_response = keycloak_client.create_client_authz_resource_based_permission(client_id, permission_payload) + + return response_resource + except KeycloakPostError as error: + return custom_error(error.error_message, error.response_code) + except: + return custom_error("Unknown server error", 500)""" @resources.route("//resources/", methods=["PUT"]) def update_resource(client_id: str, resource_id: str): @@ -25,6 +104,122 @@ def update_resource(client_id: str, resource_id: str): @resources.route("//resources/", methods=["DELETE"]) def delete_resource(client_id: str, resource_id: str): - return keycloak_client.delete_resource(resource_id, client_id) + try: + response = keycloak_client.delete_resource(resource_id, client_id) + return response + except KeycloakDeleteError as error: + return custom_error(error.error_message, error.response_code) + except: + return custom_error("Unknown server error", 500) + + def custom_error(message, status_code): + return message, status_code + + def _validate_register_resource(item): + payload_minimum_example = """ + payload example -> + [{ + "resource":{ + "name": "resource1", + "uris": ["/resource1/", "/resource2/"] + }, + "permissions": { + "user": ["user1","user2"], + } + }] + """ + time_options = """time must be a dictionary with one of: + "notAfter":"1970-01-01 00:00:00" + "notBefore":"1970-01-01 00:00:00" + "dayMonth": + "dayMonthEnd": + "month": + "monthEnd": + "year": + "yearEnd": + "hour": + "hourEnd": + "minute": + "minuteEnd":""" + + policy_types = ['user', 'client', 'role', 'time', 'regex', 'group', 'scope', 'aggregated'] + policy_accepted_fields = ['logic', 'decisionStrategy', 'name', 'description', 'groupsClaim', 'targetClaim'] + time_accepted_fields = ["notAfter","notBefore","dayMonth","dayMonthEnd","month","monthEnd","year","yearEnd","hour","hourEnd","minute","minuteEnd"] + if 'resource' not in item: + return 'Resource field required. ' + payload_minimum_example + if 'permissions' not in item or item['permissions'] == {}: + return 'Permissions field required. ' + payload_minimum_example + if 'name' not in item['resource']: + return 'Resource name required. '+ payload_minimum_example + if 'uris' not in item['resource']: + return 'Resource uris required. '+ payload_minimum_example + + for key in item['permissions']: + if not isinstance(item['permissions'][key], list) and not isinstance(item['permissions'][key], dict): + return "The value of {} ".format(key) + "must be a list of strings or a dictionary" + if key not in policy_types: + return 'Permissions type not found. Needs to be one of the following: ' + ', '.join(policy_types) + if key == 'time': + if not isinstance(item['permissions']['time'], dict): + return time_options + for time_key in item['permissions']['time']: + if time_key in time_accepted_fields or time_key in policy_accepted_fields: + continue + else: + return 'There are fields not accepted or ' + time_options + if key == 'regex': + if not isinstance(item['permissions'][key], dict): + return 'Regex must be a dictionary like {"pattern":}' + for regex_key in item['permissions'][key]: + if regex_key == 'pattern' or regex_key in policy_accepted_fields: + continue + else: + return 'The field "pattern" is not in the regex dictionary or there are fields not accepted' + if key == 'user': + if not isinstance(item['permissions'][key], list): + for user_key in item['permissions'][key]: + if user_key == 'users' or user_key in policy_accepted_fields: + continue + else: + return 'The field "users" is not in the user dictionary or there are fields not accepted' + if key == 'role': + if not isinstance(item['permissions'][key], list): + for role_key in item['permissions'][key]: + if role_key == 'roles' or role_key in policy_accepted_fields: + continue + else: + return 'The field "roles" is not in the role dictionary or there are fields not accepted' + if key == 'group': + if not isinstance(item['permissions'][key], list): + for group_key in item['permissions'][key]: + if group_key == 'groups' or group_key in policy_accepted_fields: + continue + else: + return 'The field "groups" is not in the group dictionary or there are fields not accepted' + if key == 'client-scope': + if not isinstance(item['permissions'][key], list): + for client_scope_key in item['permissions'][key]: + if client_scope_key in policy_accepted_fields: + continue + else: + return 'There are fields not accepted' + + if key == 'aggregated': + if not isinstance(item['permissions'][key], list): + for aggregated_key in item['permissions'][key]: + if aggregated_key == 'policies' or aggregated_key in policy_accepted_fields: + continue + else: + return 'The field "policies" is not in the aggregated dictionary or there are fields not accepted' + + if key == 'client': + if not isinstance(item['permissions'][key], list): + for client_key in item['permissions'][key]: + if client_key in policy_accepted_fields: + continue + else: + return 'There are fields not accepted' + + return None return resources From ffe7a1de82145b4b01f22b4d6c2a33a1b1b6b24a Mon Sep 17 00:00:00 2001 From: flaviorosadme Date: Thu, 19 Oct 2023 09:55:11 +0100 Subject: [PATCH 28/64] feat: register and protect resources endpoint working --- src/blueprints/resources.py | 42 +++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/src/blueprints/resources.py b/src/blueprints/resources.py index 340770a..bcb1d3e 100644 --- a/src/blueprints/resources.py +++ b/src/blueprints/resources.py @@ -40,7 +40,7 @@ def register_resource(client_id: str ): @resources.route("//register-resources", methods=["POST"]) def register_and_protect_resources(client_id: str ): - payload = [{ + """payload = [{ "resource":{ "name": "resource1", "uris": ["/resource1/", "/resource2/"], @@ -59,7 +59,7 @@ def register_and_protect_resources(client_id: str ): }, }, "decisionStrategy": "UNANIMOUS" - }] + }]""" payload = request.get_json() policy_list = [] @@ -68,9 +68,8 @@ def register_and_protect_resources(client_id: str ): error = _validate_register_resource(item) if error: return custom_error(error, 400) - else: - return item - """resource = item["resource"] + + resource = item["resource"] policies = item["permissions"] decisionStrategy = item['decisionStrategy'] if 'decisionStrategy' in item else "UNANIMOUS" type = 'urn:' + client_id + ':resources:default' @@ -78,17 +77,22 @@ def register_and_protect_resources(client_id: str ): try: # reconstruct resource object so it works when user sends unknown fields and to change field names to match what keycloak api expects - response_resource = keycloak_client.register_resource({ - "name": resource["name"] - "uris": resource["uris"] - "attributes": resource["attributes"] - "resource_scopes": resource["scopes"] - "ownerManagedAccess": resource["ownerManagedAccess"] - }, client_id) + response_resource = keycloak_client.register_resource( resource, client_id) for policy_type in policies: - policy = {"name": resource["name"].trim().replace(" ", "") + "" + policy_type + "_policy"} - for _key in policies[policy_type]: - policy[_key] = policies[policy_type][_key] + policy = {"name": resource["name"].replace(" ", "") + "_" + policy_type + "_policy"} + if isinstance(policies[policy_type], list): + match policy_type: + case 'user': + policy['users'] = policies[policy_type] + case 'role': + policy['roles'] = policies[policy_type] + case 'aggregated': + policy['policies'] = policies[policy_type] + case 'group': + policy['groups'] = policies[policy_type] + else: + for _key in policies[policy_type]: + policy[_key] = policies[policy_type][_key] policy_list.append(policy["name"]) response_policy = keycloak_client.register_general_policy(policy, client_id, policy_type) @@ -108,7 +112,7 @@ def register_and_protect_resources(client_id: str ): except KeycloakPostError as error: return custom_error(error.error_message, error.response_code) except: - return custom_error("Unknown server error", 500)""" + return custom_error("Unknown server error", 500) @resources.route("//resources/", methods=["PUT"]) def update_resource(client_id: str, resource_id: str): @@ -162,6 +166,7 @@ def _validate_register_resource(item): "minuteEnd":""" policy_types = ['user', 'client', 'role', 'time', 'regex', 'group', 'scope', 'aggregated'] + resource_accepted_fields = ['name','uris','attributes', 'ownerManagedAccess', 'resource_scopes', 'type'] policy_accepted_fields = ['logic', 'decisionStrategy', 'name', 'description', 'groupsClaim', 'targetClaim'] time_accepted_fields = ["notAfter","notBefore","dayMonth","dayMonthEnd","month","monthEnd","year","yearEnd","hour","hourEnd","minute","minuteEnd"] if 'resource' not in item: @@ -172,6 +177,11 @@ def _validate_register_resource(item): return 'Resource name required. '+ payload_minimum_example if 'uris' not in item['resource']: return 'Resource uris required. '+ payload_minimum_example + for resource_key in item['resource']: + if resource_key in resource_accepted_fields: + continue + else: + return 'There are fields not accepted in "resource"' for key in item['permissions']: if not isinstance(item['permissions'][key], list) and not isinstance(item['permissions'][key], dict): From a8c62a6edbf877f3082a96ea122b65b60b5ec63a Mon Sep 17 00:00:00 2001 From: flaviorosadme Date: Fri, 20 Oct 2023 12:34:57 +0100 Subject: [PATCH 29/64] feat: added delete resources, policies and permissions --- src/blueprints/resources.py | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/src/blueprints/resources.py b/src/blueprints/resources.py index bcb1d3e..ae03a96 100644 --- a/src/blueprints/resources.py +++ b/src/blueprints/resources.py @@ -62,6 +62,8 @@ def register_and_protect_resources(client_id: str ): }]""" payload = request.get_json() policy_list = [] + + response_list = [] for item in payload: # validate item fields @@ -77,6 +79,7 @@ def register_and_protect_resources(client_id: str ): try: # reconstruct resource object so it works when user sends unknown fields and to change field names to match what keycloak api expects + resource["name"] = resource["name"].replace(" ", "_") response_resource = keycloak_client.register_resource( resource, client_id) for policy_type in policies: policy = {"name": resource["name"].replace(" ", "") + "_" + policy_type + "_policy"} @@ -98,7 +101,7 @@ def register_and_protect_resources(client_id: str ): permission_payload = { "type": "resource", - "name": resource["name"] + "permission", + "name": resource["name"] + "_permission", "decisionStrategy": decisionStrategy, "resources": [ resource["name"] @@ -108,11 +111,38 @@ def register_and_protect_resources(client_id: str ): permission_response = keycloak_client.create_client_authz_resource_based_permission(client_id, permission_payload) - return response_resource + response_list.append(response_resource) except KeycloakPostError as error: return custom_error(error.error_message, error.response_code) except: return custom_error("Unknown server error", 500) + return response_list + + + @resources.route("//delete-resources/", methods=["DELETE"]) + def delete_resource_and_policies(client_id: str, resource_name: str): + try: + client_policies = keycloak_client.get_client_authz_policies(client_id) + policy_types = ['user', 'client', 'role', 'time', 'regex', 'group', 'scope', 'aggregated'] + for policy in client_policies: + for policy_type in policy_types: + if policy['name'] == resource_name + '_' + policy_type + '_policy': + keycloak_client.delete_policy(policy['id'], client_id) + permissions = keycloak_client.get_client_resource_permissions(client_id) + for permission in permissions: + if permission['name'] == resource_name +'permission': + keycloak_client.delete_resource_permissions(client_id, permission['id']) + + _resources = keycloak_client.get_resources(client_id) + for resource in _resources: + if resource['name'] == resource_name: + resource_delete_response = keycloak_client.delete_resource(resource['_id'], client_id) + return resource_delete_response + except KeycloakDeleteError as error: + return custom_error(error.error_message, error.response_code) + except: + return custom_error("Unknown server error", 500) + @resources.route("//resources/", methods=["PUT"]) def update_resource(client_id: str, resource_id: str): From 734244ecf53580f4f16b72e79aedeb69475ba0da Mon Sep 17 00:00:00 2001 From: Daniel Pimenta Date: Mon, 23 Oct 2023 12:24:40 +0100 Subject: [PATCH 30/64] Update ci --- .github/workflows/docker-publish-develop.yml | 66 ++++++++++---------- .github/workflows/docker-publish-master.yml | 63 ++++++++++--------- 2 files changed, 65 insertions(+), 64 deletions(-) diff --git a/.github/workflows/docker-publish-develop.yml b/.github/workflows/docker-publish-develop.yml index d167db5..21f47db 100644 --- a/.github/workflows/docker-publish-develop.yml +++ b/.github/workflows/docker-publish-develop.yml @@ -16,9 +16,9 @@ on: env: # Use docker.io for Docker Hub if empty REGISTRY: ghcr.io + DOCKER_REGISTRY: docker.io # github.repository as / IMAGE_NAME: ${{ github.repository }} - jobs: build: @@ -81,35 +81,35 @@ jobs: cache-to: type=gha,mode=max -# # Sign the resulting Docker image digest except on PRs. -# # This will only write to the public Rekor transparency log when the Docker -# # repository is public to avoid leaking data. If you would like to publish -# # transparency data even for private images, pass --force to cosign below. -# # https://github.com/sigstore/cosign -# - name: Sign the published Docker image -# if: ${{ github.event_name != 'pull_request' }} -# env: -# COSIGN_EXPERIMENTAL: "true" -# # This step uses the identity token to provision an ephemeral certificate -# # against the sigstore community Fulcio instance. -# run: echo "${{ steps.meta.outputs.tags }}" | xargs -I {} cosign sign {}@${{ steps.build-and-push.outputs.digest }} -# -# - name: Log into registry ${{ env.DOCKER_REGISTRY }} -# uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 -# with: -# username: ${{ secrets.DOCKER_USERNAME }} -# password: ${{ secrets.DOCKER_PASSWORD }} -# -# - name: Extract metadata (tags, labels) for Docker -# id: docker_meta -# uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 -# with: -# images: ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }} -# -# - name: Build and push Docker image -# uses: docker/build-push-action@v2 -# with: -# context: "{{defaultContext}}" -# push: true -# tags: ${{ steps.docker_meta.outputs.tags }} -# labels: ${{ steps.docker_meta.outputs.labels }} \ No newline at end of file + # Sign the resulting Docker image digest except on PRs. + # This will only write to the public Rekor transparency log when the Docker + # repository is public to avoid leaking data. If you would like to publish + # transparency data even for private images, pass --force to cosign below. + # https://github.com/sigstore/cosign + - name: Sign the published Docker image + if: ${{ github.event_name != 'pull_request' }} + env: + COSIGN_EXPERIMENTAL: "true" + # This step uses the identity token to provision an ephemeral certificate + # against the sigstore community Fulcio instance. + run: echo "${{ steps.meta.outputs.tags }}" | xargs -I {} cosign sign {}@${{ steps.build-and-push.outputs.digest }} + + - name: Log into registry ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Extract metadata (tags, labels) for Docker + id: docker_meta + uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 + with: + images: ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }} + + - name: Build and push Docker image + uses: docker/build-push-action@v2 + with: + context: "{{defaultContext}}" + push: true + tags: ${{ steps.docker_meta.outputs.tags }} + labels: ${{ steps.docker_meta.outputs.labels }} \ No newline at end of file diff --git a/.github/workflows/docker-publish-master.yml b/.github/workflows/docker-publish-master.yml index 12611ec..7bb2919 100644 --- a/.github/workflows/docker-publish-master.yml +++ b/.github/workflows/docker-publish-master.yml @@ -16,6 +16,7 @@ on: env: # Use docker.io for Docker Hub if empty REGISTRY: ghcr.io + DOCKER_REGISTRY: docker.io # github.repository as / IMAGE_NAME: ${{ github.repository }} @@ -80,34 +81,34 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max -# # Sign the resulting Docker image digest except on PRs. -# # This will only write to the public Rekor transparency log when the Docker -# # repository is public to avoid leaking data. If you would like to publish -# # transparency data even for private images, pass --force to cosign below. -# # https://github.com/sigstore/cosign -# - name: Sign the published Docker image -# if: ${{ github.event_name != 'pull_request' }} -# env: -# COSIGN_EXPERIMENTAL: "true" -# # This step uses the identity token to provision an ephemeral certificate -# # against the sigstore community Fulcio instance. -# run: echo "${{ steps.meta.outputs.tags }}" | xargs -I {} cosign sign {}@${{ steps.build-and-push.outputs.digest }} -# -# - name: Log into registry ${{ env.DOCKER_REGISTRY }} -# uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 -# with: -# username: ${{ secrets.DOCKER_USERNAME }} -# password: ${{ secrets.DOCKER_PASSWORD }} -# - name: Extract metadata (tags, labels) for Docker -# id: docker_meta -# uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 -# with: -# images: ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }} -# -# - name: Build and push Docker image -# uses: docker/build-push-action@v2 -# with: -# context: "{{defaultContext}}" -# push: true -# tags: ${{ steps.docker_meta.outputs.tags }} -# labels: ${{ steps.docker_meta.outputs.labels }} \ No newline at end of file + # Sign the resulting Docker image digest except on PRs. + # This will only write to the public Rekor transparency log when the Docker + # repository is public to avoid leaking data. If you would like to publish + # transparency data even for private images, pass --force to cosign below. + # https://github.com/sigstore/cosign + - name: Sign the published Docker image + if: ${{ github.event_name != 'pull_request' }} + env: + COSIGN_EXPERIMENTAL: "true" + # This step uses the identity token to provision an ephemeral certificate + # against the sigstore community Fulcio instance. + run: echo "${{ steps.meta.outputs.tags }}" | xargs -I {} cosign sign {}@${{ steps.build-and-push.outputs.digest }} + + - name: Log into registry ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + - name: Extract metadata (tags, labels) for Docker + id: docker_meta + uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 + with: + images: ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }} + + - name: Build and push Docker image + uses: docker/build-push-action@v2 + with: + context: "{{defaultContext}}" + push: true + tags: ${{ steps.docker_meta.outputs.tags }} + labels: ${{ steps.docker_meta.outputs.labels }} \ No newline at end of file From acc4fed61f79adbbd061a573c7a0110b7337f876 Mon Sep 17 00:00:00 2001 From: Daniel Pimenta Date: Mon, 23 Oct 2023 14:03:09 +0100 Subject: [PATCH 31/64] Update ci --- .github/workflows/docker-publish-develop.yml | 10 ++++------ .github/workflows/docker-publish-master.yml | 10 ++++------ 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/.github/workflows/docker-publish-develop.yml b/.github/workflows/docker-publish-develop.yml index 21f47db..1b6d974 100644 --- a/.github/workflows/docker-publish-develop.yml +++ b/.github/workflows/docker-publish-develop.yml @@ -14,10 +14,8 @@ on: branches: [ "develop" ] env: - # Use docker.io for Docker Hub if empty - REGISTRY: ghcr.io + GITHUB_REGISTRY: ghcr.io DOCKER_REGISTRY: docker.io - # github.repository as / IMAGE_NAME: ${{ github.repository }} jobs: build: @@ -48,11 +46,11 @@ jobs: # Login against a Docker registry except on PR # https://github.com/docker/login-action - - name: Log into registry ${{ env.REGISTRY }} + - name: Log into registry ${{ env.GITHUB_REGISTRY }} if: github.event_name != 'pull_request' uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c with: - registry: ${{ env.REGISTRY }} + registry: ${{ env.GITHUB_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} @@ -62,7 +60,7 @@ jobs: id: meta uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + images: ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }} tags: | type=raw,value=develop type=ref,event=tag diff --git a/.github/workflows/docker-publish-master.yml b/.github/workflows/docker-publish-master.yml index 7bb2919..3b0225c 100644 --- a/.github/workflows/docker-publish-master.yml +++ b/.github/workflows/docker-publish-master.yml @@ -14,10 +14,8 @@ on: branches: [ "master" ] env: - # Use docker.io for Docker Hub if empty - REGISTRY: ghcr.io + GITHUB_REGISTRY: ghcr.io DOCKER_REGISTRY: docker.io - # github.repository as / IMAGE_NAME: ${{ github.repository }} jobs: @@ -49,11 +47,11 @@ jobs: # Login against a Docker registry except on PR # https://github.com/docker/login-action - - name: Log into registry ${{ env.REGISTRY }} + - name: Log into registry ${{ env.GITHUB_REGISTRY }} if: github.event_name != 'pull_request' uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c with: - registry: ${{ env.REGISTRY }} + registry: ${{ env.GITHUB_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} @@ -63,7 +61,7 @@ jobs: id: meta uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + images: ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }} tags: | type=raw,value=production type=ref,event=tag From 004b489ef7174a715fd7a34f022c336dc9af565f Mon Sep 17 00:00:00 2001 From: Daniel Pimenta Date: Tue, 24 Oct 2023 18:24:13 +0100 Subject: [PATCH 32/64] Fix ci --- .github/workflows/docker-publish-develop.yml | 4 ++-- .github/workflows/docker-publish-master.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker-publish-develop.yml b/.github/workflows/docker-publish-develop.yml index 1b6d974..3d94ce3 100644 --- a/.github/workflows/docker-publish-develop.yml +++ b/.github/workflows/docker-publish-develop.yml @@ -95,8 +95,8 @@ jobs: - name: Log into registry ${{ env.DOCKER_REGISTRY }} uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} - name: Extract metadata (tags, labels) for Docker id: docker_meta diff --git a/.github/workflows/docker-publish-master.yml b/.github/workflows/docker-publish-master.yml index 3b0225c..307617f 100644 --- a/.github/workflows/docker-publish-master.yml +++ b/.github/workflows/docker-publish-master.yml @@ -95,8 +95,8 @@ jobs: - name: Log into registry ${{ env.DOCKER_REGISTRY }} uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} - name: Extract metadata (tags, labels) for Docker id: docker_meta uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 From 9d8c034a088b92e325f2cfa0b23cc6e754948767 Mon Sep 17 00:00:00 2001 From: Daniel Pimenta Date: Sun, 29 Oct 2023 19:57:46 +0000 Subject: [PATCH 33/64] Add options method to endpoints --- src/blueprints/permissions.py | 20 ++++++++++---------- src/blueprints/policies.py | 12 ++++++------ src/blueprints/resources.py | 16 ++++++++-------- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/blueprints/permissions.py b/src/blueprints/permissions.py index 4f0b27e..23bc084 100644 --- a/src/blueprints/permissions.py +++ b/src/blueprints/permissions.py @@ -6,7 +6,7 @@ def construct_blueprint(keycloak_client): keycloak_client = keycloak_client permissions = Blueprint('permissions', __name__) - @permissions.route("//permissions", methods=["GET"]) + @permissions.route("//permissions", methods=["OPTIONS", "GET"]) def get_client_authz_permissions(client_id: str): try: response = keycloak_client.get_client_authz_permissions(client_id) @@ -16,7 +16,7 @@ def get_client_authz_permissions(client_id: str): except: return custom_error("Unknown server error", 500) - @permissions.route("//permissions/management", methods=["GET"]) + @permissions.route("//permissions/management", methods=["OPTIONS", "GET"]) def get_client_management_permissions(client_id: str): try: response = keycloak_client.get_client_management_permissions(client_id) @@ -26,7 +26,7 @@ def get_client_management_permissions(client_id: str): except: return custom_error("Unknown server error", 500) - @permissions.route("//permissions/resources", methods=["GET"]) + @permissions.route("//permissions/resources", methods=["OPTIONS", "GET"]) def get_client_resource_permissions(client_id: str): try: response = keycloak_client.get_client_resource_permissions(client_id) @@ -36,16 +36,16 @@ def get_client_resource_permissions(client_id: str): except: return custom_error("Unknown server error", 500) - #@permissions.route("/client_authz_scope_permissions//", methods=["GET"]) + #@permissions.route("/client_authz_scope_permissions//", methods=["OPTIONS", "GET"]) #def get_client_authz_scope_permissions(client_id: str, scope_id: str): # return keycloak_client.get_client_authz_scope_permissions(client_id, scope_id) - #@permissions.route("/client_authz_scope_permissions/", methods=["POST"]) + #@permissions.route("/client_authz_scope_permissions/", methods=["OPTIONS", "POST"]) #def create_client_authz_scope_based_permissions(client_id: str): # payload = request.get_json() # return keycloak_client.create_client_authz_scope_based_permission(client_id, payload) - @permissions.route("//permissions/resources", methods=["POST"]) + @permissions.route("//permissions/resources", methods=["OPTIONS", "POST"]) def create_client_authz_resource_based_permission(client_id: str): payload = request.get_json() try: @@ -56,7 +56,7 @@ def create_client_authz_resource_based_permission(client_id: str): except: return custom_error("Unknown server error", 500) - @permissions.route("//permissions/management", methods=["PUT"]) + @permissions.route("//permissions/management", methods=["OPTIONS", "PUT"]) def update_client_management_permissions(client_id: str): payload = request.get_json() try: @@ -67,7 +67,7 @@ def update_client_management_permissions(client_id: str): except: return custom_error("Unknown server error", 500) - @permissions.route("//permissions/resources/", methods=["PUT"]) + @permissions.route("//permissions/resources/", methods=["OPTIONS", "PUT"]) def update_client_authz_resource_permission(client_id: str, permission_id): payload = request.get_json() try: @@ -78,7 +78,7 @@ def update_client_authz_resource_permission(client_id: str, permission_id): except: return custom_error("Unknown server error", 500) - #@permissions.route("//permissions/scopes/", methods=["PUT"]) + #@permissions.route("//permissions/scopes/", methods=["OPTIONS", "PUT"]) #def update_client_authz_scope_permissions(client_id: str, scope_id): # payload = request.get_json() # return keycloak_client.update_client_authz_scope_permission(client_id, payload, scope_id) @@ -86,4 +86,4 @@ def update_client_authz_resource_permission(client_id: str, permission_id): def custom_error(message, status_code): return message, status_code - return permissions + return permissions \ No newline at end of file diff --git a/src/blueprints/policies.py b/src/blueprints/policies.py index d0d8f0e..46a293d 100644 --- a/src/blueprints/policies.py +++ b/src/blueprints/policies.py @@ -7,7 +7,7 @@ def construct_blueprint(keycloak_client): policies = Blueprint('policies', __name__) # -------- Always returns empty ------- - #@policies.route("/policies", methods=["GET"]) + #@policies.route("/policies", methods=["OPTIONS", "GET"]) #def get_policies(): # resource = request.args.get('resource', "") # name = request.args.get('name', "") @@ -17,7 +17,7 @@ def construct_blueprint(keycloak_client): # return keycloak_client.get_policies(resource, name, scope, first, maximum) # --------------- GET ----------------- - @policies.route("//policies", methods=["GET"]) + @policies.route("//policies", methods=["OPTIONS", "GET"]) def get_client_authz_policies(client_id: str): try: response = keycloak_client.get_client_authz_policies(client_id) @@ -29,7 +29,7 @@ def get_client_authz_policies(client_id: str): # --------------- POST ----------------- - @policies.route("//policies/client", methods=["POST"]) + @policies.route("//policies/client", methods=["OPTIONS", "POST"]) def create_client_policy(client_id: str): policy = request.get_json() try: @@ -149,7 +149,7 @@ def create_user_policy(client_id: str): # --------------- UPDATE ----------------- - @policies.route("//policies/", methods=["PUT"]) + @policies.route("//policies/", methods=["OPTIONS", "PUT"]) def update_policy(client_id: str, policy_id: str): policy = request.get_json() try: @@ -162,7 +162,7 @@ def update_policy(client_id: str, policy_id: str): # --------------- DELETE ----------------- - @policies.route("//policies/", methods=["DELETE"]) + @policies.route("//policies/", methods=["OPTIONS", "DELETE"]) def delete_policy(client_id: str ,policy_id: str): try: response = keycloak_client.delete_policy(policy_id, client_id) @@ -175,4 +175,4 @@ def delete_policy(client_id: str ,policy_id: str): def custom_error(message, status_code): return message, status_code - return policies + return policies \ No newline at end of file diff --git a/src/blueprints/resources.py b/src/blueprints/resources.py index ae03a96..51fe2fc 100644 --- a/src/blueprints/resources.py +++ b/src/blueprints/resources.py @@ -6,7 +6,7 @@ def construct_blueprint(keycloak_client): keycloak_client = keycloak_client resources = Blueprint('resources', __name__) - @resources.route("//resources", methods=["GET"]) + @resources.route("//resources", methods=["OPTIONS", "GET"]) def get_resources(client_id: str): try: response = keycloak_client.get_resources(client_id) @@ -16,7 +16,7 @@ def get_resources(client_id: str): except: return custom_error("Unknown server error", 500) - @resources.route("/resources/", methods=["GET"]) + @resources.route("/resources/", methods=["OPTIONS", "GET"]) def get_resource(resource_id: str): try: response = keycloak_client.get_resource(resource_id) @@ -26,7 +26,7 @@ def get_resource(resource_id: str): except: return custom_error("Unknown server error", 500) - @resources.route("//resources", methods=["POST"]) + @resources.route("//resources", methods=["OPTIONS", "POST"]) def register_resource(client_id: str ): resource = request.get_json() try: @@ -38,7 +38,7 @@ def register_resource(client_id: str ): return custom_error("Unknown server error", 500) - @resources.route("//register-resources", methods=["POST"]) + @resources.route("//register-resources", methods=["OPTIONS", "POST"]) def register_and_protect_resources(client_id: str ): """payload = [{ "resource":{ @@ -119,7 +119,7 @@ def register_and_protect_resources(client_id: str ): return response_list - @resources.route("//delete-resources/", methods=["DELETE"]) + @resources.route("//delete-resources/", methods=["OPTIONS", "DELETE"]) def delete_resource_and_policies(client_id: str, resource_name: str): try: client_policies = keycloak_client.get_client_authz_policies(client_id) @@ -144,7 +144,7 @@ def delete_resource_and_policies(client_id: str, resource_name: str): return custom_error("Unknown server error", 500) - @resources.route("//resources/", methods=["PUT"]) + @resources.route("//resources/", methods=["OPTIONS", "PUT"]) def update_resource(client_id: str, resource_id: str): resource = request.get_json() try: @@ -155,7 +155,7 @@ def update_resource(client_id: str, resource_id: str): except: return custom_error("Unknown server error", 500) - @resources.route("//resources/", methods=["DELETE"]) + @resources.route("//resources/", methods=["OPTIONS", "DELETE"]) def delete_resource(client_id: str, resource_id: str): try: response = keycloak_client.delete_resource(resource_id, client_id) @@ -281,4 +281,4 @@ def _validate_register_resource(item): return None - return resources + return resources \ No newline at end of file From 82a5a5f212d789feb0138a5b33fe929cc9738738 Mon Sep 17 00:00:00 2001 From: flaviorosadme Date: Wed, 8 Nov 2023 17:24:30 +0000 Subject: [PATCH 34/64] feat: added endpoint to create client, add resources and protect them if provided --- src/blueprints/resources.py | 76 +++++++++++++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 3 deletions(-) diff --git a/src/blueprints/resources.py b/src/blueprints/resources.py index ae03a96..af12591 100644 --- a/src/blueprints/resources.py +++ b/src/blueprints/resources.py @@ -39,7 +39,7 @@ def register_resource(client_id: str ): @resources.route("//register-resources", methods=["POST"]) - def register_and_protect_resources(client_id: str ): + def register_and_protect_resources(client_id: str, payload=None ): """payload = [{ "resource":{ "name": "resource1", @@ -60,7 +60,8 @@ def register_and_protect_resources(client_id: str ): }, "decisionStrategy": "UNANIMOUS" }]""" - payload = request.get_json() + if payload == None: + payload = request.get_json() policy_list = [] response_list = [] @@ -164,7 +165,76 @@ def delete_resource(client_id: str, resource_id: str): return custom_error(error.error_message, error.response_code) except: return custom_error("Unknown server error", 500) - + + + @resources.route("/create-client", methods=["POST"]) + def create_client(): + payload = request.get_json() + helper_text = """ The following fields are allowed: +clientId*: String +name: String +description: String +rootUrl: String +adminUrl: String +baseUrl: String +surrogateAuthRequired: Boolean +enabled: Boolean +alwaysDisplayInConsole: Boolean +clientAuthenticatorType: String +secret: String +registrationAccessToken: String +defaultRoles: List of [string] +redirectUris: List of [string] +webOrigins: List of [string] +notBefore: Integer +bearerOnly: Boolean +consentRequired: Boolean +standardFlowEnabled: Boolean +implicitFlowEnabled: Boolean +directAccessGrantsEnabled: Boolean +serviceAccountsEnabled: Boolean +oauth2DeviceAuthorizationGrantEnabled: Boolean +authorizationServicesEnabled: Boolean +directGrantsOnly: Boolean +publicClient: Boolean +frontchannelLogout: Boolean +protocol: String +attributes: Map of [string] +authenticationFlowBindingOverrides: Map of [string] +fullScopeAllowed: Boolean +nodeReRegistrationTimeout: Integer +registeredNodes: Map of [integer] +protocolMappers: List of ProtocolMapperRepresentation +clientTemplate: String +useTemplateConfig: Boolean +useTemplateScope: Boolean +useTemplateMappers: Boolean +defaultClientScopes: List of [string] +ClientScopes: List of [string] +authorizationSettings: ResourceServerRepresentation +access: Map of [boolean] +origin: String +resources: List of[Resource Representation]""" + if 'clientId' not in payload: + return custom_error("The field 'client_id' is mandatory", 400) + if 'redirectUris' not in payload: + payload['redirectUris'] = ['*'] + if 'standardFlowEnabled' not in payload: + payload['standardFlowEnabled'] = True + if 'protocol' not in payload: + payload['protocol'] = 'openid-connect' + if 'resources' in payload: + resources = payload['resources'] + del payload['resources'] + keycloak_client.create_client(payload) + return register_and_protect_resources(payload['clientId'], resources) + try: + return keycloak_client.create_client(payload) + except KeycloakPostError as error: + return custom_error(error.error_message, error.response_code) + except: + return custom_error("Unknown server error", 500) + def custom_error(message, status_code): return message, status_code From deb3afef424e2d982689da54947d500f516ed509 Mon Sep 17 00:00:00 2001 From: flaviorosadme Date: Wed, 8 Nov 2023 17:57:19 +0000 Subject: [PATCH 35/64] Revert "Add options method to endpoints" This reverts commit 9d8c034a088b92e325f2cfa0b23cc6e754948767. --- src/blueprints/permissions.py | 18 +++++++++--------- src/blueprints/policies.py | 12 ++++++------ src/blueprints/resources.py | 18 +++++++++--------- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/blueprints/permissions.py b/src/blueprints/permissions.py index 446c8cf..4f0b27e 100644 --- a/src/blueprints/permissions.py +++ b/src/blueprints/permissions.py @@ -6,7 +6,7 @@ def construct_blueprint(keycloak_client): keycloak_client = keycloak_client permissions = Blueprint('permissions', __name__) - @permissions.route("//permissions", methods=["OPTIONS", "GET"]) + @permissions.route("//permissions", methods=["GET"]) def get_client_authz_permissions(client_id: str): try: response = keycloak_client.get_client_authz_permissions(client_id) @@ -16,7 +16,7 @@ def get_client_authz_permissions(client_id: str): except: return custom_error("Unknown server error", 500) - @permissions.route("//permissions/management", methods=["OPTIONS", "GET"]) + @permissions.route("//permissions/management", methods=["GET"]) def get_client_management_permissions(client_id: str): try: response = keycloak_client.get_client_management_permissions(client_id) @@ -26,7 +26,7 @@ def get_client_management_permissions(client_id: str): except: return custom_error("Unknown server error", 500) - @permissions.route("//permissions/resources", methods=["OPTIONS", "GET"]) + @permissions.route("//permissions/resources", methods=["GET"]) def get_client_resource_permissions(client_id: str): try: response = keycloak_client.get_client_resource_permissions(client_id) @@ -36,16 +36,16 @@ def get_client_resource_permissions(client_id: str): except: return custom_error("Unknown server error", 500) - #@permissions.route("/client_authz_scope_permissions//", methods=["OPTIONS", "GET"]) + #@permissions.route("/client_authz_scope_permissions//", methods=["GET"]) #def get_client_authz_scope_permissions(client_id: str, scope_id: str): # return keycloak_client.get_client_authz_scope_permissions(client_id, scope_id) - #@permissions.route("/client_authz_scope_permissions/", methods=["OPTIONS", "POST"]) + #@permissions.route("/client_authz_scope_permissions/", methods=["POST"]) #def create_client_authz_scope_based_permissions(client_id: str): # payload = request.get_json() # return keycloak_client.create_client_authz_scope_based_permission(client_id, payload) - @permissions.route("//permissions/resources", methods=["OPTIONS", "POST"]) + @permissions.route("//permissions/resources", methods=["POST"]) def create_client_authz_resource_based_permission(client_id: str): payload = request.get_json() try: @@ -56,7 +56,7 @@ def create_client_authz_resource_based_permission(client_id: str): except: return custom_error("Unknown server error", 500) - @permissions.route("//permissions/management", methods=["OPTIONS", "PUT"]) + @permissions.route("//permissions/management", methods=["PUT"]) def update_client_management_permissions(client_id: str): payload = request.get_json() try: @@ -67,7 +67,7 @@ def update_client_management_permissions(client_id: str): except: return custom_error("Unknown server error", 500) - @permissions.route("//permissions/resources/", methods=["OPTIONS", "PUT"]) + @permissions.route("//permissions/resources/", methods=["PUT"]) def update_client_authz_resource_permission(client_id: str, permission_id): payload = request.get_json() try: @@ -78,7 +78,7 @@ def update_client_authz_resource_permission(client_id: str, permission_id): except: return custom_error("Unknown server error", 500) - #@permissions.route("//permissions/scopes/", methods=["OPTIONS", "PUT"]) + #@permissions.route("//permissions/scopes/", methods=["PUT"]) #def update_client_authz_scope_permissions(client_id: str, scope_id): # payload = request.get_json() # return keycloak_client.update_client_authz_scope_permission(client_id, payload, scope_id) diff --git a/src/blueprints/policies.py b/src/blueprints/policies.py index 46a293d..d0d8f0e 100644 --- a/src/blueprints/policies.py +++ b/src/blueprints/policies.py @@ -7,7 +7,7 @@ def construct_blueprint(keycloak_client): policies = Blueprint('policies', __name__) # -------- Always returns empty ------- - #@policies.route("/policies", methods=["OPTIONS", "GET"]) + #@policies.route("/policies", methods=["GET"]) #def get_policies(): # resource = request.args.get('resource', "") # name = request.args.get('name', "") @@ -17,7 +17,7 @@ def construct_blueprint(keycloak_client): # return keycloak_client.get_policies(resource, name, scope, first, maximum) # --------------- GET ----------------- - @policies.route("//policies", methods=["OPTIONS", "GET"]) + @policies.route("//policies", methods=["GET"]) def get_client_authz_policies(client_id: str): try: response = keycloak_client.get_client_authz_policies(client_id) @@ -29,7 +29,7 @@ def get_client_authz_policies(client_id: str): # --------------- POST ----------------- - @policies.route("//policies/client", methods=["OPTIONS", "POST"]) + @policies.route("//policies/client", methods=["POST"]) def create_client_policy(client_id: str): policy = request.get_json() try: @@ -149,7 +149,7 @@ def create_user_policy(client_id: str): # --------------- UPDATE ----------------- - @policies.route("//policies/", methods=["OPTIONS", "PUT"]) + @policies.route("//policies/", methods=["PUT"]) def update_policy(client_id: str, policy_id: str): policy = request.get_json() try: @@ -162,7 +162,7 @@ def update_policy(client_id: str, policy_id: str): # --------------- DELETE ----------------- - @policies.route("//policies/", methods=["OPTIONS", "DELETE"]) + @policies.route("//policies/", methods=["DELETE"]) def delete_policy(client_id: str ,policy_id: str): try: response = keycloak_client.delete_policy(policy_id, client_id) @@ -175,4 +175,4 @@ def delete_policy(client_id: str ,policy_id: str): def custom_error(message, status_code): return message, status_code - return policies \ No newline at end of file + return policies diff --git a/src/blueprints/resources.py b/src/blueprints/resources.py index ce69015..250e109 100644 --- a/src/blueprints/resources.py +++ b/src/blueprints/resources.py @@ -6,7 +6,7 @@ def construct_blueprint(keycloak_client): keycloak_client = keycloak_client resources = Blueprint('resources', __name__) - @resources.route("//resources", methods=["OPTIONS", "GET"]) + @resources.route("//resources", methods=["GET"]) def get_resources(client_id: str): try: response = keycloak_client.get_resources(client_id) @@ -16,7 +16,7 @@ def get_resources(client_id: str): except: return custom_error("Unknown server error", 500) - @resources.route("/resources/", methods=["OPTIONS", "GET"]) + @resources.route("/resources/", methods=["GET"]) def get_resource(resource_id: str): try: response = keycloak_client.get_resource(resource_id) @@ -26,7 +26,7 @@ def get_resource(resource_id: str): except: return custom_error("Unknown server error", 500) - @resources.route("//resources", methods=["OPTIONS", "POST"]) + @resources.route("//resources", methods=["POST"]) def register_resource(client_id: str ): resource = request.get_json() try: @@ -38,8 +38,8 @@ def register_resource(client_id: str ): return custom_error("Unknown server error", 500) - @resources.route("//register-resources", methods=["POST"]) - def register_and_protect_resources(client_id: str, payload=None ): + @resources.route("//register-resources", methods=["OPTIONS", "POST"]) + def register_and_protect_resources(client_id: str ): """payload = [{ "resource":{ "name": "resource1", @@ -120,7 +120,7 @@ def register_and_protect_resources(client_id: str, payload=None ): return response_list - @resources.route("//delete-resources/", methods=["OPTIONS", "DELETE"]) + @resources.route("//delete-resources/", methods=["DELETE"]) def delete_resource_and_policies(client_id: str, resource_name: str): try: client_policies = keycloak_client.get_client_authz_policies(client_id) @@ -145,7 +145,7 @@ def delete_resource_and_policies(client_id: str, resource_name: str): return custom_error("Unknown server error", 500) - @resources.route("//resources/", methods=["OPTIONS", "PUT"]) + @resources.route("//resources/", methods=["PUT"]) def update_resource(client_id: str, resource_id: str): resource = request.get_json() try: @@ -156,7 +156,7 @@ def update_resource(client_id: str, resource_id: str): except: return custom_error("Unknown server error", 500) - @resources.route("//resources/", methods=["OPTIONS", "DELETE"]) + @resources.route("//resources/", methods=["DELETE"]) def delete_resource(client_id: str, resource_id: str): try: response = keycloak_client.delete_resource(resource_id, client_id) @@ -351,4 +351,4 @@ def _validate_register_resource(item): return None - return resources \ No newline at end of file + return resources From b3514a6da46a3d09b6dc15c34189a894f3558b20 Mon Sep 17 00:00:00 2001 From: flaviorosadme Date: Wed, 8 Nov 2023 18:13:40 +0000 Subject: [PATCH 36/64] fea: commit fixes --- src/blueprints/resources.py | 73 +++++++++++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 4 deletions(-) diff --git a/src/blueprints/resources.py b/src/blueprints/resources.py index 95a0299..9f763e0 100644 --- a/src/blueprints/resources.py +++ b/src/blueprints/resources.py @@ -143,9 +143,6 @@ def delete_resource_and_policies(client_id: str, resource_name: str): except: return custom_error("Unknown server error", 500) - - @resources.route("//resources/", methods=["OPTIONS", "PUT"]) - @resources.route("//register-resources", methods=["OPTIONS", "POST"]) def register_and_protect_resources(client_id: str ): """payload = [{ @@ -253,7 +250,7 @@ def delete_resource_and_policies(client_id: str, resource_name: str): return custom_error("Unknown server error", 500) - @resources.route("//resources/", methods=["PUT"]) + @resources.route("//resources/", methods=["OPTIONS", "PUT"]) def update_resource(client_id: str, resource_id: str): resource = request.get_json() try: @@ -273,6 +270,74 @@ def delete_resource(client_id: str, resource_id: str): return custom_error(error.error_message, error.response_code) except: return custom_error("Unknown server error", 500) + + @resources.route("/create-client", methods=["POST"]) + def create_client(): + payload = request.get_json() + helper_text = """ The following fields are allowed: +clientId*: String +name: String +description: String +rootUrl: String +adminUrl: String +baseUrl: String +surrogateAuthRequired: Boolean +enabled: Boolean +alwaysDisplayInConsole: Boolean +clientAuthenticatorType: String +secret: String +registrationAccessToken: String +defaultRoles: List of [string] +redirectUris: List of [string] +webOrigins: List of [string] +notBefore: Integer +bearerOnly: Boolean +consentRequired: Boolean +standardFlowEnabled: Boolean +implicitFlowEnabled: Boolean +directAccessGrantsEnabled: Boolean +serviceAccountsEnabled: Boolean +oauth2DeviceAuthorizationGrantEnabled: Boolean +authorizationServicesEnabled: Boolean +directGrantsOnly: Boolean +publicClient: Boolean +frontchannelLogout: Boolean +protocol: String +attributes: Map of [string] +authenticationFlowBindingOverrides: Map of [string] +fullScopeAllowed: Boolean +nodeReRegistrationTimeout: Integer +registeredNodes: Map of [integer] +protocolMappers: List of ProtocolMapperRepresentation +clientTemplate: String +useTemplateConfig: Boolean +useTemplateScope: Boolean +useTemplateMappers: Boolean +defaultClientScopes: List of [string] +ClientScopes: List of [string] +authorizationSettings: ResourceServerRepresentation +access: Map of [boolean] +origin: String +resources: List of[Resource Representation]""" + if 'clientId' not in payload: + return custom_error("The field 'client_id' is mandatory", 400) + if 'redirectUris' not in payload: + payload['redirectUris'] = ['*'] + if 'standardFlowEnabled' not in payload: + payload['standardFlowEnabled'] = True + if 'protocol' not in payload: + payload['protocol'] = 'openid-connect' + if 'resources' in payload: + resources = payload['resources'] + del payload['resources'] + keycloak_client.create_client(payload) + return register_and_protect_resources(payload['clientId'], resources) + try: + return keycloak_client.create_client(payload) + except KeycloakPostError as error: + return custom_error(error.error_message, error.response_code) + except: + return custom_error("Unknown server error", 500) def custom_error(message, status_code): return message, status_code From 2d6426cc39c80dc6dcc1f29bede2cb0ae68dfda3 Mon Sep 17 00:00:00 2001 From: flaviorosadme Date: Wed, 8 Nov 2023 18:21:33 +0000 Subject: [PATCH 37/64] feat: more fixes, some endpoint were dounbled --- src/blueprints/resources.py | 107 ------------------------------------ 1 file changed, 107 deletions(-) diff --git a/src/blueprints/resources.py b/src/blueprints/resources.py index 9f763e0..12056f4 100644 --- a/src/blueprints/resources.py +++ b/src/blueprints/resources.py @@ -142,113 +142,6 @@ def delete_resource_and_policies(client_id: str, resource_name: str): return custom_error(error.error_message, error.response_code) except: return custom_error("Unknown server error", 500) - - @resources.route("//register-resources", methods=["OPTIONS", "POST"]) - def register_and_protect_resources(client_id: str ): - """payload = [{ - "resource":{ - "name": "resource1", - "uris": ["/resource1/", "/resource2/"], - 'attributes': {}, - 'scopes': ['view'], - 'ownerManagedAccess': False, - }, - "permissions": { - "user": { - "users":["user1","user2"], - "logic":"NEGATIVE" - }, - "role": { - "roles":["role1","role2"], - "logic":"POSITIVE" - }, - }, - "decisionStrategy": "UNANIMOUS" - }]""" - if payload == None: - payload = request.get_json() - policy_list = [] - - response_list = [] - - for item in payload: - # validate item fields - error = _validate_register_resource(item) - if error: - return custom_error(error, 400) - - resource = item["resource"] - policies = item["permissions"] - decisionStrategy = item['decisionStrategy'] if 'decisionStrategy' in item else "UNANIMOUS" - type = 'urn:' + client_id + ':resources:default' - scopes = resource['scopes'] if 'scopes' in resource and resource['scopes'] != [] else ['access'] - - try: - # reconstruct resource object so it works when user sends unknown fields and to change field names to match what keycloak api expects - resource["name"] = resource["name"].replace(" ", "_") - response_resource = keycloak_client.register_resource( resource, client_id) - for policy_type in policies: - policy = {"name": resource["name"].replace(" ", "") + "_" + policy_type + "_policy"} - if isinstance(policies[policy_type], list): - match policy_type: - case 'user': - policy['users'] = policies[policy_type] - case 'role': - policy['roles'] = policies[policy_type] - case 'aggregated': - policy['policies'] = policies[policy_type] - case 'group': - policy['groups'] = policies[policy_type] - else: - for _key in policies[policy_type]: - policy[_key] = policies[policy_type][_key] - policy_list.append(policy["name"]) - response_policy = keycloak_client.register_general_policy(policy, client_id, policy_type) - - permission_payload = { - "type": "resource", - "name": resource["name"] + "_permission", - "decisionStrategy": decisionStrategy, - "resources": [ - resource["name"] - ], - "policies": policy_list - } - - permission_response = keycloak_client.create_client_authz_resource_based_permission(client_id, permission_payload) - - response_list.append(response_resource) - except KeycloakPostError as error: - return custom_error(error.error_message, error.response_code) - except: - return custom_error("Unknown server error", 500) - return response_list - - - @resources.route("//delete-resources/", methods=["DELETE"]) - def delete_resource_and_policies(client_id: str, resource_name: str): - try: - client_policies = keycloak_client.get_client_authz_policies(client_id) - policy_types = ['user', 'client', 'role', 'time', 'regex', 'group', 'scope', 'aggregated'] - for policy in client_policies: - for policy_type in policy_types: - if policy['name'] == resource_name + '_' + policy_type + '_policy': - keycloak_client.delete_policy(policy['id'], client_id) - permissions = keycloak_client.get_client_resource_permissions(client_id) - for permission in permissions: - if permission['name'] == resource_name +'permission': - keycloak_client.delete_resource_permissions(client_id, permission['id']) - - _resources = keycloak_client.get_resources(client_id) - for resource in _resources: - if resource['name'] == resource_name: - resource_delete_response = keycloak_client.delete_resource(resource['_id'], client_id) - return resource_delete_response - except KeycloakDeleteError as error: - return custom_error(error.error_message, error.response_code) - except: - return custom_error("Unknown server error", 500) - @resources.route("//resources/", methods=["OPTIONS", "PUT"]) def update_resource(client_id: str, resource_id: str): From 8d777486ff491e1b8ddc8e8606afdd9f33abcc1b Mon Sep 17 00:00:00 2001 From: flaviorosadme Date: Wed, 8 Nov 2023 18:29:12 +0000 Subject: [PATCH 38/64] fix: last fix --- src/blueprints/resources.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/blueprints/resources.py b/src/blueprints/resources.py index 12056f4..7060bd2 100644 --- a/src/blueprints/resources.py +++ b/src/blueprints/resources.py @@ -39,7 +39,7 @@ def register_resource(client_id: str ): @resources.route("//register-resources", methods=["OPTIONS", "POST"]) - def register_and_protect_resources(client_id: str ): + def register_and_protect_resources(client_id: str, payload=None ): """payload = [{ "resource":{ "name": "resource1", @@ -60,7 +60,8 @@ def register_and_protect_resources(client_id: str ): }, "decisionStrategy": "UNANIMOUS" }]""" - payload = request.get_json() + if payload == None: + payload = request.get_json() policy_list = [] response_list = [] From 7d57858a9ba308b293bf09937f174415b72ae945 Mon Sep 17 00:00:00 2001 From: Daniel Pimenta Date: Thu, 9 Nov 2023 15:23:58 +0000 Subject: [PATCH 39/64] Update ci --- .github/workflows/docker-publish-develop.yml | 11 ++++++----- .github/workflows/docker-publish-master.yml | 10 ++++++---- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/.github/workflows/docker-publish-develop.yml b/.github/workflows/docker-publish-develop.yml index 3d94ce3..42543fb 100644 --- a/.github/workflows/docker-publish-develop.yml +++ b/.github/workflows/docker-publish-develop.yml @@ -36,9 +36,9 @@ jobs: # https://github.com/sigstore/cosign-installer - name: Install cosign if: github.event_name != 'pull_request' - uses: sigstore/cosign-installer@f3c664df7af409cb4873aa5068053ba9d61a57b6 #v2.6.0 + uses: sigstore/cosign-installer@6e04d228eb30da1757ee4e1dd75a0ec73a653e06 #v3.1.1 with: - cosign-release: 'v1.13.1' + cosign-release: 'v2.1.1' # Workaround: https://github.com/docker/build-push-action/issues/461 - name: Setup Docker buildx @@ -78,7 +78,6 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max - # Sign the resulting Docker image digest except on PRs. # This will only write to the public Rekor transparency log when the Docker # repository is public to avoid leaking data. If you would like to publish @@ -87,10 +86,12 @@ jobs: - name: Sign the published Docker image if: ${{ github.event_name != 'pull_request' }} env: - COSIGN_EXPERIMENTAL: "true" + # https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable + TAGS: ${{ steps.meta.outputs.tags }} + DIGEST: ${{ steps.build-and-push.outputs.digest }} # This step uses the identity token to provision an ephemeral certificate # against the sigstore community Fulcio instance. - run: echo "${{ steps.meta.outputs.tags }}" | xargs -I {} cosign sign {}@${{ steps.build-and-push.outputs.digest }} + run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST} - name: Log into registry ${{ env.DOCKER_REGISTRY }} uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 diff --git a/.github/workflows/docker-publish-master.yml b/.github/workflows/docker-publish-master.yml index 307617f..0bd2d8c 100644 --- a/.github/workflows/docker-publish-master.yml +++ b/.github/workflows/docker-publish-master.yml @@ -37,9 +37,9 @@ jobs: # https://github.com/sigstore/cosign-installer - name: Install cosign if: github.event_name != 'pull_request' - uses: sigstore/cosign-installer@f3c664df7af409cb4873aa5068053ba9d61a57b6 #v2.6.0 + uses: sigstore/cosign-installer@6e04d228eb30da1757ee4e1dd75a0ec73a653e06 #v3.1.1 with: - cosign-release: 'v1.13.1' + cosign-release: 'v2.1.1' # Workaround: https://github.com/docker/build-push-action/issues/461 - name: Setup Docker buildx @@ -87,10 +87,12 @@ jobs: - name: Sign the published Docker image if: ${{ github.event_name != 'pull_request' }} env: - COSIGN_EXPERIMENTAL: "true" + # https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable + TAGS: ${{ steps.meta.outputs.tags }} + DIGEST: ${{ steps.build-and-push.outputs.digest }} # This step uses the identity token to provision an ephemeral certificate # against the sigstore community Fulcio instance. - run: echo "${{ steps.meta.outputs.tags }}" | xargs -I {} cosign sign {}@${{ steps.build-and-push.outputs.digest }} + run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST} - name: Log into registry ${{ env.DOCKER_REGISTRY }} uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 From 11cae2ff9d922e90f200f3f2862e8da01a995550 Mon Sep 17 00:00:00 2001 From: flaviorosadme Date: Fri, 10 Nov 2023 11:40:51 +0000 Subject: [PATCH 40/64] fix: policies fix, response now return client id and resources created --- src/blueprints/resources.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/blueprints/resources.py b/src/blueprints/resources.py index 7060bd2..0fb9314 100644 --- a/src/blueprints/resources.py +++ b/src/blueprints/resources.py @@ -62,8 +62,6 @@ def register_and_protect_resources(client_id: str, payload=None ): }]""" if payload == None: payload = request.get_json() - policy_list = [] - response_list = [] for item in payload: @@ -79,6 +77,7 @@ def register_and_protect_resources(client_id: str, payload=None ): scopes = resource['scopes'] if 'scopes' in resource and resource['scopes'] != [] else ['access'] try: + policy_list = [] # reconstruct resource object so it works when user sends unknown fields and to change field names to match what keycloak api expects resource["name"] = resource["name"].replace(" ", "_") response_resource = keycloak_client.register_resource( resource, client_id) @@ -224,8 +223,8 @@ def create_client(): if 'resources' in payload: resources = payload['resources'] del payload['resources'] - keycloak_client.create_client(payload) - return register_and_protect_resources(payload['clientId'], resources) + created_client = keycloak_client.create_client(payload) + return {'client':created_client, 'resources':register_and_protect_resources(payload['clientId'], resources)} try: return keycloak_client.create_client(payload) except KeycloakPostError as error: From 31e1db00d85d1b02d3acc283b72a68a60f397faa Mon Sep 17 00:00:00 2001 From: flaviorosadme Date: Tue, 14 Nov 2023 14:53:30 +0000 Subject: [PATCH 41/64] feat: create client default to confidential and authorization enabled --- src/blueprints/resources.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/blueprints/resources.py b/src/blueprints/resources.py index 0fb9314..d7fbc3d 100644 --- a/src/blueprints/resources.py +++ b/src/blueprints/resources.py @@ -220,6 +220,20 @@ def create_client(): payload['standardFlowEnabled'] = True if 'protocol' not in payload: payload['protocol'] = 'openid-connect' + if 'publicClient' not in payload: + payload['publicClient'] = False + if 'authorizationServicesEnabled' not in payload: + payload['authorizationServicesEnabled'] = True + if 'serviceAccountsEnabled' not in payload: + payload['serviceAccountsEnabled'] = True + if 'implicitFlowEnabled' not in payload: + payload['implicitFlowEnabled'] = False + if 'directAccessGrantsEnabled' not in payload: + payload['directAccessGrantsEnabled'] = True + if 'standardFlowEnabled' not in payload: + payload['standardFlowEnabled'] = True + if 'frontchannelLogout' not in payload: + payload['frontchannelLogout'] = True if 'resources' in payload: resources = payload['resources'] del payload['resources'] From 9d3aae1a7844d713a62e50e0e5f6ce360b68b41b Mon Sep 17 00:00:00 2001 From: Daniel Pimenta Date: Tue, 14 Nov 2023 17:14:46 +0000 Subject: [PATCH 42/64] Convert to FastAPI --- .flaskenv | 3 - Dockerfile | 17 +- {src => app}/__init __.py | 0 app/dependencies.py | 0 src/app.py => app/main.py | 11 +- app/routers/__init __.py | 0 .../resources.py => app/routers/clients.py | 16 +- app/routers/health.py | 0 {src/blueprints => app/routers}/policies.py | 0 app/routers/resources.py | 29 ++ conf/swagger.json | 255 ------------------ conf/config.ini => config.ini | 0 docker-compose.yml | 0 conf/logging.yaml => logging.yaml | 0 src/blueprints/permissions.py | 89 ------ 15 files changed, 42 insertions(+), 378 deletions(-) delete mode 100644 .flaskenv rename {src => app}/__init __.py (100%) create mode 100644 app/dependencies.py rename src/app.py => app/main.py (90%) create mode 100644 app/routers/__init __.py rename src/blueprints/resources.py => app/routers/clients.py (96%) create mode 100644 app/routers/health.py rename {src/blueprints => app/routers}/policies.py (100%) create mode 100644 app/routers/resources.py delete mode 100644 conf/swagger.json rename conf/config.ini => config.ini (100%) create mode 100644 docker-compose.yml rename conf/logging.yaml => logging.yaml (100%) delete mode 100644 src/blueprints/permissions.py diff --git a/.flaskenv b/.flaskenv deleted file mode 100644 index 912fe18..0000000 --- a/.flaskenv +++ /dev/null @@ -1,3 +0,0 @@ -FLASK_APP=src/app:create_app() -FLASK_ENV=develop -FLASK_DEBUG=0 diff --git a/Dockerfile b/Dockerfile index bde3033..5ece881 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,6 @@ -FROM python:alpine -RUN apk add --no-cache git -RUN mkdir /app -WORKDIR /app -COPY . . -ENV FLASK_APP "src/app:create_app()" -ENV FLASK_ENV local -ENV FLASK_DEBUG 1 -RUN pip install -r requirements.txt -EXPOSE 5566 -CMD [ "python", "-m" , "flask", "run", "--host=0.0.0.0", "--port=5566"] \ No newline at end of file +FROM python:3.9 +WORKDIR /code +COPY ./requirements.txt /code/requirements.txt +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt +COPY ./app /code/app +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] \ No newline at end of file diff --git a/src/__init __.py b/app/__init __.py similarity index 100% rename from src/__init __.py rename to app/__init __.py diff --git a/app/dependencies.py b/app/dependencies.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app.py b/app/main.py similarity index 90% rename from src/app.py rename to app/main.py index 5978c57..842f7e0 100644 --- a/src/app.py +++ b/app/main.py @@ -6,26 +6,22 @@ from random import choice from string import ascii_lowercase -from flask import Flask from flask_swagger_ui import get_swaggerui_blueprint from keycloak import KeycloakConnectionError -from urllib3.exceptions import NewConnectionError -import blueprints.permissions as permissions -import blueprints.policies as policies -import blueprints.resources as resources import identityutils.logger as logger from identityutils.configuration import load_configuration from identityutils.keycloak_client import KeycloakClient from retry.api import retry_call -from flask_healthz import healthz + +from fastapi import FastAPI logger.Logger.get_instance().load_configuration(os.path.join(os.path.dirname(__file__), "../conf/logging.yaml")) logger = logging.getLogger("IDENTITY_API") config_path = os.path.join(os.path.dirname(__file__), "../conf/config.ini") -app = Flask(__name__) +app = FastAPI() app.secret_key = ''.join(choice(ascii_lowercase) for _ in range(30)) # Random key app.config['HEALTHZ'] = { "live": lambda: None, @@ -33,6 +29,7 @@ } def register_endpoints(config, keycloak): + app.register_blueprint(clients.construct_blueprint(keycloak_client=keycloak)) app.register_blueprint(resources.construct_blueprint(keycloak_client=keycloak)) app.register_blueprint(policies.construct_blueprint(keycloak_client=keycloak)) app.register_blueprint(permissions.construct_blueprint(keycloak_client=keycloak)) diff --git a/app/routers/__init __.py b/app/routers/__init __.py new file mode 100644 index 0000000..e69de29 diff --git a/src/blueprints/resources.py b/app/routers/clients.py similarity index 96% rename from src/blueprints/resources.py rename to app/routers/clients.py index 7060bd2..a70a8e3 100644 --- a/src/blueprints/resources.py +++ b/app/routers/clients.py @@ -4,7 +4,7 @@ def construct_blueprint(keycloak_client): keycloak_client = keycloak_client - resources = Blueprint('resources', __name__) + resources = Blueprint('clients', __name__) @resources.route("//resources", methods=["OPTIONS", "GET"]) def get_resources(client_id: str): @@ -16,16 +16,6 @@ def get_resources(client_id: str): except: return custom_error("Unknown server error", 500) - @resources.route("/resources/", methods=["OPTIONS", "GET"]) - def get_resource(resource_id: str): - try: - response = keycloak_client.get_resource(resource_id) - return response - except KeycloakGetError as error: - return custom_error(error.error_message, error.response_code) - except: - return custom_error("Unknown server error", 500) - @resources.route("//resources", methods=["OPTIONS", "POST"]) def register_resource(client_id: str ): resource = request.get_json() @@ -62,7 +52,6 @@ def register_and_protect_resources(client_id: str, payload=None ): }]""" if payload == None: payload = request.get_json() - policy_list = [] response_list = [] @@ -81,7 +70,8 @@ def register_and_protect_resources(client_id: str, payload=None ): try: # reconstruct resource object so it works when user sends unknown fields and to change field names to match what keycloak api expects resource["name"] = resource["name"].replace(" ", "_") - response_resource = keycloak_client.register_resource( resource, client_id) + response_resource = keycloak_client.register_resource(resource, client_id) + policy_list = [] for policy_type in policies: policy = {"name": resource["name"].replace(" ", "") + "_" + policy_type + "_policy"} if isinstance(policies[policy_type], list): diff --git a/app/routers/health.py b/app/routers/health.py new file mode 100644 index 0000000..e69de29 diff --git a/src/blueprints/policies.py b/app/routers/policies.py similarity index 100% rename from src/blueprints/policies.py rename to app/routers/policies.py diff --git a/app/routers/resources.py b/app/routers/resources.py new file mode 100644 index 0000000..9c160a9 --- /dev/null +++ b/app/routers/resources.py @@ -0,0 +1,29 @@ +from flask import Blueprint +from keycloak import KeycloakGetError + + +def construct_blueprint(keycloak_client): + keycloak_client = keycloak_client + resources = Blueprint('resources', __name__) + + @resources.route("/resources", methods=["OPTIONS", "GET"]) + def get_resources(): + try: + response = keycloak_client.get_resources() + return response + except KeycloakGetError as error: + return error.error_message, error.response_code + except: + return "Unknown server error", 500 + + @resources.route("/resources/", methods=["OPTIONS", "GET"]) + def get_resource(resource_id: str): + try: + response = keycloak_client.get_resource(resource_id) + return response + except KeycloakGetError as error: + return error.error_message, error.response_code + except: + return "Unknown server error", 500 + + return resources \ No newline at end of file diff --git a/conf/swagger.json b/conf/swagger.json deleted file mode 100644 index 8a3e183..0000000 --- a/conf/swagger.json +++ /dev/null @@ -1,255 +0,0 @@ -{ - "openapi": "3.0.0", - "info": { - "version": "1.0.0", - "title": "Policy Enforcement Point Interfaces", - "description": "This OpenAPI Document describes the endpoints exposed by Policy Enforcement Point Building Block deployments.

Using this API will allow to register resources that can be protected using both the Login Service and the Policy Decision Point and access them through the Policy Enforcement Endpoint.

As an example this documentation uses \"proxy\" as the configured base URL for Policy Enforcement, but this can be manipulated through configuration parameters." - }, - "tags": [ - { - "name": "Resources", - "description": "Operations to create, modify or delete resources" - } - ], - "paths": { - "/resources": { - "parameters": [ - { - "in": "header", - "name": "Authorization", - "description": "JWT or Bearer Token", - "schema": { - "type": "string" - } - } - ], - "get": { - "tags": [ - "Resources" - ], - "summary": "List all owned resources", - "description": "This operation lists all resources filtered by ownership ID. Ownership ID is extracted from the OpenID Connect Token", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/resource" - } - } - } - } - } - } - }, - "post": { - "tags": [ - "Resources" - ], - "summary": "Creates a new Resource reference in the Platform", - "description": "This operation generates a new resource reference object that can be protected. Ownership ID is set to the unique ID of the End-User", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/new_resource" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/resource" - } - } - } - }, - "401": { - "description": "UNAUTHORIZED" - }, - "404": { - "description": "NOT FOUND" - } - } - } - }, - "/resources/{resource_id}": { - "parameters": [ - { - "in": "path", - "name": "resource_id", - "description": "Unique Resource ID", - "required": true, - "schema": { - "type": "string" - } - }, - { - "in": "header", - "name": "Authorization", - "description": "JWT or Bearer Token", - "schema": { - "type": "string" - } - } - ], - "get": { - "tags": [ - "Resources" - ], - "summary": "Retrieve a specific owned resource", - "description": "This operation retrieves information about an owned resource.", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/resource" - } - } - } - }, - "404": { - "description": "NOT FOUND" - } - } - }, - "put": { - "tags": [ - "Resources" - ], - "summary": "Updates an existing Resource reference in the Platform", - "description": "This operation updates an existing 'owned' resource reference. ", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/resource" - } - } - } - }, - "responses": { - "200": { - "description": "OK" - }, - "401": { - "description": "UNAUTHORIZED" - }, - "404": { - "description": "NOT FOUND" - } - } - }, - "delete": { - "tags": [ - "Resources" - ], - "summary": "Deletes an owned Resource Reference from the Platform", - "description": "This operation removes an existing Resource reference owned by the user.", - "responses": { - "200": { - "description": "OK" - }, - "401": { - "description": "UNAUTHORIZED" - }, - "404": { - "description": "NOT FOUND" - } - } - } - } - }, - "components": { - "responses": { - "UMAUnauthorized": { - "description": "Unauthorized access request.", - "headers": { - "WWW-Authenticate": { - "schema": { - "type": "string" - }, - "description": "'UMA_realm=\"example\",as_uri=\"https://as.example.com\",ticket=\"016f84e8-f9b9-11e0-bd6f-0021cc6004de\"'" - } - } - } - }, - "schemas": { - "new_resource": { - "type": "object", - "properties": { - "name": { - "description": "Human readable name for the resource", - "type": "string", - "example": "My Beautiful Resource" - }, - "icon_uri": { - "description": "Protected uri of the resource.\n", - "type": "string", - "example": "/wps3/processes/" - }, - "scopes": { - "description": "List of scopes associated with the resource", - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "public", - "myOtherAttr" - ] - } - } - }, - "resource": { - "type": "object", - "properties": { - "ownership_id": { - "description": "UUID of the Owner End-User", - "type": "string", - "format": "uuid", - "example": "d290f1ee-6c54-4b01-90e6-288571188183" - }, - "id": { - "description": "UUID of the resource", - "type": "string", - "format": "uuid", - "example": "d290f1ee-6c54-4b01-90e6-d701748f0851" - }, - "name": { - "description": "Human readable name for the resource", - "type": "string", - "example": "My Beautiful Resource" - }, - "icon_uri": { - "description": "Protected uri of the resource.\n", - "type": "string", - "example": "/wps3/processes/" - }, - "scopes": { - "description": "List of scopes associated with the resource", - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "public", - "myOtherAttr" - ] - } - } - } - } - } -} diff --git a/conf/config.ini b/config.ini similarity index 100% rename from conf/config.ini rename to config.ini diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e69de29 diff --git a/conf/logging.yaml b/logging.yaml similarity index 100% rename from conf/logging.yaml rename to logging.yaml diff --git a/src/blueprints/permissions.py b/src/blueprints/permissions.py deleted file mode 100644 index 446c8cf..0000000 --- a/src/blueprints/permissions.py +++ /dev/null @@ -1,89 +0,0 @@ -from flask import Blueprint, request -from keycloak import KeycloakGetError, KeycloakPostError, KeycloakPutError - - -def construct_blueprint(keycloak_client): - keycloak_client = keycloak_client - permissions = Blueprint('permissions', __name__) - - @permissions.route("//permissions", methods=["OPTIONS", "GET"]) - def get_client_authz_permissions(client_id: str): - try: - response = keycloak_client.get_client_authz_permissions(client_id) - return response - except KeycloakGetError as error: - return custom_error(error.error_message, error.response_code) - except: - return custom_error("Unknown server error", 500) - - @permissions.route("//permissions/management", methods=["OPTIONS", "GET"]) - def get_client_management_permissions(client_id: str): - try: - response = keycloak_client.get_client_management_permissions(client_id) - return response - except KeycloakGetError as error: - return custom_error(error.error_message, error.response_code) - except: - return custom_error("Unknown server error", 500) - - @permissions.route("//permissions/resources", methods=["OPTIONS", "GET"]) - def get_client_resource_permissions(client_id: str): - try: - response = keycloak_client.get_client_resource_permissions(client_id) - return response - except KeycloakGetError as error: - return custom_error(error.error_message, error.response_code) - except: - return custom_error("Unknown server error", 500) - - #@permissions.route("/client_authz_scope_permissions//", methods=["OPTIONS", "GET"]) - #def get_client_authz_scope_permissions(client_id: str, scope_id: str): - # return keycloak_client.get_client_authz_scope_permissions(client_id, scope_id) - - #@permissions.route("/client_authz_scope_permissions/", methods=["OPTIONS", "POST"]) - #def create_client_authz_scope_based_permissions(client_id: str): - # payload = request.get_json() - # return keycloak_client.create_client_authz_scope_based_permission(client_id, payload) - - @permissions.route("//permissions/resources", methods=["OPTIONS", "POST"]) - def create_client_authz_resource_based_permission(client_id: str): - payload = request.get_json() - try: - response = keycloak_client.create_client_authz_resource_based_permission(client_id, payload) - return response - except KeycloakPostError as error: - return custom_error(error.error_message, error.response_code) - except: - return custom_error("Unknown server error", 500) - - @permissions.route("//permissions/management", methods=["OPTIONS", "PUT"]) - def update_client_management_permissions(client_id: str): - payload = request.get_json() - try: - response = keycloak_client.update_client_management_permissions(client_id, payload) - return response - except KeycloakPostError as error: - return custom_error(error.error_message, error.response_code) - except: - return custom_error("Unknown server error", 500) - - @permissions.route("//permissions/resources/", methods=["OPTIONS", "PUT"]) - def update_client_authz_resource_permission(client_id: str, permission_id): - payload = request.get_json() - try: - response = keycloak_client.update_client_authz_resource_permission(client_id, payload, permission_id) - return response - except KeycloakPutError as error: - return custom_error(error.error_message, error.response_code) - except: - return custom_error("Unknown server error", 500) - - #@permissions.route("//permissions/scopes/", methods=["OPTIONS", "PUT"]) - #def update_client_authz_scope_permissions(client_id: str, scope_id): - # payload = request.get_json() - # return keycloak_client.update_client_authz_scope_permission(client_id, payload, scope_id) - - def custom_error(message, status_code): - return message, status_code - - return permissions From 5931be49a60a49166345cb34128562a08a1f4ef7 Mon Sep 17 00:00:00 2001 From: Daniel Pimenta Date: Tue, 14 Nov 2023 17:17:01 +0000 Subject: [PATCH 43/64] Convert to FastAPI --- .dockerignore | 6 +- .gitignore | 1 + Dockerfile | 6 +- README.md | 13 +- app/__init __.py | 1 - app/dependencies.py | 37 ++++ app/main.py | 102 ++++------ app/routers/clients.py~HEAD | 360 ++++++++++++++++++++++++++++++++++++ app/routers/health.py | 37 ++++ app/routers/policies.py | 202 +++----------------- app/routers/resources.py | 42 ++--- config.ini | 4 +- docker-compose.yml | 42 +++++ logging.yaml | 4 +- requirements.txt | 16 +- 15 files changed, 579 insertions(+), 294 deletions(-) create mode 100644 app/routers/clients.py~HEAD diff --git a/.dockerignore b/.dockerignore index b0cde71..f4be6c6 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,5 @@ * -!src -!conf +!app !requirements.* -!.flaskenv +!config.ini +!logging.yaml \ No newline at end of file diff --git a/.gitignore b/.gitignore index cbb3138..7530f86 100644 --- a/.gitignore +++ b/.gitignore @@ -62,3 +62,4 @@ env/ .vscode .idea +postgres \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 5ece881..4cf6fc4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,8 @@ -FROM python:3.9 +FROM python:3.12 WORKDIR /code COPY ./requirements.txt /code/requirements.txt RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt +COPY ./config.ini /code/config.ini +COPY ./logging.yaml /code/logging.yaml COPY ./app /code/app -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] \ No newline at end of file +CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "8080"] \ No newline at end of file diff --git a/README.md b/README.md index f8cc1f5..e017a7f 100644 --- a/README.md +++ b/README.md @@ -38,15 +38,12 @@ - [Table of Contents](#table-of-contents) - [About The Project](#about-the-project) - - [Built With](#built-with) + - [Built With](#built-with) - [Getting Started](#getting-started) - - [Prerequisites](#prerequisites) - - [Installation](#installation) - - [Testing](#testing) + - [Prerequisites](#prerequisites) + - [Installation](#installation) - [Documentation](#documentation) - [Usage](#usage) - - [Running the template service](#running-the-template-service) - - [Upgrading Gradle Wrapper](#upgrading-gradle-wrapper) - [Roadmap](#roadmap) - [Contributing](#contributing) - [License](#license) @@ -109,7 +106,7 @@ cd um-identity-api 5.1 Run locally with Python ```sh pip install -r requirements.txt - python -m "flask" run --host=0.0.0.0 --port=5566 + uvicorn app.main:app ``` 5.2 Run locally with Docker ```sh @@ -182,4 +179,4 @@ Project Link: [https://github.com/EOEPCA/um-identity-api](https://github.com/EOE [issues-url]: https://github.com/EOEPCA/um-identity-api/issues [license-shield]: https://img.shields.io/github/license/EOEPCA/um-identity-apisvg?style=flat-square [license-url]: https://github.com/EOEPCA/um-identity-api/blob/master/LICENSE -[build-shield]: https://www.travis-ci.com/EOEPCA/um-identity-apisvg?branch=master \ No newline at end of file +[build-shield]: https://www.travis-ci.com/EOEPCA/um-identity-apisvg?branch=master diff --git a/app/__init __.py b/app/__init __.py index 819ccf0..e69de29 100644 --- a/app/__init __.py +++ b/app/__init __.py @@ -1 +0,0 @@ -from .app import create_app diff --git a/app/dependencies.py b/app/dependencies.py index e69de29..44941e9 100644 --- a/app/dependencies.py +++ b/app/dependencies.py @@ -0,0 +1,37 @@ +import logging +import os + +import identityutils.logger as logger +from identityutils.configuration import load_configuration +from identityutils.keycloak_client import KeycloakClient +from keycloak import KeycloakConnectionError +from retry.api import retry_call +from urllib3.exceptions import NewConnectionError + +logger.Logger.get_instance().load_configuration(os.path.join(os.path.dirname(__file__), "../logging.yaml")) +logger = logging.getLogger("IDENTITY_API") + + +def __create_keycloak_client(): + config_path = os.path.join(os.path.dirname(__file__), "../config.ini") + config = load_configuration(config_path) + auth_server_url = config.get("Keycloak", "auth_server_url") + realm = config.get("Keycloak", "realm") + logger.info("Starting Keycloak client for: " + auth_server_url + "/realms/" + realm) + return KeycloakClient( + server_url=auth_server_url, + realm=realm, + username=config.get("Keycloak", "admin_username"), + password=config.get("Keycloak", "admin_password") + ) + + +def keycloak_client(): + return retry_call( + __create_keycloak_client, + exceptions=(KeycloakConnectionError, NewConnectionError), + delay=0.5, + backoff=1.2, + jitter=(1, 2), + logger=logger + ) \ No newline at end of file diff --git a/app/main.py b/app/main.py index 842f7e0..d204a8a 100644 --- a/app/main.py +++ b/app/main.py @@ -1,66 +1,40 @@ #!/usr/bin/env python3 -import json -import logging -import os -from random import choice -from string import ascii_lowercase - -from flask_swagger_ui import get_swaggerui_blueprint -from keycloak import KeycloakConnectionError - -import identityutils.logger as logger -from identityutils.configuration import load_configuration -from identityutils.keycloak_client import KeycloakClient -from retry.api import retry_call - -from fastapi import FastAPI - -logger.Logger.get_instance().load_configuration(os.path.join(os.path.dirname(__file__), "../conf/logging.yaml")) -logger = logging.getLogger("IDENTITY_API") - -config_path = os.path.join(os.path.dirname(__file__), "../conf/config.ini") - -app = FastAPI() -app.secret_key = ''.join(choice(ascii_lowercase) for _ in range(30)) # Random key -app.config['HEALTHZ'] = { - "live": lambda: None, - "ready": lambda: None -} - -def register_endpoints(config, keycloak): - app.register_blueprint(clients.construct_blueprint(keycloak_client=keycloak)) - app.register_blueprint(resources.construct_blueprint(keycloak_client=keycloak)) - app.register_blueprint(policies.construct_blueprint(keycloak_client=keycloak)) - app.register_blueprint(permissions.construct_blueprint(keycloak_client=keycloak)) - app.register_blueprint(healthz, url_prefix="/health") - swagger_spec_resources = json.load(open(os.path.join(os.path.dirname(__file__), "../conf/swagger.json"))) - swaggerui_resources_blueprint = get_swaggerui_blueprint( - config.get('Swagger', 'swagger_url'), - config.get('Swagger', 'swagger_api_url'), - config={ - 'app_name': config.get('Swagger', 'swagger_app_name'), - 'spec': swagger_spec_resources - }, - ) - app.register_blueprint(swaggerui_resources_blueprint) - - -def keycloak_client(config): - auth_server_url = config.get("Keycloak", "auth_server_url") - realm = config.get("Keycloak", "realm") - logger.info("Starting Keycloak client for: " + str(auth_server_url) + " realm: " + str(realm)) - return KeycloakClient(server_url=auth_server_url, - realm=realm, - username=config.get("Keycloak", "admin_username"), - password=config.get("Keycloak", "admin_password") - ) - - -def create_app(): - """Create a Flask application using the app factory pattern.""" - config = load_configuration(config_path) - keycloak = retry_call(keycloak_client, fargs=[config], exceptions=(KeycloakConnectionError, NewConnectionError), - delay=0.5, backoff=1.2, jitter=(1, 2), logger=logger) - register_endpoints(config, keycloak) - return app \ No newline at end of file +import uvicorn +from fastapi.middleware.cors import CORSMiddleware +from fastapi import FastAPI, Depends +from fastapi.encoders import jsonable_encoder +from fastapi.responses import JSONResponse + +from app.dependencies import keycloak_client +from app.routers import clients, health, policies, resources + + +app = FastAPI(dependencies=[Depends(keycloak_client)]) +app.add_middleware( + CORSMiddleware, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + allow_origins=["*"], +) +app.include_router(clients.router) +app.include_router(health.router) +app.include_router(policies.router) +app.include_router(resources.router) + + +@app.exception_handler(500) +async def internal_exception_handler(): + return JSONResponse(status_code=500, content=jsonable_encoder({"code": 500, "msg": "Internal Server Error"})) + +@app.exception_handler(400) +async def bad_request_handler(): + return JSONResponse(status_code=400, content=jsonable_encoder({"code": 400, "msg": "Bad request"})) + +def main() -> None: + """Entrypoint to invoke when this module is invoked on the remote server.""" + uvicorn.run("main:app", host="0.0.0.0") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/app/routers/clients.py~HEAD b/app/routers/clients.py~HEAD new file mode 100644 index 0000000..fe36684 --- /dev/null +++ b/app/routers/clients.py~HEAD @@ -0,0 +1,360 @@ +from flask import Blueprint, request +from keycloak import KeycloakDeleteError, KeycloakGetError, KeycloakPostError, KeycloakPutError + + +def construct_blueprint(keycloak_client): + keycloak_client = keycloak_client + resources = Blueprint('clients', __name__) + + @resources.route("//resources", methods=["OPTIONS", "GET"]) + def get_resources(client_id: str): + try: + response = keycloak_client.get_resources(client_id) + return response + except KeycloakGetError as error: + return custom_error(error.error_message, error.response_code) + except: + return custom_error("Unknown server error", 500) + + @resources.route("//resources", methods=["OPTIONS", "POST"]) + def register_resource(client_id: str ): + resource = request.get_json() + try: + response = keycloak_client.register_resource(resource, client_id) + return response + except KeycloakPostError as error: + return custom_error(error.error_message, error.response_code) + except: + return custom_error("Unknown server error", 500) + + + @resources.route("//register-resources", methods=["OPTIONS", "POST"]) + def register_and_protect_resources(client_id: str, payload=None ): + """payload = [{ + "resource":{ + "name": "resource1", + "uris": ["/resource1/", "/resource2/"], + 'attributes': {}, + 'scopes': ['view'], + 'ownerManagedAccess': False, + }, + "permissions": { + "user": { + "users":["user1","user2"], + "logic":"NEGATIVE" + }, + "role": { + "roles":["role1","role2"], + "logic":"POSITIVE" + }, + }, + "decisionStrategy": "UNANIMOUS" + }]""" + if payload == None: + payload = request.get_json() +<<<<<<< HEAD:app/routers/clients.py + +======= +>>>>>>> 31e1db00d85d1b02d3acc283b72a68a60f397faa:src/blueprints/resources.py + response_list = [] + + for item in payload: + # validate item fields + error = _validate_register_resource(item) + if error: + return custom_error(error, 400) + + resource = item["resource"] + policies = item["permissions"] + decisionStrategy = item['decisionStrategy'] if 'decisionStrategy' in item else "UNANIMOUS" + type = 'urn:' + client_id + ':resources:default' + scopes = resource['scopes'] if 'scopes' in resource and resource['scopes'] != [] else ['access'] + + try: + policy_list = [] + # reconstruct resource object so it works when user sends unknown fields and to change field names to match what keycloak api expects + resource["name"] = resource["name"].replace(" ", "_") + response_resource = keycloak_client.register_resource(resource, client_id) + policy_list = [] + for policy_type in policies: + policy = {"name": resource["name"].replace(" ", "") + "_" + policy_type + "_policy"} + if isinstance(policies[policy_type], list): + match policy_type: + case 'user': + policy['users'] = policies[policy_type] + case 'role': + policy['roles'] = policies[policy_type] + case 'aggregated': + policy['policies'] = policies[policy_type] + case 'group': + policy['groups'] = policies[policy_type] + else: + for _key in policies[policy_type]: + policy[_key] = policies[policy_type][_key] + policy_list.append(policy["name"]) + response_policy = keycloak_client.register_general_policy(policy, client_id, policy_type) + + permission_payload = { + "type": "resource", + "name": resource["name"] + "_permission", + "decisionStrategy": decisionStrategy, + "resources": [ + resource["name"] + ], + "policies": policy_list + } + + permission_response = keycloak_client.create_client_authz_resource_based_permission(client_id, permission_payload) + + response_list.append(response_resource) + except KeycloakPostError as error: + return custom_error(error.error_message, error.response_code) + except: + return custom_error("Unknown server error", 500) + return response_list + + + @resources.route("//delete-resources/", methods=["OPTIONS", "DELETE"]) + def delete_resource_and_policies(client_id: str, resource_name: str): + try: + client_policies = keycloak_client.get_client_authz_policies(client_id) + policy_types = ['user', 'client', 'role', 'time', 'regex', 'group', 'scope', 'aggregated'] + for policy in client_policies: + for policy_type in policy_types: + if policy['name'] == resource_name + '_' + policy_type + '_policy': + keycloak_client.delete_policy(policy['id'], client_id) + permissions = keycloak_client.get_client_resource_permissions(client_id) + for permission in permissions: + if permission['name'] == resource_name +'permission': + keycloak_client.delete_resource_permissions(client_id, permission['id']) + + _resources = keycloak_client.get_resources(client_id) + for resource in _resources: + if resource['name'] == resource_name: + resource_delete_response = keycloak_client.delete_resource(resource['_id'], client_id) + return resource_delete_response + except KeycloakDeleteError as error: + return custom_error(error.error_message, error.response_code) + except: + return custom_error("Unknown server error", 500) + + @resources.route("//resources/", methods=["OPTIONS", "PUT"]) + def update_resource(client_id: str, resource_id: str): + resource = request.get_json() + try: + response = keycloak_client.update_resource(resource_id, resource, client_id) + return response + except KeycloakPutError as error: + return custom_error(error.error_message, error.response_code) + except: + return custom_error("Unknown server error", 500) + + @resources.route("//resources/", methods=["OPTIONS", "DELETE"]) + def delete_resource(client_id: str, resource_id: str): + try: + response = keycloak_client.delete_resource(resource_id, client_id) + return response + except KeycloakDeleteError as error: + return custom_error(error.error_message, error.response_code) + except: + return custom_error("Unknown server error", 500) + + @resources.route("/create-client", methods=["POST"]) + def create_client(): + payload = request.get_json() + helper_text = """ The following fields are allowed: +clientId*: String +name: String +description: String +rootUrl: String +adminUrl: String +baseUrl: String +surrogateAuthRequired: Boolean +enabled: Boolean +alwaysDisplayInConsole: Boolean +clientAuthenticatorType: String +secret: String +registrationAccessToken: String +defaultRoles: List of [string] +redirectUris: List of [string] +webOrigins: List of [string] +notBefore: Integer +bearerOnly: Boolean +consentRequired: Boolean +standardFlowEnabled: Boolean +implicitFlowEnabled: Boolean +directAccessGrantsEnabled: Boolean +serviceAccountsEnabled: Boolean +oauth2DeviceAuthorizationGrantEnabled: Boolean +authorizationServicesEnabled: Boolean +directGrantsOnly: Boolean +publicClient: Boolean +frontchannelLogout: Boolean +protocol: String +attributes: Map of [string] +authenticationFlowBindingOverrides: Map of [string] +fullScopeAllowed: Boolean +nodeReRegistrationTimeout: Integer +registeredNodes: Map of [integer] +protocolMappers: List of ProtocolMapperRepresentation +clientTemplate: String +useTemplateConfig: Boolean +useTemplateScope: Boolean +useTemplateMappers: Boolean +defaultClientScopes: List of [string] +ClientScopes: List of [string] +authorizationSettings: ResourceServerRepresentation +access: Map of [boolean] +origin: String +resources: List of[Resource Representation]""" + if 'clientId' not in payload: + return custom_error("The field 'client_id' is mandatory", 400) + if 'redirectUris' not in payload: + payload['redirectUris'] = ['*'] + if 'standardFlowEnabled' not in payload: + payload['standardFlowEnabled'] = True + if 'protocol' not in payload: + payload['protocol'] = 'openid-connect' + if 'publicClient' not in payload: + payload['publicClient'] = False + if 'authorizationServicesEnabled' not in payload: + payload['authorizationServicesEnabled'] = True + if 'serviceAccountsEnabled' not in payload: + payload['serviceAccountsEnabled'] = True + if 'implicitFlowEnabled' not in payload: + payload['implicitFlowEnabled'] = False + if 'directAccessGrantsEnabled' not in payload: + payload['directAccessGrantsEnabled'] = True + if 'standardFlowEnabled' not in payload: + payload['standardFlowEnabled'] = True + if 'frontchannelLogout' not in payload: + payload['frontchannelLogout'] = True + if 'resources' in payload: + resources = payload['resources'] + del payload['resources'] + created_client = keycloak_client.create_client(payload) + return {'client':created_client, 'resources':register_and_protect_resources(payload['clientId'], resources)} + try: + return keycloak_client.create_client(payload) + except KeycloakPostError as error: + return custom_error(error.error_message, error.response_code) + except: + return custom_error("Unknown server error", 500) + + def custom_error(message, status_code): + return message, status_code + + def _validate_register_resource(item): + payload_minimum_example = """ + payload example -> + [{ + "resource":{ + "name": "resource1", + "uris": ["/resource1/", "/resource2/"] + }, + "permissions": { + "user": ["user1","user2"], + } + }] + """ + time_options = """time must be a dictionary with one of: + "notAfter":"1970-01-01 00:00:00" + "notBefore":"1970-01-01 00:00:00" + "dayMonth": + "dayMonthEnd": + "month": + "monthEnd": + "year": + "yearEnd": + "hour": + "hourEnd": + "minute": + "minuteEnd":""" + + policy_types = ['user', 'client', 'role', 'time', 'regex', 'group', 'scope', 'aggregated'] + resource_accepted_fields = ['name','uris','attributes', 'ownerManagedAccess', 'resource_scopes', 'type'] + policy_accepted_fields = ['logic', 'decisionStrategy', 'name', 'description', 'groupsClaim', 'targetClaim'] + time_accepted_fields = ["notAfter","notBefore","dayMonth","dayMonthEnd","month","monthEnd","year","yearEnd","hour","hourEnd","minute","minuteEnd"] + if 'resource' not in item: + return 'Resource field required. ' + payload_minimum_example + if 'permissions' not in item or item['permissions'] == {}: + return 'Permissions field required. ' + payload_minimum_example + if 'name' not in item['resource']: + return 'Resource name required. '+ payload_minimum_example + if 'uris' not in item['resource']: + return 'Resource uris required. '+ payload_minimum_example + for resource_key in item['resource']: + if resource_key in resource_accepted_fields: + continue + else: + return 'There are fields not accepted in "resource"' + + for key in item['permissions']: + if not isinstance(item['permissions'][key], list) and not isinstance(item['permissions'][key], dict): + return "The value of {} ".format(key) + "must be a list of strings or a dictionary" + if key not in policy_types: + return 'Permissions type not found. Needs to be one of the following: ' + ', '.join(policy_types) + if key == 'time': + if not isinstance(item['permissions']['time'], dict): + return time_options + for time_key in item['permissions']['time']: + if time_key in time_accepted_fields or time_key in policy_accepted_fields: + continue + else: + return 'There are fields not accepted or ' + time_options + if key == 'regex': + if not isinstance(item['permissions'][key], dict): + return 'Regex must be a dictionary like {"pattern":}' + for regex_key in item['permissions'][key]: + if regex_key == 'pattern' or regex_key in policy_accepted_fields: + continue + else: + return 'The field "pattern" is not in the regex dictionary or there are fields not accepted' + if key == 'user': + if not isinstance(item['permissions'][key], list): + for user_key in item['permissions'][key]: + if user_key == 'users' or user_key in policy_accepted_fields: + continue + else: + return 'The field "users" is not in the user dictionary or there are fields not accepted' + if key == 'role': + if not isinstance(item['permissions'][key], list): + for role_key in item['permissions'][key]: + if role_key == 'roles' or role_key in policy_accepted_fields: + continue + else: + return 'The field "roles" is not in the role dictionary or there are fields not accepted' + if key == 'group': + if not isinstance(item['permissions'][key], list): + for group_key in item['permissions'][key]: + if group_key == 'groups' or group_key in policy_accepted_fields: + continue + else: + return 'The field "groups" is not in the group dictionary or there are fields not accepted' + if key == 'client-scope': + if not isinstance(item['permissions'][key], list): + for client_scope_key in item['permissions'][key]: + if client_scope_key in policy_accepted_fields: + continue + else: + return 'There are fields not accepted' + + if key == 'aggregated': + if not isinstance(item['permissions'][key], list): + for aggregated_key in item['permissions'][key]: + if aggregated_key == 'policies' or aggregated_key in policy_accepted_fields: + continue + else: + return 'The field "policies" is not in the aggregated dictionary or there are fields not accepted' + + if key == 'client': + if not isinstance(item['permissions'][key], list): + for client_key in item['permissions'][key]: + if client_key in policy_accepted_fields: + continue + else: + return 'There are fields not accepted' + + return None + + return resources \ No newline at end of file diff --git a/app/routers/health.py b/app/routers/health.py index e69de29..656b28e 100644 --- a/app/routers/health.py +++ b/app/routers/health.py @@ -0,0 +1,37 @@ +from fastapi import status, APIRouter +from pydantic import BaseModel + +router = APIRouter( + prefix="/health", + tags=["health"] +) + + +class HealthCheck(BaseModel): + """Response model to validate and return when performing a health check.""" + status: str = "OK" + + +@router.get( + "/liveness", + summary="Perform a liveness Health Check", + response_description="Return HTTP Status Code 200 (OK)", + status_code=status.HTTP_200_OK +) +@router.get( + "/readiness", + summary="Perform a readiness Health Check", + response_description="Return HTTP Status Code 200 (OK)", + status_code=status.HTTP_200_OK +) +def get_health() -> HealthCheck: + """ + ## Perform a Health Check + Endpoint to perform a healthcheck on. This endpoint can primarily be used Docker + to ensure a robust container orchestration and management is in place. Other + services which rely on proper functioning of the API service will not deploy if this + endpoint returns any other HTTP status code except 200 (OK). + Returns: + HealthCheck: Returns a JSON response with the health status + """ + return HealthCheck(status="OK") \ No newline at end of file diff --git a/app/routers/policies.py b/app/routers/policies.py index 46a293d..e603548 100644 --- a/app/routers/policies.py +++ b/app/routers/policies.py @@ -1,178 +1,24 @@ -from flask import Blueprint, request -from keycloak import KeycloakDeleteError, KeycloakGetError, KeycloakPostError, KeycloakPutError - - -def construct_blueprint(keycloak_client): - keycloak_client = keycloak_client - policies = Blueprint('policies', __name__) - - # -------- Always returns empty ------- - #@policies.route("/policies", methods=["OPTIONS", "GET"]) - #def get_policies(): - # resource = request.args.get('resource', "") - # name = request.args.get('name', "") - # scope = request.args.get('uri', "") - # first = int(request.args.get('first', 0)) - # maximum = int(request.args.get('maximum', -1)) - # return keycloak_client.get_policies(resource, name, scope, first, maximum) - # --------------- GET ----------------- - - @policies.route("//policies", methods=["OPTIONS", "GET"]) - def get_client_authz_policies(client_id: str): - try: - response = keycloak_client.get_client_authz_policies(client_id) - return response - except KeycloakGetError as error: - return custom_error(error.error_message, error.response_code) - except: - return custom_error("Unknown server error", 500) - - # --------------- POST ----------------- - - @policies.route("//policies/client", methods=["OPTIONS", "POST"]) - def create_client_policy(client_id: str): - policy = request.get_json() - try: - response = keycloak_client.register_client_policy(policy, client_id) - return response - except KeycloakPostError as error: - return custom_error(error.error_message, error.response_code) - except: - return custom_error("Unknown server error", 500) - - - @policies.route("//policies/aggregated", methods = ["POST"]) - def create_aggregated_policy(client_id: str): - policy = request.get_json() - try: - response = keycloak_client.register_aggregated_policy(policy, client_id) - return response - except KeycloakPostError as error: - return custom_error(error.error_message, error.response_code) - except: - return custom_error("Unknown server error", 500) - - @policies.route("//policies/scope", methods = ["POST"]) - def create_client_scope_policy(client_id: str): - policy = request.get_json() - try: - response = keycloak_client.register_client_scope_policy(policy, client_id) - return response - except KeycloakPostError as error: - return custom_error(error.error_message, error.response_code) - except: - return custom_error("Unknown server error", 500) - - @policies.route("//policies/group", methods = ["POST"]) - def create_group_policy(client_id: str): - policy = request.get_json() - try: - response = keycloak_client.register_group_policy(policy, client_id) - return response - except KeycloakPostError as error: - return custom_error(error.error_message, error.response_code) - except: - return custom_error("Unknown server error", 500) - - @policies.route("//policies/regex", methods = ["POST"]) - def create_regex_policy(client_id: str): - policy = request.get_json() - try: - response = keycloak_client.register_regex_policy(policy, client_id) - return response - except KeycloakPostError as error: - return custom_error(error.error_message, error.response_code) - except: - return custom_error("Unknown server error", 500) - - @policies.route("//policies/role", methods = ["POST"]) - def create_role_policy(client_id: str): - policy = request.get_json() - try: - response = keycloak_client.register_role_policy(policy, client_id) - return response - except KeycloakPostError as error: - return custom_error(error.error_message, error.response_code) - except: - return custom_error("Unknown server error", 500) - - @policies.route("//policies/time", methods = ["POST"]) - def create_time_policy(client_id: str): - # time can be one of: - # "notAfter":"1970-01-01 00:00:00" - # "notBefore":"1970-01-01 00:00:00" - # "dayMonth": - # "dayMonthEnd": - # "month": - # "monthEnd": - # "year": - # "yearEnd": - # "hour": - # "hourEnd": - # "minute": - # "minuteEnd": - possible_times = [ - "notAfter", - "notBefore", - "dayMonth", - "dayMonthEnd", - "month", - "monthEnd", - "year", - "yearEnd", - "hour", - "hourEnd", - "minute", - "minuteEnd" - ] - policy = request.get_json() - try: - response = keycloak_client.register_time_policy(policy, client_id) - return response - except KeycloakPostError as error: - return custom_error(error.error_message, error.response_code) - except: - return custom_error("Unknown server error", 500) - - @policies.route("//policies/user", methods = ["POST"]) - def create_user_policy(client_id: str): - policy = request.get_json() - try: - response = keycloak_client.register_user_policy(policy, client_id) - return response - except KeycloakPostError as error: - return custom_error(error.error_message, error.response_code) - except: - return custom_error("Unknown server error", 500) - - - - # --------------- UPDATE ----------------- - - @policies.route("//policies/", methods=["OPTIONS", "PUT"]) - def update_policy(client_id: str, policy_id: str): - policy = request.get_json() - try: - response = keycloak_client.update_policy(policy_id, policy, client_id) - return response - except KeycloakPutError as error: - return custom_error(error.error_message, error.response_code) - except: - return custom_error("Unknown server error", 500) - - # --------------- DELETE ----------------- - - @policies.route("//policies/", methods=["OPTIONS", "DELETE"]) - def delete_policy(client_id: str ,policy_id: str): - try: - response = keycloak_client.delete_policy(policy_id, client_id) - return response - except KeycloakDeleteError as error: - return custom_error(error.error_message, error.response_code) - except: - return custom_error("Unknown server error", 500) - - def custom_error(message, status_code): - return message, status_code - - return policies \ No newline at end of file +from fastapi import Depends, APIRouter +from pydantic import BaseModel, PositiveInt + +router = APIRouter( + prefix="/policies", + tags=["policies"], +) + +class SearchPolicies(BaseModel): + resource: str = '' + name: str = '' + uri: str = '' + first: PositiveInt = 0 + maximum: int = -1 + +@router.get("/") +def search_policies(keycloak, search_params: SearchPolicies): + return keycloak.get_policies( + search_params.resource, + search_params.name, + search_params.scope, + search_params.first, + search_params.maximum + ) \ No newline at end of file diff --git a/app/routers/resources.py b/app/routers/resources.py index 9c160a9..afd2cc0 100644 --- a/app/routers/resources.py +++ b/app/routers/resources.py @@ -1,29 +1,25 @@ -from flask import Blueprint +from http.client import HTTPException + +from fastapi import APIRouter from keycloak import KeycloakGetError +router = APIRouter( + prefix="/resources", + tags=["resouces"], +) -def construct_blueprint(keycloak_client): - keycloak_client = keycloak_client - resources = Blueprint('resources', __name__) - @resources.route("/resources", methods=["OPTIONS", "GET"]) - def get_resources(): - try: - response = keycloak_client.get_resources() - return response - except KeycloakGetError as error: - return error.error_message, error.response_code - except: - return "Unknown server error", 500 +@router.get("/") +def get_resources(keycloak): + try: + return keycloak.get_resources() + except KeycloakGetError as e: + return HTTPException(e.response_code, e.error_message) - @resources.route("/resources/", methods=["OPTIONS", "GET"]) - def get_resource(resource_id: str): - try: - response = keycloak_client.get_resource(resource_id) - return response - except KeycloakGetError as error: - return error.error_message, error.response_code - except: - return "Unknown server error", 500 - return resources \ No newline at end of file +@router.get("/resources/{resource_id}") +def get_resource(keycloak, resource_id: str): + try: + return keycloak.get_resource(resource_id) + except KeycloakGetError as e: + return HTTPException(e.response_code, e.error_message) \ No newline at end of file diff --git a/config.ini b/config.ini index cbb9eea..6af0e99 100644 --- a/config.ini +++ b/config.ini @@ -1,7 +1,7 @@ [Keycloak] -auth_server_url = http://localhost:8080/ +auth_server_url = http://localhost:80/ admin_username = admin -admin_password = CHANGE ME +admin_password = admin realm = master [Swagger] swagger_url = /swagger-ui diff --git a/docker-compose.yml b/docker-compose.yml index e69de29..6084907 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -0,0 +1,42 @@ +version: "3.5" +services: + identity-api: + build: . + container_name: um-identity-api + environment: + - AUTH_SERVER_URL=http://keycloak:8080/ + ports: + - '8080:8080' + keycloak: + image: quay.io/keycloak/keycloak:22.0.5 + container_name: keycloak + ports: + - "80:8080" + environment: + - KEYCLOAK_LOGLEVEL=DEBUG + - WILDFLY_LOGLEVEL=DEBUG + - KEYCLOAK_ADMIN=admin + - KEYCLOAK_ADMIN_PASSWORD=admin + - KC_PROXY=edge + - KC_LOGLEVEL=WARN + - PROXY_ADDRESS_FORWARDING=true + - KC_HOSTNAME_STRICT=false + - KC_DB=postgres + - KC_DB_URL_HOST=postgres + - KC_DB_PASSWORD=123456 + - KC_DB_USERNAME=keycloak + - KC_DB_URL_PORT=5432 + entrypoint: /opt/keycloak/bin/kc.sh start + restart: on-failure + postgres: + image: postgres:16.0 + container_name: postgres + volumes: + - ./postgres/data:/var/lib/postgresql/data + environment: + - POSTGRES_DB=keycloak + - POSTGRES_USER=keycloak + - POSTGRES_PASSWORD=123456 + - PGPASSWORD=123 + - PGDATA=/var/lib/postgresql/data/keycloak + restart: on-failure diff --git a/logging.yaml b/logging.yaml index 57a2fe9..ec04412 100644 --- a/logging.yaml +++ b/logging.yaml @@ -15,7 +15,7 @@ handlers: log: class: logging.handlers.RotatingFileHandler - filename: ../../logs/identity-api.log + filename: logs/identity-api.log formatter: verbose level: DEBUG maxBytes: 1073741824 ## 1 GB log file size before rotation @@ -26,4 +26,4 @@ loggers: level: DEBUG handlers: [ console, log ] qualname: IDENTITY_API - propagate: false + propagate: false \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 464652f..863deb0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,15 +1,9 @@ -Flask==3.0.0 -WellKnownHandler==0.2.0 -requests==2.31.0 -flask-swagger-ui==4.11.1 +fastapi==0.104.1 +uvicorn==0.24.0.post1 python-keycloak==3.3.0 -mock==5.1.0 pyyaml==6.0.1 -elasticsearch==8.8.0 -lxml==4.9.3 configparser==6.0.0 -waitress==2.1.2 -python-dotenv==1.0.0 retry==0.9.2 -flask-healthz==1.0.0 -identityutils @ git+https://github.com/eoepca/um-identity-service@master \ No newline at end of file +urllib3==2.0.7 +pydantic==2.5.0 +identityutils @ git+https://github.com/eoepca/um-identity-service@master From f4d31c5d92dec07e0f1797fc2df7bc0a4537ffa7 Mon Sep 17 00:00:00 2001 From: flaviorosadme Date: Tue, 14 Nov 2023 17:58:39 +0000 Subject: [PATCH 44/64] changes to models --- app/routers/clients.py | 63 +++++++++++++++++++++++------------------- 1 file changed, 34 insertions(+), 29 deletions(-) diff --git a/app/routers/clients.py b/app/routers/clients.py index 8c91efb..0d5eb29 100644 --- a/app/routers/clients.py +++ b/app/routers/clients.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import Any, List +from typing import Any, List, Optional from fastapi.exceptions import HTTPException from fastapi import APIRouter, Depends @@ -14,12 +14,13 @@ POLICY_TYPES = ['user', 'client', 'role', 'time', 'regex', 'group', 'scope', 'aggregated'] + class Resource(BaseModel): name: str uris: List[str] - attributes: Any = {} - scopes: List[str] - ownerManagedAccess: bool = True + attributes: Optional[Any] = {} + scopes: Optional[List[str]] = None + ownerManagedAccess: Optional[bool] = True class Logic(Enum): @@ -29,18 +30,22 @@ class Logic(Enum): class UserPermission(BaseModel): users: List[str] - logic: Logic + logic: Optional[Logic] = None class RolePermission(BaseModel): users: List[str] - logic: Logic + logic: Optional[Logic] = None class Permission(BaseModel): user: List[UserPermission] role: List[RolePermission] +class Resources(BaseModel): + resource: Resource + permissions: Permission + class DecisionStrategy(Enum): AFFIRMATIVE = 'AFFIRMATIVE' @@ -51,32 +56,32 @@ class DecisionStrategy(Enum): class ResourcePermissions(BaseModel): resource: List[Resource] permissions: List[Permission] - decisionStrategy: DecisionStrategy = DecisionStrategy.UNANIMOUS + decisionStrategy: Optional[DecisionStrategy] = DecisionStrategy.UNANIMOUS class Client(BaseModel): clientId: str - name: str - description: str - rootUrl: str - adminUrl: str - baseUrl: str - secret: str - protocol: str - defaultRoles: List[str] - redirectUris: List[str] - webOrigins: List[str] - bearerOnly: bool - consentRequired: bool - standardFlowEnabled: bool - implicitFlowEnabled: bool - directAccessGrantsEnabled: bool - serviceAccountsEnabled: bool - oauth2DeviceAuthorizationGrantEnabled: bool - authorizationServicesEnabled: bool - directGrantsOnly: bool - publicClient: bool - resources: List[Resource] + name: Optional[str] = None + description: Optional[str] = None + rootUrl: Optional[str] = None + adminUrl: Optional[str] = None + baseUrl: Optional[str] = None + secret: Optional[str] = None + protocol: Optional[str] = None + defaultRoles: Optional[List[str]] = None + redirectUris: Optional[List[str]] = None + webOrigins: Optional[List[str]] = None + bearerOnly: Optional[bool] = None + consentRequired: Optional[bool] = None + standardFlowEnabled: Optional[bool] = None + implicitFlowEnabled: Optional[bool] = None + directAccessGrantsEnabled: Optional[bool] = None + serviceAccountsEnabled: Optional[bool] = None + oauth2DeviceAuthorizationGrantEnabled: Optional[bool] = None + authorizationServicesEnabled: Optional[bool] = None + directGrantsOnly: Optional[bool] = None + publicClient: Optional[bool] = None + resources: Optional[List[Resources]] = None class ResourceBasedPermission(BaseModel): @@ -364,7 +369,7 @@ def delete_resource(keycloak, client_id: str, resource_id: str): return HTTPException(e.response_code, e.error_message) -@router.post("/") +@router.post("") def create_client(keycloak, client: Client): if 'clientId' not in client: return HTTPException(400, "The field 'client_id' is mandatory") From 025490c666aa0cc08acba0c32635748e79628eab Mon Sep 17 00:00:00 2001 From: Daniel Pimenta Date: Tue, 14 Nov 2023 17:59:26 +0000 Subject: [PATCH 45/64] Remove file --- app/routers/clients.py~HEAD | 360 ------------------------------------ 1 file changed, 360 deletions(-) delete mode 100644 app/routers/clients.py~HEAD diff --git a/app/routers/clients.py~HEAD b/app/routers/clients.py~HEAD deleted file mode 100644 index fe36684..0000000 --- a/app/routers/clients.py~HEAD +++ /dev/null @@ -1,360 +0,0 @@ -from flask import Blueprint, request -from keycloak import KeycloakDeleteError, KeycloakGetError, KeycloakPostError, KeycloakPutError - - -def construct_blueprint(keycloak_client): - keycloak_client = keycloak_client - resources = Blueprint('clients', __name__) - - @resources.route("//resources", methods=["OPTIONS", "GET"]) - def get_resources(client_id: str): - try: - response = keycloak_client.get_resources(client_id) - return response - except KeycloakGetError as error: - return custom_error(error.error_message, error.response_code) - except: - return custom_error("Unknown server error", 500) - - @resources.route("//resources", methods=["OPTIONS", "POST"]) - def register_resource(client_id: str ): - resource = request.get_json() - try: - response = keycloak_client.register_resource(resource, client_id) - return response - except KeycloakPostError as error: - return custom_error(error.error_message, error.response_code) - except: - return custom_error("Unknown server error", 500) - - - @resources.route("//register-resources", methods=["OPTIONS", "POST"]) - def register_and_protect_resources(client_id: str, payload=None ): - """payload = [{ - "resource":{ - "name": "resource1", - "uris": ["/resource1/", "/resource2/"], - 'attributes': {}, - 'scopes': ['view'], - 'ownerManagedAccess': False, - }, - "permissions": { - "user": { - "users":["user1","user2"], - "logic":"NEGATIVE" - }, - "role": { - "roles":["role1","role2"], - "logic":"POSITIVE" - }, - }, - "decisionStrategy": "UNANIMOUS" - }]""" - if payload == None: - payload = request.get_json() -<<<<<<< HEAD:app/routers/clients.py - -======= ->>>>>>> 31e1db00d85d1b02d3acc283b72a68a60f397faa:src/blueprints/resources.py - response_list = [] - - for item in payload: - # validate item fields - error = _validate_register_resource(item) - if error: - return custom_error(error, 400) - - resource = item["resource"] - policies = item["permissions"] - decisionStrategy = item['decisionStrategy'] if 'decisionStrategy' in item else "UNANIMOUS" - type = 'urn:' + client_id + ':resources:default' - scopes = resource['scopes'] if 'scopes' in resource and resource['scopes'] != [] else ['access'] - - try: - policy_list = [] - # reconstruct resource object so it works when user sends unknown fields and to change field names to match what keycloak api expects - resource["name"] = resource["name"].replace(" ", "_") - response_resource = keycloak_client.register_resource(resource, client_id) - policy_list = [] - for policy_type in policies: - policy = {"name": resource["name"].replace(" ", "") + "_" + policy_type + "_policy"} - if isinstance(policies[policy_type], list): - match policy_type: - case 'user': - policy['users'] = policies[policy_type] - case 'role': - policy['roles'] = policies[policy_type] - case 'aggregated': - policy['policies'] = policies[policy_type] - case 'group': - policy['groups'] = policies[policy_type] - else: - for _key in policies[policy_type]: - policy[_key] = policies[policy_type][_key] - policy_list.append(policy["name"]) - response_policy = keycloak_client.register_general_policy(policy, client_id, policy_type) - - permission_payload = { - "type": "resource", - "name": resource["name"] + "_permission", - "decisionStrategy": decisionStrategy, - "resources": [ - resource["name"] - ], - "policies": policy_list - } - - permission_response = keycloak_client.create_client_authz_resource_based_permission(client_id, permission_payload) - - response_list.append(response_resource) - except KeycloakPostError as error: - return custom_error(error.error_message, error.response_code) - except: - return custom_error("Unknown server error", 500) - return response_list - - - @resources.route("//delete-resources/", methods=["OPTIONS", "DELETE"]) - def delete_resource_and_policies(client_id: str, resource_name: str): - try: - client_policies = keycloak_client.get_client_authz_policies(client_id) - policy_types = ['user', 'client', 'role', 'time', 'regex', 'group', 'scope', 'aggregated'] - for policy in client_policies: - for policy_type in policy_types: - if policy['name'] == resource_name + '_' + policy_type + '_policy': - keycloak_client.delete_policy(policy['id'], client_id) - permissions = keycloak_client.get_client_resource_permissions(client_id) - for permission in permissions: - if permission['name'] == resource_name +'permission': - keycloak_client.delete_resource_permissions(client_id, permission['id']) - - _resources = keycloak_client.get_resources(client_id) - for resource in _resources: - if resource['name'] == resource_name: - resource_delete_response = keycloak_client.delete_resource(resource['_id'], client_id) - return resource_delete_response - except KeycloakDeleteError as error: - return custom_error(error.error_message, error.response_code) - except: - return custom_error("Unknown server error", 500) - - @resources.route("//resources/", methods=["OPTIONS", "PUT"]) - def update_resource(client_id: str, resource_id: str): - resource = request.get_json() - try: - response = keycloak_client.update_resource(resource_id, resource, client_id) - return response - except KeycloakPutError as error: - return custom_error(error.error_message, error.response_code) - except: - return custom_error("Unknown server error", 500) - - @resources.route("//resources/", methods=["OPTIONS", "DELETE"]) - def delete_resource(client_id: str, resource_id: str): - try: - response = keycloak_client.delete_resource(resource_id, client_id) - return response - except KeycloakDeleteError as error: - return custom_error(error.error_message, error.response_code) - except: - return custom_error("Unknown server error", 500) - - @resources.route("/create-client", methods=["POST"]) - def create_client(): - payload = request.get_json() - helper_text = """ The following fields are allowed: -clientId*: String -name: String -description: String -rootUrl: String -adminUrl: String -baseUrl: String -surrogateAuthRequired: Boolean -enabled: Boolean -alwaysDisplayInConsole: Boolean -clientAuthenticatorType: String -secret: String -registrationAccessToken: String -defaultRoles: List of [string] -redirectUris: List of [string] -webOrigins: List of [string] -notBefore: Integer -bearerOnly: Boolean -consentRequired: Boolean -standardFlowEnabled: Boolean -implicitFlowEnabled: Boolean -directAccessGrantsEnabled: Boolean -serviceAccountsEnabled: Boolean -oauth2DeviceAuthorizationGrantEnabled: Boolean -authorizationServicesEnabled: Boolean -directGrantsOnly: Boolean -publicClient: Boolean -frontchannelLogout: Boolean -protocol: String -attributes: Map of [string] -authenticationFlowBindingOverrides: Map of [string] -fullScopeAllowed: Boolean -nodeReRegistrationTimeout: Integer -registeredNodes: Map of [integer] -protocolMappers: List of ProtocolMapperRepresentation -clientTemplate: String -useTemplateConfig: Boolean -useTemplateScope: Boolean -useTemplateMappers: Boolean -defaultClientScopes: List of [string] -ClientScopes: List of [string] -authorizationSettings: ResourceServerRepresentation -access: Map of [boolean] -origin: String -resources: List of[Resource Representation]""" - if 'clientId' not in payload: - return custom_error("The field 'client_id' is mandatory", 400) - if 'redirectUris' not in payload: - payload['redirectUris'] = ['*'] - if 'standardFlowEnabled' not in payload: - payload['standardFlowEnabled'] = True - if 'protocol' not in payload: - payload['protocol'] = 'openid-connect' - if 'publicClient' not in payload: - payload['publicClient'] = False - if 'authorizationServicesEnabled' not in payload: - payload['authorizationServicesEnabled'] = True - if 'serviceAccountsEnabled' not in payload: - payload['serviceAccountsEnabled'] = True - if 'implicitFlowEnabled' not in payload: - payload['implicitFlowEnabled'] = False - if 'directAccessGrantsEnabled' not in payload: - payload['directAccessGrantsEnabled'] = True - if 'standardFlowEnabled' not in payload: - payload['standardFlowEnabled'] = True - if 'frontchannelLogout' not in payload: - payload['frontchannelLogout'] = True - if 'resources' in payload: - resources = payload['resources'] - del payload['resources'] - created_client = keycloak_client.create_client(payload) - return {'client':created_client, 'resources':register_and_protect_resources(payload['clientId'], resources)} - try: - return keycloak_client.create_client(payload) - except KeycloakPostError as error: - return custom_error(error.error_message, error.response_code) - except: - return custom_error("Unknown server error", 500) - - def custom_error(message, status_code): - return message, status_code - - def _validate_register_resource(item): - payload_minimum_example = """ - payload example -> - [{ - "resource":{ - "name": "resource1", - "uris": ["/resource1/", "/resource2/"] - }, - "permissions": { - "user": ["user1","user2"], - } - }] - """ - time_options = """time must be a dictionary with one of: - "notAfter":"1970-01-01 00:00:00" - "notBefore":"1970-01-01 00:00:00" - "dayMonth": - "dayMonthEnd": - "month": - "monthEnd": - "year": - "yearEnd": - "hour": - "hourEnd": - "minute": - "minuteEnd":""" - - policy_types = ['user', 'client', 'role', 'time', 'regex', 'group', 'scope', 'aggregated'] - resource_accepted_fields = ['name','uris','attributes', 'ownerManagedAccess', 'resource_scopes', 'type'] - policy_accepted_fields = ['logic', 'decisionStrategy', 'name', 'description', 'groupsClaim', 'targetClaim'] - time_accepted_fields = ["notAfter","notBefore","dayMonth","dayMonthEnd","month","monthEnd","year","yearEnd","hour","hourEnd","minute","minuteEnd"] - if 'resource' not in item: - return 'Resource field required. ' + payload_minimum_example - if 'permissions' not in item or item['permissions'] == {}: - return 'Permissions field required. ' + payload_minimum_example - if 'name' not in item['resource']: - return 'Resource name required. '+ payload_minimum_example - if 'uris' not in item['resource']: - return 'Resource uris required. '+ payload_minimum_example - for resource_key in item['resource']: - if resource_key in resource_accepted_fields: - continue - else: - return 'There are fields not accepted in "resource"' - - for key in item['permissions']: - if not isinstance(item['permissions'][key], list) and not isinstance(item['permissions'][key], dict): - return "The value of {} ".format(key) + "must be a list of strings or a dictionary" - if key not in policy_types: - return 'Permissions type not found. Needs to be one of the following: ' + ', '.join(policy_types) - if key == 'time': - if not isinstance(item['permissions']['time'], dict): - return time_options - for time_key in item['permissions']['time']: - if time_key in time_accepted_fields or time_key in policy_accepted_fields: - continue - else: - return 'There are fields not accepted or ' + time_options - if key == 'regex': - if not isinstance(item['permissions'][key], dict): - return 'Regex must be a dictionary like {"pattern":}' - for regex_key in item['permissions'][key]: - if regex_key == 'pattern' or regex_key in policy_accepted_fields: - continue - else: - return 'The field "pattern" is not in the regex dictionary or there are fields not accepted' - if key == 'user': - if not isinstance(item['permissions'][key], list): - for user_key in item['permissions'][key]: - if user_key == 'users' or user_key in policy_accepted_fields: - continue - else: - return 'The field "users" is not in the user dictionary or there are fields not accepted' - if key == 'role': - if not isinstance(item['permissions'][key], list): - for role_key in item['permissions'][key]: - if role_key == 'roles' or role_key in policy_accepted_fields: - continue - else: - return 'The field "roles" is not in the role dictionary or there are fields not accepted' - if key == 'group': - if not isinstance(item['permissions'][key], list): - for group_key in item['permissions'][key]: - if group_key == 'groups' or group_key in policy_accepted_fields: - continue - else: - return 'The field "groups" is not in the group dictionary or there are fields not accepted' - if key == 'client-scope': - if not isinstance(item['permissions'][key], list): - for client_scope_key in item['permissions'][key]: - if client_scope_key in policy_accepted_fields: - continue - else: - return 'There are fields not accepted' - - if key == 'aggregated': - if not isinstance(item['permissions'][key], list): - for aggregated_key in item['permissions'][key]: - if aggregated_key == 'policies' or aggregated_key in policy_accepted_fields: - continue - else: - return 'The field "policies" is not in the aggregated dictionary or there are fields not accepted' - - if key == 'client': - if not isinstance(item['permissions'][key], list): - for client_key in item['permissions'][key]: - if client_key in policy_accepted_fields: - continue - else: - return 'There are fields not accepted' - - return None - - return resources \ No newline at end of file From c5a0042c08538022606e7a56d9ae24b1aad79c9c Mon Sep 17 00:00:00 2001 From: Daniel Pimenta Date: Wed, 15 Nov 2023 16:22:26 +0000 Subject: [PATCH 46/64] Add error handling, pydantic models, files restructuring --- app/error_handling.py | 78 ++ app/{dependencies.py => keycloak_client.py} | 24 +- app/log.py | 7 + app/main.py | 38 +- app/models/__init __.py | 0 app/models/base.py | 7 + app/models/clients.py | 263 +++++++ app/models/policies.py | 11 + app/routers/clients.py | 776 ++++---------------- app/routers/clients_permissions.py | 30 + app/routers/clients_policies.py | 81 ++ app/routers/clients_resources.py | 79 ++ app/routers/health.py | 6 +- app/routers/policies.py | 18 +- app/routers/resources.py | 23 +- config.ini | 6 +- 16 files changed, 751 insertions(+), 696 deletions(-) create mode 100644 app/error_handling.py rename app/{dependencies.py => keycloak_client.py} (57%) create mode 100644 app/log.py create mode 100644 app/models/__init __.py create mode 100644 app/models/base.py create mode 100644 app/models/clients.py create mode 100644 app/models/policies.py create mode 100644 app/routers/clients_permissions.py create mode 100644 app/routers/clients_policies.py create mode 100644 app/routers/clients_resources.py diff --git a/app/error_handling.py b/app/error_handling.py new file mode 100644 index 0000000..eed00cc --- /dev/null +++ b/app/error_handling.py @@ -0,0 +1,78 @@ +import json +from typing import Sequence, Any + +from fastapi import Request, status, FastAPI +from fastapi.encoders import jsonable_encoder +from fastapi.exceptions import RequestValidationError, HTTPException +from fastapi.responses import JSONResponse +from keycloak import KeycloakPostError, KeycloakGetError, KeycloakPutError, KeycloakDeleteError + + +def exception_handler(app: FastAPI) -> None: + @app.middleware("http") + async def keycloak_error_handling(request: Request, call_next): + try: + return await call_next(request) + except (KeycloakGetError, KeycloakPostError, KeycloakPutError, KeycloakDeleteError) as e: + return JSONResponse(status_code=400, content=jsonable_encoder({"code": 400, "msg": f"{json.loads(e.error_message)['errorMessage']}"})) + + @app.exception_handler(500) + async def internal_exception_handler(): + return JSONResponse(status_code=500, content=jsonable_encoder({"code": 500, "msg": "Internal Server Error"})) + + @app.exception_handler(400) + async def bad_request_handler(): + return JSONResponse(status_code=400, content=jsonable_encoder({"code": 400, "msg": "Bad request"})) + + @app.exception_handler(RequestValidationError) + async def request_validation_error_handler(request: Request, exc: RequestValidationError) -> JSONResponse: + errors = jsonable_encoder(exc.errors()) + status_code = ( + status.HTTP_400_BAD_REQUEST + if __is_bad_request(errors) + else status.HTTP_422_UNPROCESSABLE_ENTITY + ) + return JSONResponse( + status_code=status_code, + content={"detail": errors}, + ) + + +def __is_bad_request(errors: Sequence[Any]) -> bool: + """Check if the given error indicates a malformed request.""" + if not len(errors) == 1: + return False + + error_item = errors[0] + + if not isinstance(error_item, dict): + return False + + if not isinstance(error_item.get("loc"), list): + return False + + loc = error_item["loc"] + + if not 1 <= len(loc) <= 2: + return False + + loc_item1 = loc[0] + + if loc_item1 != "body": + return False + + loc_item2 = loc[1] if len(loc) > 1 else None + + if loc_item2: + return False + + if not isinstance(error_item.get("msg"), str): + return False + + msg = error_item["msg"] + + return ( + msg == "field required" + or msg == "value is not a valid dict" + or msg.startswith("Expecting value:") + ) \ No newline at end of file diff --git a/app/dependencies.py b/app/keycloak_client.py similarity index 57% rename from app/dependencies.py rename to app/keycloak_client.py index 44941e9..70a5ba2 100644 --- a/app/dependencies.py +++ b/app/keycloak_client.py @@ -1,15 +1,12 @@ -import logging import os -import identityutils.logger as logger from identityutils.configuration import load_configuration from identityutils.keycloak_client import KeycloakClient from keycloak import KeycloakConnectionError from retry.api import retry_call from urllib3.exceptions import NewConnectionError -logger.Logger.get_instance().load_configuration(os.path.join(os.path.dirname(__file__), "../logging.yaml")) -logger = logging.getLogger("IDENTITY_API") +from app.log import log def __create_keycloak_client(): @@ -17,7 +14,7 @@ def __create_keycloak_client(): config = load_configuration(config_path) auth_server_url = config.get("Keycloak", "auth_server_url") realm = config.get("Keycloak", "realm") - logger.info("Starting Keycloak client for: " + auth_server_url + "/realms/" + realm) + log.info("Starting Keycloak client for: " + auth_server_url + "/realms/" + realm) return KeycloakClient( server_url=auth_server_url, realm=realm, @@ -26,12 +23,11 @@ def __create_keycloak_client(): ) -def keycloak_client(): - return retry_call( - __create_keycloak_client, - exceptions=(KeycloakConnectionError, NewConnectionError), - delay=0.5, - backoff=1.2, - jitter=(1, 2), - logger=logger - ) \ No newline at end of file +keycloak = retry_call( + __create_keycloak_client, + exceptions=(KeycloakConnectionError, NewConnectionError), + delay=0.5, + backoff=1.2, + jitter=(1, 2), + logger=log +) \ No newline at end of file diff --git a/app/log.py b/app/log.py new file mode 100644 index 0000000..4914348 --- /dev/null +++ b/app/log.py @@ -0,0 +1,7 @@ +import logging +import os + +import identityutils.logger as logger + +logger.Logger.get_instance().load_configuration(os.path.join(os.path.dirname(__file__), "../logging.yaml")) +log = logging.getLogger("IDENTITY_API") \ No newline at end of file diff --git a/app/main.py b/app/main.py index d204a8a..5a34532 100644 --- a/app/main.py +++ b/app/main.py @@ -1,16 +1,26 @@ #!/usr/bin/env python3 +import os +from typing import Mapping import uvicorn +from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from fastapi import FastAPI, Depends -from fastapi.encoders import jsonable_encoder -from fastapi.responses import JSONResponse +from fastapi.responses import RedirectResponse +from identityutils.configuration import load_configuration -from app.dependencies import keycloak_client -from app.routers import clients, health, policies, resources +from app.error_handling import exception_handler +from app.routers import clients, health, policies, resources, clients_permissions, clients_resources, clients_policies -app = FastAPI(dependencies=[Depends(keycloak_client)]) +config: Mapping[str, str] = ( + load_configuration(os.path.join(os.path.dirname(__file__), "../config.ini")) +) + +app = FastAPI( + title=config.get("Swagger", "swagger_title"), + description=config.get("Swagger", "swagger_description"), + version=config.get("Swagger", "swagger_version"), +) app.add_middleware( CORSMiddleware, allow_credentials=True, @@ -18,23 +28,25 @@ allow_headers=["*"], allow_origins=["*"], ) +exception_handler(app) app.include_router(clients.router) -app.include_router(health.router) +app.include_router(clients_permissions.router) +app.include_router(clients_policies.router) +app.include_router(clients_resources.router) app.include_router(policies.router) app.include_router(resources.router) +app.include_router(health.router) -@app.exception_handler(500) -async def internal_exception_handler(): - return JSONResponse(status_code=500, content=jsonable_encoder({"code": 500, "msg": "Internal Server Error"})) +@app.get("/", include_in_schema=False) +async def docs_redirect(): + return RedirectResponse(url='/docs') -@app.exception_handler(400) -async def bad_request_handler(): - return JSONResponse(status_code=400, content=jsonable_encoder({"code": 400, "msg": "Bad request"})) def main() -> None: """Entrypoint to invoke when this module is invoked on the remote server.""" uvicorn.run("main:app", host="0.0.0.0") + if __name__ == "__main__": main() \ No newline at end of file diff --git a/app/models/__init __.py b/app/models/__init __.py new file mode 100644 index 0000000..e69de29 diff --git a/app/models/base.py b/app/models/base.py new file mode 100644 index 0000000..aabc57e --- /dev/null +++ b/app/models/base.py @@ -0,0 +1,7 @@ +from pydantic import BaseModel, ConfigDict + + +class APIBaseModel(BaseModel): + model_config = ConfigDict(extra='forbid', use_enum_values=True) + def model_dump(self, exclude_none=True, **kwargs): + return super().model_dump(exclude_none=exclude_none, **kwargs) \ No newline at end of file diff --git a/app/models/clients.py b/app/models/clients.py new file mode 100644 index 0000000..e0ff162 --- /dev/null +++ b/app/models/clients.py @@ -0,0 +1,263 @@ +from enum import Enum +from typing import Any, List, Optional + +from pydantic import PositiveInt, Field, ConfigDict + +from app.models.base import APIBaseModel + +POLICY_TYPES = ['user', 'client', 'role', 'time', 'regex', 'group', 'scope', 'aggregated'] + + +class Logic(Enum): + POSITIVE = 'POSITIVE' + NEGATIVE = 'NEGATIVE' + + +class UserPermission(APIBaseModel): + users: List[str] = Field(None, description="List of usernames") + logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") + + +class RolePermission(APIBaseModel): + roles: List[str] = Field(None, description="List of roles") + logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") + + +class Permission(APIBaseModel): + user: List[str] | List[UserPermission] = Field([], description="User based permission") + role: List[str] | List[RolePermission] = Field([], description="Role based permission") + + +class DecisionStrategy(Enum): + AFFIRMATIVE = 'AFFIRMATIVE' + UNANIMOUS = 'UNANIMOUS' + CONSENSUS = 'CONSENSUS' + + +class Resource(APIBaseModel): + name: str = Field(description="Resource name") + uris: List[str] = Field(description="Resource URIs") + attributes: Optional[Any] = Field({}, description="Resource attributes") + scopes: Optional[List[str]] = Field(["access"], description="Resource scopes") + ownerManagedAccess: Optional[bool] = Field(False, description="Enable/Disable management by the resource owner") + permissions: Optional[Permission] = Field(None, description="Resource permissions") + decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, description="Decision strategy to decide how to apply permissions") + + +class Client(APIBaseModel): + clientId: str = Field(description="Client id") + name: Optional[str] = Field(None, description="Client name") + description: Optional[str] = Field(None, description="Client description") + secret: Optional[str] = Field(None, description="Client secret") + rootUrl: Optional[str] = Field(None, description="Client root URL") + adminUrl: Optional[str] = Field(None, description="Client admin URL") + baseUrl: Optional[str] = Field(None, description="Client base URL") + redirectUris: Optional[List[str]] = Field(['*'], description="Client Redirect URIs") + webOrigins: Optional[List[str]] = Field(['*'], description="Client Web origins") + protocol: Optional[str] = Field('openid-connect', description="Client protocol: openid-connect / SAML") + defaultRoles: Optional[List[str]] = Field(None, description="Client Default roles") + bearerOnly: Optional[bool] = Field(None, description="Enable/Disable Bearer only") + consentRequired: Optional[bool] = Field(None, description="Enable/Disable Consent required") + publicClient: Optional[bool] = Field(None, description="Disable/Enable authentication to the client") + authorizationServicesEnabled: Optional[bool] = Field(None, description="Enable Authorization Services") + serviceAccountsEnabled: Optional[bool] = Field(None, description="Either or not to create a Service Account for the client") + standardFlowEnabled: Optional[bool] = Field(True, description="Enable/Disable Standard Flow") + implicitFlowEnabled: Optional[bool] = Field(None, description="Client name") + directAccessGrantsEnabled: Optional[bool] = Field(None, description="Enable/Disable Direct Access Grants Flow") + oauth2DeviceAuthorizationGrantEnabled: Optional[bool] = Field(None, description="Enable/Disable OAuth2 Device Authorization Grant Flow") + directGrantsOnly: Optional[bool] = Field(None, description="Enable/Disable Direct Grants Flow") + resources: Optional[List[Resource]] = Field([], description="List of resources to be added to the client") + + +class ResourceBasedPermission(APIBaseModel): + logic: Logic + decisionStrategy: DecisionStrategy + name: str + resources: List[str] + policies: List[str] + + +class ClientPolicy(APIBaseModel): + logic: Logic = Logic.POSITIVE + decisionStrategy: DecisionStrategy = DecisionStrategy.UNANIMOUS.value + name: str + clients: List[str] + description: str = "" + + +class AggregatedPolicy(APIBaseModel): + logic: Logic = Logic.POSITIVE + decisionStrategy: DecisionStrategy = DecisionStrategy.UNANIMOUS.value + name: str + policies: List[str] + description: str = "" + + +class ClientScope(APIBaseModel): + id: str + + +class ScopePolicy(APIBaseModel): + logic: Logic = Logic.POSITIVE + decisionStrategy: DecisionStrategy = DecisionStrategy.UNANIMOUS.value + name: str + clientScopes: List[ClientScope] + description: str = "" + + +class Group(APIBaseModel): + id: str + path: str + + +class GroupPolicy(APIBaseModel): + logic: Logic = Logic.POSITIVE + decisionStrategy: DecisionStrategy = DecisionStrategy.UNANIMOUS.value + name: str + groups: List[Group] + groupsClaim: str = "" + description: str = "" + + +class RegexPolicy(APIBaseModel): + logic: Logic = Logic.POSITIVE + decisionStrategy: DecisionStrategy = DecisionStrategy.UNANIMOUS.value + name: str + pattern: str + targetClaim: str = "" + description: str = "" + + +class Role(APIBaseModel): + id: str + + +class RolePolicy(APIBaseModel): + logic: Logic = Logic.POSITIVE + decisionStrategy: DecisionStrategy = DecisionStrategy.UNANIMOUS.value + name: str + roles: List[Role] + description: str = "" + + +class RelativeTimePolicy(APIBaseModel): + logic: Logic = Logic.POSITIVE + decisionStrategy: DecisionStrategy = DecisionStrategy.UNANIMOUS.value + name: str + notAfter: str + notBefore: str + description: str = "" + + +class DayMonthTimePolicy(APIBaseModel): + logic: Logic = Logic.POSITIVE + decisionStrategy: DecisionStrategy = DecisionStrategy.UNANIMOUS.value + name: str + dayMonth: PositiveInt + dayMonthEnd: PositiveInt + description: str = "" + + +class MonthTimePolicy(APIBaseModel): + logic: Logic = Logic.POSITIVE + decisionStrategy: DecisionStrategy = DecisionStrategy.UNANIMOUS.value + name: str + month: PositiveInt + monthEnd: PositiveInt + description: str = "" + + +class YearTimePolicy(APIBaseModel): + logic: Logic = Logic.POSITIVE + decisionStrategy: DecisionStrategy = DecisionStrategy.UNANIMOUS.value + name: str + year: PositiveInt + yearEnd: PositiveInt + description: str = "" + + +class HourTimePolicy(APIBaseModel): + logic: Logic = Logic.POSITIVE + decisionStrategy: DecisionStrategy = DecisionStrategy.UNANIMOUS.value + name: str + hour: PositiveInt + hourEnd: PositiveInt + description: str = "" + + +class MinuteTimePolicy(APIBaseModel): + logic: Logic = Logic.POSITIVE + decisionStrategy: DecisionStrategy = DecisionStrategy.UNANIMOUS.value + name: str + minute: PositiveInt + minuteEnd: PositiveInt + description: str = "" + + +class UserPolicy(APIBaseModel): + logic: Logic = Logic.POSITIVE + decisionStrategy: DecisionStrategy = DecisionStrategy.UNANIMOUS.value + name: str + users: List[str] + + +class PolicyType(Enum): + CLIENT = 'client' + AGGREGATE = 'aggregate' + SCOPE = 'scope' + GROUP = 'group' + REGEX = 'regex' + ROLE = 'role' + TIME = 'time' + + +class ModifyClientPolicy(ClientPolicy): + type: PolicyType + + +class ModifyAggregatedPolicy(AggregatedPolicy): + type: PolicyType + + +class ModifyScopePolicy(ScopePolicy): + type: PolicyType + + +class ModifyGroupPolicy(GroupPolicy): + type: PolicyType + + +class ModifyRegexPolicy(RegexPolicy): + type: PolicyType + + +class ModifyRolePolicy(RolePolicy): + type: PolicyType + + +class ModifyRelativeTimePolicy(RelativeTimePolicy): + type: PolicyType + + +class ModifyDayMonthTimePolicy(DayMonthTimePolicy): + type: PolicyType + + +class ModifyMonthTimePolicy(MonthTimePolicy): + type: PolicyType + + +class ModifyYearTimePolicy(YearTimePolicy): + type: PolicyType + + +class ModifyHourTimePolicy(HourTimePolicy): + type: PolicyType + + +class ModifyMinuteTimePolicy(MinuteTimePolicy): + type: PolicyType + + +class ModifyUserPolicy(UserPolicy): + type: PolicyType \ No newline at end of file diff --git a/app/models/policies.py b/app/models/policies.py new file mode 100644 index 0000000..d2c784b --- /dev/null +++ b/app/models/policies.py @@ -0,0 +1,11 @@ +from pydantic import PositiveInt + +from app.models.base import APIBaseModel + + +class SearchPolicies(APIBaseModel): + resource: str = '' + name: str = '' + uri: str = '' + first: PositiveInt = 0 + maximum: int = -1 \ No newline at end of file diff --git a/app/routers/clients.py b/app/routers/clients.py index 0d5eb29..54dc53d 100644 --- a/app/routers/clients.py +++ b/app/routers/clients.py @@ -1,648 +1,148 @@ -from enum import Enum -from typing import Any, List, Optional +import logging +import os -from fastapi.exceptions import HTTPException -from fastapi import APIRouter, Depends -from keycloak import KeycloakDeleteError, KeycloakGetError, KeycloakPostError, KeycloakPutError -from pydantic import BaseModel, PositiveInt +from fastapi import APIRouter + +from app.keycloak_client import keycloak +from app.log import log +from app.models.clients import Client +from app.routers.clients_resources import register_resources router = APIRouter( prefix="/clients", - tags=["clients"], + tags=["Clients"] ) -POLICY_TYPES = ['user', 'client', 'role', 'time', 'regex', 'group', 'scope', 'aggregated'] - - - -class Resource(BaseModel): - name: str - uris: List[str] - attributes: Optional[Any] = {} - scopes: Optional[List[str]] = None - ownerManagedAccess: Optional[bool] = True - - -class Logic(Enum): - NEGATIVE = 'NEGATIVE' - POSITIVE = 'POSITIVE' - - -class UserPermission(BaseModel): - users: List[str] - logic: Optional[Logic] = None - - -class RolePermission(BaseModel): - users: List[str] - logic: Optional[Logic] = None - - -class Permission(BaseModel): - user: List[UserPermission] - role: List[RolePermission] - -class Resources(BaseModel): - resource: Resource - permissions: Permission - - -class DecisionStrategy(Enum): - AFFIRMATIVE = 'AFFIRMATIVE' - UNANIMOUS = 'UNANIMOUS' - CONSENSUS = 'CONSENSUS' - - -class ResourcePermissions(BaseModel): - resource: List[Resource] - permissions: List[Permission] - decisionStrategy: Optional[DecisionStrategy] = DecisionStrategy.UNANIMOUS - - -class Client(BaseModel): - clientId: str - name: Optional[str] = None - description: Optional[str] = None - rootUrl: Optional[str] = None - adminUrl: Optional[str] = None - baseUrl: Optional[str] = None - secret: Optional[str] = None - protocol: Optional[str] = None - defaultRoles: Optional[List[str]] = None - redirectUris: Optional[List[str]] = None - webOrigins: Optional[List[str]] = None - bearerOnly: Optional[bool] = None - consentRequired: Optional[bool] = None - standardFlowEnabled: Optional[bool] = None - implicitFlowEnabled: Optional[bool] = None - directAccessGrantsEnabled: Optional[bool] = None - serviceAccountsEnabled: Optional[bool] = None - oauth2DeviceAuthorizationGrantEnabled: Optional[bool] = None - authorizationServicesEnabled: Optional[bool] = None - directGrantsOnly: Optional[bool] = None - publicClient: Optional[bool] = None - resources: Optional[List[Resources]] = None - - -class ResourceBasedPermission(BaseModel): - logic: Logic - decisionStrategy: DecisionStrategy - name: str - resources: List[str] - policies: List[str] - - -class ClientPolicy(BaseModel): - logic: Logic = Logic.POSITIVE - decisionStrategy: DecisionStrategy = DecisionStrategy.UNANIMOUS - name: str - clients: List[str] - description: str = "" - - -class AggregatedPolicy(BaseModel): - logic: Logic = Logic.POSITIVE - decisionStrategy: DecisionStrategy = DecisionStrategy.UNANIMOUS - name: str - policies: List[str] - description: str = "" - - -class ClientScope(BaseModel): - id: str - - -class ScopePolicy(BaseModel): - logic: Logic = Logic.POSITIVE - decisionStrategy: DecisionStrategy = DecisionStrategy.UNANIMOUS - name: str - clientScopes: List[ClientScope] - description: str = "" - - -class Group(BaseModel): - id: str - path: str - - -class GroupPolicy(BaseModel): - logic: Logic = Logic.POSITIVE - decisionStrategy: DecisionStrategy = DecisionStrategy.UNANIMOUS - name: str - groups: List[Group] - groupsClaim: str = "" - description: str = "" - - -class RegexPolicy(BaseModel): - logic: Logic = Logic.POSITIVE - decisionStrategy: DecisionStrategy = DecisionStrategy.UNANIMOUS - name: str - pattern: str - targetClaim: str = "" - description: str = "" - - -class Role(BaseModel): - id: str - - -class RolePolicy(BaseModel): - logic: Logic = Logic.POSITIVE - decisionStrategy: DecisionStrategy = DecisionStrategy.UNANIMOUS - name: str - roles: List[Role] - description: str = "" - - -class RelativeTimePolicy(BaseModel): - logic: Logic = Logic.POSITIVE - decisionStrategy: DecisionStrategy = DecisionStrategy.UNANIMOUS - name: str - notAfter: str - notBefore: str - description: str = "" - - -class DayMonthTimePolicy(BaseModel): - logic: Logic = Logic.POSITIVE - decisionStrategy: DecisionStrategy = DecisionStrategy.UNANIMOUS - name: str - dayMonth: PositiveInt - dayMonthEnd: PositiveInt - description: str = "" - - -class MonthTimePolicy(BaseModel): - logic: Logic = Logic.POSITIVE - decisionStrategy: DecisionStrategy = DecisionStrategy.UNANIMOUS - name: str - month: PositiveInt - monthEnd: PositiveInt - description: str = "" - - -class YearTimePolicy(BaseModel): - logic: Logic = Logic.POSITIVE - decisionStrategy: DecisionStrategy = DecisionStrategy.UNANIMOUS - name: str - year: PositiveInt - yearEnd: PositiveInt - description: str = "" - - -class HourTimePolicy(BaseModel): - logic: Logic = Logic.POSITIVE - decisionStrategy: DecisionStrategy = DecisionStrategy.UNANIMOUS - name: str - hour: PositiveInt - hourEnd: PositiveInt - description: str = "" - - -class MinuteTimePolicy(BaseModel): - logic: Logic = Logic.POSITIVE - decisionStrategy: DecisionStrategy = DecisionStrategy.UNANIMOUS - name: str - minute: PositiveInt - minuteEnd: PositiveInt - description: str = "" - - -class UserPolicy(BaseModel): - logic: Logic = Logic.POSITIVE - decisionStrategy: DecisionStrategy = DecisionStrategy.UNANIMOUS - name: str - users: List[str] - - -class PolicyType(Enum): - CLIENT = 'client' - AGGREGATE = 'aggregate' - SCOPE = 'scope' - GROUP = 'group' - REGEX = 'regex' - ROLE = 'role' - TIME = 'time' - - -class ModifyClientPolicy(ClientPolicy): - type: PolicyType - - -class ModifyAggregatedPolicy(AggregatedPolicy): - type: PolicyType - - -class ModifyScopePolicy(ScopePolicy): - type: PolicyType - - -class ModifyGroupPolicy(GroupPolicy): - type: PolicyType - - -class ModifyRegexPolicy(RegexPolicy): - type: PolicyType - - -class ModifyRolePolicy(RolePolicy): - type: PolicyType - - -class ModifyRelativeTimePolicy(RelativeTimePolicy): - type: PolicyType - - -class ModifyDayMonthTimePolicy(DayMonthTimePolicy): - type: PolicyType - - -class ModifyMonthTimePolicy(MonthTimePolicy): - type: PolicyType - - -class ModifyYearTimePolicy(YearTimePolicy): - type: PolicyType - - -class ModifyHourTimePolicy(HourTimePolicy): - type: PolicyType - - -class ModifyMinuteTimePolicy(MinuteTimePolicy): - type: PolicyType - - -class ModifyUserPolicy(UserPolicy): - type: PolicyType - - -@router.post("/{client_id}/resources") -def register_resources(keycloak, client_id: str, resources: List[ResourcePermissions]): - # validate request before trying to register any resource - for item in resources: - error = _validate_register_resource(item) - if error: - return HTTPException(400, error) - response_list = [] - for item in resources: - resource = item["resource"] - resource["name"] = resource["name"].replace(" ", "_") - resource["scopes"] = resource['scopes'] if 'scopes' in resource and resource['scopes'] != [] else ['access'] - policies = item["permissions"] - decision_strategy = item['decisionStrategy'] if 'decisionStrategy' in item else "UNANIMOUS" - try: - response_resource = keycloak.register_resource(resource, client_id) - response_list.append(response_resource) - policy_list = [] - for policy_type in policies: - policy = { - "name": f'{resource["name"]}_{policy_type}_policy' - } - if isinstance(policies[policy_type], list): - match policy_type: - case 'user': - policy['users'] = policies[policy_type] - case 'role': - policy['roles'] = policies[policy_type] - case 'aggregated': - policy['policies'] = policies[policy_type] - case 'group': - policy['groups'] = policies[policy_type] - else: - for _key in policies[policy_type]: - policy[_key] = policies[policy_type][_key] - policy_list.append(policy["name"]) - keycloak.register_general_policy(policy, client_id, policy_type) - permission_payload = { - "type": "resource", - "name": f'{resource["name"]}_permission', - "decisionStrategy": decision_strategy, - "resources": [ - resource["name"] - ], - "policies": policy_list - } - keycloak.create_client_authz_resource_based_permission(client_id, permission_payload) - except KeycloakPostError as e: - return HTTPException(e.response_code, e.error_message) - return response_list - - -@router.delete("/{client_id}/resources/{resource_name}/all") -def delete_resource_and_policies(keycloak, client_id: str, resource_name: str): - try: - # delete policies - client_policies = keycloak.get_client_authz_policies(client_id) - for policy in client_policies: - for policy_type in POLICY_TYPES: - if policy['name'] == f'{resource_name}_{policy_type}_policy': - keycloak.delete_policy(policy['id'], client_id) - # delete permissions - permissions = keycloak.get_client_resource_permissions(client_id) - for permission in permissions: - if permission['name'] == f'{resource_name}_permission': - keycloak.delete_resource_permissions(client_id, permission['id']) - # delete resources - resources = keycloak.get_resources(client_id) - for resource in resources: - if resource['name'] == resource_name: - return keycloak.delete_resource(resource['_id'], client_id) - except KeycloakDeleteError as e: - return HTTPException(e.response_code, e.error_message) - - -@router.put("/{client_id}/resources/{resource_id}") -def update_resource(keycloak, client_id: str, resource_id: str, resource: Resource): - try: - return keycloak.update_resource(resource_id, resource, client_id) - except KeycloakPutError as e: - return HTTPException(e.response_code, e.error_message) - - -@router.delete("/{client_id}/resources/{resource_id}") -def delete_resource(keycloak, client_id: str, resource_id: str): - try: - return keycloak.delete_resource(resource_id, client_id) - except KeycloakDeleteError as e: - return HTTPException(e.response_code, e.error_message) - @router.post("") -def create_client(keycloak, client: Client): - if 'clientId' not in client: - return HTTPException(400, "The field 'client_id' is mandatory") - if 'redirectUris' not in client: - client['redirectUris'] = ['*'] - if 'standardFlowEnabled' not in client: - client['standardFlowEnabled'] = True - if 'protocol' not in client: - client['protocol'] = 'openid-connect' - resources = client['resources'] if 'resources' in client else [] - if 'resources' in client: - del client['resources'] - try: - response_client = keycloak.create_client(client) - if resources: - response_resources = register_resources(client['clientId'], resources) - return { - client: response_client, - resources: response_resources - } +def create_client(client: Client): + resources = client.resources + client_dict = client.model_dump() + if resources: + del client_dict['resources'] + response_client = keycloak.create_client(client_dict) + if resources: + response_resources = register_resources(client.clientId, resources) return { - client: response_client + client: response_client, + resources: response_resources } - except KeycloakPostError as e: - return HTTPException(e.response_code, e.error_message) - - -@router.get("/{client_id}/permissions") -def get_client_authz_permissions(keycloak, client_id: str): - try: - return keycloak.get_client_authz_permissions(client_id) - except KeycloakGetError as e: - return HTTPException(e.response_code, e.error_message) - - -@router.get("/{client_id}/permissions/management") -def get_client_management_permissions(keycloak, client_id: str): - try: - return keycloak.get_client_management_permissions(client_id) - except KeycloakGetError as e: - return HTTPException(e.response_code, e.error_message) - - -@router.get("/{client_id}/permissions/resources") -def get_client_resource_permissions(keycloak, client_id: str): - try: - return keycloak.get_client_resource_permissions(client_id) - except KeycloakGetError as e: - return HTTPException(e.response_code, e.error_message) - - -@router.post("/{client_id}/permissions/resources") -def create_client_authz_resource_based_permission(keycloak, client_id: str, - resource_based_permission: ResourceBasedPermission): - try: - resource_based_permission['type'] = 'resource' - return keycloak.create_client_authz_resource_based_permission(client_id, resource_based_permission) - except KeycloakPostError as e: - return HTTPException(e.response_code, e.error_message) - - -@router.get("/{client_id}/policies") -def get_client_authz_policies(keycloak, client_id: str): - try: - return keycloak.get_client_authz_policies(client_id) - except KeycloakGetError as e: - return HTTPException(e.response_code, e.error_message) - - -@router.post("/{client_id}/policies/client") -def create_client_policy(keycloak, client_id: str, client_policy: ClientPolicy): - client_policy["type"] = "client" - try: - return keycloak.register_client_policy(client_policy, client_id) - except KeycloakPostError as e: - return HTTPException(e.response_code, e.error_message) - - -@router.post("/{client_id}/policies/aggregated") -def create_aggregated_policy(keycloak, client_id: str, aggregated_policy: AggregatedPolicy): - aggregated_policy["type"] = "aggregated" - try: - return keycloak.register_aggregated_policy(aggregated_policy, client_id) - except KeycloakPostError as e: - return HTTPException(e.response_code, e.error_message) - - -@router.post("/{client_id}/policies/scope") -def create_client_scope_policy(keycloak, client_id: str, scope_policy: ScopePolicy): - scope_policy["type"] = "scope" - try: - return keycloak.register_client_scope_policy(scope_policy, client_id) - except KeycloakPostError as e: - return HTTPException(e.response_code, e.error_message) - - -@router.post("/{client_id}/policies/group") -def create_group_policy(keycloak, client_id: str, group_policy: GroupPolicy): - group_policy["type"] = "group" - try: - return keycloak.register_group_policy(group_policy, client_id) - except KeycloakPostError as e: - return HTTPException(e.response_code, e.error_message) - - -@router.post("/{client_id}/policies/regex") -def create_regex_policy(keycloak, client_id: str, regex_policy: RegexPolicy): - regex_policy["type"] = "regex" - try: - return keycloak.register_regex_policy(regex_policy, client_id) - except KeycloakPostError as e: - return HTTPException(e.response_code, e.error_message) - - -@router.post("/{client_id}/policies/role") -def create_role_policy(keycloak, client_id: str, role_policy: RolePolicy): - role_policy["type"] = "role" - try: - return keycloak.register_role_policy(role_policy, client_id) - except KeycloakPostError as e: - return HTTPException(e.response_code, e.error_message) - - -@router.post("/{client_id}/policies/time") -def create_time_policy(keycloak, client_id: str, - time_policy: RelativeTimePolicy | DayMonthTimePolicy | MonthTimePolicy | - YearTimePolicy | HourTimePolicy | MinuteTimePolicy): - time_policy["type"] = "time" - try: - return keycloak.register_time_policy(time_policy, client_id) - except KeycloakPostError as e: - return HTTPException(e.response_code, e.error_message) - - -@router.post("/{client_id}/policies/user") -def create_user_policy(keycloak, client_id: str, user_policy: UserPolicy): - try: - return keycloak.register_user_policy(user_policy, client_id) - except KeycloakPostError as e: - return HTTPException(e.response_code, e.error_message) - - -@router.put("/{client_id}/policies/{policy_id}") -def update_policy(keycloak, client_id: str, policy_id: str, - policy: ModifyClientPolicy | ModifyAggregatedPolicy | ModifyScopePolicy | - ModifyRegexPolicy | ModifyRolePolicy | ModifyRelativeTimePolicy | ModifyDayMonthTimePolicy | - ModifyMonthTimePolicy | ModifyYearTimePolicy | ModifyHourTimePolicy | ModifyMinuteTimePolicy | - ModifyUserPolicy): - try: - return keycloak.update_policy(client_id, policy_id, policy) - except KeycloakPutError as e: - return HTTPException(e.response_code, e.error_message) - - -@router.delete("/{client_id}/policies/{policy_id}") -def delete_policy(keycloak, client_id: str, policy_id: str): - try: - return keycloak.delete_policy(policy_id, client_id) - except KeycloakDeleteError as e: - return HTTPException(e.response_code, e.error_message) - - -def _validate_register_resource(resource_permissions: ResourcePermissions): - payload_minimum_example = """ - payload example -> - [{ - "resource":{ - "name": "resource1", - "uris": ["/resource1/", "/resource2/"] - }, - "permissions": { - "user": ["user1","user2"], - } - }] - """ - time_options = """time must be a dictionary with one of: - "notAfter":"1970-01-01 00:00:00" - "notBefore":"1970-01-01 00:00:00" - "dayMonth": - "dayMonthEnd": - "month": - "monthEnd": - "year": - "yearEnd": - "hour": - "hourEnd": - "minute": - "minuteEnd":""" - - policy_types = ['user', 'client', 'role', 'time', 'regex', 'group', 'scope', 'aggregated'] - resource_accepted_fields = ['name', 'uris', 'attributes', 'ownerManagedAccess', 'resource_scopes', 'type'] - policy_accepted_fields = ['logic', 'decisionStrategy', 'name', 'description', 'groupsClaim', 'targetClaim'] - time_accepted_fields = ["notAfter", "notBefore", "dayMonth", "dayMonthEnd", "month", "monthEnd", "year", "yearEnd", - "hour", "hourEnd", "minute", "minuteEnd"] - if 'resource' not in resource_permissions: - return 'Resource field required. ' + payload_minimum_example - if 'permissions' not in resource_permissions or resource_permissions['permissions'] == {}: - return 'Permissions field required. ' + payload_minimum_example - if 'name' not in resource_permissions['resource']: - return 'Resource name required. ' + payload_minimum_example - if 'uris' not in resource_permissions['resource']: - return 'Resource uris required. ' + payload_minimum_example - for resource_key in resource_permissions['resource']: - if resource_key in resource_accepted_fields: - continue - else: - return 'There are fields not accepted in "resource"' - - for key in resource_permissions['permissions']: - if not isinstance(resource_permissions['permissions'][key], list) and not isinstance( - resource_permissions['permissions'][key], dict): - return "The value of {} ".format(key) + "must be a list of strings or a dictionary" - if key not in policy_types: - return 'Permissions type not found. Needs to be one of the following: ' + ', '.join(policy_types) - if key == 'time': - if not isinstance(resource_permissions['permissions']['time'], dict): - return time_options - for time_key in resource_permissions['permissions']['time']: - if time_key in time_accepted_fields or time_key in policy_accepted_fields: - continue - else: - return 'There are fields not accepted or ' + time_options - if key == 'regex': - if not isinstance(resource_permissions['permissions'][key], dict): - return 'Regex must be a dictionary like {"pattern":}' - for regex_key in resource_permissions['permissions'][key]: - if regex_key == 'pattern' or regex_key in policy_accepted_fields: - continue - else: - return 'The field "pattern" is not in the regex dictionary or there are fields not accepted' - if key == 'user': - if not isinstance(resource_permissions['permissions'][key], list): - for user_key in resource_permissions['permissions'][key]: - if user_key == 'users' or user_key in policy_accepted_fields: - continue - else: - return 'The field "users" is not in the user dictionary or there are fields not accepted' - if key == 'role': - if not isinstance(resource_permissions['permissions'][key], list): - for role_key in resource_permissions['permissions'][key]: - if role_key == 'roles' or role_key in policy_accepted_fields: - continue - else: - return 'The field "roles" is not in the role dictionary or there are fields not accepted' - if key == 'group': - if not isinstance(resource_permissions['permissions'][key], list): - for group_key in resource_permissions['permissions'][key]: - if group_key == 'groups' or group_key in policy_accepted_fields: - continue - else: - return 'The field "groups" is not in the group dictionary or there are fields not accepted' - if key == 'client-scope': - if not isinstance(resource_permissions['permissions'][key], list): - for client_scope_key in resource_permissions['permissions'][key]: - if client_scope_key in policy_accepted_fields: - continue - else: - return 'There are fields not accepted' - - if key == 'aggregated': - if not isinstance(resource_permissions['permissions'][key], list): - for aggregated_key in resource_permissions['permissions'][key]: - if aggregated_key == 'policies' or aggregated_key in policy_accepted_fields: - continue - else: - return 'The field "policies" is not in the aggregated dictionary or there are fields not accepted' - - if key == 'client': - if not isinstance(resource_permissions['permissions'][key], list): - for client_key in resource_permissions['permissions'][key]: - if client_key in policy_accepted_fields: - continue - else: - return 'There are fields not accepted' - - return None \ No newline at end of file + return { + client: response_client + } + + +# def _validate_register_resource(resources: Resources): +# payload_minimum_example = """ +# payload example -> +# [{ +# "resource":{ +# "name": "resource1", +# "uris": ["/resource1/", "/resource2/"] +# }, +# "permissions": { +# "user": ["user1","user2"], +# } +# }] +# """ +# time_options = """time must be a dictionary with one of: +# "notAfter":"1970-01-01 00:00:00" +# "notBefore":"1970-01-01 00:00:00" +# "dayMonth": +# "dayMonthEnd": +# "month": +# "monthEnd": +# "year": +# "yearEnd": +# "hour": +# "hourEnd": +# "minute": +# "minuteEnd":""" +# +# policy_types = ['user', 'client', 'role', 'time', 'regex', 'group', 'scope', 'aggregated'] +# resource_accepted_fields = ['name', 'uris', 'attributes', 'ownerManagedAccess', 'resource_scopes', 'type'] +# policy_accepted_fields = ['logic', 'decisionStrategy', 'name', 'description', 'groupsClaim', 'targetClaim'] +# time_accepted_fields = ["notAfter", "notBefore", "dayMonth", "dayMonthEnd", "month", "monthEnd", "year", "yearEnd", +# "hour", "hourEnd", "minute", "minuteEnd"] +# if 'resource' not in resources: +# return 'Resource field required. ' + payload_minimum_example +# if 'permissions' not in resources or resources['permissions'] == {}: +# return 'Permissions field required. ' + payload_minimum_example +# if 'name' not in resources['resource']: +# return 'Resource name required. ' + payload_minimum_example +# if 'uris' not in resources['resource']: +# return 'Resource uris required. ' + payload_minimum_example +# for resource_key in resources['resource']: +# if resource_key in resource_accepted_fields: +# continue +# else: +# return 'There are fields not accepted in "resource"' +# +# for key in resources['permissions']: +# if not isinstance(resources['permissions'][key], list) and not isinstance( +# resources['permissions'][key], dict): +# return "The value of {} ".format(key) + "must be a list of strings or a dictionary" +# if key not in policy_types: +# return 'Permissions type not found. Needs to be one of the following: ' + ', '.join(policy_types) +# if key == 'time': +# if not isinstance(resources['permissions']['time'], dict): +# return time_options +# for time_key in resources['permissions']['time']: +# if time_key in time_accepted_fields or time_key in policy_accepted_fields: +# continue +# else: +# return 'There are fields not accepted or ' + time_options +# if key == 'regex': +# if not isinstance(resources['permissions'][key], dict): +# return 'Regex must be a dictionary like {"pattern":}' +# for regex_key in resources['permissions'][key]: +# if regex_key == 'pattern' or regex_key in policy_accepted_fields: +# continue +# else: +# return 'The field "pattern" is not in the regex dictionary or there are fields not accepted' +# if key == 'user': +# if not isinstance(resources['permissions'][key], list): +# for user_key in resources['permissions'][key]: +# if user_key == 'users' or user_key in policy_accepted_fields: +# continue +# else: +# return 'The field "users" is not in the user dictionary or there are fields not accepted' +# if key == 'role': +# if not isinstance(resources['permissions'][key], list): +# for role_key in resources['permissions'][key]: +# if role_key == 'roles' or role_key in policy_accepted_fields: +# continue +# else: +# return 'The field "roles" is not in the role dictionary or there are fields not accepted' +# if key == 'group': +# if not isinstance(resources['permissions'][key], list): +# for group_key in resources['permissions'][key]: +# if group_key == 'groups' or group_key in policy_accepted_fields: +# continue +# else: +# return 'The field "groups" is not in the group dictionary or there are fields not accepted' +# if key == 'client-scope': +# if not isinstance(resources['permissions'][key], list): +# for client_scope_key in resources['permissions'][key]: +# if client_scope_key in policy_accepted_fields: +# continue +# else: +# return 'There are fields not accepted' +# +# if key == 'aggregated': +# if not isinstance(resources['permissions'][key], list): +# for aggregated_key in resources['permissions'][key]: +# if aggregated_key == 'policies' or aggregated_key in policy_accepted_fields: +# continue +# else: +# return 'The field "policies" is not in the aggregated dictionary or there are fields not accepted' +# +# if key == 'client': +# if not isinstance(resources['permissions'][key], list): +# for client_key in resources['permissions'][key]: +# if client_key in policy_accepted_fields: +# continue +# else: +# return 'There are fields not accepted' +# +# return None \ No newline at end of file diff --git a/app/routers/clients_permissions.py b/app/routers/clients_permissions.py new file mode 100644 index 0000000..fe3b8f8 --- /dev/null +++ b/app/routers/clients_permissions.py @@ -0,0 +1,30 @@ +from fastapi import APIRouter + +from app.keycloak_client import keycloak +from app.models.clients import ResourceBasedPermission + +router = APIRouter( + prefix="/{client_id}/permissions", + tags=["Clients Permissions"], +) + + +@router.get("") +def get_client_authz_permissions(client_id: str): + return keycloak.get_client_authz_permissions(client_id) + + +@router.get("/management") +def get_client_management_permissions(client_id: str): + return keycloak.get_client_management_permissions(client_id) + + +@router.get("/resources") +def get_client_resource_permissions(client_id: str): + return keycloak.get_client_resource_permissions(client_id) + + +@router.post("/resources") +def create_client_authz_resource_based_permission(client_id: str, resource_based_permission: ResourceBasedPermission): + resource_based_permission['type'] = 'resource' + return keycloak.create_client_authz_resource_based_permission(client_id, resource_based_permission) \ No newline at end of file diff --git a/app/routers/clients_policies.py b/app/routers/clients_policies.py new file mode 100644 index 0000000..760a0b3 --- /dev/null +++ b/app/routers/clients_policies.py @@ -0,0 +1,81 @@ +from fastapi import APIRouter + +from app.keycloak_client import keycloak +from app.models.clients import ClientPolicy, AggregatedPolicy, \ + ScopePolicy, GroupPolicy, RegexPolicy, RolePolicy, RelativeTimePolicy, YearTimePolicy, HourTimePolicy, \ + DayMonthTimePolicy, MonthTimePolicy, MinuteTimePolicy, UserPolicy, ModifyClientPolicy, ModifyRegexPolicy, \ + ModifyMonthTimePolicy, ModifyUserPolicy, ModifyAggregatedPolicy, ModifyRolePolicy, ModifyYearTimePolicy, \ + ModifyRelativeTimePolicy, ModifyScopePolicy, ModifyHourTimePolicy, ModifyDayMonthTimePolicy, ModifyMinuteTimePolicy + +router = APIRouter( + prefix="/{client_id}/policies", + tags=["Clients Policies"], +) + + +@router.get("") +def get_client_authz_policies(client_id: str): + return keycloak.get_client_authz_policies(client_id) + + +@router.post("/client") +def create_client_policy(client_id: str, client_policy: ClientPolicy): + client_policy["type"] = "client" + return keycloak.register_client_policy(client_policy, client_id) + + +@router.post("/aggregated") +def create_aggregated_policy(client_id: str, aggregated_policy: AggregatedPolicy): + aggregated_policy["type"] = "aggregated" + return keycloak.register_aggregated_policy(aggregated_policy, client_id) + + +@router.post("/scope") +def create_client_scope_policy(client_id: str, scope_policy: ScopePolicy): + scope_policy["type"] = "scope" + return keycloak.register_client_scope_policy(scope_policy, client_id) + + +@router.post("/group") +def create_group_policy(client_id: str, group_policy: GroupPolicy): + group_policy["type"] = "group" + return keycloak.register_group_policy(group_policy, client_id) + + +@router.post("/regex") +def create_regex_policy(client_id: str, regex_policy: RegexPolicy): + regex_policy["type"] = "regex" + return keycloak.register_regex_policy(regex_policy, client_id) + + +@router.post("/role") +def create_role_policy(client_id: str, role_policy: RolePolicy): + role_policy["type"] = "role" + return keycloak.register_role_policy(role_policy, client_id) + + +@router.post("/time") +def create_time_policy(client_id: str, + time_policy: RelativeTimePolicy | DayMonthTimePolicy | MonthTimePolicy | + YearTimePolicy | HourTimePolicy | MinuteTimePolicy): + time_policy["type"] = "time" + return keycloak.register_time_policy(time_policy, client_id) + + +@router.post("/user") +def create_user_policy(client_id: str, user_policy: UserPolicy): + return keycloak.register_user_policy(user_policy, client_id) + + +@router.put("/{policy_id}") +def update_policy(client_id: str, policy_id: str, + policy: ModifyClientPolicy | ModifyAggregatedPolicy | ModifyScopePolicy | + ModifyRegexPolicy | ModifyRolePolicy | ModifyRelativeTimePolicy | ModifyDayMonthTimePolicy | + ModifyMonthTimePolicy | ModifyYearTimePolicy | ModifyHourTimePolicy | ModifyMinuteTimePolicy | + ModifyUserPolicy): + return keycloak.update_policy(client_id, policy_id, policy) + + +@router.delete("/{policy_id}") +def delete_policy(client_id: str, policy_id: str): + return keycloak.delete_policy(policy_id, client_id) \ No newline at end of file diff --git a/app/routers/clients_resources.py b/app/routers/clients_resources.py new file mode 100644 index 0000000..cefa7de --- /dev/null +++ b/app/routers/clients_resources.py @@ -0,0 +1,79 @@ +from typing import List + +from fastapi import APIRouter + +from app.keycloak_client import keycloak +from app.models.clients import POLICY_TYPES, Resource + +router = APIRouter( + prefix="/{client_id}/resources", + tags=["Clients Resources"], +) + + +@router.post("") +def register_resources(client_id: str, resources: List[Resource]): + response_list = [] + for resource in resources: + resource_name = resource.name.replace(" ", "_") + scopes = resource.scopes + permissions = resource.permissions + decision_strategy = resource.decisionStrategy + response_resource = keycloak.register_resource(resource.model_dump(), client_id) + response_list.append(response_resource) + policy_list = [] + if permissions.role: + policy = { + "name": f'{resource_name}_role_policy', + "roles": permissions.role + } + policy_response = keycloak.register_role_policy(policy, policy, client_id) + policy_list.append(policy_response["name"]) + if permissions.user: + policy = { + "name": f'{resource["name"]}_user_policy', + "users": permissions.user + } + policy_response = keycloak.register_role_policy(policy, policy, client_id) + policy_list.append(policy_response["name"]) + permission_payload = { + "type": "resource", + "name": f'{resource_name}_permission', + "decisionStrategy": decision_strategy, + "resources": [ + resource_name + ], + "policies": policy_list + } + keycloak.create_client_authz_resource_based_permission(client_id, permission_payload) + return response_list + + +@router.delete("/{resource_name}/all") +def delete_resource_and_policies(client_id: str, resource_name: str): + # delete policies + client_policies = keycloak.get_client_authz_policies(client_id) + for policy in client_policies: + for policy_type in POLICY_TYPES: + if policy['name'] == f'{resource_name}_{policy_type}_policy': + keycloak.delete_policy(policy['id'], client_id) + # delete permissions + permissions = keycloak.get_client_resource_permissions(client_id) + for permission in permissions: + if permission['name'] == f'{resource_name}_permission': + keycloak.delete_resource_permissions(client_id, permission['id']) + # delete resources + resources = keycloak.get_resources(client_id) + for resource in resources: + if resource['name'] == resource_name: + return keycloak.delete_resource(resource['_id'], client_id) + + +@router.put("/{resource_id}") +def update_resource(client_id: str, resource_id: str, resource: Resource): + return keycloak.update_resource(resource_id, resource, client_id) + + +@router.delete("/{resource_id}") +def delete_resource(client_id: str, resource_id: str): + return keycloak.delete_resource(resource_id, client_id) \ No newline at end of file diff --git a/app/routers/health.py b/app/routers/health.py index 656b28e..d3df75f 100644 --- a/app/routers/health.py +++ b/app/routers/health.py @@ -1,13 +1,15 @@ from fastapi import status, APIRouter from pydantic import BaseModel +from app.models.base import APIBaseModel + router = APIRouter( prefix="/health", - tags=["health"] + tags=["Health checks"] ) -class HealthCheck(BaseModel): +class HealthCheck(APIBaseModel): """Response model to validate and return when performing a health check.""" status: str = "OK" diff --git a/app/routers/policies.py b/app/routers/policies.py index e603548..f8c7f57 100644 --- a/app/routers/policies.py +++ b/app/routers/policies.py @@ -1,20 +1,16 @@ -from fastapi import Depends, APIRouter -from pydantic import BaseModel, PositiveInt +from fastapi import APIRouter + +from app.keycloak_client import keycloak +from app.models.policies import SearchPolicies router = APIRouter( prefix="/policies", - tags=["policies"], + tags=["Policies"], ) -class SearchPolicies(BaseModel): - resource: str = '' - name: str = '' - uri: str = '' - first: PositiveInt = 0 - maximum: int = -1 -@router.get("/") -def search_policies(keycloak, search_params: SearchPolicies): +@router.get("") +def search_policies(search_params: SearchPolicies): return keycloak.get_policies( search_params.resource, search_params.name, diff --git a/app/routers/resources.py b/app/routers/resources.py index afd2cc0..e20bd81 100644 --- a/app/routers/resources.py +++ b/app/routers/resources.py @@ -1,25 +1,18 @@ -from http.client import HTTPException - from fastapi import APIRouter -from keycloak import KeycloakGetError + +from app.keycloak_client import keycloak router = APIRouter( prefix="/resources", - tags=["resouces"], + tags=["Resouces"], ) -@router.get("/") -def get_resources(keycloak): - try: - return keycloak.get_resources() - except KeycloakGetError as e: - return HTTPException(e.response_code, e.error_message) +@router.get("") +def get_resources(): + return keycloak.get_resources() @router.get("/resources/{resource_id}") -def get_resource(keycloak, resource_id: str): - try: - return keycloak.get_resource(resource_id) - except KeycloakGetError as e: - return HTTPException(e.response_code, e.error_message) \ No newline at end of file +def get_resource(resource_id: str): + return keycloak.get_resource(resource_id) \ No newline at end of file diff --git a/config.ini b/config.ini index 6af0e99..b46ded2 100644 --- a/config.ini +++ b/config.ini @@ -4,6 +4,6 @@ admin_username = admin admin_password = admin realm = master [Swagger] -swagger_url = /swagger-ui -swagger_api_url = /swagger-ui-api -swagger_app_name = Identity API \ No newline at end of file +swagger_title = Identity API Documentation +swagger_description = API endpoints +swagger_version = v0.0.1 \ No newline at end of file From 32303ec52e2718b5a766a065feef98acd2439967 Mon Sep 17 00:00:00 2001 From: Daniel Pimenta Date: Wed, 15 Nov 2023 17:27:27 +0000 Subject: [PATCH 47/64] Fix issues --- app/error_handling.py | 5 ++++- app/models/clients.py | 8 ++++---- app/routers/clients.py | 9 ++++----- app/routers/clients_policies.py | 2 +- app/routers/clients_resources.py | 23 ++++++++++++++--------- 5 files changed, 27 insertions(+), 20 deletions(-) diff --git a/app/error_handling.py b/app/error_handling.py index eed00cc..021ce7f 100644 --- a/app/error_handling.py +++ b/app/error_handling.py @@ -1,5 +1,6 @@ import json from typing import Sequence, Any +import traceback from fastapi import Request, status, FastAPI from fastapi.encoders import jsonable_encoder @@ -14,7 +15,9 @@ async def keycloak_error_handling(request: Request, call_next): try: return await call_next(request) except (KeycloakGetError, KeycloakPostError, KeycloakPutError, KeycloakDeleteError) as e: - return JSONResponse(status_code=400, content=jsonable_encoder({"code": 400, "msg": f"{json.loads(e.error_message)['errorMessage']}"})) + print(traceback.format_exc()) + return JSONResponse(status_code=400, content=jsonable_encoder({"code": 400, "msg": e.error_message})) + #return JSONResponse(status_code=400, content=jsonable_encoder({"code": 400, "msg": f"{json.loads(e.error_message)['errorMessage']}"})) @app.exception_handler(500) async def internal_exception_handler(): diff --git a/app/models/clients.py b/app/models/clients.py index e0ff162..e776111 100644 --- a/app/models/clients.py +++ b/app/models/clients.py @@ -1,7 +1,7 @@ from enum import Enum from typing import Any, List, Optional -from pydantic import PositiveInt, Field, ConfigDict +from pydantic import PositiveInt, Field from app.models.base import APIBaseModel @@ -58,9 +58,9 @@ class Client(APIBaseModel): defaultRoles: Optional[List[str]] = Field(None, description="Client Default roles") bearerOnly: Optional[bool] = Field(None, description="Enable/Disable Bearer only") consentRequired: Optional[bool] = Field(None, description="Enable/Disable Consent required") - publicClient: Optional[bool] = Field(None, description="Disable/Enable authentication to the client") - authorizationServicesEnabled: Optional[bool] = Field(None, description="Enable Authorization Services") - serviceAccountsEnabled: Optional[bool] = Field(None, description="Either or not to create a Service Account for the client") + publicClient: Optional[bool] = Field(False, description="Disable/Enable authentication to the client") + authorizationServicesEnabled: Optional[bool] = Field(True, description="Enable Authorization Services") + serviceAccountsEnabled: Optional[bool] = Field(True, description="Either or not to create a Service Account for the client") standardFlowEnabled: Optional[bool] = Field(True, description="Enable/Disable Standard Flow") implicitFlowEnabled: Optional[bool] = Field(None, description="Client name") directAccessGrantsEnabled: Optional[bool] = Field(None, description="Enable/Disable Direct Access Grants Flow") diff --git a/app/routers/clients.py b/app/routers/clients.py index 54dc53d..72db35a 100644 --- a/app/routers/clients.py +++ b/app/routers/clients.py @@ -18,17 +18,16 @@ def create_client(client: Client): resources = client.resources client_dict = client.model_dump() - if resources: - del client_dict['resources'] + del client_dict['resources'] response_client = keycloak.create_client(client_dict) if resources: response_resources = register_resources(client.clientId, resources) return { - client: response_client, - resources: response_resources + "client": response_client, + "resources": response_resources } return { - client: response_client + "client": response_client } diff --git a/app/routers/clients_policies.py b/app/routers/clients_policies.py index 760a0b3..2f35b71 100644 --- a/app/routers/clients_policies.py +++ b/app/routers/clients_policies.py @@ -73,7 +73,7 @@ def update_policy(client_id: str, policy_id: str, ModifyRegexPolicy | ModifyRolePolicy | ModifyRelativeTimePolicy | ModifyDayMonthTimePolicy | ModifyMonthTimePolicy | ModifyYearTimePolicy | ModifyHourTimePolicy | ModifyMinuteTimePolicy | ModifyUserPolicy): - return keycloak.update_policy(client_id, policy_id, policy) + return keycloak.update_policy(client_id, policy_id, policy.model_dump()) @router.delete("/{policy_id}") diff --git a/app/routers/clients_resources.py b/app/routers/clients_resources.py index cefa7de..8bca0d7 100644 --- a/app/routers/clients_resources.py +++ b/app/routers/clients_resources.py @@ -3,6 +3,7 @@ from fastapi import APIRouter from app.keycloak_client import keycloak +from app.log import log from app.models.clients import POLICY_TYPES, Resource router = APIRouter( @@ -16,30 +17,34 @@ def register_resources(client_id: str, resources: List[Resource]): response_list = [] for resource in resources: resource_name = resource.name.replace(" ", "_") - scopes = resource.scopes - permissions = resource.permissions - decision_strategy = resource.decisionStrategy - response_resource = keycloak.register_resource(resource.model_dump(), client_id) + res = { + "name": resource_name, + "uris": resource.uris, + "scopes": resource.scopes, + } + response_resource = keycloak.register_resource(res, client_id) response_list.append(response_resource) + permissions = resource.permissions policy_list = [] if permissions.role: policy = { "name": f'{resource_name}_role_policy', - "roles": permissions.role + "roles": [{"id": p} for p in permissions.role] } - policy_response = keycloak.register_role_policy(policy, policy, client_id) + log.info("pol " + str(policy)) + policy_response = keycloak.register_role_policy(policy, client_id) policy_list.append(policy_response["name"]) if permissions.user: policy = { - "name": f'{resource["name"]}_user_policy', + "name": f'{resource_name}_user_policy', "users": permissions.user } - policy_response = keycloak.register_role_policy(policy, policy, client_id) + policy_response = keycloak.register_user_policy(policy, client_id) policy_list.append(policy_response["name"]) permission_payload = { "type": "resource", "name": f'{resource_name}_permission', - "decisionStrategy": decision_strategy, + "decisionStrategy": resource.decisionStrategy, "resources": [ resource_name ], From 4c5f607b8d6b21391eba8c9b564af072c7ec514f Mon Sep 17 00:00:00 2001 From: Daniel Pimenta Date: Wed, 15 Nov 2023 17:49:20 +0000 Subject: [PATCH 48/64] Handle keycloak error message --- app/error_handling.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/error_handling.py b/app/error_handling.py index 021ce7f..8dc4434 100644 --- a/app/error_handling.py +++ b/app/error_handling.py @@ -16,8 +16,7 @@ async def keycloak_error_handling(request: Request, call_next): return await call_next(request) except (KeycloakGetError, KeycloakPostError, KeycloakPutError, KeycloakDeleteError) as e: print(traceback.format_exc()) - return JSONResponse(status_code=400, content=jsonable_encoder({"code": 400, "msg": e.error_message})) - #return JSONResponse(status_code=400, content=jsonable_encoder({"code": 400, "msg": f"{json.loads(e.error_message)['errorMessage']}"})) + return JSONResponse(status_code=e.response_code, content=jsonable_encoder(json.loads(e.error_message))) @app.exception_handler(500) async def internal_exception_handler(): From fd9b791af6b8dfabcaf196c33aa70b29be288993 Mon Sep 17 00:00:00 2001 From: flaviorosadme Date: Thu, 16 Nov 2023 10:25:49 +0000 Subject: [PATCH 49/64] added fildes to models and descriptions --- app/models/clients.py | 162 +++++++++++++++++++++--------------------- 1 file changed, 81 insertions(+), 81 deletions(-) diff --git a/app/models/clients.py b/app/models/clients.py index e776111..843819c 100644 --- a/app/models/clients.py +++ b/app/models/clients.py @@ -70,135 +70,135 @@ class Client(APIBaseModel): class ResourceBasedPermission(APIBaseModel): - logic: Logic - decisionStrategy: DecisionStrategy - name: str - resources: List[str] - policies: List[str] + logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") + decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, description="Decision strategy to decide how to apply permissions") + name: str = Field(description="Resource based permission name") + resources: List[str] = Field(description="Resource based permission resources") + policies: List[str] = Field(description="Resource based permission policies") class ClientPolicy(APIBaseModel): - logic: Logic = Logic.POSITIVE - decisionStrategy: DecisionStrategy = DecisionStrategy.UNANIMOUS.value - name: str - clients: List[str] - description: str = "" + logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") + decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, description="Decision strategy to decide how to apply permissions") + name: str = Field(description="Client policy name") + clients: List[str] = Field(description="Client policy clients") + description: Optional[str] = Field(description="Client policy description") class AggregatedPolicy(APIBaseModel): - logic: Logic = Logic.POSITIVE - decisionStrategy: DecisionStrategy = DecisionStrategy.UNANIMOUS.value - name: str - policies: List[str] - description: str = "" + logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") + decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, description="Decision strategy to decide how to apply permissions") + name: str = Field(description="Aggregated Policy name") + policies: List[str] = Field(description="Aggregated Policy policies") + description: Optional[str] = Field(description="Aggregated Policy description") class ClientScope(APIBaseModel): - id: str + id: str = Field(description="Client scope id") class ScopePolicy(APIBaseModel): - logic: Logic = Logic.POSITIVE - decisionStrategy: DecisionStrategy = DecisionStrategy.UNANIMOUS.value - name: str - clientScopes: List[ClientScope] - description: str = "" + logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") + decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, description="Decision strategy to decide how to apply permissions") + name: str = Field(description="Scope policy name") + clientScopes: List[ClientScope] = Field(description="Scope policy client scopes") + description: Optional[str] = Field(description="Scope policy description") class Group(APIBaseModel): - id: str - path: str + id: str = Field(description="Group id") + path: str = Field(description="Group path") class GroupPolicy(APIBaseModel): - logic: Logic = Logic.POSITIVE - decisionStrategy: DecisionStrategy = DecisionStrategy.UNANIMOUS.value - name: str - groups: List[Group] - groupsClaim: str = "" - description: str = "" + logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") + decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, description="Decision strategy to decide how to apply permissions") + name: str = Field(description="Group policy name") + groups: List[Group] = Field(description="Group policy groups") + groupsClaim: Optional[str] = Field(description="Group policy groups claim") + description: Optional[str] = Field(description="Group policy description") class RegexPolicy(APIBaseModel): - logic: Logic = Logic.POSITIVE - decisionStrategy: DecisionStrategy = DecisionStrategy.UNANIMOUS.value - name: str - pattern: str - targetClaim: str = "" - description: str = "" + logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") + decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, description="Decision strategy to decide how to apply permissions") + name: str = Field(description="Regex policy name") + pattern: str = Field(description="Regex policy regex pattern") + targetClaim: Optional[str] = Field(description="Regex policy target claim") + description: Optional[str] = Field(description="Regex policy description") class Role(APIBaseModel): - id: str + id: str = Field(description="Role id") class RolePolicy(APIBaseModel): - logic: Logic = Logic.POSITIVE - decisionStrategy: DecisionStrategy = DecisionStrategy.UNANIMOUS.value - name: str - roles: List[Role] - description: str = "" + logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") + decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, description="Decision strategy to decide how to apply permissions") + name: str = Field(description="Role policy name") + roles: List[Role] = Field(description="Role policy roles") + description: Optional[str] = Field(description="Role policy description") class RelativeTimePolicy(APIBaseModel): - logic: Logic = Logic.POSITIVE - decisionStrategy: DecisionStrategy = DecisionStrategy.UNANIMOUS.value - name: str - notAfter: str - notBefore: str - description: str = "" + logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") + decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, description="Decision strategy to decide how to apply permissions") + name: str = Field(description="Relative time policy name") + notAfter: str = Field(description="Relative time policy end date") + notBefore: str = Field(description="Relative time policy start date") + description: Optional[str] = Field(description="Relative time policy description") class DayMonthTimePolicy(APIBaseModel): - logic: Logic = Logic.POSITIVE - decisionStrategy: DecisionStrategy = DecisionStrategy.UNANIMOUS.value - name: str - dayMonth: PositiveInt - dayMonthEnd: PositiveInt - description: str = "" + logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") + decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, description="Decision strategy to decide how to apply permissions") + name: str = Field(description="Day month time policy name") + dayMonth: PositiveInt = Field(description="Day month time policy day month start") + dayMonthEnd: PositiveInt = Field(description="Day month time policy day month end") + description: Optional[str] = Field(description="Day month time policy description") class MonthTimePolicy(APIBaseModel): - logic: Logic = Logic.POSITIVE - decisionStrategy: DecisionStrategy = DecisionStrategy.UNANIMOUS.value - name: str - month: PositiveInt - monthEnd: PositiveInt - description: str = "" + logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") + decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, description="Decision strategy to decide how to apply permissions") + name: str = Field(description="Month time policy name") + month: PositiveInt = Field(description="Month time policy month start") + monthEnd: PositiveInt = Field(description="Month time policy month end") + description: Optional[str] = Field(description="Month time policy description") class YearTimePolicy(APIBaseModel): - logic: Logic = Logic.POSITIVE - decisionStrategy: DecisionStrategy = DecisionStrategy.UNANIMOUS.value - name: str - year: PositiveInt - yearEnd: PositiveInt - description: str = "" + logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") + decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, description="Decision strategy to decide how to apply permissions") + name: str = Field(description="Year time policy name") + year: PositiveInt = Field(description="Year time policy year start") + yearEnd: PositiveInt = Field(description="Year time policy year end") + description: Optional[str] = Field(description="Year time policy description") class HourTimePolicy(APIBaseModel): - logic: Logic = Logic.POSITIVE - decisionStrategy: DecisionStrategy = DecisionStrategy.UNANIMOUS.value - name: str - hour: PositiveInt - hourEnd: PositiveInt - description: str = "" + logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") + decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, description="Decision strategy to decide how to apply permissions") + name: str = Field(description="Hour time policy name") + hour: PositiveInt = Field(description="Hour time policy hour start") + hourEnd: PositiveInt = Field(description="Hour time policy hour end") + description: Optional[str] = Field(description="Hour time policy description") class MinuteTimePolicy(APIBaseModel): - logic: Logic = Logic.POSITIVE - decisionStrategy: DecisionStrategy = DecisionStrategy.UNANIMOUS.value - name: str - minute: PositiveInt - minuteEnd: PositiveInt - description: str = "" + logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") + decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, description="Decision strategy to decide how to apply permissions") + name: str = Field(description="Minute time policy name") + minute: PositiveInt = Field(description="Minute time policy minute start") + minuteEnd: PositiveInt = Field(description="Minute time policy minute end") + description: Optional[str] = Field(description="Minute time policy description") class UserPolicy(APIBaseModel): - logic: Logic = Logic.POSITIVE - decisionStrategy: DecisionStrategy = DecisionStrategy.UNANIMOUS.value - name: str - users: List[str] + logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") + decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, description="Decision strategy to decide how to apply permissions") + name: str = Field(description="User policy name") + users: List[str] = Field(description="User policy users list") class PolicyType(Enum): From 5030bc622d565b129a4e369c80c165c71e8f7a32 Mon Sep 17 00:00:00 2001 From: Daniel Pimenta Date: Thu, 16 Nov 2023 11:12:55 +0000 Subject: [PATCH 50/64] Add authenticated field --- app/error_handling.py | 8 ++++---- app/models/clients.py | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/error_handling.py b/app/error_handling.py index 8dc4434..63fbbde 100644 --- a/app/error_handling.py +++ b/app/error_handling.py @@ -4,7 +4,7 @@ from fastapi import Request, status, FastAPI from fastapi.encoders import jsonable_encoder -from fastapi.exceptions import RequestValidationError, HTTPException +from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse from keycloak import KeycloakPostError, KeycloakGetError, KeycloakPutError, KeycloakDeleteError @@ -20,11 +20,11 @@ async def keycloak_error_handling(request: Request, call_next): @app.exception_handler(500) async def internal_exception_handler(): - return JSONResponse(status_code=500, content=jsonable_encoder({"code": 500, "msg": "Internal Server Error"})) + return JSONResponse(status_code=500, content=jsonable_encoder({"error": "Internal Server Error"})) @app.exception_handler(400) async def bad_request_handler(): - return JSONResponse(status_code=400, content=jsonable_encoder({"code": 400, "msg": "Bad request"})) + return JSONResponse(status_code=400, content=jsonable_encoder({"error": "Bad request"})) @app.exception_handler(RequestValidationError) async def request_validation_error_handler(request: Request, exc: RequestValidationError) -> JSONResponse: @@ -36,7 +36,7 @@ async def request_validation_error_handler(request: Request, exc: RequestValidat ) return JSONResponse( status_code=status_code, - content={"detail": errors}, + content={"error": errors}, ) diff --git a/app/models/clients.py b/app/models/clients.py index 843819c..c66a5ef 100644 --- a/app/models/clients.py +++ b/app/models/clients.py @@ -26,6 +26,7 @@ class RolePermission(APIBaseModel): class Permission(APIBaseModel): user: List[str] | List[UserPermission] = Field([], description="User based permission") role: List[str] | List[RolePermission] = Field([], description="Role based permission") + authenticated: bool = Field(False, description="Authenticated only permission") class DecisionStrategy(Enum): From 450134773c4d16ac77647c6045dcea23fb19f75c Mon Sep 17 00:00:00 2001 From: Daniel Pimenta Date: Thu, 16 Nov 2023 12:02:44 +0000 Subject: [PATCH 51/64] Clean and reformat --- app/error_handling.py | 2 +- app/main.py | 2 - app/models/base.py | 1 + app/models/clients.py | 247 +---------------------------- app/models/permissions.py | 211 ++++++++++++++++++++++++ app/models/policies.py | 31 +++- app/models/resources.py | 23 +++ app/routers/clients.py | 132 +-------------- app/routers/clients_permissions.py | 2 +- app/routers/clients_policies.py | 40 ++--- app/routers/clients_resources.py | 7 +- app/routers/health.py | 1 - 12 files changed, 305 insertions(+), 394 deletions(-) create mode 100644 app/models/permissions.py create mode 100644 app/models/resources.py diff --git a/app/error_handling.py b/app/error_handling.py index 63fbbde..7465a29 100644 --- a/app/error_handling.py +++ b/app/error_handling.py @@ -1,6 +1,6 @@ import json -from typing import Sequence, Any import traceback +from typing import Sequence, Any from fastapi import Request, status, FastAPI from fastapi.encoders import jsonable_encoder diff --git a/app/main.py b/app/main.py index 5a34532..d8748eb 100644 --- a/app/main.py +++ b/app/main.py @@ -11,7 +11,6 @@ from app.error_handling import exception_handler from app.routers import clients, health, policies, resources, clients_permissions, clients_resources, clients_policies - config: Mapping[str, str] = ( load_configuration(os.path.join(os.path.dirname(__file__), "../config.ini")) ) @@ -44,7 +43,6 @@ async def docs_redirect(): def main() -> None: - """Entrypoint to invoke when this module is invoked on the remote server.""" uvicorn.run("main:app", host="0.0.0.0") diff --git a/app/models/base.py b/app/models/base.py index aabc57e..9433421 100644 --- a/app/models/base.py +++ b/app/models/base.py @@ -3,5 +3,6 @@ class APIBaseModel(BaseModel): model_config = ConfigDict(extra='forbid', use_enum_values=True) + def model_dump(self, exclude_none=True, **kwargs): return super().model_dump(exclude_none=exclude_none, **kwargs) \ No newline at end of file diff --git a/app/models/clients.py b/app/models/clients.py index c66a5ef..f10f06f 100644 --- a/app/models/clients.py +++ b/app/models/clients.py @@ -1,48 +1,9 @@ -from enum import Enum -from typing import Any, List, Optional +from typing import List, Optional -from pydantic import PositiveInt, Field +from pydantic import Field from app.models.base import APIBaseModel - -POLICY_TYPES = ['user', 'client', 'role', 'time', 'regex', 'group', 'scope', 'aggregated'] - - -class Logic(Enum): - POSITIVE = 'POSITIVE' - NEGATIVE = 'NEGATIVE' - - -class UserPermission(APIBaseModel): - users: List[str] = Field(None, description="List of usernames") - logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") - - -class RolePermission(APIBaseModel): - roles: List[str] = Field(None, description="List of roles") - logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") - - -class Permission(APIBaseModel): - user: List[str] | List[UserPermission] = Field([], description="User based permission") - role: List[str] | List[RolePermission] = Field([], description="Role based permission") - authenticated: bool = Field(False, description="Authenticated only permission") - - -class DecisionStrategy(Enum): - AFFIRMATIVE = 'AFFIRMATIVE' - UNANIMOUS = 'UNANIMOUS' - CONSENSUS = 'CONSENSUS' - - -class Resource(APIBaseModel): - name: str = Field(description="Resource name") - uris: List[str] = Field(description="Resource URIs") - attributes: Optional[Any] = Field({}, description="Resource attributes") - scopes: Optional[List[str]] = Field(["access"], description="Resource scopes") - ownerManagedAccess: Optional[bool] = Field(False, description="Enable/Disable management by the resource owner") - permissions: Optional[Permission] = Field(None, description="Resource permissions") - decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, description="Decision strategy to decide how to apply permissions") +from app.models.resources import Resource class Client(APIBaseModel): @@ -61,204 +22,12 @@ class Client(APIBaseModel): consentRequired: Optional[bool] = Field(None, description="Enable/Disable Consent required") publicClient: Optional[bool] = Field(False, description="Disable/Enable authentication to the client") authorizationServicesEnabled: Optional[bool] = Field(True, description="Enable Authorization Services") - serviceAccountsEnabled: Optional[bool] = Field(True, description="Either or not to create a Service Account for the client") + serviceAccountsEnabled: Optional[bool] = Field(True, + description="Either or not to create a Service Account for the client") standardFlowEnabled: Optional[bool] = Field(True, description="Enable/Disable Standard Flow") implicitFlowEnabled: Optional[bool] = Field(None, description="Client name") directAccessGrantsEnabled: Optional[bool] = Field(None, description="Enable/Disable Direct Access Grants Flow") - oauth2DeviceAuthorizationGrantEnabled: Optional[bool] = Field(None, description="Enable/Disable OAuth2 Device Authorization Grant Flow") + oauth2DeviceAuthorizationGrantEnabled: Optional[bool] = Field(None, + description="Enable/Disable OAuth2 Device Authorization Grant Flow") directGrantsOnly: Optional[bool] = Field(None, description="Enable/Disable Direct Grants Flow") - resources: Optional[List[Resource]] = Field([], description="List of resources to be added to the client") - - -class ResourceBasedPermission(APIBaseModel): - logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") - decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, description="Decision strategy to decide how to apply permissions") - name: str = Field(description="Resource based permission name") - resources: List[str] = Field(description="Resource based permission resources") - policies: List[str] = Field(description="Resource based permission policies") - - -class ClientPolicy(APIBaseModel): - logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") - decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, description="Decision strategy to decide how to apply permissions") - name: str = Field(description="Client policy name") - clients: List[str] = Field(description="Client policy clients") - description: Optional[str] = Field(description="Client policy description") - - -class AggregatedPolicy(APIBaseModel): - logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") - decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, description="Decision strategy to decide how to apply permissions") - name: str = Field(description="Aggregated Policy name") - policies: List[str] = Field(description="Aggregated Policy policies") - description: Optional[str] = Field(description="Aggregated Policy description") - - -class ClientScope(APIBaseModel): - id: str = Field(description="Client scope id") - - -class ScopePolicy(APIBaseModel): - logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") - decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, description="Decision strategy to decide how to apply permissions") - name: str = Field(description="Scope policy name") - clientScopes: List[ClientScope] = Field(description="Scope policy client scopes") - description: Optional[str] = Field(description="Scope policy description") - - -class Group(APIBaseModel): - id: str = Field(description="Group id") - path: str = Field(description="Group path") - - -class GroupPolicy(APIBaseModel): - logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") - decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, description="Decision strategy to decide how to apply permissions") - name: str = Field(description="Group policy name") - groups: List[Group] = Field(description="Group policy groups") - groupsClaim: Optional[str] = Field(description="Group policy groups claim") - description: Optional[str] = Field(description="Group policy description") - - -class RegexPolicy(APIBaseModel): - logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") - decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, description="Decision strategy to decide how to apply permissions") - name: str = Field(description="Regex policy name") - pattern: str = Field(description="Regex policy regex pattern") - targetClaim: Optional[str] = Field(description="Regex policy target claim") - description: Optional[str] = Field(description="Regex policy description") - - -class Role(APIBaseModel): - id: str = Field(description="Role id") - - -class RolePolicy(APIBaseModel): - logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") - decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, description="Decision strategy to decide how to apply permissions") - name: str = Field(description="Role policy name") - roles: List[Role] = Field(description="Role policy roles") - description: Optional[str] = Field(description="Role policy description") - - -class RelativeTimePolicy(APIBaseModel): - logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") - decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, description="Decision strategy to decide how to apply permissions") - name: str = Field(description="Relative time policy name") - notAfter: str = Field(description="Relative time policy end date") - notBefore: str = Field(description="Relative time policy start date") - description: Optional[str] = Field(description="Relative time policy description") - - -class DayMonthTimePolicy(APIBaseModel): - logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") - decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, description="Decision strategy to decide how to apply permissions") - name: str = Field(description="Day month time policy name") - dayMonth: PositiveInt = Field(description="Day month time policy day month start") - dayMonthEnd: PositiveInt = Field(description="Day month time policy day month end") - description: Optional[str] = Field(description="Day month time policy description") - - -class MonthTimePolicy(APIBaseModel): - logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") - decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, description="Decision strategy to decide how to apply permissions") - name: str = Field(description="Month time policy name") - month: PositiveInt = Field(description="Month time policy month start") - monthEnd: PositiveInt = Field(description="Month time policy month end") - description: Optional[str] = Field(description="Month time policy description") - - -class YearTimePolicy(APIBaseModel): - logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") - decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, description="Decision strategy to decide how to apply permissions") - name: str = Field(description="Year time policy name") - year: PositiveInt = Field(description="Year time policy year start") - yearEnd: PositiveInt = Field(description="Year time policy year end") - description: Optional[str] = Field(description="Year time policy description") - - -class HourTimePolicy(APIBaseModel): - logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") - decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, description="Decision strategy to decide how to apply permissions") - name: str = Field(description="Hour time policy name") - hour: PositiveInt = Field(description="Hour time policy hour start") - hourEnd: PositiveInt = Field(description="Hour time policy hour end") - description: Optional[str] = Field(description="Hour time policy description") - - -class MinuteTimePolicy(APIBaseModel): - logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") - decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, description="Decision strategy to decide how to apply permissions") - name: str = Field(description="Minute time policy name") - minute: PositiveInt = Field(description="Minute time policy minute start") - minuteEnd: PositiveInt = Field(description="Minute time policy minute end") - description: Optional[str] = Field(description="Minute time policy description") - - -class UserPolicy(APIBaseModel): - logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") - decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, description="Decision strategy to decide how to apply permissions") - name: str = Field(description="User policy name") - users: List[str] = Field(description="User policy users list") - - -class PolicyType(Enum): - CLIENT = 'client' - AGGREGATE = 'aggregate' - SCOPE = 'scope' - GROUP = 'group' - REGEX = 'regex' - ROLE = 'role' - TIME = 'time' - - -class ModifyClientPolicy(ClientPolicy): - type: PolicyType - - -class ModifyAggregatedPolicy(AggregatedPolicy): - type: PolicyType - - -class ModifyScopePolicy(ScopePolicy): - type: PolicyType - - -class ModifyGroupPolicy(GroupPolicy): - type: PolicyType - - -class ModifyRegexPolicy(RegexPolicy): - type: PolicyType - - -class ModifyRolePolicy(RolePolicy): - type: PolicyType - - -class ModifyRelativeTimePolicy(RelativeTimePolicy): - type: PolicyType - - -class ModifyDayMonthTimePolicy(DayMonthTimePolicy): - type: PolicyType - - -class ModifyMonthTimePolicy(MonthTimePolicy): - type: PolicyType - - -class ModifyYearTimePolicy(YearTimePolicy): - type: PolicyType - - -class ModifyHourTimePolicy(HourTimePolicy): - type: PolicyType - - -class ModifyMinuteTimePolicy(MinuteTimePolicy): - type: PolicyType - - -class ModifyUserPolicy(UserPolicy): - type: PolicyType \ No newline at end of file + resources: Optional[List[Resource]] = Field([], description="List of resources to be added to the client") \ No newline at end of file diff --git a/app/models/permissions.py b/app/models/permissions.py new file mode 100644 index 0000000..af8f78f --- /dev/null +++ b/app/models/permissions.py @@ -0,0 +1,211 @@ +from enum import Enum +from typing import List, Optional + +from pydantic import PositiveInt, Field + +from app.models.base import APIBaseModel +from app.models.policies import Logic, PolicyType + + +class DecisionStrategy(Enum): + AFFIRMATIVE = 'AFFIRMATIVE' + UNANIMOUS = 'UNANIMOUS' + CONSENSUS = 'CONSENSUS' + + +class ClientPermission(APIBaseModel): + logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") + decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, + description="Decision strategy to decide how to apply permissions") + name: str = Field(description="Client policy name") + clients: List[str] = Field(description="Client policy clients") + description: Optional[str] = Field(description="Client policy description") + + +class AggregatedPermission(APIBaseModel): + logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") + decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, + description="Decision strategy to decide how to apply permissions") + name: str = Field(description="Aggregated Policy name") + policies: List[str] = Field(description="Aggregated Policy policies") + description: Optional[str] = Field(description="Aggregated Policy description") + + +class ClientScope(APIBaseModel): + id: str = Field(description="Client scope id") + + +class ScopePermission(APIBaseModel): + logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") + decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, + description="Decision strategy to decide how to apply permissions") + name: str = Field(description="Scope policy name") + clientScopes: List[ClientScope] = Field(description="Scope policy client scopes") + description: Optional[str] = Field(description="Scope policy description") + + +class Group(APIBaseModel): + id: str = Field(description="Group id") + path: str = Field(description="Group path") + + +class GroupPermission(APIBaseModel): + logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") + decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, + description="Decision strategy to decide how to apply permissions") + name: str = Field(description="Group policy name") + groups: List[Group] = Field(description="Group policy groups") + groupsClaim: Optional[str] = Field(description="Group policy groups claim") + description: Optional[str] = Field(description="Group policy description") + + +class RegexPermission(APIBaseModel): + logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") + decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, + description="Decision strategy to decide how to apply permissions") + name: str = Field(description="Regex policy name") + pattern: str = Field(description="Regex policy regex pattern") + targetClaim: Optional[str] = Field(description="Regex policy target claim") + description: Optional[str] = Field(description="Regex policy description") + + +class Role(APIBaseModel): + id: str = Field(description="Role id") + + +class RolePermission(APIBaseModel): + logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") + decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, + description="Decision strategy to decide how to apply permissions") + name: str = Field(description="Role policy name") + roles: List[Role] = Field(description="Role policy roles") + description: Optional[str] = Field(description="Role policy description") + + +class RelativeTimePermission(APIBaseModel): + logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") + decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, + description="Decision strategy to decide how to apply permissions") + name: str = Field(description="Relative time policy name") + notAfter: str = Field(description="Relative time policy end date") + notBefore: str = Field(description="Relative time policy start date") + description: Optional[str] = Field(description="Relative time policy description") + + +class DayMonthTimePermission(APIBaseModel): + logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") + decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, + description="Decision strategy to decide how to apply permissions") + name: str = Field(description="Day month time policy name") + dayMonth: PositiveInt = Field(description="Day month time policy day month start") + dayMonthEnd: PositiveInt = Field(description="Day month time policy day month end") + description: Optional[str] = Field(description="Day month time policy description") + + +class MonthTimePermission(APIBaseModel): + logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") + decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, + description="Decision strategy to decide how to apply permissions") + name: str = Field(description="Month time policy name") + month: PositiveInt = Field(description="Month time policy month start") + monthEnd: PositiveInt = Field(description="Month time policy month end") + description: Optional[str] = Field(description="Month time policy description") + + +class YearTimePermission(APIBaseModel): + logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") + decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, + description="Decision strategy to decide how to apply permissions") + name: str = Field(description="Year time policy name") + year: PositiveInt = Field(description="Year time policy year start") + yearEnd: PositiveInt = Field(description="Year time policy year end") + description: Optional[str] = Field(description="Year time policy description") + + +class HourTimePermission(APIBaseModel): + logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") + decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, + description="Decision strategy to decide how to apply permissions") + name: str = Field(description="Hour time policy name") + hour: PositiveInt = Field(description="Hour time policy hour start") + hourEnd: PositiveInt = Field(description="Hour time policy hour end") + description: Optional[str] = Field(description="Hour time policy description") + + +class MinuteTimePermission(APIBaseModel): + logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") + decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, + description="Decision strategy to decide how to apply permissions") + name: str = Field(description="Minute time policy name") + minute: PositiveInt = Field(description="Minute time policy minute start") + minuteEnd: PositiveInt = Field(description="Minute time policy minute end") + description: Optional[str] = Field(description="Minute time policy description") + + +class UserPermission(APIBaseModel): + logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") + decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, + description="Decision strategy to decide how to apply permissions") + name: str = Field(description="User policy name") + users: List[str] = Field(description="User policy users list") + + +class ModifyClientPermission(ClientPermission): + type: PolicyType = Field(PolicyType.CLIENT.value, description="Policy type") + + +class ModifyAggregatedPermission(AggregatedPermission): + type: PolicyType = Field(PolicyType.AGGREGATE.value, description="Policy type") + + +class ModifyScopePermission(ScopePermission): + type: PolicyType = Field(PolicyType.SCOPE.value, description="Policy type") + + +class ModifyGroupPermission(GroupPermission): + type: PolicyType = Field(PolicyType.GROUP.value, description="Policy type") + + +class ModifyRegexPermission(RegexPermission): + type: PolicyType = Field(PolicyType.REGEX.value, description="Policy type") + + +class ModifyRolePermission(RolePermission): + type: PolicyType = Field(PolicyType.ROLE.value, description="Policy type") + + +class ModifyRelativeTimePermission(RelativeTimePermission): + type: PolicyType = Field(PolicyType.TIME.value, description="Policy type") + + +class ModifyDayMonthTimePermission(DayMonthTimePermission): + type: PolicyType = Field(PolicyType.TIME.value, description="Policy type") + + +class ModifyMonthTimePermission(MonthTimePermission): + type: PolicyType = Field(PolicyType.TIME.value, description="Policy type") + + +class ModifyYearTimePermission(YearTimePermission): + type: PolicyType = Field(PolicyType.TIME.value, description="Policy type") + + +class ModifyHourTimePermission(HourTimePermission): + type: PolicyType = Field(PolicyType.TIME.value, description="Policy type") + + +class ModifyMinuteTimePermission(MinuteTimePermission): + type: PolicyType = Field(PolicyType.TIME.value, description="Policy type") + + +class ModifyUserPermission(UserPermission): + type: PolicyType = Field(PolicyType.USER.value, description="Policy type") + + +class ResourceBasedPermission(APIBaseModel): + logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") + decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, + description="Decision strategy to decide how to apply permissions") + name: str = Field(description="Resource based permission name") + resources: List[str] = Field(description="Resource based permission resources") + policies: List[str] = Field(description="Resource based permission policies") \ No newline at end of file diff --git a/app/models/policies.py b/app/models/policies.py index d2c784b..d46adf1 100644 --- a/app/models/policies.py +++ b/app/models/policies.py @@ -1,8 +1,37 @@ -from pydantic import PositiveInt +from enum import Enum +from typing import List, Optional + +from pydantic import PositiveInt, Field from app.models.base import APIBaseModel +class PolicyType(Enum): + ROLE = 'role' + USER = 'user' + CLIENT = 'client' + AGGREGATE = 'aggregate' + SCOPE = 'scope' + GROUP = 'group' + REGEX = 'regex' + TIME = 'time' + + +class Logic(Enum): + POSITIVE = 'POSITIVE' + NEGATIVE = 'NEGATIVE' + + +class UserPolicy(APIBaseModel): + logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") + users: List[str] = Field(None, description="List of usernames") + + +class RolePolicy(APIBaseModel): + logic: Optional[Logic] = Field(Logic.POSITIVE, description="Logic to apply, either POSITIVE or NEGATIVE") + roles: List[str] = Field(None, description="List of roles") + + class SearchPolicies(APIBaseModel): resource: str = '' name: str = '' diff --git a/app/models/resources.py b/app/models/resources.py new file mode 100644 index 0000000..75c7340 --- /dev/null +++ b/app/models/resources.py @@ -0,0 +1,23 @@ +from typing import List, Optional, Any + +from pydantic import Field + +from app.models.base import APIBaseModel +from app.models.permissions import DecisionStrategy, UserPermission, RolePermission + + +class ResourcePermission(APIBaseModel): + user: List[str] | List[UserPermission] = Field([], description="User based permission") + role: List[str] | List[RolePermission] = Field([], description="Role based permission") + authenticated: bool = Field(False, description="Authenticated only permission") + + +class Resource(APIBaseModel): + name: str = Field(description="Resource name") + uris: List[str] = Field(description="Resource URIs") + attributes: Optional[Any] = Field({}, description="Resource attributes") + scopes: Optional[List[str]] = Field(["access"], description="Resource scopes") + ownerManagedAccess: Optional[bool] = Field(False, description="Enable/Disable management by the resource owner") + permissions: Optional[ResourcePermission] = Field(None, description="Resource permissions") + decisionStrategy: Optional[DecisionStrategy] = Field(DecisionStrategy.UNANIMOUS.value, + description="Decision strategy to decide how to apply permissions") \ No newline at end of file diff --git a/app/routers/clients.py b/app/routers/clients.py index 72db35a..ffc8ec8 100644 --- a/app/routers/clients.py +++ b/app/routers/clients.py @@ -1,10 +1,6 @@ -import logging -import os - from fastapi import APIRouter from app.keycloak_client import keycloak -from app.log import log from app.models.clients import Client from app.routers.clients_resources import register_resources @@ -20,128 +16,10 @@ def create_client(client: Client): client_dict = client.model_dump() del client_dict['resources'] response_client = keycloak.create_client(client_dict) - if resources: - response_resources = register_resources(client.clientId, resources) - return { - "client": response_client, - "resources": response_resources - } - return { + response = { "client": response_client } - - -# def _validate_register_resource(resources: Resources): -# payload_minimum_example = """ -# payload example -> -# [{ -# "resource":{ -# "name": "resource1", -# "uris": ["/resource1/", "/resource2/"] -# }, -# "permissions": { -# "user": ["user1","user2"], -# } -# }] -# """ -# time_options = """time must be a dictionary with one of: -# "notAfter":"1970-01-01 00:00:00" -# "notBefore":"1970-01-01 00:00:00" -# "dayMonth": -# "dayMonthEnd": -# "month": -# "monthEnd": -# "year": -# "yearEnd": -# "hour": -# "hourEnd": -# "minute": -# "minuteEnd":""" -# -# policy_types = ['user', 'client', 'role', 'time', 'regex', 'group', 'scope', 'aggregated'] -# resource_accepted_fields = ['name', 'uris', 'attributes', 'ownerManagedAccess', 'resource_scopes', 'type'] -# policy_accepted_fields = ['logic', 'decisionStrategy', 'name', 'description', 'groupsClaim', 'targetClaim'] -# time_accepted_fields = ["notAfter", "notBefore", "dayMonth", "dayMonthEnd", "month", "monthEnd", "year", "yearEnd", -# "hour", "hourEnd", "minute", "minuteEnd"] -# if 'resource' not in resources: -# return 'Resource field required. ' + payload_minimum_example -# if 'permissions' not in resources or resources['permissions'] == {}: -# return 'Permissions field required. ' + payload_minimum_example -# if 'name' not in resources['resource']: -# return 'Resource name required. ' + payload_minimum_example -# if 'uris' not in resources['resource']: -# return 'Resource uris required. ' + payload_minimum_example -# for resource_key in resources['resource']: -# if resource_key in resource_accepted_fields: -# continue -# else: -# return 'There are fields not accepted in "resource"' -# -# for key in resources['permissions']: -# if not isinstance(resources['permissions'][key], list) and not isinstance( -# resources['permissions'][key], dict): -# return "The value of {} ".format(key) + "must be a list of strings or a dictionary" -# if key not in policy_types: -# return 'Permissions type not found. Needs to be one of the following: ' + ', '.join(policy_types) -# if key == 'time': -# if not isinstance(resources['permissions']['time'], dict): -# return time_options -# for time_key in resources['permissions']['time']: -# if time_key in time_accepted_fields or time_key in policy_accepted_fields: -# continue -# else: -# return 'There are fields not accepted or ' + time_options -# if key == 'regex': -# if not isinstance(resources['permissions'][key], dict): -# return 'Regex must be a dictionary like {"pattern":}' -# for regex_key in resources['permissions'][key]: -# if regex_key == 'pattern' or regex_key in policy_accepted_fields: -# continue -# else: -# return 'The field "pattern" is not in the regex dictionary or there are fields not accepted' -# if key == 'user': -# if not isinstance(resources['permissions'][key], list): -# for user_key in resources['permissions'][key]: -# if user_key == 'users' or user_key in policy_accepted_fields: -# continue -# else: -# return 'The field "users" is not in the user dictionary or there are fields not accepted' -# if key == 'role': -# if not isinstance(resources['permissions'][key], list): -# for role_key in resources['permissions'][key]: -# if role_key == 'roles' or role_key in policy_accepted_fields: -# continue -# else: -# return 'The field "roles" is not in the role dictionary or there are fields not accepted' -# if key == 'group': -# if not isinstance(resources['permissions'][key], list): -# for group_key in resources['permissions'][key]: -# if group_key == 'groups' or group_key in policy_accepted_fields: -# continue -# else: -# return 'The field "groups" is not in the group dictionary or there are fields not accepted' -# if key == 'client-scope': -# if not isinstance(resources['permissions'][key], list): -# for client_scope_key in resources['permissions'][key]: -# if client_scope_key in policy_accepted_fields: -# continue -# else: -# return 'There are fields not accepted' -# -# if key == 'aggregated': -# if not isinstance(resources['permissions'][key], list): -# for aggregated_key in resources['permissions'][key]: -# if aggregated_key == 'policies' or aggregated_key in policy_accepted_fields: -# continue -# else: -# return 'The field "policies" is not in the aggregated dictionary or there are fields not accepted' -# -# if key == 'client': -# if not isinstance(resources['permissions'][key], list): -# for client_key in resources['permissions'][key]: -# if client_key in policy_accepted_fields: -# continue -# else: -# return 'There are fields not accepted' -# -# return None \ No newline at end of file + if resources: + response_resources = register_resources(client.clientId, resources) + response["resources"] = response_resources + return response \ No newline at end of file diff --git a/app/routers/clients_permissions.py b/app/routers/clients_permissions.py index fe3b8f8..ee26ae4 100644 --- a/app/routers/clients_permissions.py +++ b/app/routers/clients_permissions.py @@ -1,7 +1,7 @@ from fastapi import APIRouter from app.keycloak_client import keycloak -from app.models.clients import ResourceBasedPermission +from app.models.permissions import ResourceBasedPermission router = APIRouter( prefix="/{client_id}/permissions", diff --git a/app/routers/clients_policies.py b/app/routers/clients_policies.py index 2f35b71..275a06f 100644 --- a/app/routers/clients_policies.py +++ b/app/routers/clients_policies.py @@ -1,11 +1,15 @@ from fastapi import APIRouter from app.keycloak_client import keycloak -from app.models.clients import ClientPolicy, AggregatedPolicy, \ - ScopePolicy, GroupPolicy, RegexPolicy, RolePolicy, RelativeTimePolicy, YearTimePolicy, HourTimePolicy, \ - DayMonthTimePolicy, MonthTimePolicy, MinuteTimePolicy, UserPolicy, ModifyClientPolicy, ModifyRegexPolicy, \ - ModifyMonthTimePolicy, ModifyUserPolicy, ModifyAggregatedPolicy, ModifyRolePolicy, ModifyYearTimePolicy, \ - ModifyRelativeTimePolicy, ModifyScopePolicy, ModifyHourTimePolicy, ModifyDayMonthTimePolicy, ModifyMinuteTimePolicy +from app.models.permissions import ClientPermission, AggregatedPermission, \ + ScopePermission, GroupPermission, RegexPermission, RolePermission, RelativeTimePermission, YearTimePermission, \ + HourTimePermission, \ + DayMonthTimePermission, MonthTimePermission, MinuteTimePermission, UserPermission, ModifyClientPermission, \ + ModifyRegexPermission, \ + ModifyMonthTimePermission, ModifyUserPermission, ModifyAggregatedPermission, ModifyRolePermission, \ + ModifyYearTimePermission, \ + ModifyRelativeTimePermission, ModifyScopePermission, ModifyHourTimePermission, ModifyDayMonthTimePermission, \ + ModifyMinuteTimePermission router = APIRouter( prefix="/{client_id}/policies", @@ -19,60 +23,60 @@ def get_client_authz_policies(client_id: str): @router.post("/client") -def create_client_policy(client_id: str, client_policy: ClientPolicy): +def create_client_policy(client_id: str, client_policy: ClientPermission): client_policy["type"] = "client" return keycloak.register_client_policy(client_policy, client_id) @router.post("/aggregated") -def create_aggregated_policy(client_id: str, aggregated_policy: AggregatedPolicy): +def create_aggregated_policy(client_id: str, aggregated_policy: AggregatedPermission): aggregated_policy["type"] = "aggregated" return keycloak.register_aggregated_policy(aggregated_policy, client_id) @router.post("/scope") -def create_client_scope_policy(client_id: str, scope_policy: ScopePolicy): +def create_client_scope_policy(client_id: str, scope_policy: ScopePermission): scope_policy["type"] = "scope" return keycloak.register_client_scope_policy(scope_policy, client_id) @router.post("/group") -def create_group_policy(client_id: str, group_policy: GroupPolicy): +def create_group_policy(client_id: str, group_policy: GroupPermission): group_policy["type"] = "group" return keycloak.register_group_policy(group_policy, client_id) @router.post("/regex") -def create_regex_policy(client_id: str, regex_policy: RegexPolicy): +def create_regex_policy(client_id: str, regex_policy: RegexPermission): regex_policy["type"] = "regex" return keycloak.register_regex_policy(regex_policy, client_id) @router.post("/role") -def create_role_policy(client_id: str, role_policy: RolePolicy): +def create_role_policy(client_id: str, role_policy: RolePermission): role_policy["type"] = "role" return keycloak.register_role_policy(role_policy, client_id) @router.post("/time") def create_time_policy(client_id: str, - time_policy: RelativeTimePolicy | DayMonthTimePolicy | MonthTimePolicy | - YearTimePolicy | HourTimePolicy | MinuteTimePolicy): + time_policy: RelativeTimePermission | DayMonthTimePermission | MonthTimePermission | + YearTimePermission | HourTimePermission | MinuteTimePermission): time_policy["type"] = "time" return keycloak.register_time_policy(time_policy, client_id) @router.post("/user") -def create_user_policy(client_id: str, user_policy: UserPolicy): +def create_user_policy(client_id: str, user_policy: UserPermission): return keycloak.register_user_policy(user_policy, client_id) @router.put("/{policy_id}") def update_policy(client_id: str, policy_id: str, - policy: ModifyClientPolicy | ModifyAggregatedPolicy | ModifyScopePolicy | - ModifyRegexPolicy | ModifyRolePolicy | ModifyRelativeTimePolicy | ModifyDayMonthTimePolicy | - ModifyMonthTimePolicy | ModifyYearTimePolicy | ModifyHourTimePolicy | ModifyMinuteTimePolicy | - ModifyUserPolicy): + policy: ModifyClientPermission | ModifyAggregatedPermission | ModifyScopePermission | + ModifyRegexPermission | ModifyRolePermission | ModifyRelativeTimePermission | ModifyDayMonthTimePermission | + ModifyMonthTimePermission | ModifyYearTimePermission | ModifyHourTimePermission | ModifyMinuteTimePermission | + ModifyUserPermission): return keycloak.update_policy(client_id, policy_id, policy.model_dump()) diff --git a/app/routers/clients_resources.py b/app/routers/clients_resources.py index 8bca0d7..618c839 100644 --- a/app/routers/clients_resources.py +++ b/app/routers/clients_resources.py @@ -3,8 +3,8 @@ from fastapi import APIRouter from app.keycloak_client import keycloak -from app.log import log -from app.models.clients import POLICY_TYPES, Resource +from app.models.policies import PolicyType +from app.models.resources import Resource router = APIRouter( prefix="/{client_id}/resources", @@ -31,7 +31,6 @@ def register_resources(client_id: str, resources: List[Resource]): "name": f'{resource_name}_role_policy', "roles": [{"id": p} for p in permissions.role] } - log.info("pol " + str(policy)) policy_response = keycloak.register_role_policy(policy, client_id) policy_list.append(policy_response["name"]) if permissions.user: @@ -59,7 +58,7 @@ def delete_resource_and_policies(client_id: str, resource_name: str): # delete policies client_policies = keycloak.get_client_authz_policies(client_id) for policy in client_policies: - for policy_type in POLICY_TYPES: + for policy_type in [e.value for e in PolicyType]: if policy['name'] == f'{resource_name}_{policy_type}_policy': keycloak.delete_policy(policy['id'], client_id) # delete permissions diff --git a/app/routers/health.py b/app/routers/health.py index d3df75f..d7912f6 100644 --- a/app/routers/health.py +++ b/app/routers/health.py @@ -1,5 +1,4 @@ from fastapi import status, APIRouter -from pydantic import BaseModel from app.models.base import APIBaseModel From 935e825882b78f779aa04fb1a1714fdc7c8009dd Mon Sep 17 00:00:00 2001 From: Daniel Pimenta Date: Thu, 16 Nov 2023 12:12:02 +0000 Subject: [PATCH 52/64] Point to keycloak client 1.0.0 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 863deb0..2d4e2d8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,4 +6,4 @@ configparser==6.0.0 retry==0.9.2 urllib3==2.0.7 pydantic==2.5.0 -identityutils @ git+https://github.com/eoepca/um-identity-service@master +identityutils @ git+https://github.com/eoepca/um-identity-service@v1.0.0 \ No newline at end of file From 0389d1554318a5cab240a0e97cf376d9de61379b Mon Sep 17 00:00:00 2001 From: Daniel Pimenta Date: Thu, 16 Nov 2023 14:48:15 +0000 Subject: [PATCH 53/64] Change logging --- .travis.yml | 38 ------------------------------------- Dockerfile | 1 - app/keycloak_client.py | 7 +++---- app/log.py | 12 +++++++----- config.ini | 4 ++-- docker-compose.yml | 4 ++-- logging.yaml | 29 ---------------------------- travis/acceptanceTest.sh | 16 ---------------- travis/containerCreation.sh | 19 ------------------- travis/release.sh | 19 ------------------- 10 files changed, 14 insertions(+), 135 deletions(-) delete mode 100644 .travis.yml delete mode 100644 logging.yaml delete mode 100755 travis/acceptanceTest.sh delete mode 100755 travis/containerCreation.sh delete mode 100755 travis/release.sh diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 08fa5b0..0000000 --- a/.travis.yml +++ /dev/null @@ -1,38 +0,0 @@ -language: python -python: 3.6.9 - -env: - DOCKER_COMPOSE_VERSION: 1.25.4 -services: - - docker - -jobs: - include: - - stage: Testing - install: pip install -r src/requirements.txt - script: pytest src - - - stage: container creation and publishing - install: skip - script: travis/containerCreation.sh um-service-template - - - stage: smoke and acceptance test - install: skip # without this there's a `git clone` executed! - script: travis/acceptanceTest.sh um-service-template 8080 7000 # Service name + external port + internal port for docker - - - stage: release - if: branch = master AND NOT type IN (pull_request) - install: skip - script: travis/release.sh um-service-template - -import: - - docs/.travis.yml - -#notifications: -# slack: eoepca:Msk9hjQKAbwSYcVWiepenPim -# email: -# recipients: -# - a.person@acme.com -# - a.n.other@acme.com -# on_success: never # default: change -# on_failure: never # default: always diff --git a/Dockerfile b/Dockerfile index 4cf6fc4..738f77c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,6 +3,5 @@ WORKDIR /code COPY ./requirements.txt /code/requirements.txt RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt COPY ./config.ini /code/config.ini -COPY ./logging.yaml /code/logging.yaml COPY ./app /code/app CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "8080"] \ No newline at end of file diff --git a/app/keycloak_client.py b/app/keycloak_client.py index 70a5ba2..e91aad7 100644 --- a/app/keycloak_client.py +++ b/app/keycloak_client.py @@ -1,3 +1,4 @@ +import logging import os from identityutils.configuration import load_configuration @@ -6,15 +7,13 @@ from retry.api import retry_call from urllib3.exceptions import NewConnectionError -from app.log import log - def __create_keycloak_client(): config_path = os.path.join(os.path.dirname(__file__), "../config.ini") config = load_configuration(config_path) auth_server_url = config.get("Keycloak", "auth_server_url") realm = config.get("Keycloak", "realm") - log.info("Starting Keycloak client for: " + auth_server_url + "/realms/" + realm) + logging.info("Starting Keycloak client for: " + auth_server_url + "/realms/" + realm) return KeycloakClient( server_url=auth_server_url, realm=realm, @@ -29,5 +28,5 @@ def __create_keycloak_client(): delay=0.5, backoff=1.2, jitter=(1, 2), - logger=log + logger=logging.getLogger("um-identity-api") ) \ No newline at end of file diff --git a/app/log.py b/app/log.py index 4914348..4808374 100644 --- a/app/log.py +++ b/app/log.py @@ -1,7 +1,9 @@ import logging -import os -import identityutils.logger as logger - -logger.Logger.get_instance().load_configuration(os.path.join(os.path.dirname(__file__), "../logging.yaml")) -log = logging.getLogger("IDENTITY_API") \ No newline at end of file +# create logger with 'spam_application' +logger = logging.getLogger('um-identity-api') +logger.setLevel(logging.DEBUG) +# create file handler which logs even debug messages +fh = logging.FileHandler('um-identity-api.log') +fh.setLevel(logging.DEBUG) +logger.addHandler(fh) \ No newline at end of file diff --git a/config.ini b/config.ini index b46ded2..a89a45d 100644 --- a/config.ini +++ b/config.ini @@ -1,9 +1,9 @@ [Keycloak] -auth_server_url = http://localhost:80/ +auth_server_url = http://localhost admin_username = admin admin_password = admin realm = master [Swagger] swagger_title = Identity API Documentation swagger_description = API endpoints -swagger_version = v0.0.1 \ No newline at end of file +swagger_version = v1.0.0 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 6084907..4d63a95 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,7 @@ services: build: . container_name: um-identity-api environment: - - AUTH_SERVER_URL=http://keycloak:8080/ + - AUTH_SERVER_URL=http://keycloak:8080 ports: - '8080:8080' keycloak: @@ -39,4 +39,4 @@ services: - POSTGRES_PASSWORD=123456 - PGPASSWORD=123 - PGDATA=/var/lib/postgresql/data/keycloak - restart: on-failure + restart: on-failure \ No newline at end of file diff --git a/logging.yaml b/logging.yaml deleted file mode 100644 index ec04412..0000000 --- a/logging.yaml +++ /dev/null @@ -1,29 +0,0 @@ -version: 1 -disable_existing_loggers: true - -formatters: - verbose: - format: '%(asctime)s:%(levelname)s:%(message)s' - datefmt: '%Y-%m-%dT%H:%M:%S%z' - -handlers: - console: - class: logging.StreamHandler - level: INFO - formatter: verbose - stream: ext://sys.stdout - - log: - class: logging.handlers.RotatingFileHandler - filename: logs/identity-api.log - formatter: verbose - level: DEBUG - maxBytes: 1073741824 ## 1 GB log file size before rotation - backupCount: 10 ## Saves 10 most recent log files - -loggers: - IDENTITY_API: - level: DEBUG - handlers: [ console, log ] - qualname: IDENTITY_API - propagate: false \ No newline at end of file diff --git a/travis/acceptanceTest.sh b/travis/acceptanceTest.sh deleted file mode 100755 index 5439bdd..0000000 --- a/travis/acceptanceTest.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash - -# fail fast settings from https://dougrichardson.org/2018/08/03/fail-fast-bash-scripting.html -set -euov pipefail - -# Check presence of environment variables -TRAVIS_BUILD_NUMBER="${TRAVIS_BUILD_NUMBER:-0}" - -buildTag=travis_$TRAVIS_BUILD_NUMBER # We use a temporary build number for tagging, since this is a transient artefact - -docker run --rm -d -p $2:$3 --name $1 eoepca/$1:${buildTag} # Runs container from EOEPCA repository - -sleep 15 # wait until the container is running - -# INSERT BELOW THE ACCEPTANCE TEST: -#curl -s http://localhost:$2/search # trivial smoke test diff --git a/travis/containerCreation.sh b/travis/containerCreation.sh deleted file mode 100755 index cfd5f00..0000000 --- a/travis/containerCreation.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash - -# fail fast settings from https://dougrichardson.org/2018/08/03/fail-fast-bash-scripting.html -set -euov pipefail - - -# Check presence of environment variables -TRAVIS_BUILD_NUMBER="${TRAVIS_BUILD_NUMBER:-0}" - -# Create a Docker image and tag it as 'travis_' -buildTag=travis_$TRAVIS_BUILD_NUMBER # We use a temporary build number for tagging, since this is a transient artefact - -docker build -t eoepca/$1 . -docker tag eoepca/$1 eoepca/$1:$buildTag - -echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin - -docker push eoepca/$1:$buildTag # defaults to docker hub EOEPCA repo - diff --git a/travis/release.sh b/travis/release.sh deleted file mode 100755 index 68ef4ae..0000000 --- a/travis/release.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash - -# fail fast settings from https://dougrichardson.org/2018/08/03/fail-fast-bash-scripting.html -set -euov pipefail - -# Check presence of environment variables -TRAVIS_BUILD_NUMBER="${TRAVIS_BUILD_NUMBER:-0}" -buildTag=travis_$TRAVIS_BUILD_NUMBER # We use a temporary build number for tagging, since this is a transient artefact - -echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin -docker pull eoepca/$1:$buildTag # have to pull locally in order to tag as a release - -# Tag and push as a Release -docker tag eoepca/$1:$buildTag eoepca/$1:release_$TRAVIS_BUILD_NUMBER -docker push eoepca/$1:release_$TRAVIS_BUILD_NUMBER - -# Tag and push as `latest` -docker tag eoepca/$1:$buildTag eoepca/$1:latest -docker push eoepca/$1:latest From c6c3904998d5f4722218173df95ddf9c091746d0 Mon Sep 17 00:00:00 2001 From: Daniel Pimenta Date: Thu, 16 Nov 2023 15:24:55 +0000 Subject: [PATCH 54/64] Fix readme --- README.md | 114 +++++++++++++++++++++++++----------------------------- 1 file changed, 53 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index e017a7f..853b76b 100644 --- a/README.md +++ b/README.md @@ -12,36 +12,30 @@ [![MIT License][license-shield]][license-url] ![Build][build-shield] -

- - Logo -

Identity API

- Flask application to enable a REST API server to manage Keycloak through Keycloak Admin API (https://www.keycloak.org/docs-api/21.0.1/rest-api/index.html) and Protection API (https://www.keycloak.org/docs/latest/authorization_services/index.html#_service_protection_api). + FastAPI application exposing a Restful API to manage Keycloak through Keycloak Admin API (https://www.keycloak.org/docs-api/22.0.1/rest-api/index.html) and Protection API (https://www.keycloak.org/docs/latest/authorization_services/index.html#_service_protection_api).
Explore the docs »
- View Demo · Report Bug · Request Feature -

## Table of Contents - [Table of Contents](#table-of-contents) - [About The Project](#about-the-project) - - [Built With](#built-with) + - [Built With](#built-with) - [Getting Started](#getting-started) - - [Prerequisites](#prerequisites) - - [Installation](#installation) + - [Prerequisites](#prerequisites) + - [Installation](#installation) - [Documentation](#documentation) - [Usage](#usage) - [Roadmap](#roadmap) @@ -50,25 +44,20 @@ - [Contact](#contact) - [Acknowledgements](#acknowledgements) - ## About The Project -Flask application to enable a REST API server to manage Keycloak through Keycloak Admin API (https://www.keycloak.org/docs-api/21.0.1/rest-api/index.html) and Protection API (https://www.keycloak.org/docs/latest/authorization_services/index.html#_service_protection_api). +FastAPI application exposing a Restful API to manage Keycloak through Keycloak Admin +API (https://www.keycloak.org/docs-api/21.0.1/rest-api/index.html) and Protection +API (https://www.keycloak.org/docs/latest/authorization_services/index.html#_service_protection_api). -Includes three main paths: -- **Resources** - CRUD operations to manage resources -- **Policies** - CRUD operations to manage policies -- **Permissions** - CRUD operations to manage permissions +Swagger docs are available at /docs. +Redoc docs are available at /redoc. ### Built With -- [Python](https://www.python.org//) -- [PyTest](https://docs.pytest.org) -- [YAML](https://yaml.org/) -- [Travis CI](https://travis-ci.com/) - - +- [Python](https://www.python.org) +- [FastAPI](https://fastapi.tiangolo.com) ## Getting Started @@ -76,75 +65,69 @@ To get a local copy up and running follow these simple steps. ### Prerequisites -This is an example of how to list things you need to use the software and how to install them. - -- [Docker](https://www.docker.com/) -- [Python](https://www.python.org//) +- [Docker](https://www.docker.com) +or +- [Docker compose](https://docs.docker.com/compose) +or +- [Python](https://www.python.org) ### Installation -1. Get into EOEPCA's development environment - -```sh -vagrant ssh -``` - -3. Clone the repo +1. Clone the repo ```sh git clone https://github.com/EOEPCA/um-identity-api ``` -4. Change local directory +2. Change local directory ```sh cd um-identity-api ``` -5. Execute +3. Execute - 5.1 Run locally with Python + 3.1 Run with docker compose + ```sh + docker compose up -d --build + ``` + 3.2 Run with Python ```sh pip install -r requirements.txt uvicorn app.main:app ``` - 5.2 Run locally with Docker + 3.3 Run with Docker ```sh - docker build . --progress=plain -t um-identity-api:develop - docker run --rm -dp 5566:5566 --name um-identity-api um-identity-api:develop + docker build . --progress=plain -t um-identity-api:local + docker run --rm -dp 8080:8080 --name um-identity-api um-identity-api:local ``` - 5.3 Run develop branch with Docker + 3.4 Run develop branch with Docker ```sh - docker run --rm -dp 5566:5566 --name um-identity-api ghcr.io/eoepca/um-identity-api:develop + docker run --rm -dp 8080:8080 --name um-identity-api ghcr.io/eoepca/um-identity-api:develop ``` - 5.4 Run master branch with Docker + 3.5 Run master branch with Docker ```sh - docker run --rm -dp 5566:5566 --name um-identity-api ghcr.io/eoepca/um-identity-api:production + docker run --rm -dp 8080:8080 --name um-identity-api ghcr.io/eoepca/um-identity-api:production ``` ## Documentation The component documentation can be found at https://eoepca.github.io/um-identity-api/. - ## Usage -Use this space to show useful examples of how a project can be used. Additional screenshots, code examples and demos work well in this space. You may also link to more resources. - -_For more examples, please refer to the [Documentation](https://example.com)_ - - +Check Redoc page to try out the API, available at http://localhost:8080/redoc ## Roadmap -See the [open issues](https://github.com/EOEPCA/um-identity-api/issues) for a list of proposed features (and known issues). - - +See the [open issues](https://github.com/EOEPCA/um-identity-api/issues) for a list of proposed features (and known +issues). ## Contributing -Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**. +Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any +contributions you make are **greatly appreciated**. 1. Fork the Project 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) @@ -152,7 +135,6 @@ Contributions are what make the open source community such an amazing place to b 4. Push to the Branch (`git push origin feature/AmazingFeature`) 5. Open a Pull Request - ## License @@ -166,17 +148,27 @@ Project Link: [https://github.com/EOEPCA/um-identity-api](https://github.com/EOE ## Acknowledgements -- README.md is based on [this template](https://github.com/othneildrew/Best-README-Template) by [Othneil Drew](https://github.com/othneildrew). +- README.md is based on [this template](https://github.com/othneildrew/Best-README-Template) + by [Othneil Drew](https://github.com/othneildrew). +[contributors-shield]: https://img.shields.io/github/contributors/EOEPCA/um-identity-api.svg?style=flat-square -[contributors-shield]: https://img.shields.io/github/contributors/EOEPCA/um-identity-apisvg?style=flat-square [contributors-url]: https://github.com/EOEPCA/um-identity-api/graphs/contributors -[forks-shield]: https://img.shields.io/github/forks/EOEPCA/um-identity-apisvg?style=flat-square + +[forks-shield]: https://img.shields.io/github/forks/EOEPCA/um-identity-api.svg?style=flat-square + [forks-url]: https://github.com/EOEPCA/um-identity-api/network/members -[stars-shield]: https://img.shields.io/github/stars/EOEPCA/um-identity-apisvg?style=flat-square + +[stars-shield]: https://img.shields.io/github/stars/EOEPCA/um-identity-api.svg?style=flat-square + [stars-url]: https://github.com/EOEPCA/um-identity-api/stargazers -[issues-shield]: https://img.shields.io/github/issues/EOEPCA/um-identity-apisvg?style=flat-square + +[issues-shield]: https://img.shields.io/github/issues/EOEPCA/um-identity-api.svg?style=flat-square + [issues-url]: https://github.com/EOEPCA/um-identity-api/issues -[license-shield]: https://img.shields.io/github/license/EOEPCA/um-identity-apisvg?style=flat-square + +[license-shield]: https://img.shields.io/github/license/EOEPCA/um-identity-api.svg?style=flat-square + [license-url]: https://github.com/EOEPCA/um-identity-api/blob/master/LICENSE -[build-shield]: https://www.travis-ci.com/EOEPCA/um-identity-apisvg?branch=master + +[build-shield]: https://www.travis-ci.com/EOEPCA/um-identity-api.svg?branch=master \ No newline at end of file From e92f49b8cf6053aa60cc4ea2da02c4b2a54f864f Mon Sep 17 00:00:00 2001 From: Daniel Pimenta Date: Thu, 16 Nov 2023 15:26:00 +0000 Subject: [PATCH 55/64] Clean --- README.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/README.md b/README.md index 853b76b..e0df669 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,3 @@ - - - [![Contributors][contributors-shield]][contributors-url] [![Forks][forks-shield]][forks-url] [![Stargazers][stars-shield]][stars-url] From a2fb5d8544048e3732e81073c279d0658e0d6aae Mon Sep 17 00:00:00 2001 From: Daniel Pimenta Date: Thu, 16 Nov 2023 15:46:51 +0000 Subject: [PATCH 56/64] Change logging --- app/configuration.py | 8 + app/keycloak_client.py | 13 +- app/log.py | 29 +- app/main.py | 5 +- config.ini | 2 + docker-compose.yml | 5 +- docs/.gitignore | 3 - docs/.travis.yml | 25 -- docs/01.introduction/00.introduction.adoc | 20 - docs/01.introduction/03.reference-docs.adoc | 162 -------- docs/01.introduction/04.terminology.adoc | 187 --------- docs/01.introduction/05.glossary.adoc | 49 --- docs/02.overview/00.overview.adoc | 4 - docs/03.design/00.design.adoc | 4 - docs/README.adoc | 32 -- docs/amendment-history.adoc | 16 - docs/bin/generate-docs.sh | 32 -- docs/bin/publish-docs.sh | 44 -- docs/end-of-document.adoc | 3 - docs/gh-page-README.adoc | 15 - docs/gh-page-root.html | 18 - docs/images/logo.png | Bin 72755 -> 0 bytes docs/index.adoc | 59 --- docs/preface.adoc | 22 - docs/resources/themes/eoepca-theme.yml | 28 -- docs/resources/themes/origdefault-theme.yml | 274 ------------- docs/stylesheets/asciidoctor.css | 420 -------------------- docs/stylesheets/eoepca.css | 25 -- 28 files changed, 45 insertions(+), 1459 deletions(-) create mode 100644 app/configuration.py delete mode 100644 docs/.gitignore delete mode 100644 docs/.travis.yml delete mode 100644 docs/01.introduction/00.introduction.adoc delete mode 100644 docs/01.introduction/03.reference-docs.adoc delete mode 100644 docs/01.introduction/04.terminology.adoc delete mode 100644 docs/01.introduction/05.glossary.adoc delete mode 100644 docs/02.overview/00.overview.adoc delete mode 100644 docs/03.design/00.design.adoc delete mode 100644 docs/README.adoc delete mode 100644 docs/amendment-history.adoc delete mode 100755 docs/bin/generate-docs.sh delete mode 100755 docs/bin/publish-docs.sh delete mode 100644 docs/end-of-document.adoc delete mode 100644 docs/gh-page-README.adoc delete mode 100644 docs/gh-page-root.html delete mode 100644 docs/images/logo.png delete mode 100644 docs/index.adoc delete mode 100644 docs/preface.adoc delete mode 100644 docs/resources/themes/eoepca-theme.yml delete mode 100644 docs/resources/themes/origdefault-theme.yml delete mode 100644 docs/stylesheets/asciidoctor.css delete mode 100644 docs/stylesheets/eoepca.css diff --git a/app/configuration.py b/app/configuration.py new file mode 100644 index 0000000..1beff0e --- /dev/null +++ b/app/configuration.py @@ -0,0 +1,8 @@ +import os +from typing import Mapping + +from identityutils.configuration import load_configuration + +config: Mapping[str, str] = ( + load_configuration(os.path.join(os.path.dirname(__file__), "../config.ini")) +) \ No newline at end of file diff --git a/app/keycloak_client.py b/app/keycloak_client.py index e91aad7..05585ee 100644 --- a/app/keycloak_client.py +++ b/app/keycloak_client.py @@ -1,19 +1,16 @@ -import logging -import os - -from identityutils.configuration import load_configuration from identityutils.keycloak_client import KeycloakClient from keycloak import KeycloakConnectionError from retry.api import retry_call from urllib3.exceptions import NewConnectionError +from app.configuration import config +from app.log import logger + def __create_keycloak_client(): - config_path = os.path.join(os.path.dirname(__file__), "../config.ini") - config = load_configuration(config_path) auth_server_url = config.get("Keycloak", "auth_server_url") realm = config.get("Keycloak", "realm") - logging.info("Starting Keycloak client for: " + auth_server_url + "/realms/" + realm) + logger.info("Starting Keycloak client for: " + auth_server_url + "/realms/" + realm) return KeycloakClient( server_url=auth_server_url, realm=realm, @@ -28,5 +25,5 @@ def __create_keycloak_client(): delay=0.5, backoff=1.2, jitter=(1, 2), - logger=logging.getLogger("um-identity-api") + logger=logger ) \ No newline at end of file diff --git a/app/log.py b/app/log.py index 4808374..a619015 100644 --- a/app/log.py +++ b/app/log.py @@ -1,9 +1,30 @@ import logging -# create logger with 'spam_application' +from app.configuration import config + + +def get_logging_level(): + level = config.get("App", "logging_level") + if not level: + level = 'info' + level = level.lower() + if level == 'critical' or level == 'critical': + return logging.CRITICAL + if level == 'error': + return logging.ERROR + if level == 'warning' or level == 'warn': + return logging.WARNING + if level == 'info': + return logging.INFO + if level == 'debug': + return logging.DEBUG + if level == 'notset': + return logging.NOTSET + + logger = logging.getLogger('um-identity-api') -logger.setLevel(logging.DEBUG) -# create file handler which logs even debug messages +logging_level = get_logging_level() +logger.setLevel(logging_level) fh = logging.FileHandler('um-identity-api.log') -fh.setLevel(logging.DEBUG) +fh.setLevel(logging_level) logger.addHandler(fh) \ No newline at end of file diff --git a/app/main.py b/app/main.py index d8748eb..0291832 100644 --- a/app/main.py +++ b/app/main.py @@ -6,14 +6,11 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import RedirectResponse -from identityutils.configuration import load_configuration +from app.configuration import config from app.error_handling import exception_handler from app.routers import clients, health, policies, resources, clients_permissions, clients_resources, clients_policies -config: Mapping[str, str] = ( - load_configuration(os.path.join(os.path.dirname(__file__), "../config.ini")) -) app = FastAPI( title=config.get("Swagger", "swagger_title"), diff --git a/config.ini b/config.ini index a89a45d..09469c7 100644 --- a/config.ini +++ b/config.ini @@ -1,3 +1,5 @@ +[App] +logging_level = info [Keycloak] auth_server_url = http://localhost admin_username = admin diff --git a/docker-compose.yml b/docker-compose.yml index 4d63a95..7d406f7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,10 @@ version: "3.5" services: identity-api: - build: . + build: + context: . + dockerfile: Dockerfile + image: um-identity-api:local container_name: um-identity-api environment: - AUTH_SERVER_URL=http://keycloak:8080 diff --git a/docs/.gitignore b/docs/.gitignore deleted file mode 100644 index 1ad7ea0..0000000 --- a/docs/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ - -output/ -repos/ diff --git a/docs/.travis.yml b/docs/.travis.yml deleted file mode 100644 index 1da5117..0000000 --- a/docs/.travis.yml +++ /dev/null @@ -1,25 +0,0 @@ -jobs: - include: - - stage: generate docs - if: branch = master AND NOT type IN (pull_request) - - # Assume that 'docker' service is stated at the Travis global level. It seems that re-stating it here confuses Travis. - # services: - # - docker - - before_install: - - docker pull asciidoctor/docker-asciidoctor - - script: - - ./docs/bin/generate-docs.sh - - after_error: - - docker logs asciidoc-to-html - - docker logs asciidoc-to-pdf - - after_failure: - - docker logs asciidoc-to-html - - docker logs asciidoc-to-pdf - - after_success: - - ./docs/bin/publish-docs.sh diff --git a/docs/01.introduction/00.introduction.adoc b/docs/01.introduction/00.introduction.adoc deleted file mode 100644 index 98a7802..0000000 --- a/docs/01.introduction/00.introduction.adoc +++ /dev/null @@ -1,20 +0,0 @@ - -= Introduction - -== Purpose and Scope - -This document presents the {component-name} Design for the Common Architecture. - -== Structure of the Document - -Section 2 - <>:: -Provides an over of the {component-name} component, within the context of the wider Common Architecture design. - -Section 3 - <>:: -Provides the design of the {component-name} component. - -include::03.reference-docs.adoc[leveloffset=+1] - -include::04.terminology.adoc[leveloffset=+1] - -include::05.glossary.adoc[leveloffset=+1] diff --git a/docs/01.introduction/03.reference-docs.adoc b/docs/01.introduction/03.reference-docs.adoc deleted file mode 100644 index eae80fb..0000000 --- a/docs/01.introduction/03.reference-docs.adoc +++ /dev/null @@ -1,162 +0,0 @@ - -= Reference Documents - -The following is a list of Reference Documents with a direct bearing on the content of this document. - -[cols="2,7a,2a"] -|=== -| Reference | Document Details | Version - -| [[EOEPCA-UC]][EOEPCA-UC] -| EOEPCA - Use Case Analysis + -EOEPCA.TN.005 + -https://eoepca.github.io/use-case-analysis -| Issue 1.0, + -02/08/2019 - -| [[EP-FM]][EP-FM] -| Exploitation Platform - Functional Model, + -ESA-EOPSDP-TN-17-050 -| Issue 1.0, + -30/11/2017 - -| [[TEP-OA]][TEP-OA] -| Thematic Exploitation Platform Open Architecture, + -EMSS-EOPS-TN-17-002 -| Issue 1, + -12/12/2017 - -| [[WPS-T]][WPS-T] -| OGC Testbed-14: WPS-T Engineering Report, + -OGC 18-036r1, + -http://docs.opengeospatial.org/per/18-036r1.html -| 18-036r1, + -07/02/2019 - -| [[WPS-REST-JSON]][WPS-REST-JSON] -| OGC WPS 2.0 REST/JSON Binding Extension, Draft, + -OGC 18-062, + -https://raw.githubusercontent.com/opengeospatial/wps-rest-binding/develop/docs/18-062.pdf -| 1.0-draft - -| [[CWL]][CWL] -| Common Workflow Language Specifications, + -https://www.commonwl.org/v1.0/ -| v1.0.2 - -| [[TB13-AP]][TB13-AP] -| OGC Testbed-13, EP Application Package Engineering Report, + -OGC 17-023, + -http://docs.opengeospatial.org/per/17-023.html -| 17-023, + -30/01/2018 - -| [[TB13-ADES]][TB13-ADES] -| OGC Testbed-13, Application Deployment and Execution Service Engineering Report, + -OGC 17-024, + -http://docs.opengeospatial.org/per/17-024.html -| 17-024, + -11/01/2018 - -| [[TB14-AP]][TB14-AP] -| OGC Testbed-14, Application Package Engineering Report, + -OGC 18-049r1, + -http://docs.opengeospatial.org/per/18-049r1.html -| 18-049r1, + -07/02/2019 - -| [[TB14-ADES]][TB14-ADES] -| OGC Testbed-14, ADES & EMS Results and Best Practices Engineering Report, + -OGC 18-050r1, http://docs.opengeospatial.org/per/18-050r1.html -| 18-050r1, + -08/02/2019 - -| [[OS-GEO-TIME]][OS-GEO-TIME] -| OpenSearch GEO: OpenSearch Geo and Time Extensions, + -OGC 10-032r8, + -http://www.opengeospatial.org/standards/opensearchgeo -| 10-032r8, + -14/04/2014 - -| [[OS-EO]][OS-EO] -| OpenSearch EO: OGC OpenSearch Extension for Earth Observation, + -OGC 13-026r9, + -http://docs.opengeospatial.org/is/13-026r8/13-026r8.html -| 13-026r9, + -16/12/2016 - -| [[GEOJSON-LD]][GEOJSON-LD] -| OGC EO Dataset Metadata GeoJSON(-LD) Encoding Standard, + -OGC 17-003r1/17-084 -| 17-003r1/17-084 - -| [[GEOJSON-LD-RESP]][GEOJSON-LD-RESP] -| OGC OpenSearch-EO GeoJSON(-LD) Response Encoding Standard, + -OGC 17-047 -| 17-047 - -| [[PCI-DSS]][PCI-DSS] -| The Payment Card Industry Data Security Standard, + -https://www.pcisecuritystandards.org/document_library?category=pcidss&document=pci_dss -| v3.2.1 - -| [[CEOS-OS-BP]][CEOS-OS-BP] -| CEOS OpenSearch Best Practise, + -http://ceos.org/ourwork/workinggroups/wgiss/access/opensearch/ -| v1.2, + -13/06/2017 - -| [[OIDC]][OIDC] -| OpenID Connect Core 1.0, + -https://openid.net/specs/openid-connect-core-1_0.html -| v1.0, + -08/11/2014 - -| [[OGC-CSW]][OGC-CSW] -| OGC Catalogue Services 3.0 Specification - HTTP Protocol Binding (Catalogue Services for the Web), + -OGC 12-176r7, + -http://docs.opengeospatial.org/is/12-176r7/12-176r7.html -| v3.0, + -10/06/2016 - -| [[OGC-WMS]][OGC-WMS] -| OGC Web Map Server Implementation Specification, + -OGC 06-042, + -http://portal.opengeospatial.org/files/?artifact_id=14416 -| v1.3.0, + -05/03/2006 - -| [[OGC-WMTS]][OGC-WMTS] -| OGC Web Map Tile Service Implementation Standard, + -OGC 07-057r7, + -http://portal.opengeospatial.org/files/?artifact_id=35326 -| v1.0.0, + -06/04/2010 - -| [[OGC-WFS]][OGC-WFS] -| OGC Web Feature Service 2.0 Interface Standard – With Corrigendum, + -OGC 09-025r2, + -http://docs.opengeospatial.org/is/09-025r2/09-025r2.html -| v2.0.2, + -10/07/2014 - -| [[OGC-WCS]][OGC-WCS] -| OGC Web Coverage Service (WCS) 2.1 Interface Standard - Core, + -OGC 17-089r1, + -http://docs.opengeospatial.org/is/17-089r1/17-089r1.html -| v2.1, + -16/08/2018 - -| [[OGC-WCPS]][OGC-WCPS] -| Web Coverage Processing Service (WCPS) Language Interface Standard, + -OGC 08-068r2, + -http://portal.opengeospatial.org/files/?artifact_id=32319 -| v1.0.0, + -25/03/2009 - -| [[AWS-S3]][AWS-S3] -| Amazon Simple Storage Service REST API, + -https://docs.aws.amazon.com/AmazonS3/latest/API -| API Version 2006-03-01 - -|=== diff --git a/docs/01.introduction/04.terminology.adoc b/docs/01.introduction/04.terminology.adoc deleted file mode 100644 index 7d2d906..0000000 --- a/docs/01.introduction/04.terminology.adoc +++ /dev/null @@ -1,187 +0,0 @@ - -= Terminology - -The following terms are used in the Master System Design. - -[cols="1,3"] -|=== -| Term | Meaning - -| Admin -| User with administrative capability on the EP - -| Algorithm -| A self-contained set of operations to be performed, typically to achieve a desired data manipulation. The algorithm must be implemented (codified) for deployment and execution on the platform. - -| Analysis Result -| The _Products_ produced as output of an _Interactive Application_ analysis session. - -| Analytics -| A set of activities aimed to discover, interpret and communicate meaningful patters within the data. Analytics considered here are performed manually (or in a semi-automatic way) on-line with the aid of _Interactive Applications_. - -| Application Artefact -| The 'software' component that provides the execution unit of the _Application Package_. - -| Application Deployment and Execution Service (ADES) -| WPS-T (REST/JSON) service that incorporates the Docker execution engine, and is responsible for the execution of the processing service (as a WPS request) within the ‘target’ Exploitation Platform. - -| Application Descriptor -| A file that provides the metadata part of the _Application Package_. Provides all the metadata required to accommodate the processor within the WPS service and make it available for execution. - -| Application Package -| A platform independent and self-contained representation of a software item, providing executable, metadata and dependencies such that it can be deployed to and executed within an Exploitation Platform. Comprises the _Application Descriptor_ and the _Application Artefact_. - -| Bulk Processing -| Execution of a _Processing Service_ on large amounts of data specified by AOI and TOI. - -| Code -| The codification of an algorithm performed with a given programming language - compiled to Software or directly executed (interpretted) within the platform. - -| Compute Platform -| The Platform on which execution occurs (this may differ from the Host or Home platform where federated processing is happening) - -| Consumer -| User accessing existing services/products within the EP. Consumers may be scientific/research or commercial, and may or may not be experts of the domain - -| Data Access Library -| An abstraction of the interface to the data layer of the resource tier. The library provides bindings for common languages (including python, Javascript) and presents a common object model to the code. - -| Development -| The act of building new products/services/applications to be exposed within the platform and made available for users to conduct exploitation activities. Development may be performed inside or outside of the platform. If performed outside, an integration activity will be required to accommodate the developed service so that it is exposed within the platform. - -| Discovery -| User finds products/services of interest to them based upon search criteria. - -| Execution -| The act to start a _Processing Service_ or an _Interactive Application_. - -| Execution Management Service (EMS) -| The EMS is responsible for the orchestration of workflows, including the possibility of steps running on other (remote) platforms, and the on-demand deployment of processors to local/remote ADES as required. - -| Expert -| User developing and integrating added-value to the EP (Scientific Researcher or Service Developer) - -| Exploitation Tier -| The Exploitation Tier represents the end-users who exploit the services of the platform to perform analysis, or using high-level applications built-in on top of the platform’s services - -| External Application -| An application or script that is developed and executed outside of the Exploitation Platform, but is able to use the data/services of the EP via a programmatic interface (API). - -| Guest -| An unregistered User or an unauthenticated Consumer with limited access to the EP's services - -| Home Platform -| The Platform on which a User is based or from which an action was initiated by a User - -| Host Platform -| The Platform through which a Resource has been published - -| Identity Provider (IdP) -| The source for validating user identity in a federated identity system, (user authentication as a service). - -| Interactive Application -| A stand-alone application provided within the exploitation platform for on-line hosted processing. Provides an interactive interface through which the user is able to conduct their analysis of the data, producing _Analysis Results_ as output. Interactive Applications include at least the following types: console application, web application (rich browser interface), remote desktop to a hosted VM. - -| Interactive Console Application -| A simple _Interactive Application_ for analysis in which a console interface to a platform-hosted terminal is provided to the user. The console interface can be provided through the user's browser session or through a remote SSH connection. - -| Interactive Remote Desktop -| An Interactive Application for analysis provided as a remote desktop session to an OS-session (or directly to a 'native' application) on the exploitation platform. The user will have access to a number of applications within the hosted OS. The remote desktop session is provided through the user’s web browser. - -| Interactive Web Application -| An Interactive Application for analysis provided as a rich user interface through the user's web browser. - -| Key-Value Pair -| A key-value pair (KVP) is an abstract data type that includes a group of key identifiers and a set of associated values. Key-value pairs are frequently used in lookup tables, hash tables and configuration files. - -| Kubernetes (K8s) -| Container orchestration system for automating application deployment, scaling and management. - -| Login Service -| An encapsulation of Authenticated Login provision within the Exploitation Platform context. The Login Service is an OpenID Connect Provider that is used purely for authentication. It acts as a Relying Party in flows with external IdPs to obtain access to the user's identity. - -| EO Network of Resources -| The coordinated collection of European EO resources (platforms, data sources, etc.). - -| Object Store -| A computer data storage architecture that manages data as objects. Each object typically includes the data itself, a variable amount of metadata, and a globally unique identifier. - -| On-demand Processing Service -| A _Processing Service_ whose execution is initiated directly by the user on an ad-hoc basis. - -| Platform (EP) -| An on-line collection of products, services and tools for exploitation of EO data - -| Platform Tier -| The Platform Tier represents the Exploitation Platform and the services it offers to end-users - -| Processing -| A set of pre-defined activities that interact to achieve a result. For the exploitation platform, comprises on-line processing to derive data products from input data, conducted by a hosted processing service execution. - -| Processing Result -| The _Products_ produced as output of a _Processing Service_ execution. - -| Processing Service -| A non-interactive data processing that has a well-defined set of input data types, input parameterisation, producing _Processing Results_ with a well-defined output data type. - -| Products -| EO data (commercial and non-commercial) and Value-added products and made available through the EP. _It is assumed that the Hosting Environment for the EP makes available an existing supply of EO Data_ - -| Resource -| A entity, such as a Product, Processing Service or Interactive Application, which is of interest to a user, is indexed in a catalogue and can be returned as a single meaningful search result - -| Resource Tier -| The Resource Tier represents the hosting infrastructure and provides the EO data, storage and compute upon which the exploitation platform is deployed - -| Reusable Research Object -| An encapsulation of some research/analysis that describes all aspects required to reproduce the analysis, including data used, processing performed etc. - -| Scientific Researcher -| Expert user with the objective to perform scientific research. Having minimal IT knowledge with no desire to acquire it, they want the effort for the translation of their algorithm into a service/product to be minimised by the platform. - -| Service Developer -| Expert user with the objective to provide a performing, stable and reliable service/product. Having deeper IT knowledge or a willingness to acquire it, they require deeper access to the platform IT functionalities for optimisation of their algorithm. - -| Software -| The compilation of code into a binary program to be executed within the platform on-line computing environment. - -| Systematic Processing Service -| A _Processing Service_ whose execution is initiated automatically (on behalf of a user), either according to a schedule (routine) or triggered by an event (e.g. arrival of new data). - -| Terms & Conditions (T&Cs) -| The obligations that the user agrees to abide by in regard of usage of products/services of the platform. T&Cs are set by the provider of each product/service. - -| Transactional Web Processing Service (WPS-T) -| Transactional extension to WPS that allows adhoc deployment / undeployment of user-provided processors. - -| User -| An individual using the EP, of any type (Admin/Consumer/Expert/Guest) - -| Value-added products -| Products generated from processing services of the EP (or external processing) and made available through the EP. This includes products uploaded to the EP by users and published for collaborative consumption - -| Visualisation -| To obtain a visual representation of any data/products held within the platform - presented to the user within their web browser session. - -| Web Coverage Service (WCS) -| OGC standard that provides an open specification for sharing raster datasets on the web. - -| Web Coverage Processing Service (WCPS) -| OGC standard that defines a protocol-independent language for the extraction, processing, and analysis of multi-dimentional coverages representing sensor, image, or statistics data. - -| Web Feature Service (WFS) -| OGC standard that makes geographic feature data (vector geospatial datasets) available on the web. - -| Web Map Service (WMS) -| OGC standard that provides a simple HTTP interface for requesting geo-registered map images from one or more distributed geospatial databases. - -| Web Map Tile Service (WMTS) -| OGC standard that provides a simple HTTP interface for requesting map tiles of spatially referenced data using the images with predefined content, extent, and resolution. - -| Web Processing Services (WPS) -| OGC standard that defines how a client can request the execution of a process, and how the output from the process is handled. - -| Workspace -| A user-scoped 'container' in the EP, in which each user maintains their own links to resources (products and services) that have been collected by a user during their usage of the EP. The workspace acts as the hub for a user's exploitation activities within the EP - -|=== diff --git a/docs/01.introduction/05.glossary.adoc b/docs/01.introduction/05.glossary.adoc deleted file mode 100644 index 18031f4..0000000 --- a/docs/01.introduction/05.glossary.adoc +++ /dev/null @@ -1,49 +0,0 @@ - -= Glossary - -The following acronyms and abbreviations have been used in this report. - -[cols="1,6"] -|=== -| Term | Definition - -| AAI | Authentication & Authorization Infrastructure -| ABAC | Attribute Based Access Control -| ADES | Application Deployment and Execution Service -| ALFA | Abbreviated Language For Authorization -| AOI | Area of Interest -| API | Application Programming Interface -| CMS | Content Management System -| CWL | Common Workflow Language -| DAL | Data Access Library -| EMS | Execution Management Service -| EO | Earth Observation -| EP | Exploitation Platform -| FUSE | Filesystem in Userspace -| GeoXACML | Geo-specific extension to the XACML Policy Language -| IAM | Identity and Access Management -| IdP | Identity Provider -| JSON | JavaScript Object Notation -| K8s | Kubernetes -| KVP | Key-value Pair -| M2M | Machine-to-machine -| OGC | Open Geospatial Consortium -| PDE | Processor Development Environment -| PDP | Policy Decision Point -| PEP | Policy Enforcement Point -| PIP | Policy Information Point -| RBAC | Role Based Access Control -| REST | Representational State Transfer -| SSH | Secure Shell -| TOI | Time of Interest -| UMA | User-Managed Access -| VNC | Virtual Network Computing -| WCS | Web Coverage Service -| WCPS | Web Coverage Processing Service -| WFS | Web Feature Service -| WMS | Web Map Service -| WMTS | Web Map Tile Service -| WPS | Web Processing Service -| WPS-T | Transactional Web Processing Service -| XACML | eXtensible Access Control Markup Language -|=== diff --git a/docs/02.overview/00.overview.adoc b/docs/02.overview/00.overview.adoc deleted file mode 100644 index e7845bf..0000000 --- a/docs/02.overview/00.overview.adoc +++ /dev/null @@ -1,4 +0,0 @@ -[[mainOverview]] -= Overview - -TBD diff --git a/docs/03.design/00.design.adoc b/docs/03.design/00.design.adoc deleted file mode 100644 index a5dbe96..0000000 --- a/docs/03.design/00.design.adoc +++ /dev/null @@ -1,4 +0,0 @@ -[[mainDesign]] -= Design - -TBD diff --git a/docs/README.adoc b/docs/README.adoc deleted file mode 100644 index 9a653cb..0000000 --- a/docs/README.adoc +++ /dev/null @@ -1,32 +0,0 @@ -= Documentation -:component-name: