From 34005b3aa78c0c1fab3a6c3fc706f654bea153fb Mon Sep 17 00:00:00 2001 From: Daniel Pimenta <105205108+daniel-pimenta-DME@users.noreply.github.com> Date: Thu, 16 Nov 2023 12:07:38 +0000 Subject: [PATCH] Release (#14) * Eoepca 910 um keycloak develop an identity api based on keycloak api (#9) * feat: keycloak_client methods added for identity_api * feat: added scopes crud (for future?) and fixes on permissions and policies cruds * Merge (#10) * Update docker compose * Fix keycloak client constructor * Update README.md * Update docker compose * Add nginx config * Change nginx * Add realm admin role * Remove auth keycloak client (#11) * Add dummy service demo * Add dummy-service nginx * Update demos * Update nginx configs * Update gatekeeper config * feat: added register_general_policy function * fix: small fix in register_general_policy * fix: one more fix * feat: added delete permissions * feat: added create client function * Add gatekeeper cookie name * Improve create client script * feat: added inputs to script but not working yet * Update create-client.sh * Update create-client.sh * Update create-client.sh * Update create-client.sh * Update create-client.sh * Update create-client.sh * Update create-client.sh * Fix issues * Improve create client script * Fix issue --------- Co-authored-by: flaviorosadme <82375986+flaviorosadme@users.noreply.github.com> Co-authored-by: flaviorosadme --- create-client.sh | 296 +++++++++++++++++++++++++++++++ identityutils/keycloak_client.py | 54 +++--- 2 files changed, 328 insertions(+), 22 deletions(-) create mode 100644 create-client.sh diff --git a/create-client.sh b/create-client.sh new file mode 100644 index 0000000..cec7ec7 --- /dev/null +++ b/create-client.sh @@ -0,0 +1,296 @@ +#!/bin/bash + +args_count=$# + +usage=" +Add a client with protected resources. +$(basename "$0") [-h] [-e] [-u] [-p] [-t | --token t] --id id [--name name] [--default] [--authenticated] [--resource name] [--uris u1,u2] [--scopes s1,s2] [--users u1,u2] [--roles r1,r2] + +where: + -h show help message + -e enviroment - local, develop, demo, production - defaults to local + -u username used for authentication + -p password used for authentication + -t or --token access token used for authentication + --id client id + --name client name + --default add default resource - /* authenticated + --authenticated allow access to the resource when authenticated + --resource resource name + --uris resource uris - separated by comma (,) + --scopes resource scopes - separated by comma (,) + --users user names with access to the resource - separated by comma (,) + --roles role names with access to the resource - separated by comma (,) +" + +TEMP=$(getopt -o he:u:p:t: --long id:,name:,description:,default,authenticated,resource:,uris:,scopes:,users:,roles: \ + -n $(basename "$0") -- "$@") + +if [ $? != 0 ]; then + exit 1 +fi + +eval set -- "$TEMP" + +environment="local" +client_id= +client_name= +client_description= +resource_name= +resource_uris= +resource_scopes= +users= +roles= +authenticated=false + +resources=() + +add_resource() { + if [ -z "${resource_scopes}" ]; then + resource_scopes="access" + fi + IFS=',' read -ra resource_uris_array <<<"$resource_uris" + IFS=',' read -ra resource_scopes_array <<<"$resource_scopes" + IFS=',' read -ra users_array <<<"$users" + IFS=',' read -ra roles_array <<<"$roles" + if ((${#users_array[@]} == 0 && ${#roles_array[@]} == 0)); then + resource="{ + \"name\": \"${resource_name}\", + \"uris\": $(json_array "${resource_uris_array[@]}"), + \"scopes\": $(json_array "${resource_scopes_array[@]}"), + \"permissions\": { + \"authenticated\": "${authenticated}" + } + }" + else + resource="{ + \"name\": \"${resource_name}\", + \"uris\": $(json_array "${resource_uris_array[@]}"), + \"scopes\": $(json_array "${resource_scopes_array[@]}"), + \"permissions\": { + \"user\": $(json_array "${users_array[@]}"), + \"role\": $(json_array "${roles_array[@]}") + } + }" + fi + resources+=("$resource") + resource_name= + resource_uris= + resource_scopes= + users= + roles= + authenticated=false +} + +json_array() { + echo -n '[' + while [ $# -gt 0 ]; do + x=${1//\\/\\\\} + echo -n "\"${x//\"/\\\"}\"" + [ $# -gt 1 ] && echo -n ', ' + shift + done + echo ']' +} + +join_array() { + local IFS="$1" + shift + echo "$*" +} + +while true; do + case "$1" in + --id) + client_id="$2" + shift 2 + ;; + --name) + client_name="$2" + shift 2 + ;; + --description) + client_description="$2" + shift 2 + ;; + --resource) + if [ -n "${resource_name}" ]; then + add_resource + fi + resource_name="$2" + shift 2 + ;; + --default) + resource_name="Default Resource" + resource_uris="/*" + resource_scopes="access" + users= + roles= + authenticated=true + shift + ;; + --authenticated) + authenticated="$1" + shift + ;; + --uris) + resource_uris="$2" + shift 2 + ;; + --scopes) + resource_scopes="$2" + shift 2 + ;; + --users) + users="$2" + shift 2 + ;; + --roles) + roles="$2" + shift 2 + ;; + -e) + environment="$2" + shift 2 + ;; + -u) + username="$2" + shift 2 + ;; + -p) + password="$2" + shift 2 + ;; + -t | --token) + access_token="$2" + shift 2 + ;; + -h) + echo "$usage" + exit 1 + ;; + --) + shift + break + ;; + *) break ;; + esac +done + +if [ "$args_count" -ne 0 ]; then + if [ -n "${client_id}" ]; then + add_resource + fi +else + read -rp "> Environment (local/develop/demo/production): " environment + if [ -z "$environment" ]; then + echo "Using default environment (local)" + environment="local" + fi + # no args passed, ask for input + if [ "$environment" != "local" ]; then + read -rp "> Username (optional): " username + read -rsp "> Password (optional): " password + if [ -n "$password" ]; then + echo "*********" + else + echo "" + fi + if [ -z "$username" ] && [ -z "$password" ]; then + read -rsp "> Access token: " access_token + if [ -n "$access_token" ]; then + echo "******************" + else + echo "" + fi + fi + if [ -z "$username" ] && [ -z "$password" ] && [ -z "$access_token" ]; then + echo "Authentication is required" + exit 1 + fi + fi + read -rp "> Client Id: " client_id + read -rp "> Client Name (optional): " client_name + read -rp "> Client Description (optional): " client_description + read -rp "> Add resource? [y/N] " add_resource_answer + resources=() + while [ "$add_resource_answer" == y ]; do + read -rp "> Resource name: " resource_name + read -rp "> Resource URIs: " resource_uris + read -rp "> Resource scopes (optional): " resource_scopes + if [ -z "${resource_scopes}" ]; then + echo "Using default scope (access)" + resource_scopes="access" + fi + read -rp "> Users (optional): " users + read -rp "> Roles (optional): " roles + if [ -z "$users" ] && [ -z "$roles" ]; then + read -rp "> Authenticated only? [y/N] " authenticated + if [ "$authenticated" == y ]; then + authenticated=true + fi + fi + add_resource + read -rp "> Add resource? [y/N] " add_resource_answer + done +fi + +if [ "$environment" != "local" ]; then + if [ -z "$username" ] && [ -z "$password" ] && [ -z "$access_token" ]; then + echo "> Authentication is required" + exit 1 + fi +fi + +if [ -z "$client_id" ]; then + echo "Missing client id" + exit 1 +fi + +url= +if [ "$environment" == "local" ]; then + url="http://localhost:8080" +elif [[ "$environment" == "develop" || "$environment" == "demo" ]]; then + url="https://identity.api.${environment}.eoepca.org" +elif [ "$environment" == "production" ]; then + url="https://identity.api.eoepca.org" +else + echo "Invalid environment $environment" + exit 1 +fi +endpoint="$url/clients" +payload="" +if ((${#resources[@]} == 0)); then + payload="{ + \"clientId\": \"${client_id}\", + \"name\": \"${client_name}\", + \"description\": \"${client_description}\" + }" +else + payload="{ + \"clientId\": \"${client_id}\", + \"name\": \"${client_name}\", + \"description\": \"${client_description}\", + \"resources\": [$(join_array , "${resources[@]}")] + }" +fi +echo "" +echo "Adding client" +echo "$endpoint" +echo "$payload" +echo "" +if [[ -n "$username" && -n "$password" ]]; then + curl -i \ + --user "$username:$password" \ + -H "Content-Type: application/json" \ + -X POST --data "$payload" "$endpoint" +elif [ -n "$access_token" ]; then + curl -i \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $access_token" \ + -X POST --data "$payload" "$endpoint" +else + curl -i \ + -H "Content-Type: application/json" \ + -X POST --data "$payload" "$endpoint" +fi \ No newline at end of file diff --git a/identityutils/keycloak_client.py b/identityutils/keycloak_client.py index d8dcdc0..37c1c29 100644 --- a/identityutils/keycloak_client.py +++ b/identityutils/keycloak_client.py @@ -9,6 +9,7 @@ Logger.get_instance().load_configuration(os.path.join(os.path.dirname(__file__), "../logging.yml")) logger = logging.getLogger("IDENTITY_UTILS") + class KeycloakClient: def __init__(self, server_url, realm, username, password): @@ -21,10 +22,8 @@ def __init__(self, server_url, realm, username, password): verify=self.server_url.startswith('https'), timeout=10) self.keycloak_admin = KeycloakAdmin(connection=openid_connection) - self.admin_client = None - self.oauth2_proxy_client = None + # TODO init keycloak_uma self.keycloak_uma = None - self.keycloak_uma_openid = None self.set_realm(realm) # we have one admin client to do admin REST API calls admin_client_id = self.keycloak_admin.get_client_id('admin-cli') @@ -40,7 +39,6 @@ def __init__(self, server_url, realm, username, password): timeout=10) self.keycloak_admin = KeycloakAdmin(connection=openid_connection) - def set_realm(self, realm): if realm != 'master': self.keycloak_admin.create_realm(payload={"realm": self.realm, "enabled": True}, skip_exists=True) @@ -63,7 +61,6 @@ def register_resource(self, resource, client_id): logger.info('Response: ' + str(response)) return response - def update_resource(self, resource_id, resource, client_id): _client_id = self.keycloak_admin.get_client_id(client_id) if "_id" not in resource: @@ -89,7 +86,8 @@ def delete_policies(self, policies, client_id): policies = [policies] logger.info("Deleting policies: " + str(policies)) _client_id = self.keycloak_admin.get_client_id(client_id) - delete_policies = list(filter(lambda p: p.get('name') in policies, self.keycloak_admin.get_client_authz_policies(_client_id))) + delete_policies = list( + filter(lambda p: p.get('name') in policies, self.keycloak_admin.get_client_authz_policies(_client_id))) logger.info("Policies to delete: " + str(delete_policies)) if not delete_policies: logger.info("Policies not found: " + str(policies)) @@ -100,7 +98,7 @@ def delete_policies(self, policies, client_id): def __register_policy(self, policy, register_f, client_id): _client_id = self.keycloak_admin.get_client_id(client_id) logger.info("Creating policy:\n" + json.dumps(policy, indent=2)) - response = register_f(client_id=_client_id, payload=policy, skip_exists = True) + response = register_f(client_id=_client_id, payload=policy, skip_exists=True) logger.info("Response: " + str(response)) return response @@ -176,8 +174,7 @@ def register_role_policy(self, policy, client_id): data_raw, KeycloakPostError, expected_codes=[201, 409], skip_exists=True ) - - def register_time_policy(self,policy, client_id): + def register_time_policy(self, policy, client_id): # time can be one of: # "notAfter":"1970-01-01 00:00:00" # "notBefore":"1970-01-01 00:00:00" @@ -221,6 +218,15 @@ def register_general_policy(self, policy, client_id, policy_type): data_raw, KeycloakPostError, expected_codes=[201, 409], skip_exists=True ) + def register_general_policy(self, policy, client_id, policy_type): + _client_id = self.keycloak_admin.get_client_id(client_id) + params_path = {"realm-name": self.realm, "id": _client_id} + url = urls_patterns.URL_ADMIN_CLIENT_AUTHZ + "/policy/" + policy_type + "?max=-1" + data_raw = self.keycloak_admin.raw_post(url.format(**params_path), data=json.dumps(policy)) + return raise_error_from_response( + data_raw, KeycloakPostError, expected_codes=[201, 409], skip_exists=True + ) + def assign_resources_permissions(self, permissions, client_id): if not isinstance(permissions, list): permissions = [permissions] @@ -390,7 +396,7 @@ def get_policies(self, name: str = "", scope: str = "", first: int = 0, - maximum: int = -1,) -> list[str]: + maximum: int = -1, ) -> list[str]: return self.keycloak_uma.policy_query(resource, name, scope, first, maximum) @@ -398,11 +404,11 @@ def get_client_authz_policies(self, client_id): _client_id = self.keycloak_admin.get_client_id(client_id) return self.keycloak_admin.get_client_authz_policies(_client_id) - def update_policy(self, policy_id, payload, client_id): + def update_policy(self, client_id, policy_id, payload): _client_id = self.keycloak_admin.get_client_id(client_id) params_path = {"realm-name": self.realm, "id": _client_id} policy_type = payload["type"] - url = urls_patterns.URL_ADMIN_CLIENT_AUTHZ + "/policy/" + policy_type +"/"+policy_id + url = urls_patterns.URL_ADMIN_CLIENT_AUTHZ + "/policy/" + policy_type + "/" + policy_id data_raw = self.keycloak_admin.raw_put(url.format(**params_path), data=json.dumps(payload)) return raise_error_from_response( data_raw, KeycloakPostError @@ -429,10 +435,10 @@ def get_client_resource_permissions(self, client_id): data_raw, KeycloakGetError ) - #def get_client_authz_scope_permissions(self,client_id, scope_id): + # def get_client_authz_scope_permissions(self,client_id, scope_id): # return self.keycloak_admin.get_client_authz_scope_permission(client_id, scope_id) - #def create_client_authz_scope_based_permission(self, client_id, payload): + # def create_client_authz_scope_based_permission(self, client_id, payload): # return self.keycloak_admin.create_client_authz_scope_based_permission(client_id, payload, skip_exists=True) def create_client_authz_resource_based_permission(self, client_id, payload): @@ -447,12 +453,12 @@ def update_client_authz_resource_permission(self, client_id, payload, permission _client_id = self.keycloak_admin.get_client_id(client_id) params_path = {"realm-name": self.realm, "id": _client_id} url = urls_patterns.URL_ADMIN_CLIENT_AUTHZ + "/permission/resource/" + permission_id - data_raw = self.keycloak_admin.raw_put(url.format(**params_path), data = json.dumps(payload)) + data_raw = self.keycloak_admin.raw_put(url.format(**params_path), data=json.dumps(payload)) return raise_error_from_response( data_raw, KeycloakPutError ) - #def update_client_authz_scope_permission(self, client_id, payload, scope_id): + # def update_client_authz_scope_permission(self, client_id, payload, scope_id): # return self.keycloak_admin.update_client_authz_scope_permission(payload, client_id, scope_id) def get_client_scopes(self, client_id, name): @@ -468,7 +474,7 @@ def create_client_scopes(self, client_id, payload): _client_id = self.keycloak_admin.get_client_id(client_id) params_path = {"realm-name": self.realm, "id": _client_id} url = urls_patterns.URL_ADMIN_CLIENT_AUTHZ + "/scope" - data_raw = self.keycloak_admin.raw_post(url.format(**params_path), data = json.dumps(payload)) + data_raw = self.keycloak_admin.raw_post(url.format(**params_path), data=json.dumps(payload)) return raise_error_from_response( data_raw, KeycloakPostError ) @@ -477,7 +483,7 @@ def update_client_scopes(self, client_id, scope_id, payload): _client_id = self.keycloak_admin.get_client_id(client_id) params_path = {"realm-name": self.realm, "id": _client_id} url = urls_patterns.URL_ADMIN_CLIENT_AUTHZ + "/scope/" + scope_id - data_raw = self.keycloak_admin.raw_put(url.format(**params_path), data = json.dumps(payload)) + data_raw = self.keycloak_admin.raw_put(url.format(**params_path), data=json.dumps(payload)) return raise_error_from_response( data_raw, KeycloakPutError ) @@ -490,7 +496,7 @@ def delete_client_scopes(self, client_id, scope_id): return raise_error_from_response( data_raw ) - + def delete_resource_permissions(self, client_id, permission_id): _client_id = self.keycloak_admin.get_client_id(client_id) params_path = {"realm-name": self.realm, "id": _client_id} @@ -499,6 +505,10 @@ def delete_resource_permissions(self, client_id, permission_id): return raise_error_from_response( data_raw ) - - def create_client(self, payload): - return self.keycloak_admin.create_client(payload=payload) + + def create_client(self, payload, skip_exists=True): + data_raw = self.keycloak_admin.create_client(payload=payload, skip_exists=skip_exists) + return raise_error_from_response( + data_raw + ) +