From c10447edba94107f1e1582fa4f598056fdf27506 Mon Sep 17 00:00:00 2001 From: Hector Rodriguez Date: Thu, 17 Sep 2020 10:29:52 +0200 Subject: [PATCH 01/80] Testing Static Code Analysis --- .travis.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.travis.yml b/.travis.yml index c2be79e..ad05f3e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,15 @@ jobs: install: pip3 install -r src/requirements.txt script: python3 -m unittest tests/testMongo.py + - stage: Static Code Analysis + language: java + if: branch = develop + before_install: + - wget https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-3.3.0.1492-linux.zip 14 + - unzip sonar-scanner-cli-3.3.0.1492-linux.zip -d /tmp + script: /tmp/sonar-scanner-3.3.0.1492-linux/bin/sonar-scanner -Dsonar.projectKey=$SONARQUBE_PROJECT_KEY -Dsonar.sources=. -Dsonar.host.url=$SONARQUBE_URL -Dsonar.login=$SONAR_TOKEN + + - stage: container creation and publishing install: skip script: travis/containerCreation.sh um-pep-engine From bcc9d4bec1e2f2cce895e99d61a9154092571e00 Mon Sep 17 00:00:00 2001 From: Hector Rodriguez Date: Thu, 17 Sep 2020 10:33:54 +0200 Subject: [PATCH 02/80] Update .travis.yml --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index ad05f3e..69bd375 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,7 @@ jobs: language: java if: branch = develop before_install: - - wget https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-3.3.0.1492-linux.zip 14 + - wget https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-3.3.0.1492-linux.zip - unzip sonar-scanner-cli-3.3.0.1492-linux.zip -d /tmp script: /tmp/sonar-scanner-3.3.0.1492-linux/bin/sonar-scanner -Dsonar.projectKey=$SONARQUBE_PROJECT_KEY -Dsonar.sources=. -Dsonar.host.url=$SONARQUBE_URL -Dsonar.login=$SONAR_TOKEN From cdee5886757fc96e64d3e84cdc60c2fc2bae5166 Mon Sep 17 00:00:00 2001 From: TiagoMF20 Date: Mon, 21 Sep 2020 17:26:10 +0000 Subject: [PATCH 03/80] EOEPCA-35 added rsc blueprint --- src/custom_oidc.py | 27 +++-- src/main.py | 8 +- src/resources/resources.py | 195 +++++++++++++++++++++++++++++++++++++ 3 files changed, 211 insertions(+), 19 deletions(-) create mode 100644 src/resources/resources.py diff --git a/src/custom_oidc.py b/src/custom_oidc.py index e415e77..24a2c7b 100644 --- a/src/custom_oidc.py +++ b/src/custom_oidc.py @@ -36,34 +36,29 @@ def get_new_pat(self): return access_token - def verify_JWT_token(self, token): + def verify_JWT_token(self, token, key): try: payload = str(token).split(".")[1] paddedPayload = payload + '=' * (4 - len(payload) % 4) decoded = base64.b64decode(paddedPayload) - userInum = json.loads(decoded)["sub"] - return userInum + user_value = json.loads(decoded)[key] + return user_value except: print("Authenticated RPT Resource. No Valid JWT id token passed!") - return False + return None - def verify_OAuth_token(self, token): + def verify_OAuth_token(self, token, key): headers = { 'content-type': "application/json", 'Authorization' : 'Bearer '+token} - msg = "Host unreachable" - status = 401 url = self.wkh.get(TYPE_OIDC, KEY_OIDC_USERINFO_ENDPOINT ) try: res = get(url, headers=headers, verify=False) - status = res.status_code - msg = res.text user = (res.json()) - return user['sub'] + return user[key] except: - print("OIDC Handler: Get User Unique Identifier: Exception occured!") - status = 500 - return status, {} + print("OIDC Handler: Get User "+key+": Exception occured!") + return None - def verify_uid_headers(self, headers_protected): + def verify_uid_headers(self, headers_protected, key): uid = None #Retrieve the token from the headers for i in headers_protected: @@ -74,9 +69,9 @@ def verify_uid_headers(self, headers_protected): if token_protected: #Compares between JWT id_token and OAuth access token to retrieve the UUID if len(str(token_protected))>40: - uid=self.verify_JWT_token(token_protected) + uid=self.verify_JWT_token(token_protected, key) else: - uid=self.verify_OAuth_token(token_protected) + uid=self.verify_OAuth_token(token_protected, key) return uid else: diff --git a/src/main.py b/src/main.py index e99f18b..a9adeeb 100644 --- a/src/main.py +++ b/src/main.py @@ -251,7 +251,7 @@ def getResourceList(): try: head_protected = str(request.headers) headers_protected = head_protected.split() - uid = oidc_client.verify_uid_headers(headers_protected) + uid = oidc_client.verify_uid_headers(headers_protected, "sub") except Exception as e: print("Error While passing the token: "+str(uid)) response.status_code = 500 @@ -266,6 +266,7 @@ def getResourceList(): found_uid = True if found_uid is False: + #FIXME this WILL lead to an exception, as variable scopes is not defined ticket = uma_handler.request_access_ticket([{"resource_id": resource_id, "resource_scopes": scopes }]) # Return ticket response.headers["WWW-Authenticate"] = "UMA realm="+g_config["realm"]+",as_uri="+g_config["auth_server_url"]+",ticket="+ticket @@ -308,7 +309,7 @@ def resource_operation(resource_id): try: head_protected = str(request.headers) headers_protected = head_protected.split() - uid = oidc_client.verify_uid_headers(headers_protected) + uid = oidc_client.verify_uid_headers(headers_protected, "sub") except Exception as e: print("Error While passing the token: "+str(uid)) response.status_code = 500 @@ -322,6 +323,7 @@ def resource_operation(resource_id): if uid: print("UID for the user found") else: + #FIXME this WILL lead to an exception, as variable scopes is not defined ticket = uma_handler.request_access_ticket([{"resource_id": resource_id, "resource_scopes": scopes }]) # Return ticket response.headers["WWW-Authenticate"] = "UMA realm="+g_config["realm"]+",as_uri="+g_config["auth_server_url"]+",ticket="+ticket @@ -335,7 +337,7 @@ def resource_operation(resource_id): return uma_handler.create(data.get("name"), data.get("resource_scopes"), data.get("description"), uid, data.get("icon_uri")) else: response.status_code = 500 - response.headers["Error"] = "Invalid data or incorrect resource name passed on URL called for resource creation!" + response.headers["Error"] = "Invalid data passed on URL called for resource creation!" return response except Exception as e: print("Error while creating resource: "+str(e)) diff --git a/src/resources/resources.py b/src/resources/resources.py new file mode 100644 index 0000000..2aa0fba --- /dev/null +++ b/src/resources/resources.py @@ -0,0 +1,195 @@ +from flask import Blueprint, request, Response, jsonify +import json +from eoepca_scim import EOEPCA_Scim, ENDPOINT_AUTH_CLIENT_POST +from custom_oidc import OIDCHandler +from custom_uma import UMA_Handler, resource +from custom_uma import rpt as class_rpt +from custom_mongo import Mongo_Handler + +from xacml import parser, decision +from utils import ClassEncoder + +def construct_blueprint(oidc_client, uma_handler, g_config): + policy_bp = Blueprint('resources_bp', __name__) + + @resources_bp.route("/resources", methods=["GET"]) + def get_resource_list(): + print("Retrieving all registed resources...") + resources = uma_handler.get_all_resources() + rpt = request.headers.get('Authorization') + response = Response() + resourceListToReturn = [] + resourceListToValidate = [] + + custom_mongo = Mongo_Handler() + uid = None + try: + head_protected = str(request.headers) + headers_protected = head_protected.split() + uid = oidc_client.verify_uid_headers(headers_protected, "sub") + except Exception as e: + print("Error While passing the token: "+str(uid)) + response.status_code = 500 + response.headers["Error"] = str(e) + return response + + found_uid = False + for riD_uid in resources: + #If UUID exists and resource requested has same UUID + if uid and custom_mongo.verify_uid(riD_uid, uid): + print("UID for the user found") + found_uid = True + + if found_uid is False: + response.status_code = 401 + response.headers["Error"] = 'Could not get the UID for the user' + return response + + if rpt: + print("Token found: " + rpt) + rpt = rpt.replace("Bearer ","").strip() + #Token was found, check for validation + for rID in resources: + #In here we will use the loop for 2 goals: build the resource list to validate (all of them) and the potential reply list of resources, to avoid a second loop + scopes = uma_handler.get_resource_scopes(rID) + resourceListToValidate.append({"resource_id": rID, "resource_scopes": scopes }) + r = uma_handler.get_resource(rID) + entry = {'_id': r["_id"], 'name': r["name"]} + resourceListToReturn.append(entry) + if uma_handler.validate_rpt(rpt, resourceListToValidate, g_config["s_margin_rpt_valid"]) or not api_rpt_uma_validation: + return json.dumps(resourceListToReturn) + print("No auth token, or auth token is invalid") + if resourceListToValidate: + # Generate ticket if token is not present + ticket = uma_handler.request_access_ticket(resourceListToValidate) + + # Return ticket + response.headers["WWW-Authenticate"] = "UMA realm="+g_config["realm"]+",as_uri="+g_config["auth_server_url"]+",ticket="+ticket + response.status_code = 401 # Answer with "Unauthorized" as per the standard spec. + return response + response.status_code = 500 + return response + + @resources_bp.route("/resources/", methods=["GET", "PUT", "POST", "DELETE"]) + def resource_operation(resource_id): + print("Processing " + request.method + " resource request...") + response = Response() + custom_mongo = Mongo_Handler() + uid = None + #Inspect JWT token (UMA) or query OIDC userinfo endpoint (OAuth) for user id + try: + head_protected = str(request.headers) + headers_protected = head_protected.split() + uid = oidc_client.verify_uid_headers(headers_protected, "sub") + except Exception as e: + print("Error While passing the token: "+str(uid)) + response.status_code = 500 + response.headers["Error"] = str(e) + return response + + #add resource is outside of rpt validation, as it only requires a client pat to register a new resource + if request.method == "POST": + return create_resource(uid, request, uma_handler, response) + + rpt = request.headers.get('Authorization') + #If UUID exists and resource requested has same UUID + if uid and custom_mongo.verify_uid(resource_id, uid): + print("UID for the user found") + else: + response.status_code = 401 + response.headers["Error"] = 'Could not get the UID for the user' + return response + # Get resource scopes from resource_id + try: + scopes = uma_handler.get_resource_scopes(resource_id) + except Exception as e: + print("Error occured when retrieving resource scopes: " +str(e)) + scopes = None + if rpt: + #Token was found, check for validation + print("Found rpt in request, validating...") + rpt = rpt.replace("Bearer ","").strip() + if uma_handler.validate_rpt(rpt, [{"resource_id": resource_id, "resource_scopes": scopes }], g_config["s_margin_rpt_valid"]) or not api_rpt_uma_validation: + print("RPT valid, proceding...") + try: + #retrieve resource + if request.method == "GET": + return uma_handler.get_resource(resource_id) + #update resource + elif request.method == "PUT": + if request.is_json: + data = request.get_json() + if data.get("name") and data.get("resource_scopes"): + uma_handler.update(resource_id, data.get("name"), data.get("resource_scopes"), data.get("description"), uid, data.get("icon_uri")) + response.status_code = 200 + return response + #delete resource + elif request.method == "DELETE": + uma_handler.delete(resource_id) + response.status_code = 204 + return response + except Exception as e: + print("Error while redirecting to resource: "+str(e)) + response.status_code = 500 + return response + + print("No auth token, or auth token is invalid") + #Scopes have already been queried at this time, so if they are not None, we know the resource has been found. This is to avoid a second query. + if scopes is not None: + print("Matched resource: "+str(resource_id)) + # Generate ticket if token is not present + ticket = uma_handler.request_access_ticket([{"resource_id": resource_id, "resource_scopes": scopes }]) + + # Return ticket + response.headers["WWW-Authenticate"] = "UMA realm="+g_config["realm"]+",as_uri="+g_config["auth_server_url"]+",ticket="+ticket + response.status_code = 401 # Answer with "Unauthorized" as per the standard spec. + return response + else: + print("Error, resource not found!") + response.status_code = 500 + return response + + def create_resource(uid, request, uma_handler, response): + try: + #If UUID does not exist + if not uid: + print("UID for the user not found") + response.status_code = 401 + response.headers["Error"] = 'Could not get the UID for the user' + return response + + if request.is_json: + data = request.get_json() + if data.get("name") and data.get("resource_scopes"): + return uma_handler.create(data.get("name"), data.get("resource_scopes"), data.get("description"), uid, data.get("icon_uri")) + else: + response.status_code = 500 + response.headers["Error"] = "Invalid data passed on URL called for resource creation!" + return response + except Exception as e: + print("Error while creating resource: "+str(e)) + response.status_code = 500 + response.headers["Error"] = str(e) + return response + + #TODO + def update_resource(): + return + + #TODO + def delete_resource(): + return + + #TODO + def get_resource(): + return + + #TODO + def is_user_authorized(): + is_operator = False + is_owner = False + is_authorized = False + #TODO + return is_authorized + + return resources_bp \ No newline at end of file From fdbb471210fba482e76de805ac3ec5b838ef879c Mon Sep 17 00:00:00 2001 From: TiagoMF20 Date: Tue, 22 Sep 2020 15:03:29 +0000 Subject: [PATCH 04/80] EOEPCA-35 blueprint iteration --- src/custom_mongo.py | 24 +++++++++-- src/custom_oidc.py | 10 ++--- src/resources/resources.py | 85 +++++++++++++++++++++----------------- 3 files changed, 74 insertions(+), 45 deletions(-) diff --git a/src/custom_mongo.py b/src/custom_mongo.py index c7480eb..bb6494a 100644 --- a/src/custom_mongo.py +++ b/src/custom_mongo.py @@ -41,8 +41,10 @@ def resource_exists(self, resource_id): ''' col = self.db['resources'] myquery = { "resource_id": resource_id } - if col.find_one(myquery): return True - else: return False + if col.find_one(myquery): + return True + else: + return False def insert_in_mongo(self, resource_id: str, name: str, ownership_id: str, reverse_match_url: str): ''' @@ -74,6 +76,21 @@ def insert_in_mongo(self, resource_id: str, name: str, ownership_id: str, revers x = col.insert_one(myres) return x + def get_resource(self, resource_id): + ''' + Gets an existing resource from the database, or None if not found + ''' + col = self.db['resources'] + myquery = { "resource_id": resource_id } + return col.find_one(myquery) + + #TODO + def get_all_resources(self): + ''' + Gets all existing resources in database + ''' + return + def delete_resource(self, resource_id): ''' Check the existence of the resource inside the database @@ -102,7 +119,8 @@ def verify_uid(self, resource_id, uid): a= col.find_one(myquery) if a: return True - else: return False + else: + return False except: print('no resource with that UID associated') return False diff --git a/src/custom_oidc.py b/src/custom_oidc.py index 24a2c7b..3d96351 100644 --- a/src/custom_oidc.py +++ b/src/custom_oidc.py @@ -59,7 +59,7 @@ def verify_OAuth_token(self, token, key): return None def verify_uid_headers(self, headers_protected, key): - uid = None + value = None #Retrieve the token from the headers for i in headers_protected: if 'Bearer' in str(i): @@ -67,13 +67,13 @@ def verify_uid_headers(self, headers_protected, key): inputToken_protected = headers_protected[aux_protected+1] token_protected = inputToken_protected if token_protected: - #Compares between JWT id_token and OAuth access token to retrieve the UUID + #Compares between JWT id_token and OAuth access token to retrieve the requested key-value if len(str(token_protected))>40: - uid=self.verify_JWT_token(token_protected, key) + value=self.verify_JWT_token(token_protected, key) else: - uid=self.verify_OAuth_token(token_protected, key) + value=self.verify_OAuth_token(token_protected, key) - return uid + return value else: return 'NO TOKEN FOUND' diff --git a/src/resources/resources.py b/src/resources/resources.py index 2aa0fba..bad7d6f 100644 --- a/src/resources/resources.py +++ b/src/resources/resources.py @@ -86,25 +86,42 @@ def resource_operation(resource_id): response.status_code = 500 response.headers["Error"] = str(e) return response + + #If UUID does not exist + if not uid: + print("UID for the user not found") + response.status_code = 401 + response.headers["Error"] = 'Could not get the UID for the user' + return response #add resource is outside of rpt validation, as it only requires a client pat to register a new resource if request.method == "POST": return create_resource(uid, request, uma_handler, response) - rpt = request.headers.get('Authorization') - #If UUID exists and resource requested has same UUID - if uid and custom_mongo.verify_uid(resource_id, uid): - print("UID for the user found") + #Is this user the resource's owner? + is_owner = custom_mongo.verify_uid(resource_id, uid) + #Is this user an operator? + is_operator = oidc_client.verify_uid_headers(headers_protected, "operator") + #Above query returns a None in case of Exception, following condition asserts False for that case + if not is_operator: + is_operator = False + + #If UUID exists and the user has sufficient access privileges + if uid and (is_owner or is_operator): + print("UID for the user found and is authorized") else: response.status_code = 401 - response.headers["Error"] = 'Could not get the UID for the user' + response.headers["Error"] = 'No resource found for that ID or lack of access privilege' return response + # Get resource scopes from resource_id try: scopes = uma_handler.get_resource_scopes(resource_id) except Exception as e: print("Error occured when retrieving resource scopes: " +str(e)) scopes = None + + rpt = request.headers.get('Authorization') if rpt: #Token was found, check for validation print("Found rpt in request, validating...") @@ -114,20 +131,19 @@ def resource_operation(resource_id): try: #retrieve resource if request.method == "GET": - return uma_handler.get_resource(resource_id) + return get_resource(custom_mongo, resource_id) #update resource elif request.method == "PUT": - if request.is_json: - data = request.get_json() - if data.get("name") and data.get("resource_scopes"): - uma_handler.update(resource_id, data.get("name"), data.get("resource_scopes"), data.get("description"), uid, data.get("icon_uri")) - response.status_code = 200 - return response + if is_owner or is_operator: + return update_resource(request, resource_id, uid, response) + else: + return user_not_authorized(response) #delete resource elif request.method == "DELETE": - uma_handler.delete(resource_id) - response.status_code = 204 - return response + if is_owner or is_operator: + return delete_resource(uma_handler, resource_id, response) + else: + return user_not_authorized(response) except Exception as e: print("Error while redirecting to resource: "+str(e)) response.status_code = 500 @@ -151,13 +167,6 @@ def resource_operation(resource_id): def create_resource(uid, request, uma_handler, response): try: - #If UUID does not exist - if not uid: - print("UID for the user not found") - response.status_code = 401 - response.headers["Error"] = 'Could not get the UID for the user' - return response - if request.is_json: data = request.get_json() if data.get("name") and data.get("resource_scopes"): @@ -173,23 +182,25 @@ def create_resource(uid, request, uma_handler, response): return response #TODO - def update_resource(): - return + def update_resource(request, resource_id, uid, response): + if request.is_json: + data = request.get_json() + if data.get("name") and data.get("resource_scopes"): + uma_handler.update(resource_id, data.get("name"), data.get("resource_scopes"), data.get("description"), uid, data.get("icon_uri")) + response.status_code = 200 + return response - #TODO - def delete_resource(): - return + def delete_resource(uma_handler, resource_id, response): + uma_handler.delete(resource_id) + response.status_code = 204 + return response - #TODO - def get_resource(): - return + def get_resource(custom_mongo, resource_id): + return custom_mongo.get_resource(resource_id) - #TODO - def is_user_authorized(): - is_operator = False - is_owner = False - is_authorized = False - #TODO - return is_authorized + def user_not_authorized(response): + response.status_code = 403 + response.headers["Error"] = 'User lacking sufficient access privileges' + return response return resources_bp \ No newline at end of file From 765c21298a688110a0c06c00a2a669091934b610 Mon Sep 17 00:00:00 2001 From: Tiago Fernandes Date: Tue, 22 Sep 2020 18:14:31 +0000 Subject: [PATCH 05/80] EOEPCA-35 if statement change --- src/resources/resources.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/resources/resources.py b/src/resources/resources.py index bad7d6f..5ee8543 100644 --- a/src/resources/resources.py +++ b/src/resources/resources.py @@ -12,6 +12,7 @@ def construct_blueprint(oidc_client, uma_handler, g_config): policy_bp = Blueprint('resources_bp', __name__) + #TODO switch to getting resources from DB instead of UMA queries @resources_bp.route("/resources", methods=["GET"]) def get_resource_list(): print("Retrieving all registed resources...") @@ -129,21 +130,18 @@ def resource_operation(resource_id): if uma_handler.validate_rpt(rpt, [{"resource_id": resource_id, "resource_scopes": scopes }], g_config["s_margin_rpt_valid"]) or not api_rpt_uma_validation: print("RPT valid, proceding...") try: - #retrieve resource - if request.method == "GET": - return get_resource(custom_mongo, resource_id) - #update resource - elif request.method == "PUT": - if is_owner or is_operator: + if is_owner or is_operator: + #retrieve resource + if request.method == "GET": + return get_resource(custom_mongo, resource_id) + #update resource + elif request.method == "PUT": return update_resource(request, resource_id, uid, response) - else: - return user_not_authorized(response) - #delete resource - elif request.method == "DELETE": - if is_owner or is_operator: + #delete resource + elif request.method == "DELETE": return delete_resource(uma_handler, resource_id, response) - else: - return user_not_authorized(response) + else: + return user_not_authorized(response) except Exception as e: print("Error while redirecting to resource: "+str(e)) response.status_code = 500 @@ -181,11 +179,11 @@ def create_resource(uid, request, uma_handler, response): response.headers["Error"] = str(e) return response - #TODO def update_resource(request, resource_id, uid, response): if request.is_json: data = request.get_json() if data.get("name") and data.get("resource_scopes"): + #TODO determine how the ownership is changed uma_handler.update(resource_id, data.get("name"), data.get("resource_scopes"), data.get("description"), uid, data.get("icon_uri")) response.status_code = 200 return response From c143bf89609f18cb367f5d949111278310c092b1 Mon Sep 17 00:00:00 2001 From: Tiago Fernandes Date: Wed, 23 Sep 2020 18:09:25 +0000 Subject: [PATCH 06/80] EOEPCA-35 bp prototype --- src/custom_mongo.py | 7 +- src/main.py | 171 +------------------------------------ src/resources/resources.py | 84 ++++++++++++------ 3 files changed, 68 insertions(+), 194 deletions(-) diff --git a/src/custom_mongo.py b/src/custom_mongo.py index bb6494a..ff1b722 100644 --- a/src/custom_mongo.py +++ b/src/custom_mongo.py @@ -32,7 +32,8 @@ def get_id_from_uri(self,uri): k.append(found['resource_id']) if len(k)>0: return k[-1] - else: return None + else: + return None def resource_exists(self, resource_id): ''' @@ -84,12 +85,12 @@ def get_resource(self, resource_id): myquery = { "resource_id": resource_id } return col.find_one(myquery) - #TODO def get_all_resources(self): ''' Gets all existing resources in database ''' - return + col = self.db['resources'] + return col.find() def delete_resource(self, resource_id): ''' diff --git a/src/main.py b/src/main.py index a9adeeb..f949295 100644 --- a/src/main.py +++ b/src/main.py @@ -16,6 +16,7 @@ from custom_uma import UMA_Handler, resource from custom_uma import rpt as class_rpt from custom_mongo import Mongo_Handler +import resources.resources as resources import os import sys import traceback @@ -115,6 +116,9 @@ app = Flask(__name__) app.secret_key = ''.join(choice(ascii_lowercase) for i in range(30)) # Random key +# Register api blueprints (module endpoints) +app.register_blueprint(resources.construct_blueprint(oidc_client, uma_handler, g_config)) + def generateRSAKeyPair(): _rsakey = RSA.generate(2048) private_key = _rsakey.exportKey() @@ -237,173 +241,6 @@ def resource_request(path): response.status_code = 500 return response -@app.route("/resources", methods=["GET"]) -def getResourceList(): - print("Retrieving all registed resources...") - resources = uma_handler.get_all_resources() - rpt = request.headers.get('Authorization') - response = Response() - resourceListToReturn = [] - resourceListToValidate = [] - - custom_mongo = Mongo_Handler() - uid = None - try: - head_protected = str(request.headers) - headers_protected = head_protected.split() - uid = oidc_client.verify_uid_headers(headers_protected, "sub") - except Exception as e: - print("Error While passing the token: "+str(uid)) - response.status_code = 500 - response.headers["Error"] = str(e) - return response - - found_uid = False - for riD_uid in resources: - #If UUID exists and resource requested has same UUID - if uid and custom_mongo.verify_uid(riD_uid, uid): - print("UID for the user found") - found_uid = True - - if found_uid is False: - #FIXME this WILL lead to an exception, as variable scopes is not defined - ticket = uma_handler.request_access_ticket([{"resource_id": resource_id, "resource_scopes": scopes }]) - # Return ticket - response.headers["WWW-Authenticate"] = "UMA realm="+g_config["realm"]+",as_uri="+g_config["auth_server_url"]+",ticket="+ticket - response.status_code = 401 - - response.headers["Error"] = 'Could not get the UID for the user' - return response - - if rpt: - print("Token found: " + rpt) - rpt = rpt.replace("Bearer ","").strip() - #Token was found, check for validation - for rID in resources: - #In here we will use the loop for 2 goals: build the resource list to validate (all of them) and the potential reply list of resources, to avoid a second loop - scopes = uma_handler.get_resource_scopes(rID) - resourceListToValidate.append({"resource_id": rID, "resource_scopes": scopes }) - r = uma_handler.get_resource(rID) - entry = {'_id': r["_id"], 'name': r["name"]} - resourceListToReturn.append(entry) - if uma_handler.validate_rpt(rpt, resourceListToValidate, g_config["s_margin_rpt_valid"]) or not api_rpt_uma_validation: - return json.dumps(resourceListToReturn) - print("No auth token, or auth token is invalid") - if resourceListToValidate: - # Generate ticket if token is not present - ticket = uma_handler.request_access_ticket(resourceListToValidate) - - # Return ticket - response.headers["WWW-Authenticate"] = "UMA realm="+g_config["realm"]+",as_uri="+g_config["auth_server_url"]+",ticket="+ticket - response.status_code = 401 # Answer with "Unauthorized" as per the standard spec. - return response - response.status_code = 500 - return response - -@app.route("/resources/", methods=["GET", "PUT", "POST", "DELETE"]) -def resource_operation(resource_id): - print("Processing " + request.method + " resource request...") - response = Response() - custom_mongo = Mongo_Handler() - uid = None - try: - head_protected = str(request.headers) - headers_protected = head_protected.split() - uid = oidc_client.verify_uid_headers(headers_protected, "sub") - except Exception as e: - print("Error While passing the token: "+str(uid)) - response.status_code = 500 - response.headers["Error"] = str(e) - return response - - #add resource is outside of rpt validation, as it only requires a client pat to register a new resource - try: - if request.method == "POST": - #If UUID exists and resource requested has same UUID - if uid: - print("UID for the user found") - else: - #FIXME this WILL lead to an exception, as variable scopes is not defined - ticket = uma_handler.request_access_ticket([{"resource_id": resource_id, "resource_scopes": scopes }]) - # Return ticket - response.headers["WWW-Authenticate"] = "UMA realm="+g_config["realm"]+",as_uri="+g_config["auth_server_url"]+",ticket="+ticket - response.status_code = 401 - response.headers["Error"] = 'Could not get the UID for the user' - return response - - if request.is_json: - data = request.get_json() - if data.get("name") and data.get("resource_scopes"): - return uma_handler.create(data.get("name"), data.get("resource_scopes"), data.get("description"), uid, data.get("icon_uri")) - else: - response.status_code = 500 - response.headers["Error"] = "Invalid data passed on URL called for resource creation!" - return response - except Exception as e: - print("Error while creating resource: "+str(e)) - response.status_code = 500 - response.headers["Error"] = str(e) - return response - - rpt = request.headers.get('Authorization') - #If UUID exists and resource requested has same UUID - if uid and custom_mongo.verify_uid(resource_id, uid): - print("UID for the user found") - else: - response.status_code = 401 - response.headers["Error"] = 'Could not get the UID for the user' - return response - # Get resource scopes from resource_id - try: - scopes = uma_handler.get_resource_scopes(resource_id) - except Exception as e: - print("Error occured when retrieving resource scopes: " +str(e)) - scopes = None - if rpt: - #Token was found, check for validation - print("Found rpt in request, validating...") - rpt = rpt.replace("Bearer ","").strip() - if uma_handler.validate_rpt(rpt, [{"resource_id": resource_id, "resource_scopes": scopes }], g_config["s_margin_rpt_valid"]) or not api_rpt_uma_validation: - print("RPT valid, proceding...") - try: - #retrieve resource - if request.method == "GET": - return uma_handler.get_resource(resource_id) - #update resource - elif request.method == "PUT": - if request.is_json: - data = request.get_json() - if data.get("name") and data.get("resource_scopes"): - uma_handler.update(resource_id, data.get("name"), data.get("resource_scopes"), data.get("description"), uid, data.get("icon_uri")) - response.status_code = 200 - return response - #delete resource - elif request.method == "DELETE": - uma_handler.delete(resource_id) - response.status_code = 204 - return response - except Exception as e: - print("Error while redirecting to resource: "+str(e)) - response.status_code = 500 - return response - - print("No auth token, or auth token is invalid") - #Scopes have already been queried at this time, so if they are not None, we know the resource has been found. This is to avoid a second query. - if scopes is not None: - print("Matched resource: "+str(resource_id)) - # Generate ticket if token is not present - ticket = uma_handler.request_access_ticket([{"resource_id": resource_id, "resource_scopes": scopes }]) - - # Return ticket - response.headers["WWW-Authenticate"] = "UMA realm="+g_config["realm"]+",as_uri="+g_config["auth_server_url"]+",ticket="+ticket - response.status_code = 401 # Answer with "Unauthorized" as per the standard spec. - return response - else: - print("Error, resource not found!") - response.status_code = 500 - return response - - # Start reverse proxy for x endpoint app.run( diff --git a/src/resources/resources.py b/src/resources/resources.py index 5ee8543..98d7906 100644 --- a/src/resources/resources.py +++ b/src/resources/resources.py @@ -12,17 +12,18 @@ def construct_blueprint(oidc_client, uma_handler, g_config): policy_bp = Blueprint('resources_bp', __name__) - #TODO switch to getting resources from DB instead of UMA queries @resources_bp.route("/resources", methods=["GET"]) def get_resource_list(): print("Retrieving all registed resources...") - resources = uma_handler.get_all_resources() + #gets all resources registered on local DB + custom_mongo = Mongo_Handler() + resources = custom_mongo.get_all_resources() + rpt = request.headers.get('Authorization') response = Response() resourceListToReturn = [] resourceListToValidate = [] - custom_mongo = Mongo_Handler() uid = None try: head_protected = str(request.headers) @@ -34,27 +35,15 @@ def get_resource_list(): response.headers["Error"] = str(e) return response - found_uid = False - for riD_uid in resources: - #If UUID exists and resource requested has same UUID - if uid and custom_mongo.verify_uid(riD_uid, uid): - print("UID for the user found") - found_uid = True - - if found_uid is False: - response.status_code = 401 - response.headers["Error"] = 'Could not get the UID for the user' - return response - if rpt: print("Token found: " + rpt) rpt = rpt.replace("Bearer ","").strip() #Token was found, check for validation - for rID in resources: + for rsrc in resources: #In here we will use the loop for 2 goals: build the resource list to validate (all of them) and the potential reply list of resources, to avoid a second loop - scopes = uma_handler.get_resource_scopes(rID) - resourceListToValidate.append({"resource_id": rID, "resource_scopes": scopes }) - r = uma_handler.get_resource(rID) + scopes = uma_handler.get_resource_scopes(rsrc["resource_id"]) + resourceListToValidate.append({"resource_id": rsrc["resource_id"], "resource_scopes": scopes }) + r = uma_handler.get_resource(rsrc["resource_id"]) entry = {'_id': r["_id"], 'name': r["name"]} resourceListToReturn.append(entry) if uma_handler.validate_rpt(rpt, resourceListToValidate, g_config["s_margin_rpt_valid"]) or not api_rpt_uma_validation: @@ -130,10 +119,12 @@ def resource_operation(resource_id): if uma_handler.validate_rpt(rpt, [{"resource_id": resource_id, "resource_scopes": scopes }], g_config["s_margin_rpt_valid"]) or not api_rpt_uma_validation: print("RPT valid, proceding...") try: + #retrieve resource + #This is outside owner/operator check as reading authorization should be solely determined by rpt validation + if request.method == "GET": + return get_resource(custom_mongo, resource_id) + #Update/Delete requests should only be done by resource owners or operators if is_owner or is_operator: - #retrieve resource - if request.method == "GET": - return get_resource(custom_mongo, resource_id) #update resource elif request.method == "PUT": return update_resource(request, resource_id, uid, response) @@ -164,6 +155,17 @@ def resource_operation(resource_id): return response def create_resource(uid, request, uma_handler, response): + ''' + Creates a new resource. Returns either the full resource data, or an error response + :param uid: unique user ID used to register as owner of the resource + :type uid: str + :param request: resource data in JSON format + :type request: Dictionary + :param uma_handler: Custom handler for UMA operations + :type uma_handler: Object of Class custom_uma + :param response: response object + :type response: Response + ''' try: if request.is_json: data = request.get_json() @@ -180,23 +182,57 @@ def create_resource(uid, request, uma_handler, response): return response def update_resource(request, resource_id, uid, response): + ''' + Updates an existing resource. Returns a 200 OK, or nothing (in order to trigger a ticket generation) + :param uid: unique user ID used to register as owner of the resource + :type uid: str + :param resource_id: unique resource ID + :type resource_id: str + :param request: resource data in JSON format + :type request: Dictionary + :param response: response object + :type response: Response + ''' if request.is_json: data = request.get_json() if data.get("name") and data.get("resource_scopes"): - #TODO determine how the ownership is changed - uma_handler.update(resource_id, data.get("name"), data.get("resource_scopes"), data.get("description"), uid, data.get("icon_uri")) + if "ownership_id" in data: + uma_handler.update(resource_id, data.get("name"), data.get("resource_scopes"), data.get("description"), data.get("ownership_id"), data.get("icon_uri")) + else: + uma_handler.update(resource_id, data.get("name"), data.get("resource_scopes"), data.get("description"), uid, data.get("icon_uri")) response.status_code = 200 return response def delete_resource(uma_handler, resource_id, response): + ''' + Deletes an existing resource. + :param resource_id: unique resource ID + :type resource_id: str + :param uma_handler: Custom handler for UMA operations + :type uma_handler: Object of Class custom_uma + :param response: response object + :type response: Response + ''' uma_handler.delete(resource_id) response.status_code = 204 return response def get_resource(custom_mongo, resource_id): + ''' + Gets an existing resource from local database. + :param resource_id: unique resource ID + :type resource_id: str + :param custom_mongo: Custom handler for Mongo DB operations + :type custom_mongo: Object of Class custom_mongo + ''' return custom_mongo.get_resource(resource_id) def user_not_authorized(response): + ''' + Method to generate error response when user does not have sufficient edit/delete privileges. + :param response: response object + :type response: Response + ''' response.status_code = 403 response.headers["Error"] = 'User lacking sufficient access privileges' return response From cc73b9731ee0bb0b4fac40f158cdc90dc3f48815 Mon Sep 17 00:00:00 2001 From: Tiago Fernandes Date: Thu, 24 Sep 2020 11:38:50 +0000 Subject: [PATCH 07/80] EOEPCA-35 updated attribute tag --- src/resources/resources.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/resources/resources.py b/src/resources/resources.py index 98d7906..6f45a4b 100644 --- a/src/resources/resources.py +++ b/src/resources/resources.py @@ -91,7 +91,7 @@ def resource_operation(resource_id): #Is this user the resource's owner? is_owner = custom_mongo.verify_uid(resource_id, uid) #Is this user an operator? - is_operator = oidc_client.verify_uid_headers(headers_protected, "operator") + is_operator = oidc_client.verify_uid_headers(headers_protected, "isOperator") #Above query returns a None in case of Exception, following condition asserts False for that case if not is_operator: is_operator = False From d30cd3028cd10527b79873b88d92e04ea7246781 Mon Sep 17 00:00:00 2001 From: TiagoMF20 Date: Thu, 24 Sep 2020 14:34:14 +0000 Subject: [PATCH 08/80] EOEPCA-35 getAllRsrcs refactor --- src/resources/resources.py | 43 ++++++++++++++++---------------------- 1 file changed, 18 insertions(+), 25 deletions(-) diff --git a/src/resources/resources.py b/src/resources/resources.py index 6f45a4b..f7666ed 100644 --- a/src/resources/resources.py +++ b/src/resources/resources.py @@ -14,7 +14,7 @@ def construct_blueprint(oidc_client, uma_handler, g_config): @resources_bp.route("/resources", methods=["GET"]) def get_resource_list(): - print("Retrieving all registed resources...") + print("Retrieving all registered resources...") #gets all resources registered on local DB custom_mongo = Mongo_Handler() resources = custom_mongo.get_all_resources() @@ -22,7 +22,6 @@ def get_resource_list(): rpt = request.headers.get('Authorization') response = Response() resourceListToReturn = [] - resourceListToValidate = [] uid = None try: @@ -35,31 +34,25 @@ def get_resource_list(): response.headers["Error"] = str(e) return response - if rpt: - print("Token found: " + rpt) - rpt = rpt.replace("Bearer ","").strip() - #Token was found, check for validation - for rsrc in resources: - #In here we will use the loop for 2 goals: build the resource list to validate (all of them) and the potential reply list of resources, to avoid a second loop - scopes = uma_handler.get_resource_scopes(rsrc["resource_id"]) - resourceListToValidate.append({"resource_id": rsrc["resource_id"], "resource_scopes": scopes }) - r = uma_handler.get_resource(rsrc["resource_id"]) - entry = {'_id': r["_id"], 'name': r["name"]} - resourceListToReturn.append(entry) - if uma_handler.validate_rpt(rpt, resourceListToValidate, g_config["s_margin_rpt_valid"]) or not api_rpt_uma_validation: - return json.dumps(resourceListToReturn) - print("No auth token, or auth token is invalid") - if resourceListToValidate: - # Generate ticket if token is not present - ticket = uma_handler.request_access_ticket(resourceListToValidate) - - # Return ticket - response.headers["WWW-Authenticate"] = "UMA realm="+g_config["realm"]+",as_uri="+g_config["auth_server_url"]+",ticket="+ticket - response.status_code = 401 # Answer with "Unauthorized" as per the standard spec. - return response - response.status_code = 500 + found_uid = False + #We will search for any resources that are owned by the user that is making this call + for rsrc in resources: + #If UUID exists and owns the requested resource + if uid and custom_mongo.verify_uid(rsrc["resource_id"], uid): + print("Matching owned-resource found!") + #Add resource to return list + resourceListToReturn.append({'_id': rsrc["resource_id"], '_name': rsrc["name"]}) + found_uid = True + + #If user-owned resources were found, return the list + if found_uid: + return json.dumps(resourceListToReturn) + #Otherwise + response.status_code = 404 + response.headers["Error"] = "No user-owned resources found!" return response + @resources_bp.route("/resources/", methods=["GET", "PUT", "POST", "DELETE"]) def resource_operation(resource_id): print("Processing " + request.method + " resource request...") From 82884ef5d156afa63322deb89e0043fe185b0ea6 Mon Sep 17 00:00:00 2001 From: TiagoMF20 Date: Thu, 24 Sep 2020 16:26:30 +0000 Subject: [PATCH 09/80] EOEPCA-35 refactor rsrc endpoint --- src/resources/resources.py | 84 +++++++++++++------------------------- 1 file changed, 28 insertions(+), 56 deletions(-) diff --git a/src/resources/resources.py b/src/resources/resources.py index f7666ed..aa0824c 100644 --- a/src/resources/resources.py +++ b/src/resources/resources.py @@ -77,10 +77,11 @@ def resource_operation(resource_id): response.headers["Error"] = 'Could not get the UID for the user' return response - #add resource is outside of rpt validation, as it only requires a client pat to register a new resource + #add resource is outside of any extra validations, so it is called now if request.method == "POST": return create_resource(uid, request, uma_handler, response) + #otherwise continue with validations #Is this user the resource's owner? is_owner = custom_mongo.verify_uid(resource_id, uid) #Is this user an operator? @@ -89,61 +90,24 @@ def resource_operation(resource_id): if not is_operator: is_operator = False - #If UUID exists and the user has sufficient access privileges - if uid and (is_owner or is_operator): - print("UID for the user found and is authorized") - else: - response.status_code = 401 - response.headers["Error"] = 'No resource found for that ID or lack of access privilege' - return response - - # Get resource scopes from resource_id + #Process the remainder GET/PUT(Update)/DELETE scenarios try: - scopes = uma_handler.get_resource_scopes(resource_id) + #retrieve resource + #This is outside owner/operator check as reading authorization should be solely determined by rpt validation + if request.method == "GET": + return get_resource(custom_mongo, resource_id, response) + #Update/Delete requests should only be done by resource owners or operators + if is_owner or is_operator: + #update resource + elif request.method == "PUT": + return update_resource(request, resource_id, uid, response) + #delete resource + elif request.method == "DELETE": + return delete_resource(uma_handler, resource_id, response) + else: + return user_not_authorized(response) except Exception as e: - print("Error occured when retrieving resource scopes: " +str(e)) - scopes = None - - rpt = request.headers.get('Authorization') - if rpt: - #Token was found, check for validation - print("Found rpt in request, validating...") - rpt = rpt.replace("Bearer ","").strip() - if uma_handler.validate_rpt(rpt, [{"resource_id": resource_id, "resource_scopes": scopes }], g_config["s_margin_rpt_valid"]) or not api_rpt_uma_validation: - print("RPT valid, proceding...") - try: - #retrieve resource - #This is outside owner/operator check as reading authorization should be solely determined by rpt validation - if request.method == "GET": - return get_resource(custom_mongo, resource_id) - #Update/Delete requests should only be done by resource owners or operators - if is_owner or is_operator: - #update resource - elif request.method == "PUT": - return update_resource(request, resource_id, uid, response) - #delete resource - elif request.method == "DELETE": - return delete_resource(uma_handler, resource_id, response) - else: - return user_not_authorized(response) - except Exception as e: - print("Error while redirecting to resource: "+str(e)) - response.status_code = 500 - return response - - print("No auth token, or auth token is invalid") - #Scopes have already been queried at this time, so if they are not None, we know the resource has been found. This is to avoid a second query. - if scopes is not None: - print("Matched resource: "+str(resource_id)) - # Generate ticket if token is not present - ticket = uma_handler.request_access_ticket([{"resource_id": resource_id, "resource_scopes": scopes }]) - - # Return ticket - response.headers["WWW-Authenticate"] = "UMA realm="+g_config["realm"]+",as_uri="+g_config["auth_server_url"]+",ticket="+ticket - response.status_code = 401 # Answer with "Unauthorized" as per the standard spec. - return response - else: - print("Error, resource not found!") + print("Error while redirecting to resource: "+str(e)) response.status_code = 500 return response @@ -210,15 +174,23 @@ def delete_resource(uma_handler, resource_id, response): response.status_code = 204 return response - def get_resource(custom_mongo, resource_id): + def get_resource(custom_mongo, resource_id, response): ''' Gets an existing resource from local database. :param resource_id: unique resource ID :type resource_id: str :param custom_mongo: Custom handler for Mongo DB operations :type custom_mongo: Object of Class custom_mongo + :param response: response object + :type response: Response ''' - return custom_mongo.get_resource(resource_id) + resource = custom_mongo.get_resource(resource_id) + #If no resource was found, return a 404 Error + if not resource: + response.status_code = 404 + response.headers["Error"] = "Resource not found" + return response + return resource def user_not_authorized(response): ''' From 6c486ab09050fa36b8d562cd7c7762104ba8cf46 Mon Sep 17 00:00:00 2001 From: TiagoMF20 Date: Fri, 25 Sep 2020 12:39:30 +0000 Subject: [PATCH 10/80] EOEPCA-35 misc fixes --- src/custom_oidc.py | 2 + src/resources/resources.py | 33 ++++++------ tests/testPEPResources.py | 108 ++++++++++++++++--------------------- 3 files changed, 65 insertions(+), 78 deletions(-) diff --git a/src/custom_oidc.py b/src/custom_oidc.py index 3d96351..526eee3 100644 --- a/src/custom_oidc.py +++ b/src/custom_oidc.py @@ -41,6 +41,8 @@ def verify_JWT_token(self, token, key): payload = str(token).split(".")[1] paddedPayload = payload + '=' * (4 - len(payload) % 4) decoded = base64.b64decode(paddedPayload) + #to remove byte-code + decoded = decoded.decode('utf-8') user_value = json.loads(decoded)[key] return user_value except: diff --git a/src/resources/resources.py b/src/resources/resources.py index aa0824c..11e464f 100644 --- a/src/resources/resources.py +++ b/src/resources/resources.py @@ -6,11 +6,8 @@ from custom_uma import rpt as class_rpt from custom_mongo import Mongo_Handler -from xacml import parser, decision -from utils import ClassEncoder - def construct_blueprint(oidc_client, uma_handler, g_config): - policy_bp = Blueprint('resources_bp', __name__) + resources_bp = Blueprint('resources_bp', __name__) @resources_bp.route("/resources", methods=["GET"]) def get_resource_list(): @@ -34,6 +31,12 @@ def get_resource_list(): response.headers["Error"] = str(e) return response + if not uid: + print("UID for the user not found") + response.status_code = 401 + response.headers["Error"] = 'Could not get the UID for the user' + return response + found_uid = False #We will search for any resources that are owned by the user that is making this call for rsrc in resources: @@ -99,7 +102,7 @@ def resource_operation(resource_id): #Update/Delete requests should only be done by resource owners or operators if is_owner or is_operator: #update resource - elif request.method == "PUT": + if request.method == "PUT": return update_resource(request, resource_id, uid, response) #delete resource elif request.method == "DELETE": @@ -112,7 +115,7 @@ def resource_operation(resource_id): return response def create_resource(uid, request, uma_handler, response): - ''' + ''' Creates a new resource. Returns either the full resource data, or an error response :param uid: unique user ID used to register as owner of the resource :type uid: str @@ -122,7 +125,7 @@ def create_resource(uid, request, uma_handler, response): :type uma_handler: Object of Class custom_uma :param response: response object :type response: Response - ''' + ''' try: if request.is_json: data = request.get_json() @@ -139,7 +142,7 @@ def create_resource(uid, request, uma_handler, response): return response def update_resource(request, resource_id, uid, response): - ''' + ''' Updates an existing resource. Returns a 200 OK, or nothing (in order to trigger a ticket generation) :param uid: unique user ID used to register as owner of the resource :type uid: str @@ -149,7 +152,7 @@ def update_resource(request, resource_id, uid, response): :type request: Dictionary :param response: response object :type response: Response - ''' + ''' if request.is_json: data = request.get_json() if data.get("name") and data.get("resource_scopes"): @@ -161,7 +164,7 @@ def update_resource(request, resource_id, uid, response): return response def delete_resource(uma_handler, resource_id, response): - ''' + ''' Deletes an existing resource. :param resource_id: unique resource ID :type resource_id: str @@ -169,13 +172,13 @@ def delete_resource(uma_handler, resource_id, response): :type uma_handler: Object of Class custom_uma :param response: response object :type response: Response - ''' + ''' uma_handler.delete(resource_id) response.status_code = 204 return response def get_resource(custom_mongo, resource_id, response): - ''' + ''' Gets an existing resource from local database. :param resource_id: unique resource ID :type resource_id: str @@ -183,7 +186,7 @@ def get_resource(custom_mongo, resource_id, response): :type custom_mongo: Object of Class custom_mongo :param response: response object :type response: Response - ''' + ''' resource = custom_mongo.get_resource(resource_id) #If no resource was found, return a 404 Error if not resource: @@ -193,11 +196,11 @@ def get_resource(custom_mongo, resource_id, response): return resource def user_not_authorized(response): - ''' + ''' Method to generate error response when user does not have sufficient edit/delete privileges. :param response: response object :type response: Response - ''' + ''' response.status_code = 403 response.headers["Error"] = 'User lacking sufficient access privileges' return response diff --git a/tests/testPEPResources.py b/tests/testPEPResources.py index e594135..87d2d63 100644 --- a/tests/testPEPResources.py +++ b/tests/testPEPResources.py @@ -43,7 +43,7 @@ def setUpClass(cls): _rsajwk = RSAKey(kid='RSA1', key=import_rsa_key(_private_key)) _payload = { "iss": cls.g_config["client_id"], - "sub": cls.g_config["client_secret"], + "sub": cls.g_config["client_id"], "aud": cls.__TOKEN_ENDPOINT, "jti": datetime.datetime.today().strftime('%Y%m%d%s'), "exp": int(time.time())+3600 @@ -67,56 +67,66 @@ def getRPTFromAS(self, ticket): return 200, res.json()["access_token"] return 500, None - def getResourceList(self, rpt="filler"): - headers = { 'content-type': "application/x-www-form-urlencoded", "cache-control": "no-cache", "Authorization": "Bearer "+str(rpt)} + def getJWT(self): + return self.jwt + + def getResourceList(self, id_token="filler"): + headers = { 'content-type': "application/x-www-form-urlencoded", "cache-control": "no-cache", "Authorization": "Bearer "+str(id_token)} res = requests.get(self.PEP_HOST+"/resources", headers=headers, verify=False) if res.status_code == 401: - return 401, res.headers["WWW-Authenticate"].split("ticket=")[1] + return 401, res.headers["Error"] + if res.status_code == 404: + return 404, res.headers["Error"] if res.status_code == 200: return 200, res.json() - return 500, None + return 500, res.headers["Error"] - def createTestResource(self): + def createTestResource(self, id_token="filler"): payload = { "resource_scopes":[ self.scopes ], "icon_uri":"/"+self.resourceName, "name": self.resourceName } - headers = { 'content-type': "application/json", "cache-control": "no-cache" } + headers = { 'content-type': "application/json", "cache-control": "no-cache", "Authorization": "Bearer "+str(id_token) } res = requests.post(self.PEP_HOST+"/resources/"+self.resourceName, headers=headers, json=payload, verify=False) if res.status_code == 200: return 200, res.text return 500, None - def getResource(self, rpt="filler"): - headers = { 'content-type': "application/json", "cache-control": "no-cache", "Authorization": "Bearer "+rpt } + def getResource(self, id_token="filler"): + headers = { 'content-type': "application/json", "cache-control": "no-cache", "Authorization": "Bearer "+id_token } res = requests.get(self.PEP_HOST+"/resources/"+self.resourceID, headers=headers, verify=False) if res.status_code == 401: - return 401, res.headers["WWW-Authenticate"].split("ticket=")[1] + return 401, res.headers["Error"] if res.status_code == 200: return 200, res.json() - return 500, None + if res.status_code == 404: + return 404, res.headers["Error"] + return 500, res.headers["Error"] - def deleteResource(self, rpt="filler"): - headers = { 'content-type': "application/json", "cache-control": "no-cache", "Authorization": "Bearer "+rpt } + def deleteResource(self, id_token="filler"): + headers = { 'content-type': "application/json", "cache-control": "no-cache", "Authorization": "Bearer "+id_token } res = requests.delete(self.PEP_HOST+"/resources/"+self.resourceID, headers=headers, verify=False) if res.status_code == 401: - return 401, res.headers["WWW-Authenticate"].split("ticket=")[1] + return 401, res.headers["Error"] if res.status_code == 204: return 204, None - return 500, None + return 500, res.headers["Error"] - def updateResource(self, rpt="filler"): - headers = { 'content-type': "application/json", "cache-control": "no-cache", "Authorization": "Bearer "+rpt } + def updateResource(self, id_token="filler"): + headers = { 'content-type': "application/json", "cache-control": "no-cache", "Authorization": "Bearer "+id_token } payload = { "resource_scopes":[ self.scopes], "icon_uri":"/"+self.resourceName, "name":self.resourceName+"Mod" } res = requests.put(self.PEP_HOST+"/resources/"+self.resourceID, headers=headers, json=payload, verify=False) if res.status_code == 401: - return 401, res.headers["WWW-Authenticate"].split("ticket=")[1] + return 401, res.headers["Error"] if res.status_code == 200: return 200, None - return 500, None + return 500, res.headers["Error"] #Monolithic test to avoid jumping through hoops to implement ordered tests - #This test case assumes UMA is in place + #This test case assumes v0.3 of the PEP engine def test_resource_UMA(self): + #Use a JWT token as id_token + id_token = self.getJWT() + #Create resource - status, self.resourceID = self.createTestResource() + status, self.resourceID = self.createTestResource(id_token) self.assertEqual(status, 200) print("Create resource: Resource created with id: "+self.resourceID) del status @@ -124,34 +134,19 @@ def test_resource_UMA(self): print("") #Get created resource - #First attempt should return a 401 with a ticket - status, reply = self.getResource() - self.assertNotEqual(status, 500) - #Now we get a valid RPT from the Authorization Server - status, rpt = self.getRPTFromAS(reply) - self.assertEqual(status, 200) - #Now we retry the first call with the valid RPT - status, reply = self.getResource(rpt) + status, reply = self.getResource(id_token) self.assertEqual(status, 200) - #And we finally check if the returned id matches the id we got on creation + #And we check if the returned id matches the id we got on creation #The reply message is in JSON format self.assertEqual(reply["_id"], self.resourceID) print("Get resource: Resource found.") print(reply) - del status, reply, rpt + del status, reply, id_token print("=======================") print("") #Get resource list - #Same MO as above - #First attempt should return a 401 with a ticket - status, reply = self.getResourceList() - self.assertNotEqual(status, 500) - #Now we get a valid RPT from the Authorization Server - status, rpt = self.getRPTFromAS(reply) - self.assertEqual(status, 200) - #Now we retry the first call with the valid RPT - status, reply = self.getResourceList(rpt) + status, reply = self.getResourceList(id_token) self.assertEqual(status, 200) #And we finally check if the returned list contains our created resource #The reply message is a list of resources in JSON format @@ -161,55 +156,42 @@ def test_resource_UMA(self): self.assertTrue(found) print("Get resource list: Resource found on Internal List.") print(reply) - del status, reply, rpt + del status, reply, id_token print("=======================") print("") #Modify created resource #This will simply test if we can modify the pre-determined resource name with "Mod" at the end - #The MO is the same as above tests, so no further comment - status, reply = self.updateResource() - self.assertNotEqual(status, 500) - status, rpt = self.getRPTFromAS(reply) - self.assertEqual(status, 200) - status, _ = self.updateResource(rpt) + status, _ = self.updateResource(id_token) self.assertEqual(status, 200) #Get resource to check if modification actually was successfull - status, reply = self.getResource() - status, rpt = self.getRPTFromAS(reply) - status, reply = self.getResource(rpt) + status, reply = self.getResource(id_token) self.assertEqual(reply["_id"], self.resourceID) self.assertEqual(reply["name"], self.resourceName+"Mod") print("Update resource: Resource properly modified.") print(reply) - del status, reply, rpt + del status, reply, id_token print("=======================") print("") #Delete created resource - status, reply = self.deleteResource() - self.assertNotEqual(status, 500) - status, rpt = self.getRPTFromAS(reply) - self.assertEqual(status, 200) - status, reply = self.deleteResource(rpt) + status, reply = self.deleteResource(id_token) self.assertEqual(status, 204) print("Delete resource: Resource deleted.") - del status, reply, rpt + del status, reply, id_token print("=======================") print("") #Get resource to make sure it was deleted - status, _ = self.getResource() - self.assertEqual(status, 500) + status, _ = self.getResource(id_token) + self.assertEqual(status, 404) print("Get resource: Resource correctly not found.") del status print("=======================") print("") #Get resource list to make sure the resource was removed from internal cache - status, reply = self.getResourceList() - status, rpt = self.getRPTFromAS(reply) - status, reply = self.getResourceList(rpt) + status, reply = self.getResourceList(id_token) found = False for r in reply: @@ -217,7 +199,7 @@ def test_resource_UMA(self): self.assertFalse(found) print("Get resource list: Resource correctly removed from Internal List.") print(reply) - del status, reply, rpt, found + del status, reply, id_token, found print("=======================") print("") From 8e278f86902cc53a25fd948727a6abaf3c6e2890 Mon Sep 17 00:00:00 2001 From: Tiago Fernandes Date: Fri, 25 Sep 2020 18:44:21 +0000 Subject: [PATCH 11/80] EOEPCA-35 test refactoring JWT --- src/custom_oidc.py | 4 ++-- src/resources/resources.py | 25 +++++++++++++++++-------- tests/testPEPResources.py | 30 +++++++++++++----------------- 3 files changed, 32 insertions(+), 27 deletions(-) diff --git a/src/custom_oidc.py b/src/custom_oidc.py index 526eee3..e7fa607 100644 --- a/src/custom_oidc.py +++ b/src/custom_oidc.py @@ -45,8 +45,8 @@ def verify_JWT_token(self, token, key): decoded = decoded.decode('utf-8') user_value = json.loads(decoded)[key] return user_value - except: - print("Authenticated RPT Resource. No Valid JWT id token passed!") + except Exception as e: + print("Authenticated RPT Resource. No Valid JWT id token passed! " +str(e)) return None def verify_OAuth_token(self, token, key): diff --git a/src/resources/resources.py b/src/resources/resources.py index 11e464f..c3e7866 100644 --- a/src/resources/resources.py +++ b/src/resources/resources.py @@ -84,14 +84,19 @@ def resource_operation(resource_id): if request.method == "POST": return create_resource(uid, request, uma_handler, response) - #otherwise continue with validations - #Is this user the resource's owner? - is_owner = custom_mongo.verify_uid(resource_id, uid) - #Is this user an operator? - is_operator = oidc_client.verify_uid_headers(headers_protected, "isOperator") - #Above query returns a None in case of Exception, following condition asserts False for that case - if not is_operator: - is_operator = False + try: + #otherwise continue with validations + #Is this user the resource's owner? + is_owner = custom_mongo.verify_uid(resource_id, uid) + #Is this user an operator? + is_operator = oidc_client.verify_uid_headers(headers_protected, "isOperator") + #Above query returns a None in case of Exception, following condition asserts False for that case + if not is_operator: + is_operator = False + except Exception as e: + print("Error while reading token: "+str(e)) + response.status_code = 500 + return response #Process the remainder GET/PUT(Update)/DELETE scenarios try: @@ -188,11 +193,15 @@ def get_resource(custom_mongo, resource_id, response): :type response: Response ''' resource = custom_mongo.get_resource(resource_id) + #If no resource was found, return a 404 Error if not resource: response.status_code = 404 response.headers["Error"] = "Resource not found" return response + + #We only want to return resource_id (as "_id") and name, so we prune the other entries + resource = {"_id": resource["resource_id"], "_name": resource["name"]} return resource def user_not_authorized(response): diff --git a/tests/testPEPResources.py b/tests/testPEPResources.py index 87d2d63..fe600ba 100644 --- a/tests/testPEPResources.py +++ b/tests/testPEPResources.py @@ -46,7 +46,8 @@ def setUpClass(cls): "sub": cls.g_config["client_id"], "aud": cls.__TOKEN_ENDPOINT, "jti": datetime.datetime.today().strftime('%Y%m%d%s'), - "exp": int(time.time())+3600 + "exp": int(time.time())+3600, + "isOperator": True } _jws = JWS(_payload, alg="RS256") cls.jwt = _jws.sign_compact(keys=[_rsajwk]) @@ -79,7 +80,7 @@ def getResourceList(self, id_token="filler"): return 404, res.headers["Error"] if res.status_code == 200: return 200, res.json() - return 500, res.headers["Error"] + return 500, None def createTestResource(self, id_token="filler"): payload = { "resource_scopes":[ self.scopes ], "icon_uri":"/"+self.resourceName, "name": self.resourceName } @@ -98,7 +99,7 @@ def getResource(self, id_token="filler"): return 200, res.json() if res.status_code == 404: return 404, res.headers["Error"] - return 500, res.headers["Error"] + return 500, None def deleteResource(self, id_token="filler"): headers = { 'content-type': "application/json", "cache-control": "no-cache", "Authorization": "Bearer "+id_token } @@ -107,7 +108,7 @@ def deleteResource(self, id_token="filler"): return 401, res.headers["Error"] if res.status_code == 204: return 204, None - return 500, res.headers["Error"] + return 500, None def updateResource(self, id_token="filler"): headers = { 'content-type': "application/json", "cache-control": "no-cache", "Authorization": "Bearer "+id_token } @@ -117,7 +118,7 @@ def updateResource(self, id_token="filler"): return 401, res.headers["Error"] if res.status_code == 200: return 200, None - return 500, res.headers["Error"] + return 500, None #Monolithic test to avoid jumping through hoops to implement ordered tests #This test case assumes v0.3 of the PEP engine @@ -141,7 +142,7 @@ def test_resource_UMA(self): self.assertEqual(reply["_id"], self.resourceID) print("Get resource: Resource found.") print(reply) - del status, reply, id_token + del status, reply print("=======================") print("") @@ -156,7 +157,7 @@ def test_resource_UMA(self): self.assertTrue(found) print("Get resource list: Resource found on Internal List.") print(reply) - del status, reply, id_token + del status, reply print("=======================") print("") @@ -167,10 +168,10 @@ def test_resource_UMA(self): #Get resource to check if modification actually was successfull status, reply = self.getResource(id_token) self.assertEqual(reply["_id"], self.resourceID) - self.assertEqual(reply["name"], self.resourceName+"Mod") + self.assertEqual(reply["_name"], self.resourceName+"Mod") print("Update resource: Resource properly modified.") print(reply) - del status, reply, id_token + del status, reply print("=======================") print("") @@ -178,7 +179,7 @@ def test_resource_UMA(self): status, reply = self.deleteResource(id_token) self.assertEqual(status, 204) print("Delete resource: Resource deleted.") - del status, reply, id_token + del status, reply print("=======================") print("") @@ -192,14 +193,9 @@ def test_resource_UMA(self): #Get resource list to make sure the resource was removed from internal cache status, reply = self.getResourceList(id_token) - - found = False - for r in reply: - if r["_id"] == self.resourceID: found = True - self.assertFalse(found) + self.assertEqual(status, 404) print("Get resource list: Resource correctly removed from Internal List.") - print(reply) - del status, reply, id_token, found + del status, reply, id_token print("=======================") print("") From a143700f83bde529b43c27d874f25ba597ce9adf Mon Sep 17 00:00:00 2001 From: Tiago Fernandes Date: Mon, 28 Sep 2020 20:02:13 +0000 Subject: [PATCH 12/80] EOEPCA-35 unittest plus misc changes --- src/main.py | 18 ++-- src/resources/resources.py | 6 +- tests/testPEPResources.py | 10 +-- tests/testROChange.py | 168 +++++++++++++++++++++++++++++++++++++ 4 files changed, 183 insertions(+), 19 deletions(-) create mode 100644 tests/testROChange.py diff --git a/src/main.py b/src/main.py index f949295..226361f 100644 --- a/src/main.py +++ b/src/main.py @@ -76,11 +76,11 @@ print ("NOTICE: Client not found, generating one... ") scim_client = EOEPCA_Scim(g_config["auth_server_url"]) new_client = scim_client.registerClient("PEP Dynamic Client", - grantTypes = ["client_credentials"], + grantTypes = ["client_credentials", "password"], redirectURIs = [""], logoutURI = "", responseTypes = ["code","token","id_token"], - scopes = ['openid', 'uma_protection', 'permission'], + scopes = ['openid', 'uma_protection', 'permission', 'profile', 'is_operator'], token_endpoint_auth_method = ENDPOINT_AUTH_CLIENT_POST) print("NEW CLIENT created with ID '"+new_client["client_id"]+"', since no client config was found on config.json or environment") @@ -105,13 +105,13 @@ uma_handler = UMA_Handler(g_wkh, oidc_client, g_config["check_ssl_certs"]) uma_handler.status() # Demo: register a new resource if it doesn't exist -try: - pass - #uma_handler.create("ADES", ["Authenticated"], description="", ownership_id= '55b8f51f-4634-4bb0-a1dd-070ec5869d70', icon_uri="/pep/ADES") -except Exception as e: - if "already exists" in str(e): - print("Resource already existed, moving on") - else: raise e +# try: +# pass +# #uma_handler.create("ADES", ["Authenticated"], description="", ownership_id= '55b8f51f-4634-4bb0-a1dd-070ec5869d70', icon_uri="/pep/ADES") +# except Exception as e: +# if "already exists" in str(e): +# print("Resource already existed, moving on") +# else: raise e app = Flask(__name__) app.secret_key = ''.join(choice(ascii_lowercase) for i in range(30)) # Random key diff --git a/src/resources/resources.py b/src/resources/resources.py index c3e7866..e0408fc 100644 --- a/src/resources/resources.py +++ b/src/resources/resources.py @@ -97,7 +97,7 @@ def resource_operation(resource_id): print("Error while reading token: "+str(e)) response.status_code = 500 return response - + #Process the remainder GET/PUT(Update)/DELETE scenarios try: #retrieve resource @@ -167,6 +167,10 @@ def update_resource(request, resource_id, uid, response): uma_handler.update(resource_id, data.get("name"), data.get("resource_scopes"), data.get("description"), uid, data.get("icon_uri")) response.status_code = 200 return response + else: + response.status_code = 500 + response.headers["Error"] = "Invalid request" + return response def delete_resource(uma_handler, resource_id, response): ''' diff --git a/tests/testPEPResources.py b/tests/testPEPResources.py index fe600ba..fe3ce4f 100644 --- a/tests/testPEPResources.py +++ b/tests/testPEPResources.py @@ -60,14 +60,6 @@ def tearDownClass(cls): os.remove("private.pem") os.remove("public.pem") - def getRPTFromAS(self, ticket): - headers = { 'content-type': "application/x-www-form-urlencoded", "cache-control": "no-cache"} - payload = { "claim_token_format": "http://openid.net/specs/openid-connect-core-1_0.html#IDToken", "claim_token": self.jwt, "ticket": ticket, "grant_type": "urn:ietf:params:oauth:grant-type:uma-ticket", "client_id": self.g_config["client_id"], "client_secret": self.g_config["client_secret"], "scope": self.scopes} - res = requests.post(self.__TOKEN_ENDPOINT, headers=headers, data=payload, verify=False) - if res.status_code == 200: - return 200, res.json()["access_token"] - return 500, None - def getJWT(self): return self.jwt @@ -122,7 +114,7 @@ def updateResource(self, id_token="filler"): #Monolithic test to avoid jumping through hoops to implement ordered tests #This test case assumes v0.3 of the PEP engine - def test_resource_UMA(self): + def test_resource(self): #Use a JWT token as id_token id_token = self.getJWT() diff --git a/tests/testROChange.py b/tests/testROChange.py new file mode 100644 index 0000000..bd179d5 --- /dev/null +++ b/tests/testROChange.py @@ -0,0 +1,168 @@ +import unittest +import subprocess +import os +import requests +import json +import sys +import base64 +import time +import traceback +import urllib +import logging +import datetime +from jwkest.jws import JWS +from jwkest.jwk import RSAKey, import_rsa_key_from_file, load_jwks_from_url, import_rsa_key +from jwkest.jwk import load_jwks +from Crypto.PublicKey import RSA +from WellKnownHandler import WellKnownHandler, TYPE_SCIM, TYPE_OIDC, KEY_SCIM_USER_ENDPOINT, KEY_OIDC_TOKEN_ENDPOINT, KEY_OIDC_REGISTRATION_ENDPOINT, KEY_OIDC_SUPPORTED_AUTH_METHODS_TOKEN_ENDPOINT, TYPE_UMA_V2, KEY_UMA_V2_PERMISSION_ENDPOINT +from eoepca_uma import rpt, resource + +class ROChangeTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.g_config = {} + with open("../src/config/config.json") as j: + cls.g_config = json.load(j) + + wkh = WellKnownHandler(cls.g_config["auth_server_url"], secure=False) + cls.__TOKEN_ENDPOINT = wkh.get(TYPE_OIDC, KEY_OIDC_TOKEN_ENDPOINT) + + #Generate ID Token + _rsakey = RSA.generate(2048) + _private_key = _rsakey.exportKey() + _public_key = _rsakey.publickey().exportKey() + + file_out = open("private.pem", "wb") + file_out.write(_private_key) + file_out.close() + + file_out = open("public.pem", "wb") + file_out.write(_public_key) + file_out.close() + + #Admin JWT + _rsajwk = RSAKey(kid='RSA1', key=import_rsa_key(_private_key)) + _payload = { + "iss": cls.g_config["client_id"], + "sub": cls.g_config["client_id"], + "aud": cls.__TOKEN_ENDPOINT, + "jti": datetime.datetime.today().strftime('%Y%m%d%s'), + "exp": int(time.time())+3600, + "isOperator": True + } + _jws = JWS(_payload, alg="RS256") + cls.jwt_admin = _jws.sign_compact(keys=[_rsajwk]) + + #ROTest user JWT + _payload = { + "iss": cls.g_config["client_id"], + "sub": "54d10251-6cb5-4aee-8e1f-f492f1105c94", + "aud": cls.__TOKEN_ENDPOINT, + "jti": datetime.datetime.today().strftime('%Y%m%d%s'), + "exp": int(time.time())+3600, + "isOperator": False + } + _jws = JWS(_payload, alg="RS256") + cls.jwt_rotest = _jws.sign_compact(keys=[_rsajwk]) + + cls.scopes = 'public_access' + cls.resourceName = "TestROChangePEP" + cls.PEP_HOST = "http://localhost:5566" + + @classmethod + def tearDownClass(cls): + os.remove("private.pem") + os.remove("public.pem") + + def getJWTAdmin(self): + return self.jwt_admin + + def getJWTROTest(self): + return self.jwt_rotest + + def createTestResource(self, id_token="filler"): + payload = { "resource_scopes":[ self.scopes ], "icon_uri":"/"+self.resourceName, "name": self.resourceName } + headers = { 'content-type': "application/json", "cache-control": "no-cache", "Authorization": "Bearer "+str(id_token) } + res = requests.post(self.PEP_HOST+"/resources/"+self.resourceName, headers=headers, json=payload, verify=False) + if res.status_code == 200: + return 200, res.text + return 500, None + + def getResource(self, id_token="filler"): + headers = { 'content-type': "application/json", "cache-control": "no-cache", "Authorization": "Bearer "+id_token } + res = requests.get(self.PEP_HOST+"/resources/"+self.resourceID, headers=headers, verify=False) + if res.status_code == 401: + return 401, res.headers["Error"] + if res.status_code == 200: + return 200, res.json() + if res.status_code == 404: + return 404, res.headers["Error"] + return 500, None + + def deleteResource(self, id_token="filler"): + headers = { 'content-type': "application/json", "cache-control": "no-cache", "Authorization": "Bearer "+id_token } + res = requests.delete(self.PEP_HOST+"/resources/"+self.resourceID, headers=headers, verify=False) + if res.status_code == 401: + return 401, res.headers["Error"] + if res.status_code == 403: + return 403, res.headers["Error"] + if res.status_code == 204: + return 204, None + return 500, None + + def updateResource(self, id_token="filler"): + headers = { 'content-type': "application/json", "cache-control": "no-cache", "Authorization": "Bearer "+id_token } + payload = {"resource_scopes":[ self.scopes], "icon_uri":"/"+self.resourceName, "name":self.resourceName, "ownership_id": "54d10251-6cb5-4aee-8e1f-f492f1105c94"} + res = requests.put(self.PEP_HOST+"/resources/"+self.resourceID, headers=headers, json=payload, verify=False) + if res.status_code == 401: + return 401, res.headers["Error"] + if res.status_code == 403: + return 403, res.headers["Error"] + if res.status_code == 200: + return 200, None + return 500, None + + #Monolithic test to avoid jumping through hoops to implement ordered tests + #This test case assumes v0.3 of the PEP engine + def test_resource(self): + #Use a JWT token as id_token + id_token_admin = self.getJWTAdmin() + id_token_rotest = self.getJWTROTest() + + #Create resource with owner ADMIN + status, self.resourceID = self.createTestResource(id_token_admin) + self.assertEqual(status, 200) + print("Create resource: Resource created with id: "+self.resourceID) + del status + print("=======================") + print("") + + #self.resourceID = "7dec4bd4-e6c2-4b4e-9425-3d720cc8b33d" + + #Change ownership with user ROTEST - should fail + status, _ = self.updateResource(id_token_rotest) + self.assertEqual(status, 403) + del status + print("Invalid Ownership Change request successfully denied") + print("=======================") + print("") + + #Change ownership with user ADMIN - should succeed + status, _ = self.updateResource(id_token_admin) + self.assertEqual(status, 200) + del status + print("Valid Ownership Change request successfull") + print("=======================") + print("") + + #Delete created resource + status, reply = self.deleteResource(id_token_admin) + self.assertEqual(status, 204) + print("Delete resource: Resource deleted.") + del status, reply + print("=======================") + print("") + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file From 40490e12d7b2d66b1ee67d75e3ce2a9287ff32ed Mon Sep 17 00:00:00 2001 From: Tiago Fernandes Date: Tue, 29 Sep 2020 09:21:25 +0000 Subject: [PATCH 13/80] EOEPCA-35 admin operator flag fix --- tests/testROChange.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/testROChange.py b/tests/testROChange.py index bd179d5..9f62a55 100644 --- a/tests/testROChange.py +++ b/tests/testROChange.py @@ -48,7 +48,7 @@ def setUpClass(cls): "aud": cls.__TOKEN_ENDPOINT, "jti": datetime.datetime.today().strftime('%Y%m%d%s'), "exp": int(time.time())+3600, - "isOperator": True + "isOperator": False } _jws = JWS(_payload, alg="RS256") cls.jwt_admin = _jws.sign_compact(keys=[_rsajwk]) @@ -156,7 +156,7 @@ def test_resource(self): print("") #Delete created resource - status, reply = self.deleteResource(id_token_admin) + status, reply = self.deleteResource(id_token_rotest) self.assertEqual(status, 204) print("Delete resource: Resource deleted.") del status, reply From d2d6256dbc3584bde72bb3d49498897f1c800b47 Mon Sep 17 00:00:00 2001 From: mamuniz Date: Wed, 7 Oct 2020 14:50:39 +0000 Subject: [PATCH 14/80] EOEPCA-173: #comment Added new validations in the validate_rpt of the custom_uma and a mongo database for rpts --- src/custom_mongo.py | 166 ++++++++++++++++++++++--------------- src/custom_uma.py | 67 +++++++++++++-- src/main.py | 8 +- src/resources/resources.py | 8 +- 4 files changed, 167 insertions(+), 82 deletions(-) diff --git a/src/custom_mongo.py b/src/custom_mongo.py index ff1b722..87a39e2 100644 --- a/src/custom_mongo.py +++ b/src/custom_mongo.py @@ -3,51 +3,56 @@ class Mongo_Handler: - def __init__(self, **kwargs): + def __init__(self, database, database_obj, **kwargs): self.modified = [] self.__dict__.update(kwargs) self.myclient = pymongo.MongoClient('localhost', 27017) - self.db = self.myclient["resource_db"] + self.db = self.myclient[database] + self.db_obj = database_obj - def get_id_from_uri(self,uri): + def mongo_exists(self, mongo_key, mongo_value): ''' - Finds the most similar match with the uri given - Generates a list of the possible matches - Returns resource_id of the best match - ''' - total= '/' - col= self.db['resources'] - k=[] - uri_split=uri.split('/') - count=0 - for n in uri_split: - if count >= 2: - total = total + '/' + n - else: - total = total + n - count+=1 - myquery = { "reverse_match_url": total } - found=col.find_one(myquery) - if found: - k.append(found['resource_id']) - if len(k)>0: - return k[-1] - else: - return None - - def resource_exists(self, resource_id): - ''' - Check the existence of the resource inside the database + Check the existence of the value inside the database Return boolean result ''' - col = self.db['resources'] - myquery = { "resource_id": resource_id } + col = self.db[self.db_obj] + myquery = { mongo_key: mongo_value } if col.find_one(myquery): return True else: return False - def insert_in_mongo(self, resource_id: str, name: str, ownership_id: str, reverse_match_url: str): + def get_from_mongo(self, mongo_key, mongo_value): + ''' + Gets an existing object from the database, or None if not found + ''' + col = self.db[self.db_obj] + myquery = { mongo_key: mongo_value } + return col.find_one(myquery) + + def delete_in_mongo(self, mongo_key, mongo_value): + ''' + Check the existence of the key inside the database + And deletes the document with that value + ''' + if self.mongo_exists(mongo_key, mongo_value): + col = self.db[self.db_obj] + myquery = { mongo_key: mongo_value } + a= col.delete_one(myquery) + + def update_in_mongo(self, key_mongo, dict_data): + ''' + Find the object in the database by id, add or modify the changed values for the object + ''' + id=dict_data[key_mongo] + col = self.db[self.db_obj] + myquery= {key_mongo: id} + new_val= {"$set": dict_data} + x = col.update_many(myquery, new_val) + return + + #Functions only for resources db + def insert_resource_in_mongo(self, resource_id: str, name: str, ownership_id: str, reverse_match_url: str): ''' Generates a document with: -RESOURCE_ID: Unique id for each resource @@ -65,7 +70,7 @@ def insert_in_mongo(self, resource_id: str, name: str, ownership_id: str, revers myres = { "resource_id": resource_id, "name": name, "ownership_id": ownership_id, "reverse_match_url": reverse_match_url } # Check if the resource is alredy registered in the collection x=None - if self.resource_exists(resource_id): + if self.mongo_exists("resource_id", resource_id): x= self.update_resource(myres) # Add the resource since it doesn't exist on the database else: @@ -77,42 +82,32 @@ def insert_in_mongo(self, resource_id: str, name: str, ownership_id: str, revers x = col.insert_one(myres) return x - def get_resource(self, resource_id): - ''' - Gets an existing resource from the database, or None if not found - ''' - col = self.db['resources'] - myquery = { "resource_id": resource_id } - return col.find_one(myquery) - - def get_all_resources(self): + def get_id_from_uri(self,uri): ''' - Gets all existing resources in database + Finds the most similar match with the uri given + Generates a list of the possible matches + Returns resource_id of the best match ''' - col = self.db['resources'] - return col.find() + total= '/' + col= self.db[self.db_obj] + k=[] + uri_split=uri.split('/') + count=0 + for n in uri_split: + if count >= 2: + total = total + '/' + n + else: + total = total + n + count+=1 + myquery = { "reverse_match_url": total } + found=col.find_one(myquery) + if found: + k.append(found['resource_id']) + if len(k)>0: + return k[-1] + else: + return None - def delete_resource(self, resource_id): - ''' - Check the existence of the resource inside the database - And deletes the document - ''' - if self.resource_exists(resource_id): - col = self.db['resources'] - myquery = { "resource_id": resource_id } - a= col.delete_one(myquery) - - def update_resource(self, dict_data): - ''' - Find the resource in the database by id, add or modify the changed values for the resource - ''' - id=dict_data['resource_id'] - col = self.db['resources'] - myquery= {'resource_id': id} - new_val= {"$set": dict_data} - x = col.update_many(myquery, new_val) - return - def verify_uid(self, resource_id, uid): col = self.db['resources'] try: @@ -125,3 +120,40 @@ def verify_uid(self, resource_id, uid): except: print('no resource with that UID associated') return False + + def get_all_resources(self): + ''' + Gets all existing resources in database + ''' + col = self.db['resources'] + return col.find() + + #Functions for rpt db + def insert_rpt_in_mongo(self, rpt: str, rpt_limit_uses: int, timestamp: str): + ''' + Generates a document with: + -RPT: Unique id for each rpt + -RPT_LIMIT_USES: Limit of uses + -TIMESTAMP: RPT time when it is inserted + Check the existence of the rpt to be registered on the database + If alredy registered will return None + If not registered will add it and return the query result + ''' + dblist = self.myclient.list_database_names() + # Check if the database alredy exists + if "rpt_db" in dblist: + col = self.db['rpts'] + myres = { "rpt": rpt, "rpt_limit_uses": rpt_limit_uses, "timestamp": timestamp } + # Check if the resource is alredy registered in the collection + x=None + if self.mongo_exists("rpt", rpt): + x= self.update_rpt(myres) + # Add the resource since it doesn't exist on the database + else: + x = col.insert_one(myres) + return x + else: + col = self.db['rpts'] + myres = { "rpt": rpt, "rpt_limit_uses": rpt_limit_uses, "timestamp": timestamp } + x = col.insert_one(myres) + return x diff --git a/src/custom_uma.py b/src/custom_uma.py index c56f482..c8feb89 100644 --- a/src/custom_uma.py +++ b/src/custom_uma.py @@ -4,12 +4,14 @@ from WellKnownHandler import TYPE_UMA_V2, KEY_UMA_V2_RESOURCE_REGISTRATION_ENDPOINT, KEY_UMA_V2_PERMISSION_ENDPOINT, KEY_UMA_V2_INTROSPECTION_ENDPOINT from typing import List import pymongo +from datetime import datetime class UMA_Handler: def __init__(self, wkhandler, oidc_handler, verify_ssl: bool = False ): self.wkh = wkhandler - self.mongo= Mongo_Handler() + self.mongo= Mongo_Handler("resource_db", "resources") + self.mongo_rpt= Mongo_Handler("rpt_db", "rpts") self.oidch = oidc_handler self.verify = verify_ssl self.registered_resources = None @@ -28,7 +30,7 @@ def create(self, name: str, scopes: List[str], description: str, ownership_id: s new_resource_id = resource.create(pat, resource_registration_endpoint, name, scopes, description=description, icon_uri= icon_uri, secure = self.verify) print("Created resource '"+name+"' with ID :"+new_resource_id) # Register resources inside the dbs - resp=self.mongo.insert_in_mongo(new_resource_id, name, ownership_id, icon_uri) + resp=self.mongo.insert_resource_in_mongo(new_resource_id, name, ownership_id, icon_uri) if resp: print('Resource saved in DB succesfully') return new_resource_id @@ -42,7 +44,7 @@ def update(self, resource_id: str, name: str, scopes: List[str], description: st resource_registration_endpoint = self.wkh.get(TYPE_UMA_V2, KEY_UMA_V2_RESOURCE_REGISTRATION_ENDPOINT) pat = self.oidch.get_new_pat() new_resource_id = resource.update(pat, resource_registration_endpoint, resource_id, name, scopes, description=description, icon_uri= icon_uri, secure = self.verify) - resp=self.mongo.insert_in_mongo(resource_id, name, ownership_id, icon_uri) + resp=self.mongo.insert_resource_in_mongo(resource_id, name, ownership_id, icon_uri) print("Updated resource '"+name+"' with ID :"+new_resource_id) def delete(self, resource_id: str): @@ -59,7 +61,7 @@ def delete(self, resource_id: str): pat = self.oidch.get_new_pat() try: resource.delete(pat, resource_registration_endpoint, resource_id, secure = self.verify) - resp = self.mongo.delete_resource(resource_id) + resp = self.mongo.delete_in_mongo("resource_id", resource_id) print("Deleted resource with ID :"+resource_id) except Exception as e: print("Error while deleting resource: "+str(e)) @@ -67,13 +69,53 @@ def delete(self, resource_id: str): # Usage of Python library for query mongodb instance - def validate_rpt(self, user_rpt: str, resources: List[dict], margin_time_rpt_valid: float, ) -> bool: + def validate_rpt(self, user_rpt: str, resources: List[dict], margin_time_rpt_valid: float, rpt_limit_uses: int) -> bool: """ Returns True/False, if the RPT is valid for the resource(s) they are trying to access """ + results = [] + introspection_endpoint = self.wkh.get(TYPE_UMA_V2, KEY_UMA_V2_INTROSPECTION_ENDPOINT) pat = self.oidch.get_new_pat() - return rpt.is_valid_now(user_rpt, pat, introspection_endpoint, resources, time_margin= margin_time_rpt_valid ,secure= self.verify ) + rpt_class = rpt.introspect(rpt=user_rpt, pat=pat, introspection_endpoint=introspection_endpoint, secure=False) + + result = rpt.is_valid_now(user_rpt, pat, introspection_endpoint, resources, time_margin= margin_time_rpt_valid ,secure= self.verify ) + + if result is False: + return False + + resource_id_mongo = resources[0]['resource_id'] + + #We see in the database if the rpt exists + exists_rpt = self.mongo_rpt.mongo_exists("rpt", user_rpt) + #If it exists -> decrease rpt usage and check if you have limit_uses + if exists_rpt is True: + rpt_mongo_obj = self.mongo_rpt.get_from_mongo("rpt", user_rpt) + limits = rpt_mongo_obj['rpt_limit_uses'] + + if limits > 0: + rpt_mongo_obj['rpt_limit_uses'] = limits - 1 + self.mongo_rpt.update_in_mongo("rpt", rpt_mongo_obj) + else: + #If it does not exist -> it is inserted into the database with all the limit_uses obtained from the env var or config + dateTimeObj = datetime.now() + timestampStr = dateTimeObj.strftime("%d-%b-%Y (%H:%M:%S.%f)") + + self.mongo_rpt.insert_rpt_in_mongo(user_rpt, rpt_limit_uses - 1, timestampStr) + limits = rpt_limit_uses + + if rpt_class['permissions'] is not None: + result = self.validate_resources_ids(resource_id_mongo, rpt_class, limits) + results.append(result) + else: + return False + + validator = True + for i in range(0, len(results)): + if results[i] is False: + validator = False + + return validator def resource_exists(self, icon_uri: str): @@ -150,4 +192,15 @@ def get_all_resources(self): """ Updates and returns all the registed resources """ - return self.update_resources_from_as() \ No newline at end of file + return self.update_resources_from_as() + + def validate_resources_ids(self, resource_id_mongo: str, resource_id_rpt_list: List[dict], limits: int): + first_validation = False + + for i in range(0, len(resource_id_rpt_list['permissions'])): + resource_id_rpt = resource_id_rpt_list['permissions'][i]['resource_id'] + + if (resource_id_mongo == resource_id_rpt) and limits > 0: + first_validation = True + + return first_validation diff --git a/src/main.py b/src/main.py index 226361f..1cfe196 100644 --- a/src/main.py +++ b/src/main.py @@ -40,7 +40,8 @@ "PEP_USE_THREADS", "PEP_DEBUG_MODE", "PEP_RESOURCE_SERVER_ENDPOINT", -"PEP_API_RPT_UMA_VALIDATION"] +"PEP_API_RPT_UMA_VALIDATION", +"PEP_RPT_LIMIT_USES"] use_env_var = True @@ -185,11 +186,10 @@ def proxy_request(request, new_header): def resource_request(path): # Check for token print("Processing path: '"+path+"'") - custom_mongo = Mongo_Handler() + custom_mongo = Mongo_Handler("resource_db", "resources") rpt = request.headers.get('Authorization') # Get resource resource_id = custom_mongo.get_id_from_uri("/"+path) - scopes= None if resource_id: scopes = uma_handler.get_resource_scopes(resource_id) @@ -202,7 +202,7 @@ def resource_request(path): print("Token found: "+rpt) rpt = rpt.replace("Bearer ","").strip() # Validate for a specific resource - if uma_handler.validate_rpt(rpt, [{"resource_id": resource_id, "resource_scopes": scopes }], int(g_config["s_margin_rpt_valid"])) or not api_rpt_uma_validation: + if uma_handler.validate_rpt(rpt, [{"resource_id": resource_id, "resource_scopes": scopes }], int(g_config["s_margin_rpt_valid"]), int(g_config["rpt_limit_uses"])) or not api_rpt_uma_validation: print("RPT valid, accesing ") introspection_endpoint=g_wkh.get(TYPE_UMA_V2, KEY_UMA_V2_INTROSPECTION_ENDPOINT) pat = oidc_client.get_new_pat() diff --git a/src/resources/resources.py b/src/resources/resources.py index e0408fc..60ce2f4 100644 --- a/src/resources/resources.py +++ b/src/resources/resources.py @@ -13,7 +13,7 @@ def construct_blueprint(oidc_client, uma_handler, g_config): def get_resource_list(): print("Retrieving all registered resources...") #gets all resources registered on local DB - custom_mongo = Mongo_Handler() + custom_mongo = Mongo_Handler("resource_db", "resources") resources = custom_mongo.get_all_resources() rpt = request.headers.get('Authorization') @@ -60,7 +60,7 @@ def get_resource_list(): def resource_operation(resource_id): print("Processing " + request.method + " resource request...") response = Response() - custom_mongo = Mongo_Handler() + custom_mongo = Mongo_Handler("resource_db", "resources") uid = None #Inspect JWT token (UMA) or query OIDC userinfo endpoint (OAuth) for user id try: @@ -196,7 +196,7 @@ def get_resource(custom_mongo, resource_id, response): :param response: response object :type response: Response ''' - resource = custom_mongo.get_resource(resource_id) + resource = custom_mongo.get_from_mongo("resource_id", resource_id) #If no resource was found, return a 404 Error if not resource: @@ -218,4 +218,4 @@ def user_not_authorized(response): response.headers["Error"] = 'User lacking sufficient access privileges' return response - return resources_bp \ No newline at end of file + return resources_bp From 69112ff98ae974e70fe633a91f1995d8520c5d1b Mon Sep 17 00:00:00 2001 From: mamuniz Date: Wed, 7 Oct 2020 16:27:00 +0000 Subject: [PATCH 15/80] EOEPCA-173: #comment updated mongo test --- tests/testMongo.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/testMongo.py b/tests/testMongo.py index 98d9a8d..adf5945 100644 --- a/tests/testMongo.py +++ b/tests/testMongo.py @@ -54,7 +54,7 @@ def test_mongo(self, mock_test,raise_for_status=None): mock_resp.raise_for_status = mock.Mock() if raise_for_status: mock_resp.raise_for_status.side_effect = raise_for_status - mongo = Mongo_Handler() + mongo = Mongo_Handler("resource_db", "resources") self.assertEqual(str(mongo)[:-16], ' Date: Wed, 7 Oct 2020 16:31:29 +0000 Subject: [PATCH 16/80] EOEPCA-173 --- tests/testMongo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/testMongo.py b/tests/testMongo.py index adf5945..22f7f54 100644 --- a/tests/testMongo.py +++ b/tests/testMongo.py @@ -69,7 +69,7 @@ def test_find_mongo(self, mock_find_test,raise_for_status=None): #@mock.patch('pymongo.collection.Collection.find_one', side_effect=mocked_exists_mongo) - @mock.patch('src.custom_mongo.Mongo_Handler.insert_in_mongo', side_effect=mocked_insert_mongo) + @mock.patch('src.custom_mongo.Mongo_Handler.insert_resource_in_mongo', side_effect=mocked_insert_mongo) @mock.patch('pymongo.collection.Collection.find_one', side_effect=mocked_exists_mongo) def test_insert_mongo(self, mock_insert_test,raise_for_status=None): mock_resp = mock.Mock() From 94dcc0d7990b1c25553fa86bc73f50400111e4c6 Mon Sep 17 00:00:00 2001 From: mamuniz Date: Thu, 8 Oct 2020 12:39:06 +0000 Subject: [PATCH 17/80] EOEPCA-173: #comment Updated readme and documentation --- README.md | 4 +++- docs/02.overview/00.overview.adoc | 2 +- docs/03.design/00.design.adoc | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7eaaa6b..8a3e057 100644 --- a/README.md +++ b/README.md @@ -37,8 +37,9 @@ - [Usage & functionality](#usage--functionality) - [Developer documentation](#developer-documentation) - [Demo functionality](#demo-functionality) + - [Test functionality](#test-functionality) - [Endpoints](#endpoints) - - [Resources cache](#resources-cache) + - [Resources Repository](#resources-repository) - [Roadmap](#roadmap) - [Contributing](#contributing) - [License](#license) @@ -104,6 +105,7 @@ The parameters that are accepted, and their meaning, are as follows: - **use_threads**: Toggle on/off (bool) the usage of threads for the proxy. Recommended to be left as True. - **debug_mode**: Toggle on/off (bool) a debug mode of Flask. In a production environment, this should be false. - **resource_server_endpoint**: Complete url (with "https" and any port) of the Resource Server to protect with this PEP. +- **rpt_limit_uses**: Number of uses for each of the RPTs. - **client_id**: string indicating a client_id for an already registered and configured client. **This parameter is optional**. When not supplied, the PEP will generate a new client for itself and store it in this key inside the JSON. - **client_secret**: string indicating the client secret for the client_id. **This parameter is optional**. When not supplied, the PEP will generate a new client for itself and store it in this key inside the JSON. diff --git a/docs/02.overview/00.overview.adoc b/docs/02.overview/00.overview.adoc index e7d1dab..3e175ed 100644 --- a/docs/02.overview/00.overview.adoc +++ b/docs/02.overview/00.overview.adoc @@ -165,7 +165,7 @@ Ticket generation as per the UMA 2.0 standard, are only valid for that requested === Resource protection & RPT validation -The PEP when presented with an RPT in an `Authorization` HTTP header, will check the validity of this token for the requested resource. This token is only valid for a limited time, for a specific user, and for a specific resource. This makes attacks via copying an RPT extremely inneficient for an attacker. +The PEP when presented with an RPT in an `Authorization` HTTP header, will check the validity of this token for the requested resource. This token is valid for a limited time, for a specific user, and for a specific resource. This makes attacks via copying an RPT extremely inneficient for an attacker. The validation of the rpt token was extended by including a new parameter that allows to establish the number of uses of the rpt. To store the rpts that will be used will be stored in the database with the number of uses that the rpt has. The PEP will only protect the resources that it recognizes as such. This means that, even without an RPT, the PEP will alllow a client to pass-through directly to the resource server if there is no identified resource that matches what the client is requesting. diff --git a/docs/03.design/00.design.adoc b/docs/03.design/00.design.adoc index 0baaa48..48b532f 100644 --- a/docs/03.design/00.design.adoc +++ b/docs/03.design/00.design.adoc @@ -55,6 +55,7 @@ The parameters that are accepted, and their meaning, are as follows: - **service_host**: Host for the proxy to listen on. For example, "0.0.0.0" will listen on all interfaces - **service_port**: Port for the proxy to listen on. By default, **5566**. Keep in mind you will have to edit the docker file and/or kubernetes yaml file in order for all the prot forwarding to work. - **s_margin_rpt_valid**: An integer representing how many seconds of "margin" do we want when checking RPT. For example, using **5** will make sure the provided RPT is valid now AND AT LEAST in the next 5 seconds. +- **rpt_limit_uses**: Number of uses for each of the RPTs. - **check_ssl_certs**: Toggle on/off (bool) to check certificates in all requests. This should be forced to True in a production environment - **use_threads**: Toggle on/off (bool) the usage of threads for the proxy. Recommended to be left as True. - **debug_mode**: Toggle on/off (bool) a debug mode of Flask. In a production environment, this should be false. From 01a1e749fdaf1e77cda9105bc281eab590e4aef2 Mon Sep 17 00:00:00 2001 From: mamuniz Date: Thu, 8 Oct 2020 12:59:20 +0000 Subject: [PATCH 18/80] EOEPCA-173: updated config --- src/config/config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/config.json b/src/config/config.json index 456b718..acbf329 100644 --- a/src/config/config.json +++ b/src/config/config.json @@ -1 +1 @@ -{"realm": "eoepca", "auth_server_url": "https://test.eoepca.org", "proxy_endpoint": "/", "service_host": "0.0.0.0", "service_port": 5566, "s_margin_rpt_valid": 5, "check_ssl_certs": false, "use_threads": true, "debug_mode": true, "resource_server_endpoint": "http://eoepca-ades-core", "api_rpt_uma_validation": true} \ No newline at end of file +{"realm": "eoepca", "auth_server_url": "https://test.eoepca.org", "proxy_endpoint": "/", "service_host": "0.0.0.0", "service_port": 5566, "s_margin_rpt_valid": 5, "check_ssl_certs": false, "use_threads": true, "debug_mode": true, "resource_server_endpoint": "http://eoepca-ades-core", "api_rpt_uma_validation": true, "rpt_limit_uses": 5} From ebf20709cb6d4eb8c107f64f9cc6d7115a36692f Mon Sep 17 00:00:00 2001 From: Tiago Fernandes Date: Thu, 15 Oct 2020 18:05:25 +0000 Subject: [PATCH 19/80] EOEPCA-178 policy handler prototype --- src/handlers/policy_handler.py | 14 ++++++++++++++ src/resources/resources.py | 23 ++++++++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 src/handlers/policy_handler.py diff --git a/src/handlers/policy_handler.py b/src/handlers/policy_handler.py new file mode 100644 index 0000000..997e463 --- /dev/null +++ b/src/handlers/policy_handler.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python3 +from requests import post + +class policy_handler: + + def __init__(self, pdp_url: str, pdp_port: int, pdp_policy_endpoint: str): + self.url = pdp_url + self.port = pdp_port + self.endpoint = pdp_policy_endpoint + + def create_policy(self, policy_body, jwt, ownership_id): + headers = { 'content-type': "application/json", 'Authorization': 'Bearer '+str(jwt)} + data = policy_body + return post(self.url+':'+self.port+"/"+self.endpoint, headers=headers, data=data) \ No newline at end of file diff --git a/src/resources/resources.py b/src/resources/resources.py index 60ce2f4..d4689a8 100644 --- a/src/resources/resources.py +++ b/src/resources/resources.py @@ -82,7 +82,18 @@ def resource_operation(resource_id): #add resource is outside of any extra validations, so it is called now if request.method == "POST": - return create_resource(uid, request, uma_handler, response) + resource_reply = create_resource(uid, request, uma_handler, response) + #If the reply does not contain a status_code, the creation was successful + #Here we register a default ownership policy to the new resource, with the PDP + if not resource_reply.status_code: + resource_id = resource_reply + policy_reply = #TODO call to policy_handler class + if policy_reply.status_code == 200: + return resource_id + response.status_code = policy_reply.status_code + response.headers["Error"] = "Error when registering resource ownership policy!" + return response + return resource_reply try: #otherwise continue with validations @@ -218,4 +229,14 @@ def user_not_authorized(response): response.headers["Error"] = 'User lacking sufficient access privileges' return response + def get_default_ownership_policy_cfg(resource_id, user_name): + return { "resource_id": resource_id, "rules": [{ "AND": [ {"EQUAL": {"user_name" : user_name } }] }] } + + def get_default_ownership_policy_body(resource_id, user_name): + name = "Default Ownership Policy" + description = "This is the default ownership policy for created resources through PEP" + policy_cfg = get_default_ownership_policy_cfg(resource_id, user_name) + scopes = ["Authenticated"] + return {"name": name, "description": description, "policy_cfg": policy_cfg, "scopes": scopes} + return resources_bp From c7400f3a6c97d60f600fbcdb909bd44741b69576 Mon Sep 17 00:00:00 2001 From: Tiago Fernandes Date: Thu, 15 Oct 2020 18:47:59 +0000 Subject: [PATCH 20/80] EOEPCA-178 incorporate policy_handler --- src/config/config.json | 2 +- src/handlers/policy_handler.py | 16 ++++++++++++++-- src/main.py | 11 +++++++++-- src/resources/resources.py | 5 +++-- 4 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/config/config.json b/src/config/config.json index acbf329..b5b087e 100644 --- a/src/config/config.json +++ b/src/config/config.json @@ -1 +1 @@ -{"realm": "eoepca", "auth_server_url": "https://test.eoepca.org", "proxy_endpoint": "/", "service_host": "0.0.0.0", "service_port": 5566, "s_margin_rpt_valid": 5, "check_ssl_certs": false, "use_threads": true, "debug_mode": true, "resource_server_endpoint": "http://eoepca-ades-core", "api_rpt_uma_validation": true, "rpt_limit_uses": 5} +{"realm": "eoepca", "auth_server_url": "https://test.eoepca.org", "proxy_endpoint": "/", "service_host": "0.0.0.0", "service_port": 5566, "s_margin_rpt_valid": 5, "check_ssl_certs": false, "use_threads": true, "debug_mode": true, "resource_server_endpoint": "http://eoepca-ades-core", "api_rpt_uma_validation": true, "rpt_limit_uses": 5, "pdp_url": "https://test.eoepca.org", "pdp_port": 5567, "pdp_policy_endpoint": "/policy/"} diff --git a/src/handlers/policy_handler.py b/src/handlers/policy_handler.py index 997e463..bde7786 100644 --- a/src/handlers/policy_handler.py +++ b/src/handlers/policy_handler.py @@ -1,6 +1,9 @@ #!/usr/bin/env python3 from requests import post +''' + Class to deal with PDP Policy calls from inside the PEP +''' class policy_handler: def __init__(self, pdp_url: str, pdp_port: int, pdp_policy_endpoint: str): @@ -8,7 +11,16 @@ def __init__(self, pdp_url: str, pdp_port: int, pdp_policy_endpoint: str): self.port = pdp_port self.endpoint = pdp_policy_endpoint - def create_policy(self, policy_body, jwt, ownership_id): + ''' + Registers a resource with the specified policy, at the PDP Policy endpoints + :param policy_body: JSON document containing policy name, description, rules and scopes + :type policy_body: JSON + :param jwt: authorization token for the PDP + :type jwt: string + + Returns: HTTP reply from PDP Policy Endpoint + ''' + def create_policy(self, policy_body, jwt): headers = { 'content-type': "application/json", 'Authorization': 'Bearer '+str(jwt)} data = policy_body - return post(self.url+':'+self.port+"/"+self.endpoint, headers=headers, data=data) \ No newline at end of file + return post(self.url+':'+self.port+self.endpoint, headers=headers, data=data) \ No newline at end of file diff --git a/src/main.py b/src/main.py index 1cfe196..4badf29 100644 --- a/src/main.py +++ b/src/main.py @@ -16,6 +16,7 @@ from custom_uma import UMA_Handler, resource from custom_uma import rpt as class_rpt from custom_mongo import Mongo_Handler +from handlers.policy_handler import policy_handler import resources.resources as resources import os import sys @@ -41,7 +42,10 @@ "PEP_DEBUG_MODE", "PEP_RESOURCE_SERVER_ENDPOINT", "PEP_API_RPT_UMA_VALIDATION", -"PEP_RPT_LIMIT_USES"] +"PEP_RPT_LIMIT_USES", +"PEP_PDP_URL", +"PEP_PDP_PORT", +"PEP_PDP_POLICY_ENDPOINT"] use_env_var = True @@ -114,11 +118,14 @@ # print("Resource already existed, moving on") # else: raise e +#PDP Policy Handler +pdp_policy_handler = policy_handler(pdp_url=g_config["pdp_url"], pdp_port=g_config["pdp_port"], pdp_policy_endpoint=g_config["pdp_policy_endpoint"]) + app = Flask(__name__) app.secret_key = ''.join(choice(ascii_lowercase) for i in range(30)) # Random key # Register api blueprints (module endpoints) -app.register_blueprint(resources.construct_blueprint(oidc_client, uma_handler, g_config)) +app.register_blueprint(resources.construct_blueprint(oidc_client, uma_handler, pdp_policy_handler, g_config)) def generateRSAKeyPair(): _rsakey = RSA.generate(2048) diff --git a/src/resources/resources.py b/src/resources/resources.py index d4689a8..2729917 100644 --- a/src/resources/resources.py +++ b/src/resources/resources.py @@ -5,8 +5,9 @@ from custom_uma import UMA_Handler, resource from custom_uma import rpt as class_rpt from custom_mongo import Mongo_Handler +from handlers.policy_handler import policy_handler -def construct_blueprint(oidc_client, uma_handler, g_config): +def construct_blueprint(oidc_client, uma_handler, pdp_policy_handler, g_config): resources_bp = Blueprint('resources_bp', __name__) @resources_bp.route("/resources", methods=["GET"]) @@ -87,7 +88,7 @@ def resource_operation(resource_id): #Here we register a default ownership policy to the new resource, with the PDP if not resource_reply.status_code: resource_id = resource_reply - policy_reply = #TODO call to policy_handler class + policy_reply = pdp_policy_handler.create_policy(get_default_ownership_policy_body(resource_id, uid), #TODO extract jwt) if policy_reply.status_code == 200: return resource_id response.status_code = policy_reply.status_code From ead5d6c6cf808081ac7f2bfd804cb53929a1cbb4 Mon Sep 17 00:00:00 2001 From: Tiago Fernandes Date: Fri, 30 Oct 2020 16:55:55 +0000 Subject: [PATCH 21/80] EOEPCA-194 removal of prefix from rsrc --- src/resources/resources.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/resources/resources.py b/src/resources/resources.py index 60ce2f4..61ab45b 100644 --- a/src/resources/resources.py +++ b/src/resources/resources.py @@ -135,7 +135,11 @@ def create_resource(uid, request, uma_handler, response): if request.is_json: data = request.get_json() if data.get("name") and data.get("resource_scopes"): - return uma_handler.create(data.get("name"), data.get("resource_scopes"), data.get("description"), uid, data.get("icon_uri")) + #Re-issue v0.2.3: ensure registered path does NOT contain proxy endpoint prefix + icon_uri_path = data.get("icon_uri") + if icon_uri_path.startswith(g_config["proxy_endpoint"]): + icon_uri_path = icon_uri_path.replace(g_config["proxy_endpoint"], '', 1) + return uma_handler.create(data.get("name"), data.get("resource_scopes"), data.get("description"), uid, icon_uri_path) else: response.status_code = 500 response.headers["Error"] = "Invalid data passed on URL called for resource creation!" From 4ce7137dcc8db327069d785fee3ad899a86a827d Mon Sep 17 00:00:00 2001 From: Tiago Fernandes Date: Tue, 3 Nov 2020 19:52:46 +0000 Subject: [PATCH 22/80] EOEPCA-178 handler refactor --- src/{custom_mongo.py => handlers/mongo_handler.py} | 0 src/{custom_oidc.py => handlers/oidc_handler.py} | 0 src/handlers/policy_handler.py | 4 ++-- src/{custom_uma.py => handlers/uma_handler.py} | 2 +- src/main.py | 8 ++++---- src/resources/resources.py | 10 +++++----- 6 files changed, 12 insertions(+), 12 deletions(-) rename src/{custom_mongo.py => handlers/mongo_handler.py} (100%) rename src/{custom_oidc.py => handlers/oidc_handler.py} (100%) rename src/{custom_uma.py => handlers/uma_handler.py} (99%) diff --git a/src/custom_mongo.py b/src/handlers/mongo_handler.py similarity index 100% rename from src/custom_mongo.py rename to src/handlers/mongo_handler.py diff --git a/src/custom_oidc.py b/src/handlers/oidc_handler.py similarity index 100% rename from src/custom_oidc.py rename to src/handlers/oidc_handler.py diff --git a/src/handlers/policy_handler.py b/src/handlers/policy_handler.py index bde7786..9f123ba 100644 --- a/src/handlers/policy_handler.py +++ b/src/handlers/policy_handler.py @@ -20,7 +20,7 @@ def __init__(self, pdp_url: str, pdp_port: int, pdp_policy_endpoint: str): Returns: HTTP reply from PDP Policy Endpoint ''' - def create_policy(self, policy_body, jwt): - headers = { 'content-type': "application/json", 'Authorization': 'Bearer '+str(jwt)} + def create_policy(self, policy_body, input_headers): + headers = input_headers data = policy_body return post(self.url+':'+self.port+self.endpoint, headers=headers, data=data) \ No newline at end of file diff --git a/src/custom_uma.py b/src/handlers/uma_handler.py similarity index 99% rename from src/custom_uma.py rename to src/handlers/uma_handler.py index c8feb89..c913be8 100644 --- a/src/custom_uma.py +++ b/src/handlers/uma_handler.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 from eoepca_uma import rpt, resource -from custom_mongo import Mongo_Handler +from handlers.mongo_handler import Mongo_Handler from WellKnownHandler import TYPE_UMA_V2, KEY_UMA_V2_RESOURCE_REGISTRATION_ENDPOINT, KEY_UMA_V2_PERMISSION_ENDPOINT, KEY_UMA_V2_INTROSPECTION_ENDPOINT from typing import List import pymongo diff --git a/src/main.py b/src/main.py index c4a952f..da75931 100644 --- a/src/main.py +++ b/src/main.py @@ -12,10 +12,10 @@ from config import load_config, save_config from eoepca_scim import EOEPCA_Scim, ENDPOINT_AUTH_CLIENT_POST -from custom_oidc import OIDCHandler -from custom_uma import UMA_Handler, resource -from custom_uma import rpt as class_rpt -from custom_mongo import Mongo_Handler +from handlers.oidc_handler import OIDCHandler +from handlers.uma_handler import UMA_Handler, resource +from handlers.uma_handler import rpt as class_rpt +from handlers.mongo_handler import Mongo_Handler from handlers.policy_handler import policy_handler import resources.resources as resources import os diff --git a/src/resources/resources.py b/src/resources/resources.py index 2729917..4883cdf 100644 --- a/src/resources/resources.py +++ b/src/resources/resources.py @@ -1,10 +1,10 @@ from flask import Blueprint, request, Response, jsonify import json from eoepca_scim import EOEPCA_Scim, ENDPOINT_AUTH_CLIENT_POST -from custom_oidc import OIDCHandler -from custom_uma import UMA_Handler, resource -from custom_uma import rpt as class_rpt -from custom_mongo import Mongo_Handler +from handlers.oidc_handler import OIDCHandler +from handlers.uma_handler import UMA_Handler, resource +from handlers.uma_handler import rpt as class_rpt +from handlers.mongo_handler import Mongo_Handler from handlers.policy_handler import policy_handler def construct_blueprint(oidc_client, uma_handler, pdp_policy_handler, g_config): @@ -88,7 +88,7 @@ def resource_operation(resource_id): #Here we register a default ownership policy to the new resource, with the PDP if not resource_reply.status_code: resource_id = resource_reply - policy_reply = pdp_policy_handler.create_policy(get_default_ownership_policy_body(resource_id, uid), #TODO extract jwt) + policy_reply = pdp_policy_handler.create_policy(policy_body=get_default_ownership_policy_body(resource_id, uid), input_headers=request.headers) if policy_reply.status_code == 200: return resource_id response.status_code = policy_reply.status_code From bc1c0c85bb69cadb39d681246cc52ba202fe1ef5 Mon Sep 17 00:00:00 2001 From: Tiago Fernandes Date: Tue, 3 Nov 2020 20:04:10 +0000 Subject: [PATCH 23/80] testMongo fix --- tests/testMongo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/testMongo.py b/tests/testMongo.py index 22f7f54..6b6a4d1 100644 --- a/tests/testMongo.py +++ b/tests/testMongo.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 import pymongo from pymongo import MongoClient -from src.custom_mongo import Mongo_Handler +from handlers.mongo_handler import Mongo_Handler import pytest import unittest import mock From f7fd1be5529a8098450c583eb82f02cfabac0630 Mon Sep 17 00:00:00 2001 From: Tiago Fernandes Date: Tue, 3 Nov 2020 20:08:09 +0000 Subject: [PATCH 24/80] path fix --- tests/testMongo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/testMongo.py b/tests/testMongo.py index 6b6a4d1..54bed0a 100644 --- a/tests/testMongo.py +++ b/tests/testMongo.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 import pymongo from pymongo import MongoClient -from handlers.mongo_handler import Mongo_Handler +from src.handlers.mongo_handler import Mongo_Handler import pytest import unittest import mock From 6134f6d80b0ed659b4c7d80bce10e263ae6bedc5 Mon Sep 17 00:00:00 2001 From: Tiago Fernandes Date: Tue, 3 Nov 2020 20:12:58 +0000 Subject: [PATCH 25/80] assert fixes --- tests/testMongo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/testMongo.py b/tests/testMongo.py index 54bed0a..e6c147d 100644 --- a/tests/testMongo.py +++ b/tests/testMongo.py @@ -55,7 +55,7 @@ def test_mongo(self, mock_test,raise_for_status=None): if raise_for_status: mock_resp.raise_for_status.side_effect = raise_for_status mongo = Mongo_Handler("resource_db", "resources") - self.assertEqual(str(mongo)[:-16], ' Date: Wed, 4 Nov 2020 13:08:49 +0000 Subject: [PATCH 26/80] EOEPCA-187: #comment Added functionality related to the parse of RPT Token in Resource API --- src/custom_oidc.py | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/src/custom_oidc.py b/src/custom_oidc.py index e7fa607..3c7415c 100644 --- a/src/custom_oidc.py +++ b/src/custom_oidc.py @@ -2,10 +2,17 @@ from base64 import b64encode from WellKnownHandler import TYPE_OIDC, KEY_OIDC_TOKEN_ENDPOINT, KEY_OIDC_USERINFO_ENDPOINT +from WellKnownHandler import TYPE_UMA_V2, KEY_UMA_V2_RESOURCE_REGISTRATION_ENDPOINT, KEY_UMA_V2_PERMISSION_ENDPOINT, KEY_UMA_V2_INTROSPECTION_ENDPOINT from base64 import b64encode +from custom_uma import UMA_Handler, resource +from custom_uma import rpt as class_rpt import logging import base64 import json +from jwkest.jws import JWS +from jwkest.jwk import RSAKey, import_rsa_key_from_file, load_jwks_from_url, import_rsa_key +from jwkest.jwk import load_jwks +from Crypto.PublicKey import RSA from requests import post @@ -36,6 +43,23 @@ def get_new_pat(self): return access_token + def verify_RPT_token(self, token, key): + try: + introspection_endpoint = self.wkh.get(TYPE_UMA_V2, KEY_UMA_V2_INTROSPECTION_ENDPOINT) + pat = self.get_new_pat() + rpt_class = class_rpt.introspect(rpt=token, pat=pat, introspection_endpoint=introspection_endpoint, secure=False) + + if rpt_class[key] == None: + if rpt_class['pct_claims'][key][0] == None: + raise Exception + else: + return rpt_class['pct_claims'][key][0] + else: + return rpt_class[key] + except Exception as e: + print("Authenticated RPT Resource. No Valid RPT token passed! " +str(e)) + return None + def verify_JWT_token(self, token, key): try: payload = str(token).split(".")[1] @@ -70,13 +94,13 @@ def verify_uid_headers(self, headers_protected, key): token_protected = inputToken_protected if token_protected: #Compares between JWT id_token and OAuth access token to retrieve the requested key-value - if len(str(token_protected))>40: + if len(str(token_protected)) == 76: + value=self.verify_RPT_token(token_protected, key) + elif len(str(token_protected))>40: value=self.verify_JWT_token(token_protected, key) else: value=self.verify_OAuth_token(token_protected, key) return value else: - return 'NO TOKEN FOUND' - - + return 'NO TOKEN FOUND' \ No newline at end of file From ae95c4387b1b7fcabb975c4d5497bca87118aeb8 Mon Sep 17 00:00:00 2001 From: Tiago Fernandes Date: Wed, 4 Nov 2020 16:44:53 +0000 Subject: [PATCH 27/80] Changes from v0.2.3 --- src/main.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/main.py b/src/main.py index f1c5b11..a060c53 100644 --- a/src/main.py +++ b/src/main.py @@ -64,6 +64,15 @@ else: g_config[env_var_config.lower()] = os.environ[env_var].replace('"', '') +# Sanitize proxy endpoint config value, VERY IMPORTANT to ensure proper function of the endpoint +proxy_endpoint_reject_list = ["/", "/resources", "resources"] +if g_config["proxy_endpoint"] in proxy_endpoint_reject_list: + raise Exception("PROXY_ENDPOINT value contains on of invalid values: " + str(proxy_endpoint_reject_list)) +if g_config["proxy_endpoint"][0] is not "/": + g_config["proxy_endpoint"] = "/" + g_config["proxy_endpoint"] +if g_config["proxy_endpoint"][-1] is "/": + g_config["proxy_endpoint"] = g_config["proxy_endpoint"][:-1] + # Global handlers g_wkh = WellKnownHandler(g_config["auth_server_url"], secure=False) @@ -158,14 +167,15 @@ def split_headers(headers): def proxy_request(request, new_header): try: + endpoint_path = request.full_path.replace(g_config["proxy_endpoint"], '', 1) if request.method == 'POST': - res = post(g_config["resource_server_endpoint"]+"/"+request.full_path, headers=new_header, data=request.data, stream=False) + res = post(g_config["resource_server_endpoint"]+endpoint_path, headers=new_header, data=request.data, stream=False) elif request.method == 'GET': - res = get(g_config["resource_server_endpoint"]+"/"+request.full_path, headers=new_header, stream=False) + res = get(g_config["resource_server_endpoint"]+endpoint_path, headers=new_header, stream=False) elif request.method == 'PUT': - res = put(g_config["resource_server_endpoint"]+"/"+request.full_path, headers=new_header, data=request.data, stream=False) + res = put(g_config["resource_server_endpoint"]+endpoint_path, headers=new_header, data=request.data, stream=False) elif request.method == 'DELETE': - res = delete(g_config["resource_server_endpoint"]+"/"+request.full_path, headers=new_header, stream=False) + res = delete(g_config["resource_server_endpoint"]+endpoint_path, headers=new_header, stream=False) else: response = Response() response.status_code = 501 @@ -234,7 +244,9 @@ def resource_request(path): print("No matched resource, passing through to resource server to handle") # In this case, the PEP doesn't have that resource handled, and just redirects to it. try: - cont = get(g_config["resource_server_endpoint"]+request.full_path, headers=request.headers).content + #Takes the full path, which contains query parameters, and removes the proxy_endpoint at the start + endpoint_path = request.full_path.replace(g_config["proxy_endpoint"], '', 1) + cont = get(g_config["resource_server_endpoint"]+endpoint_path, headers=request.headers).content return cont except Exception as e: print("Error while redirecting to resource: "+str(e)) From eea45dd22b39b66f611da40ce0bcc37445b3a816 Mon Sep 17 00:00:00 2001 From: Tiago Fernandes Date: Wed, 4 Nov 2020 18:21:20 +0000 Subject: [PATCH 28/80] EOEPCA-178 endpoint sanitation --- src/main.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main.py b/src/main.py index 9d42040..8151df6 100644 --- a/src/main.py +++ b/src/main.py @@ -71,12 +71,18 @@ # Sanitize proxy endpoint config value, VERY IMPORTANT to ensure proper function of the endpoint proxy_endpoint_reject_list = ["/", "/resources", "resources"] if g_config["proxy_endpoint"] in proxy_endpoint_reject_list: - raise Exception("PROXY_ENDPOINT value contains on of invalid values: " + str(proxy_endpoint_reject_list)) + raise Exception("PROXY_ENDPOINT value contains one of invalid values: " + str(proxy_endpoint_reject_list)) if g_config["proxy_endpoint"][0] is not "/": g_config["proxy_endpoint"] = "/" + g_config["proxy_endpoint"] if g_config["proxy_endpoint"][-1] is "/": g_config["proxy_endpoint"] = g_config["proxy_endpoint"][:-1] +# Sanitize PDP "policy" endpoint config value, VERY IMPORTANT to ensure proper function of the endpoint +if g_config["pdp_policy_endpoint"][0] is not "/": + g_config["pdp_policy_endpoint"] = "/" + g_config["pdp_policy_endpoint"] +if g_config["pdp_policy_endpoint"][-1] is not "/": + g_config["pdp_policy_endpoint"] = "/" + g_config["pdp_policy_endpoint"] + # Global handlers g_wkh = WellKnownHandler(g_config["auth_server_url"], secure=False) From 67a30d9082cea3effd81726a49ab5642285d0618 Mon Sep 17 00:00:00 2001 From: Tiago Fernandes Date: Wed, 4 Nov 2020 19:16:07 +0000 Subject: [PATCH 29/80] EOEPCA-178 isinstance and type cast fix --- src/handlers/policy_handler.py | 2 +- src/resources/resources.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/handlers/policy_handler.py b/src/handlers/policy_handler.py index 9f123ba..843d538 100644 --- a/src/handlers/policy_handler.py +++ b/src/handlers/policy_handler.py @@ -23,4 +23,4 @@ def __init__(self, pdp_url: str, pdp_port: int, pdp_policy_endpoint: str): def create_policy(self, policy_body, input_headers): headers = input_headers data = policy_body - return post(self.url+':'+self.port+self.endpoint, headers=headers, data=data) \ No newline at end of file + return post(self.url+':'+str(self.port)+self.endpoint, headers=headers, data=data) \ No newline at end of file diff --git a/src/resources/resources.py b/src/resources/resources.py index 54cfea3..e572c6d 100644 --- a/src/resources/resources.py +++ b/src/resources/resources.py @@ -84,9 +84,9 @@ def resource_operation(resource_id): #add resource is outside of any extra validations, so it is called now if request.method == "POST": resource_reply = create_resource(uid, request, uma_handler, response) - #If the reply does not contain a status_code, the creation was successful + #If the reply is not of type Response, the creation was successful #Here we register a default ownership policy to the new resource, with the PDP - if not resource_reply.status_code: + if not isinstance(resource_reply, Response): resource_id = resource_reply policy_reply = pdp_policy_handler.create_policy(policy_body=get_default_ownership_policy_body(resource_id, uid), input_headers=request.headers) if policy_reply.status_code == 200: From aa9c97e014e34390f695c83a42c6a5cb3da94e1d Mon Sep 17 00:00:00 2001 From: Hector Rodriguez Date: Thu, 5 Nov 2020 08:13:43 +0000 Subject: [PATCH 30/80] EOEPCA-179 Initial Structure of the SDD/ICD docs --- docs/ICD/01.introduction/00.introduction.adoc | 6 + .../01.introduction/03.reference-docs.adoc | 0 .../01.introduction/04.terminology.adoc | 0 .../01.introduction/05.glossary.adoc | 0 docs/ICD/02.overview/00.overview.adoc | 7 + docs/ICD/03.interfaces/00.interfaces.adoc | 6297 +++++++++++++++++ docs/ICD/README.adoc | 32 + docs/{ => ICD}/amendment-history.adoc | 0 docs/{ => ICD}/end-of-document.adoc | 0 docs/ICD/gh-page-README.adoc | 15 + docs/ICD/gh-page-root.html | 18 + docs/ICD/images/client.png | Bin 0 -> 34327 bytes docs/ICD/images/gluu-dashboard.png | Bin 0 -> 63038 bytes docs/{ => ICD}/images/logo.png | Bin docs/ICD/images/passport.png | Bin 0 -> 84179 bytes docs/ICD/images/sessions.png | Bin 0 -> 30987 bytes .../static.png => ICD/images/static.PNG} | Bin docs/ICD/images/workflow.png | Bin 0 -> 17608 bytes docs/ICD/index.adoc | 59 + docs/{ => ICD}/preface.adoc | 0 .../resources/themes/eoepca-theme.yml | 0 .../resources/themes/origdefault-theme.yml | 0 docs/{ => ICD}/stylesheets/asciidoctor.css | 0 docs/{ => ICD}/stylesheets/eoepca.css | 0 .../01.introduction/00.introduction.adoc | 0 .../01.introduction/03.reference-docs.adoc | 162 + docs/SDD/01.introduction/04.terminology.adoc | 187 + docs/SDD/01.introduction/05.glossary.adoc | 49 + docs/{ => SDD}/02.overview/00.overview.adoc | 0 docs/{ => SDD}/03.design/00.design.adoc | 0 docs/{ => SDD}/README.adoc | 0 docs/SDD/amendment-history.adoc | 16 + docs/SDD/end-of-document.adoc | 3 + docs/{ => SDD}/gh-page-README.adoc | 0 docs/{ => SDD}/gh-page-root.html | 0 docs/{ => SDD}/images/MongoFlow.png | Bin docs/{ => SDD}/images/PEPFlow2.png | Bin docs/{ => SDD}/images/PEPflow.png | Bin docs/{ => SDD}/images/init_flow.png | Bin docs/{ => SDD}/images/init_flow2.png | Bin docs/SDD/images/logo.png | Bin 0 -> 72755 bytes docs/SDD/images/static.png | Bin 0 -> 28404 bytes docs/{ => SDD}/index.adoc | 0 docs/SDD/preface.adoc | 22 + docs/SDD/resources/themes/eoepca-theme.yml | 28 + .../resources/themes/origdefault-theme.yml | 274 + docs/SDD/stylesheets/asciidoctor.css | 420 ++ docs/SDD/stylesheets/eoepca.css | 25 + docs/bin/generate-docs.sh | 43 +- docs/bin/publish-docs.sh | 37 +- 50 files changed, 7664 insertions(+), 36 deletions(-) create mode 100644 docs/ICD/01.introduction/00.introduction.adoc rename docs/{ => ICD}/01.introduction/03.reference-docs.adoc (100%) rename docs/{ => ICD}/01.introduction/04.terminology.adoc (100%) rename docs/{ => ICD}/01.introduction/05.glossary.adoc (100%) create mode 100644 docs/ICD/02.overview/00.overview.adoc create mode 100644 docs/ICD/03.interfaces/00.interfaces.adoc create mode 100644 docs/ICD/README.adoc rename docs/{ => ICD}/amendment-history.adoc (100%) rename docs/{ => ICD}/end-of-document.adoc (100%) create mode 100644 docs/ICD/gh-page-README.adoc create mode 100644 docs/ICD/gh-page-root.html create mode 100644 docs/ICD/images/client.png create mode 100644 docs/ICD/images/gluu-dashboard.png rename docs/{ => ICD}/images/logo.png (100%) create mode 100644 docs/ICD/images/passport.png create mode 100644 docs/ICD/images/sessions.png rename docs/{images/static.png => ICD/images/static.PNG} (100%) create mode 100644 docs/ICD/images/workflow.png create mode 100644 docs/ICD/index.adoc rename docs/{ => ICD}/preface.adoc (100%) rename docs/{ => ICD}/resources/themes/eoepca-theme.yml (100%) rename docs/{ => ICD}/resources/themes/origdefault-theme.yml (100%) rename docs/{ => ICD}/stylesheets/asciidoctor.css (100%) rename docs/{ => ICD}/stylesheets/eoepca.css (100%) rename docs/{ => SDD}/01.introduction/00.introduction.adoc (100%) create mode 100644 docs/SDD/01.introduction/03.reference-docs.adoc create mode 100644 docs/SDD/01.introduction/04.terminology.adoc create mode 100644 docs/SDD/01.introduction/05.glossary.adoc rename docs/{ => SDD}/02.overview/00.overview.adoc (100%) rename docs/{ => SDD}/03.design/00.design.adoc (100%) rename docs/{ => SDD}/README.adoc (100%) create mode 100644 docs/SDD/amendment-history.adoc create mode 100644 docs/SDD/end-of-document.adoc rename docs/{ => SDD}/gh-page-README.adoc (100%) rename docs/{ => SDD}/gh-page-root.html (100%) rename docs/{ => SDD}/images/MongoFlow.png (100%) rename docs/{ => SDD}/images/PEPFlow2.png (100%) rename docs/{ => SDD}/images/PEPflow.png (100%) rename docs/{ => SDD}/images/init_flow.png (100%) rename docs/{ => SDD}/images/init_flow2.png (100%) create mode 100644 docs/SDD/images/logo.png create mode 100644 docs/SDD/images/static.png rename docs/{ => SDD}/index.adoc (100%) create mode 100644 docs/SDD/preface.adoc create mode 100644 docs/SDD/resources/themes/eoepca-theme.yml create mode 100644 docs/SDD/resources/themes/origdefault-theme.yml create mode 100644 docs/SDD/stylesheets/asciidoctor.css create mode 100644 docs/SDD/stylesheets/eoepca.css diff --git a/docs/ICD/01.introduction/00.introduction.adoc b/docs/ICD/01.introduction/00.introduction.adoc new file mode 100644 index 0000000..6b59b7c --- /dev/null +++ b/docs/ICD/01.introduction/00.introduction.adoc @@ -0,0 +1,6 @@ + += Introduction + +== Purpose and Scope + +This document presents the {component-name} Interfaces for the Common Architecture. It servers as a complementary document to its corresponding Software Design Document. \ No newline at end of file diff --git a/docs/01.introduction/03.reference-docs.adoc b/docs/ICD/01.introduction/03.reference-docs.adoc similarity index 100% rename from docs/01.introduction/03.reference-docs.adoc rename to docs/ICD/01.introduction/03.reference-docs.adoc diff --git a/docs/01.introduction/04.terminology.adoc b/docs/ICD/01.introduction/04.terminology.adoc similarity index 100% rename from docs/01.introduction/04.terminology.adoc rename to docs/ICD/01.introduction/04.terminology.adoc diff --git a/docs/01.introduction/05.glossary.adoc b/docs/ICD/01.introduction/05.glossary.adoc similarity index 100% rename from docs/01.introduction/05.glossary.adoc rename to docs/ICD/01.introduction/05.glossary.adoc diff --git a/docs/ICD/02.overview/00.overview.adoc b/docs/ICD/02.overview/00.overview.adoc new file mode 100644 index 0000000..d98d483 --- /dev/null +++ b/docs/ICD/02.overview/00.overview.adoc @@ -0,0 +1,7 @@ +[[mainOverview]] += Overview + +This Interface Control Document (ICD) is a companion to the System Design Document for the Login Service <>. The ICD provides a Building Block level specification of the interfaces exposed by the Login Service to the rest of EOEPCA components. + +Section <>:: +Provides the interface specification of the Building Block. \ No newline at end of file diff --git a/docs/ICD/03.interfaces/00.interfaces.adoc b/docs/ICD/03.interfaces/00.interfaces.adoc new file mode 100644 index 0000000..d41a437 --- /dev/null +++ b/docs/ICD/03.interfaces/00.interfaces.adoc @@ -0,0 +1,6297 @@ +[[Interfaces]] += Login Service Interfaces + +[abstract] +.Abstract +OpenID Connect Provider (OP) & UMA Authorization Server (AS) + + +// markup not found, no include::{specDir}intro.adoc[opts=optional] + + + +== Endpoints + + +[.Authentication] +=== OIDC - Authentication + + +[.endSession] +==== endSession + +`GET /end_session` + +End current session. + +===== Description + +End current session. + + +// markup not found, no include::{specDir}end_session/GET/spec.adoc[opts=optional] + + + +===== Parameters + + + + + +====== Query Parametersa + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| id_token_hint +| Previously issued ID Token (id_token) passed to the logout endpoint as a hint about the End-User's current authenticated session with the Client. This is used as an indication of the identity of the End-User that the RP is requesting be logged out by the OP. The OP need not be listed as an audience of the ID Token when it is used as an id_token_hint value. +| - + + +| post_logout_redirect_uri +| URL to which the RP is requesting that the End-User's User Agent be redirected after a logout has been performed. The value MUST have been previously registered with the OP, either using the post_logout_redirect_uris Registration parameter or via another mechanism. If supplied, the OP SHOULD honor this request following the logout. +| - + + +| state +| Opaque value used by the RP to maintain state between the logout request and the callback to the endpoint specified by the post_logout_redirect_uri parameter. If included in the logout request, the OP passes this value back to the RP using the state query parameter when redirecting the User Agent back to the RP. +| - + + +| session_id +| Session Id +| - + + +|=== + + +===== Return Type + + + +- + +===== Content Type + +* application/json + +===== Responses + +.http response codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| OK - User redirected to logout page +| <<>> + + +| 302 +| Resource Found. +| <<>> + + +| 400 +| Error codes for end session endpoint. +| <> + + +| 500 +| Internal error occured. Please check log file for details. +| <> + +|=== + +===== Samples + + +// markup not found, no include::{snippetDir}end_session/GET/http-request.adoc[opts=optional] + + +// markup not found, no include::{snippetDir}end_session/GET/http-response.adoc[opts=optional] + + + +// file not found, no * wiremock data link :end_session/GET/GET.json[] + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}end_session/GET/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + +[.getAuthorize] +==== GET Authorize + +`GET /authorize` + +The Authorization Endpoint performs Authentication of the End-User. + +===== Description + +End-User Authentication and Authorization done by sending the User Agent to the Authorization Endpoint using request parameters defined by OAuth 2.0 and OpenID Connect. + + +// markup not found, no include::{specDir}authorize/GET/spec.adoc[opts=optional] + + + +===== Parameters + + + + + +====== Query Parameters + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| scope +| OpenID Connect requests MUST contain the openid scope value. If the openid scope value is not present, the behavior is entirely unspecified. Other scope values MAY be present. +| X + + +| response_type +| OAuth 2.0 Response Type value that determines the authorization processing flow to be used, including what parameters are returned from the endpoints used. +| X + + +| client_id +| OAuth 2.0 Client Identifier valid at the Authorization Server. +| X + + +| redirect_uri +| Redirection URI to which the response will be sent. This URI MUST exactly match one of the Redirection URI values for the Client pre-registered at the OpenID Provider. +| X + + +| state +| Opaque value used to maintain state between the request and the callback. +| - + + +| response_mode +| Informs the Authorization Server of the mechanism to be used for returning parameters from the Authorization Endpoint. +| - + + +| nonce +| String value used to associate a Client session with an ID Token, and to mitigate replay attacks. +| - + + +| display +| ASCII string value that specifies how the Authorization Server displays the authentication and consent user interface pages to the End-User. +| - + + +| prompt +| Space delimited, case sensitive list of ASCII string values that specifies whether the Authorization Server prompts the End-User for reauthentication and consent. The defined values are - none, login, consent, select_account. +| - + + +| max_age +| Maximum Authentication Age. Specifies the allowable elapsed time in seconds since the last time the End-User was actively authenticated by the OP. +| - + + +| ui_locales +| End-User's preferred languages and scripts for the user interface, represented as a space-separated list of BCP47 [RFC5646] language tag values, ordered by preference. +| - + + +| id_token_hint +| ID Token previously issued by the Authorization Server being passed as a hint about the End-User's current or past authenticated session with the Client. If the End-User identified by the ID Token is logged in or is logged in by the request, then the Authorization Server returns a positive response. +| - + + +| login_hint +| Hint to the Authorization Server about the login identifier the End-User might use to log in (if necessary). +| - + + +| acr_values +| Requested Authentication Context Class Reference values. Space-separated string that specifies the acr values that the Authorization Server is being requested to use for processing this Authentication Request, with the values appearing in order of preference. +| - + + +| amr_values +| AMR Values. +| - + + +| request +| This parameter enables OpenID Connect requests to be passed in a single, self-contained parameter and to be optionally signed and/or encrypted. The parameter value is a Request Object value. It represents the request as a JWT whose Claims are the request parameters. +| - + + +| request_uri +| This parameter enables OpenID Connect requests to be passed by reference, rather than by value. The request_uri value is a URL using the https scheme referencing a resource containing a Request Object value, which is a JWT containing the request parameters. +| - + + +| request_session_id +| Request session id. +| - + + +| session_id +| Session id of this call. +| - + + +| origin_headers +| Origin headers. Used in custom workflows. +| - + + +| code_challenge +| PKCE code challenge. +| - + + +| code_challenge_method +| PKCE code challenge method. +| - + + +| custom_response_headers +| Custom Response Headers. +| - + + +| claims +| Requested Claims. +| - + + +| auth_req_id +| CIBA authentication request Id. +| - + + +|=== + + +===== Return Type + + + +- + +===== Content Type + +* application/json + +===== Responses + +.http response codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| OK +| <<>> + + +| 302 +| Error codes for authorization endpoint. +| <> + + +| 400 +| Invalid parameters are provided to endpoint. +| <> + + +| 401 +| Unauthorized access request. +| <> + + +| 500 +| Internal error occured. Please check log file for details. +| <> + +|=== + +===== Samples + + +// markup not found, no include::{snippetDir}authorize/GET/http-request.adoc[opts=optional] + + +// markup not found, no include::{snippetDir}authorize/GET/http-response.adoc[opts=optional] + + + +// file not found, no * wiremock data link :authorize/GET/GET.json[] + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}authorize/GET/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + +[.getClientinfo] +==== GET Clientinfo + +`GET /clientinfo` + +To get Claims details about the registered client. + +===== Description + +The ClientInfo Endpoint is an OAuth 2.0 Protected Resource that returns Claims about the registered client. + + +// markup not found, no include::{specDir}clientinfo/GET/spec.adoc[opts=optional] + + + +===== Parameters + + + + +====== Header Parameters + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| Authorization +| +| - + + +|=== + +====== Query Parameters + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| access_token +| +| - + + +|=== + + +===== Return Type + +<> + + +===== Content Type + +* application/json + +===== Responses + +.http response codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| OK +| <> + + +| 400 +| Invalid Request are provided to endpoint. +| <> + +|=== + +===== Samples + + +// markup not found, no include::{snippetDir}clientinfo/GET/http-request.adoc[opts=optional] + + +// markup not found, no include::{snippetDir}clientinfo/GET/http-response.adoc[opts=optional] + + + +// file not found, no * wiremock data link :clientinfo/GET/GET.json[] + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}clientinfo/GET/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + +[.getIntrospection] +==== GET Introspection + +`GET /introspection` + +The Introspection OAuth 2 Endpoint. + +===== Description + +The Introspection OAuth 2 Endpoint. + + +// markup not found, no include::{specDir}introspection/GET/spec.adoc[opts=optional] + + + +===== Parameters + + + + +====== Header Parameters + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| Authorization +| Client Authorization details that contains the access token along with other details. +| X + + +|=== + +====== Query Parameters + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| token +| +| X + + +| token_type_hint +| ID Token previously issued by the Authorization Server being passed as a hint about the End-User. +| - + + +| response_as_jwt +| OPTIONAL. Boolean value with default value false. If true, returns introspection response as JWT (signed based on client configuration used for authentication to Introspection Endpoint). +| - + + +|=== + + +===== Return Type + +<> + + +===== Content Type + +* application/json + +===== Responses + +.http response codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| OK +| <> + + +| 400 +| Error codes for introspection endpoint. +| <> + + +| 401 +| Unauthorized access request. +| <> + + +| 500 +| Internal error occured. Please check log file for details. +| <> + +|=== + +===== Samples + + +// markup not found, no include::{snippetDir}introspection/GET/http-request.adoc[opts=optional] + + +// markup not found, no include::{snippetDir}introspection/GET/http-response.adoc[opts=optional] + + + +// file not found, no * wiremock data link :introspection/GET/GET.json[] + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}introspection/GET/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + +[.getUserinfo] +==== GET Userinfo + +`GET /userinfo` + +Returns Claims about the authenticated End-User. + +===== Description + +Returns Claims about the authenticated End-User. + + +// markup not found, no include::{specDir}userinfo/GET/spec.adoc[opts=optional] + + + +===== Parameters + + + + +====== Header Parameters + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| Authorization +| +| - + + +|=== + +====== Query Parameters + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| access_token +| OAuth 2.0 Access Token. +| X + + +|=== + + +===== Return Type + + +<> + + +===== Content Type + +* application/jwt +* application/json + +===== Responses + +.http response codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| OK +| <> + + +| 400 +| Invalid parameters provided to endpoint. +| <> + + +| 401 +| Invalid parameters provided to endpoint. +| <> + + +| 403 +| Invalid parameters provided to endpoint. +| <> + + +| 500 +| Internal error occured. Please check log file for details. +| <> + +|=== + +===== Samples + + +// markup not found, no include::{snippetDir}userinfo/GET/http-request.adoc[opts=optional] + + +// markup not found, no include::{snippetDir}userinfo/GET/http-response.adoc[opts=optional] + + + +// file not found, no * wiremock data link :userinfo/GET/GET.json[] + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}userinfo/GET/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + +[.jwks] +==== jwks + +`GET /jwks` + +A JSON Web Key (JWK) used by server. JWK is a JSON data structure that represents a set of public keys as a JSON object [RFC4627]. + +===== Description + +Provides list of JWK used by server. + + +// markup not found, no include::{specDir}jwks/GET/spec.adoc[opts=optional] + + + +===== Parameters + + + + + + + +===== Return Type + +<> + + +===== Content Type + +* application/json + +===== Responses + +.http response codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| OK +| <> + + +| 500 +| Internal error occured. Please check log file for details. +| <> + +|=== + +===== Samples + + +// markup not found, no include::{snippetDir}jwks/GET/http-request.adoc[opts=optional] + + +// markup not found, no include::{snippetDir}jwks/GET/http-response.adoc[opts=optional] + + + +// file not found, no * wiremock data link :jwks/GET/GET.json[] + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}jwks/GET/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + +[.postAuthorize] +==== POST Authorize + +`POST /authorize` + +The Authorization Endpoint performs Authentication of the End-User. + +===== Description + +End-User Authentication and Authorization done by sending the User Agent to the Authorization Endpoint using request parameters defined by OAuth 2.0 and OpenID Connect. + + +// markup not found, no include::{specDir}authorize/POST/spec.adoc[opts=optional] + + + +===== Parameters + + + +===== Form Parameter + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| scope +| OpenID Connect requests MUST contain the openid scope value. If the openid scope value is not present, the behavior is entirely unspecified. Other scope values MAY be present. <> +| X + + +| response_type +| OAuth 2.0 Response Type value that determines the authorization processing flow to be used, including what parameters are returned from the endpoints used. <> +| X + + +| client_id +| OAuth 2.0 Client Identifier valid at the Authorization Server. <> +| X + + +| redirect_uri +| Redirection URI to which the response will be sent. This URI MUST exactly match one of the Redirection URI values for the Client pre-registered at the OpenID Provider. <> +| X + + +| state +| Opaque value used to maintain state between the request and the callback. <> +| - + + +| response_mode +| Informs the Authorization Server of the mechanism to be used for returning parameters from the Authorization Endpoint. <> +| - + + +| nonce +| String value used to associate a Client session with an ID Token, and to mitigate replay attacks. <> +| - + + +| display +| ASCII string value that specifies how the Authorization Server displays the authentication and consent user interface pages to the End-User. <> +| - + + +| prompt +| Space delimited, case sensitive list of ASCII string values that specifies whether the Authorization Server prompts the End-User for reauthentication and consent. <> +| - + + +| max_age +| Maximum Authentication Age. Specifies the allowable elapsed time in seconds since the last time the End-User was actively authenticated by the OP. <> +| - + + +| ui_locales +| End-User's preferred languages and scripts for the user interface, represented as a space-separated list of BCP47 [RFC5646] language tag values, ordered by preference. <> +| - + + +| id_token_hint +| ID Token previously issued by the Authorization Server being passed as a hint about the End-User's current or past authenticated session with the Client. If the End-User identified by the ID Token is logged in or is logged in by the request, then the Authorization Server returns a positive response. <> +| - + + +| login_hint +| Hint to the Authorization Server about the login identifier the End-User might use to log in (if necessary). <> +| - + + +| acr_values +| Requested Authentication Context Class Reference values. Space-separated string that specifies the acr values that the Authorization Server is being requested to use for processing this Authentication Request, with the values appearing in order of preference. <> +| - + + +| amr_values +| AMR Values. <> +| - + + +| request +| This parameter enables OpenID Connect requests to be passed in a single, self-contained parameter and to be optionally signed and/or encrypted. The parameter value is a Request Object value. It represents the request as a JWT whose Claims are the request parameters. <> +| - + + +| request_uri +| This parameter enables OpenID Connect requests to be passed by reference, rather than by value. The request_uri value is a URL using the https scheme referencing a resource containing a Request Object value, which is a JWT containing the request parameters. <> +| - + + +| request_session_id +| Request session id. <> +| - + + +| session_id +| Session id of this call. <> +| - + + +| origin_headers +| Origin headers. Used in custom workflows. <> +| - + + +| code_challenge +| PKCE code challenge. <> +| - + + +| code_challenge_method +| PKCE code challenge method. <> +| - + + +| custom_response_headers +| Custom Response Headers. <> +| - + + +| claims +| Requested Claims. <> +| - + + +|=== + + + + +===== Return Type + + + +- + +===== Content Type + +* application/json + +===== Responses + +.http response codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| OK +| <<>> + + +| 302 +| Error codes for authorization endpoint. +| <> + + +| 400 +| Invalid parameters are provided to endpoint. +| <> + + +| 401 +| Unauthorized access request. +| <> + + +| 500 +| Internal error occured. Please check log file for details. +| <> + +|=== + +===== Samples + + +// markup not found, no include::{snippetDir}authorize/POST/http-request.adoc[opts=optional] + + +// markup not found, no include::{snippetDir}authorize/POST/http-response.adoc[opts=optional] + + + +// file not found, no * wiremock data link :authorize/POST/POST.json[] + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}authorize/POST/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + +[.postClientinfo] +==== POST Clientinfo + +`POST /clientinfo` + +To get Claims details about the registered client. + +===== Description + +The ClientInfo Endpoint is an OAuth 2.0 Protected Resource that returns Claims about the registered client. + + +// markup not found, no include::{specDir}clientinfo/POST/spec.adoc[opts=optional] + + + +===== Parameters + + + +===== Form Parameter + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| access_token +| Client-specific access token. <> +| X + + +|=== + +====== Header Parameters + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| Authorization +| +| - + + +|=== + + + +===== Return Type + +<> + + +===== Content Type + +* application/json + +===== Responses + +.http response codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| OK +| <> + + +| 400 +| Invalid Request are provided to endpoint. +| <> + +|=== + +===== Samples + + +// markup not found, no include::{snippetDir}clientinfo/POST/http-request.adoc[opts=optional] + + +// markup not found, no include::{snippetDir}clientinfo/POST/http-response.adoc[opts=optional] + + + +// file not found, no * wiremock data link :clientinfo/POST/POST.json[] + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}clientinfo/POST/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + +[.postIntrospection] +==== POST Introspection + +`POST /introspection` + +The Introspection OAuth 2 Endpoint. + +===== Description + +The Introspection OAuth 2 Endpoint. + + +// markup not found, no include::{specDir}introspection/POST/spec.adoc[opts=optional] + + + +===== Parameters + + + +===== Form Parameter + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| token +| Client access token. <> +| X + + +|=== + +====== Header Parameters + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| Authorization +| Client Authorization details that contains the access token along with other details. +| X + + +|=== + + + +===== Return Type + +<> + + +===== Content Type + +* application/json + +===== Responses + +.http response codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| OK +| <> + + +| 400 +| Error codes for introspection endpoint. +| <> + + +| 401 +| Unauthorized access request. +| <> + + +| 500 +| Internal error occured. Please check log file for details. +| <> + +|=== + +===== Samples + + +// markup not found, no include::{snippetDir}introspection/POST/http-request.adoc[opts=optional] + + +// markup not found, no include::{snippetDir}introspection/POST/http-response.adoc[opts=optional] + + + +// file not found, no * wiremock data link :introspection/POST/POST.json[] + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}introspection/POST/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + +[.postToken] +==== POST Token + +`POST /token` + +To obtain an Access Token, an ID Token, and optionally a Refresh Token, the RP (Client). + +===== Description + +To obtain an Access Token, an ID Token, and optionally a Refresh Token, the RP (Client). + + +// markup not found, no include::{specDir}token/POST/spec.adoc[opts=optional] + + + +===== Parameters + + + +===== Form Parameter + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| grant_type +| Provide a list of the OAuth 2.0 grant types that the Client is declaring that it will restrict itself to using. <> +| X + + +| code +| Code which is returned by authorization endpoint. (For grant_type\=authorization_code) <> +| - + + +| redirect_uri +| Redirection URI to which the response will be sent. This URI MUST exactly match one of the Redirection URI values for the Client pre-registered at the OpenID Provider. <> +| - + + +| username +| End-User username. <> +| - + + +| password +| End-User password. <> +| - + + +| scope +| OpenID Connect requests MUST contain the openid scope value. If the openid scope value is not present, the behavior is entirely unspecified. Other scope values MAY be present. Scope values used that are not understood by an implementation SHOULD be ignored. <> +| - + + +| assertion +| Assertion. <> +| - + + +| refresh_token +| Refresh token. <> +| - + + +| client_id +| OAuth 2.0 Client Identifier valid at the Authorization Server. <> +| - + + +| client_secret +| The client secret. The client MAY omit the parameter if the client secret is an empty string. <> +| - + + +| code_verifier +| The client's PKCE code verifier. <> +| - + + +| ticket +| <> +| - + + +| claim_token +| <> +| - + + +| claim_token_format +| <> +| - + + +| pct +| <> +| - + + +| rpt +| <> +| - + + +|=== + + + + +===== Return Type + +<> + + +===== Content Type + +* application/json + +===== Responses + +.http response codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| OK +| <> + + +| 400 +| Invalid parameters provided to endpoint. +| <> + + +| 401 +| Unauthorized access request. +| <> + + +| 403 +| Invalid details provided hence access denied. +| <> + + +| 500 +| Internal error occured. Please check log file for details. +| <> + +|=== + +===== Samples + + +// markup not found, no include::{snippetDir}token/POST/http-request.adoc[opts=optional] + + +// markup not found, no include::{snippetDir}token/POST/http-response.adoc[opts=optional] + + + +// file not found, no * wiremock data link :token/POST/POST.json[] + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}token/POST/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + +[.postUserinfo] +==== POST Userinfo + +`POST /userinfo` + +Returns Claims about the authenticated End-User. + +===== Description + +Returns Claims about the authenticated End-User. + + +// markup not found, no include::{specDir}userinfo/POST/spec.adoc[opts=optional] + + + +===== Parameters + + + +===== Form Parameter + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| access_token +| OAuth 2.0 Access Token. <> +| X + + +|=== + +====== Header Parameters + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| Authorization +| Client Authorization details that contains the access token along with other details. +| - + + +|=== + + + +===== Return Type + + +<> + + +===== Content Type + +* application/jwt +* application/json + +===== Responses + +.http response codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| OK +| <> + + +| 400 +| Invalid parameters provided to endpoint. +| <> + + +| 401 +| Invalid parameters provided to endpoint. +| <> + + +| 403 +| Invalid parameters provided to endpoint. +| <> + + +| 500 +| Internal error occured. Please check log file for details. +| <> + +|=== + +===== Samples + + +// markup not found, no include::{snippetDir}userinfo/POST/http-request.adoc[opts=optional] + + +// markup not found, no include::{snippetDir}userinfo/POST/http-response.adoc[opts=optional] + + + +// file not found, no * wiremock data link :userinfo/POST/POST.json[] + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}userinfo/POST/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + +[.revoke] +==== revoke + +`POST /revoke` + +Revoke an Access Token or a Refresh Token, the RP (Client). + +===== Description + +Revoke an Access Token or a Refresh Token, the RP (Client). + + +// markup not found, no include::{specDir}revoke/POST/spec.adoc[opts=optional] + + + +===== Parameters + + + +===== Form Parameter + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| token +| The token that the client wants to get revoked. <> +| X + + +| token_type_hint +| A hint about the type of the token submitted for revocation. <> +| - + + +|=== + + + + +===== Return Type + + + +- + +===== Content Type + +* content +* application/json + +===== Responses + +.http response codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| OK +| <<>> + + +| 400 +| Invalid parameters provided to endpoint. +| <> + +|=== + +===== Samples + + +// markup not found, no include::{snippetDir}revoke/POST/http-request.adoc[opts=optional] + + +// markup not found, no include::{snippetDir}revoke/POST/http-response.adoc[opts=optional] + + + +// file not found, no * wiremock data link :revoke/POST/POST.json[] + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}revoke/POST/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + +[.revokeSession] +==== revokeSession + +`POST /revoke_session` + +Revoke all sessions for user. + +===== Description + +Revoke all sessions for user (requires revoke_session scope). + + +// markup not found, no include::{specDir}revoke_session/POST/spec.adoc[opts=optional] + + + +===== Parameters + + + +===== Form Parameter + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| user_criterion_key +| user criterion key (e.g. uid) <> +| X + + +| user_criterion_value +| user criterion value (e.g. chris) <> +| X + + +|=== + + + + +===== Return Type + + + +- + +===== Content Type + +* application/json + +===== Responses + +.http response codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| OK - Returned if request was processed successfully. Means it will return in case sessions are found as well as in case sessions are not found (error is not returned to not disclose internal information). +| <<>> + + +| 401 +| Unauthorized access request. +| <> + + +| 500 +| Internal error occured. Please check log file for details. +| <> + +|=== + +===== Samples + + +// markup not found, no include::{snippetDir}revoke_session/POST/http-request.adoc[opts=optional] + + +// markup not found, no include::{snippetDir}revoke_session/POST/http-response.adoc[opts=optional] + + + +// file not found, no * wiremock data link :revoke_session/POST/POST.json[] + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}revoke_session/POST/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + +[.sessionStatus] +==== sessionStatus + +`GET /session_status` + +Determine current sesion status. + +===== Description + +Determine current sesion status. + + +// markup not found, no include::{specDir}session_status/GET/spec.adoc[opts=optional] + + + +===== Parameters + + + + + + + +===== Return Type + +<> + + +===== Content Type + +* application/json + +===== Responses + +.http response codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| OK +| <> + +|=== + +===== Samples + + +// markup not found, no include::{snippetDir}session_status/GET/http-request.adoc[opts=optional] + + +// markup not found, no include::{snippetDir}session_status/GET/http-response.adoc[opts=optional] + + + +// file not found, no * wiremock data link :session_status/GET/GET.json[] + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}session_status/GET/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + +[.Authorization] +=== UMA - Authorization + + +[.deleteHostRsrcResourceSet] +==== DELETE HostRsrcResourceSet + +`DELETE /host/rsrc/resource_set/{rsid}` + +Deletes a previously registered resource. + +===== Description + +Deletes a previously registered resource. + + +// markup not found, no include::{specDir}host/rsrc/resource_set/\{rsid\}/DELETE/spec.adoc[opts=optional] + + + +===== Parameters + +====== Path Parameters + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| rsid +| Resource ID +| X + + +|=== + + + +====== Header Parameters + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| Authorization +| +| X + + +|=== + + + +===== Return Type + + + +- + +===== Content Type + +* application/json + +===== Responses + +.http response codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 204 +| OK +| <<>> + + +| 500 +| Invalid parameters provided to endpoint. +| <> + +|=== + +===== Samples + + +// markup not found, no include::{snippetDir}host/rsrc/resource_set/\{rsid\}/DELETE/http-request.adoc[opts=optional] + + +// markup not found, no include::{snippetDir}host/rsrc/resource_set/\{rsid\}/DELETE/http-response.adoc[opts=optional] + + + +// file not found, no * wiremock data link :host/rsrc/resource_set/{rsid}/DELETE/DELETE.json[] + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}host/rsrc/resource_set/\{rsid\}/DELETE/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + +[.getHostRsrcResourceSet] +==== GET HostRsrcResourceSet + +`GET /host/rsrc/resource_set` + +Lists all previously registered resource. + +===== Description + +Lists all previously registered resource. + + +// markup not found, no include::{specDir}host/rsrc/resource_set/GET/spec.adoc[opts=optional] + + + +===== Parameters + + + + +====== Header Parameters + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| Authorization +| +| X + + +|=== + +====== Query Parameters + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| scope +| Scope uri. +| - + + +|=== + + +===== Return Type + + +<> + + +===== Content Type + +* application/json + +===== Responses + +.http response codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| OK +| List[<>] + + +| 500 +| Invalid parameters provided to endpoint. +| <> + +|=== + +===== Samples + + +// markup not found, no include::{snippetDir}host/rsrc/resource_set/GET/http-request.adoc[opts=optional] + + +// markup not found, no include::{snippetDir}host/rsrc/resource_set/GET/http-response.adoc[opts=optional] + + + +// file not found, no * wiremock data link :host/rsrc/resource_set/GET/GET.json[] + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}host/rsrc/resource_set/GET/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + +[.getHostRsrcResourceSet/{rsid}] +==== GET HostRsrcResourceSet/{rsid} + +`GET /host/rsrc/resource_set/{rsid}` + +Reads a previously registered resource. + +===== Description + +Reads a previously registered resource. + + +// markup not found, no include::{specDir}host/rsrc/resource_set/\{rsid\}/GET/spec.adoc[opts=optional] + + + +===== Parameters + +====== Path Parameters + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| rsid +| Resource description ID. +| X + + +|=== + + + +====== Header Parameters + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| Authorization +| Client Authorization details that contains the access token along with other details. +| X + + +|=== + + + +===== Return Type + +<> + + +===== Content Type + +* application/json + +===== Responses + +.http response codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| OK +| <> + + +| 500 +| Invalid parameters provided to endpoint. +| <> + +|=== + +===== Samples + + +// markup not found, no include::{snippetDir}host/rsrc/resource_set/\{rsid\}/GET/http-request.adoc[opts=optional] + + +// markup not found, no include::{snippetDir}host/rsrc/resource_set/\{rsid\}/GET/http-response.adoc[opts=optional] + + + +// file not found, no * wiremock data link :host/rsrc/resource_set/{rsid}/GET/GET.json[] + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}host/rsrc/resource_set/\{rsid\}/GET/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + +[.getIntrospection] +==== GET Introspection + +`GET /introspection` + +The Introspection OAuth 2 Endpoint. + +===== Description + +The Introspection OAuth 2 Endpoint. + + +// markup not found, no include::{specDir}introspection/GET/spec.adoc[opts=optional] + + + +===== Parameters + + + + +====== Header Parameters + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| Authorization +| Client Authorization details that contains the access token along with other details. +| X + + +|=== + +====== Query Parameters + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| token +| +| X + + +| token_type_hint +| ID Token previously issued by the Authorization Server being passed as a hint about the End-User. +| - + + +| response_as_jwt +| OPTIONAL. Boolean value with default value false. If true, returns introspection response as JWT (signed based on client configuration used for authentication to Introspection Endpoint). +| - + + +|=== + + +===== Return Type + +<> + + +===== Content Type + +* application/json + +===== Responses + +.http response codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| OK +| <> + + +| 400 +| Error codes for introspection endpoint. +| <> + + +| 401 +| Unauthorized access request. +| <> + + +| 500 +| Internal error occured. Please check log file for details. +| <> + +|=== + +===== Samples + + +// markup not found, no include::{snippetDir}introspection/GET/http-request.adoc[opts=optional] + + +// markup not found, no include::{snippetDir}introspection/GET/http-response.adoc[opts=optional] + + + +// file not found, no * wiremock data link :introspection/GET/GET.json[] + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}introspection/GET/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + +[.getRptStatus] +==== GET RptStatus + +`GET /rpt/status` + +The Introspection OAuth 2 Endpoint for RPT. + +===== Description + +The Introspection OAuth 2 Endpoint for RPT. + + +// markup not found, no include::{specDir}rpt/status/GET/spec.adoc[opts=optional] + + + +===== Parameters + + + + +====== Header Parameters + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| Authorization +| +| X + + +|=== + +====== Query Parameters + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| token +| +| X + + +| token_type_hint +| +| - + + +|=== + + +===== Return Type + +<> + + +===== Content Type + +* application/json + +===== Responses + +.http response codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| OK +| <> + + +| 405 +| Introspection of RPT is not allowed. +| <> + + +| 500 +| Invalid parameters provided to endpoint. +| <> + +|=== + +===== Samples + + +// markup not found, no include::{snippetDir}rpt/status/GET/http-request.adoc[opts=optional] + + +// markup not found, no include::{snippetDir}rpt/status/GET/http-response.adoc[opts=optional] + + + +// file not found, no * wiremock data link :rpt/status/GET/GET.json[] + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}rpt/status/GET/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + +[.getUmaGatherClaims] +==== GET UmaGatherClaims + +`GET /uma/gather_claims` + +UMA Claims Gathering Endpoint. + +===== Description + +UMA Claims Gathering Endpoint. + + +// markup not found, no include::{specDir}uma/gather_claims/GET/spec.adoc[opts=optional] + + + +===== Parameters + + + + + +====== Query Parameters + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| client_id +| OAuth 2.0 Client Identifier valid at the Authorization Server. +| - + + +| ticket +| +| - + + +| claims_redirect_uri +| +| - + + +| state +| +| - + + +| reset +| +| - + + +| authentication +| +| - + + +|=== + + +===== Return Type + + + +- + +===== Content Type + +* application/json + +===== Responses + +.http response codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 302 +| Resource Found. +| <<>> + + +| 400 +| Invalid parameters provided to endpoint. +| <> + + +| 500 +| Invalid parameters provided to endpoint. +| <> + +|=== + +===== Samples + + +// markup not found, no include::{snippetDir}uma/gather_claims/GET/http-request.adoc[opts=optional] + + +// markup not found, no include::{snippetDir}uma/gather_claims/GET/http-response.adoc[opts=optional] + + + +// file not found, no * wiremock data link :uma/gather_claims/GET/GET.json[] + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}uma/gather_claims/GET/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + +[.hostRsrcPr] +==== hostRsrcPr + +`POST /host/rsrc_pr` + +Registers permission. + +===== Description + +Registers permission. + + +// markup not found, no include::{specDir}host/rsrc_pr/POST/spec.adoc[opts=optional] + + + +===== Parameters + + + +===== Form Parameter + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| resource_id +| The identifier for a resource to which this client is seeking access. The identifier MUST correspond to a resource that was previously registered. <> +| X + + +| resource_scopes +| An array referencing zero or more strings representing scopes to which access was granted for this resource. Each string MUST correspond to a scope that was registered by this resource server for the referenced resource. <> +| X + + +|=== + +====== Header Parameters + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| Authorization +| Client Authorization details that contains the access token along with other details. +| X + + +|=== + + + +===== Return Type + +array[<>] + + +===== Content Type + +* application/json + +===== Responses + +.http response codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 201 +| OK +| List[<>] + + +| 500 +| Invalid parameters provided to endpoint. +| <> + +|=== + +===== Samples + + +// markup not found, no include::{snippetDir}host/rsrc_pr/POST/http-request.adoc[opts=optional] + + +// markup not found, no include::{snippetDir}host/rsrc_pr/POST/http-response.adoc[opts=optional] + + + +// file not found, no * wiremock data link :host/rsrc_pr/POST/POST.json[] + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}host/rsrc_pr/POST/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + +[.jwks] +==== jwks + +`GET /jwks` + +A JSON Web Key (JWK) used by server. JWK is a JSON data structure that represents a set of public keys as a JSON object [RFC4627]. + +===== Description + +Provides list of JWK used by server. + + +// markup not found, no include::{specDir}jwks/GET/spec.adoc[opts=optional] + + + +===== Parameters + + + + + + + +===== Return Type + +<> + + +===== Content Type + +* application/json + +===== Responses + +.http response codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| OK +| <> + + +| 500 +| Internal error occured. Please check log file for details. +| <> + +|=== + +===== Samples + + +// markup not found, no include::{snippetDir}jwks/GET/http-request.adoc[opts=optional] + + +// markup not found, no include::{snippetDir}jwks/GET/http-response.adoc[opts=optional] + + + +// file not found, no * wiremock data link :jwks/GET/GET.json[] + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}jwks/GET/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + +[.postHostRsrcResourceSet] +==== POST HostRsrcResourceSet + +`POST /host/rsrc/resource_set` + +Adds a new resource description. + +===== Description + +Adds a new resource description. + + +// markup not found, no include::{specDir}host/rsrc/resource_set/POST/spec.adoc[opts=optional] + + + +===== Parameters + + +===== Body Parameter + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| UmaResource +| <> +| - +| +| + +|=== + + +====== Header Parameters + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| Authorization +| Client Authorization details that contains the access token along with other details. +| X + + +|=== + + + +===== Return Type + +<> + + +===== Content Type + +* application/json + +===== Responses + +.http response codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 201 +| OK +| <> + + +| 500 +| Invalid parameters provided to endpoint. +| <> + +|=== + +===== Samples + + +// markup not found, no include::{snippetDir}host/rsrc/resource_set/POST/http-request.adoc[opts=optional] + + +// markup not found, no include::{snippetDir}host/rsrc/resource_set/POST/http-response.adoc[opts=optional] + + + +// file not found, no * wiremock data link :host/rsrc/resource_set/POST/POST.json[] + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}host/rsrc/resource_set/POST/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + +[.postIntrospection] +==== POST Introspection + +`POST /introspection` + +The Introspection OAuth 2 Endpoint. + +===== Description + +The Introspection OAuth 2 Endpoint. + + +// markup not found, no include::{specDir}introspection/POST/spec.adoc[opts=optional] + + + +===== Parameters + + + +===== Form Parameter + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| token +| Client access token. <> +| X + + +|=== + +====== Header Parameters + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| Authorization +| Client Authorization details that contains the access token along with other details. +| X + + +|=== + + + +===== Return Type + +<> + + +===== Content Type + +* application/json + +===== Responses + +.http response codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| OK +| <> + + +| 400 +| Error codes for introspection endpoint. +| <> + + +| 401 +| Unauthorized access request. +| <> + + +| 500 +| Internal error occured. Please check log file for details. +| <> + +|=== + +===== Samples + + +// markup not found, no include::{snippetDir}introspection/POST/http-request.adoc[opts=optional] + + +// markup not found, no include::{snippetDir}introspection/POST/http-response.adoc[opts=optional] + + + +// file not found, no * wiremock data link :introspection/POST/POST.json[] + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}introspection/POST/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + +[.postRptStatus] +==== POST RptStatus + +`POST /rpt/status` + +The Introspection OAuth 2 Endpoint for RPT. + +===== Description + +The Introspection OAuth 2 Endpoint for RPT. + + +// markup not found, no include::{specDir}rpt/status/POST/spec.adoc[opts=optional] + + + +===== Parameters + + + +===== Form Parameter + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| token +| Client access token. <> +| X + + +| token_type_hint +| ID Token previously issued by the Authorization Server being passed as a hint about the End-User. <> +| - + + +|=== + +====== Header Parameters + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| Authorization +| Client Authorization details that contains the access token along with other details. +| X + + +|=== + + + +===== Return Type + +<> + + +===== Content Type + +* application/json + +===== Responses + +.http response codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| OK +| <> + + +| 405 +| Introspection of RPT is not allowed. +| <> + + +| 500 +| Invalid parameters provided to endpoint. +| <> + +|=== + +===== Samples + + +// markup not found, no include::{snippetDir}rpt/status/POST/http-request.adoc[opts=optional] + + +// markup not found, no include::{snippetDir}rpt/status/POST/http-response.adoc[opts=optional] + + + +// file not found, no * wiremock data link :rpt/status/POST/POST.json[] + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}rpt/status/POST/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + +[.postToken] +==== POST Token + +`POST /token` + +To obtain an Access Token, an ID Token, and optionally a Refresh Token, the RP (Client). + +===== Description + +To obtain an Access Token, an ID Token, and optionally a Refresh Token, the RP (Client). + + +// markup not found, no include::{specDir}token/POST/spec.adoc[opts=optional] + + + +===== Parameters + + + +===== Form Parameter + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| grant_type +| Provide a list of the OAuth 2.0 grant types that the Client is declaring that it will restrict itself to using. <> +| X + + +| code +| Code which is returned by authorization endpoint. (For grant_type\=authorization_code) <> +| - + + +| redirect_uri +| Redirection URI to which the response will be sent. This URI MUST exactly match one of the Redirection URI values for the Client pre-registered at the OpenID Provider. <> +| - + + +| username +| End-User username. <> +| - + + +| password +| End-User password. <> +| - + + +| scope +| OpenID Connect requests MUST contain the openid scope value. If the openid scope value is not present, the behavior is entirely unspecified. Other scope values MAY be present. Scope values used that are not understood by an implementation SHOULD be ignored. <> +| - + + +| assertion +| Assertion. <> +| - + + +| refresh_token +| Refresh token. <> +| - + + +| client_id +| OAuth 2.0 Client Identifier valid at the Authorization Server. <> +| - + + +| client_secret +| The client secret. The client MAY omit the parameter if the client secret is an empty string. <> +| - + + +| code_verifier +| The client's PKCE code verifier. <> +| - + + +| ticket +| <> +| - + + +| claim_token +| <> +| - + + +| claim_token_format +| <> +| - + + +| pct +| <> +| - + + +| rpt +| <> +| - + + +|=== + + + + +===== Return Type + +<> + + +===== Content Type + +* application/json + +===== Responses + +.http response codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| OK +| <> + + +| 400 +| Invalid parameters provided to endpoint. +| <> + + +| 401 +| Unauthorized access request. +| <> + + +| 403 +| Invalid details provided hence access denied. +| <> + + +| 500 +| Internal error occured. Please check log file for details. +| <> + +|=== + +===== Samples + + +// markup not found, no include::{snippetDir}token/POST/http-request.adoc[opts=optional] + + +// markup not found, no include::{snippetDir}token/POST/http-response.adoc[opts=optional] + + + +// file not found, no * wiremock data link :token/POST/POST.json[] + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}token/POST/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + +[.postUmaGatherClaims] +==== POST UmaGatherClaims + +`POST /uma/gather_claims` + +UMA Claims Gathering Endpoint + +===== Description + +UMA Claims Gathering Endpoint + + +// markup not found, no include::{specDir}uma/gather_claims/POST/spec.adoc[opts=optional] + + + +===== Parameters + + + +===== Form Parameter + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| client_id +| OAuth 2.0 Client Identifier valid at the Authorization Server. <> +| - + + +| ticket +| <> +| - + + +| claims_redirect_uri +| <> +| - + + +| state +| <> +| - + + +| reset +| <> +| - + + +| authentication +| <> +| - + + +|=== + + + + +===== Return Type + + + +- + +===== Content Type + +* application/json + +===== Responses + +.http response codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 302 +| Resource Found. +| <<>> + + +| 400 +| Invalid parameters provided to endpoint. +| <> + + +| 500 +| Invalid parameters provided to endpoint. +| <> + +|=== + +===== Samples + + +// markup not found, no include::{snippetDir}uma/gather_claims/POST/http-request.adoc[opts=optional] + + +// markup not found, no include::{snippetDir}uma/gather_claims/POST/http-response.adoc[opts=optional] + + + +// file not found, no * wiremock data link :uma/gather_claims/POST/POST.json[] + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}uma/gather_claims/POST/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + +[.putHostRsrcResourceSet{rsid}] +==== PUT HostRsrcResourceSet{rsid} + +`PUT /host/rsrc/resource_set/{rsid}` + +Updates a previously registered resource. + +===== Description + +Updates a previously registered resource. + + +// markup not found, no include::{specDir}host/rsrc/resource_set/\{rsid\}/PUT/spec.adoc[opts=optional] + + + +===== Parameters + +====== Path Parameters + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| rsid +| Resource ID. +| X + + +|=== + +===== Body Parameter + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| UmaResource1 +| <> +| - +| +| + +|=== + + +====== Header Parameters + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| Authorization +| +| X + + +|=== + + + +===== Return Type + +<> + + +===== Content Type + +* application/json + +===== Responses + +.http response codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| OK +| <> + + +| 404 +| Invalid parameters provided to endpoint. +| <> + + +| 500 +| Invalid parameters provided to endpoint. +| <> + +|=== + +===== Samples + + +// markup not found, no include::{snippetDir}host/rsrc/resource_set/\{rsid\}/PUT/http-request.adoc[opts=optional] + + +// markup not found, no include::{snippetDir}host/rsrc/resource_set/\{rsid\}/PUT/http-response.adoc[opts=optional] + + + +// file not found, no * wiremock data link :host/rsrc/resource_set/{rsid}/PUT/PUT.json[] + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}host/rsrc/resource_set/\{rsid\}/PUT/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + +[.revoke] +==== revoke + +`POST /revoke` + +Revoke an Access Token or a Refresh Token, the RP (Client). + +===== Description + +Revoke an Access Token or a Refresh Token, the RP (Client). + + +// markup not found, no include::{specDir}revoke/POST/spec.adoc[opts=optional] + + + +===== Parameters + + + +===== Form Parameter + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| token +| The token that the client wants to get revoked. <> +| X + + +| token_type_hint +| A hint about the type of the token submitted for revocation. <> +| - + + +|=== + + + + +===== Return Type + + + +- + +===== Content Type + +* content +* application/json + +===== Responses + +.http response codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| OK +| <<>> + + +| 400 +| Invalid parameters provided to endpoint. +| <> + +|=== + +===== Samples + + +// markup not found, no include::{snippetDir}revoke/POST/http-request.adoc[opts=optional] + + +// markup not found, no include::{snippetDir}revoke/POST/http-response.adoc[opts=optional] + + + +// file not found, no * wiremock data link :revoke/POST/POST.json[] + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}revoke/POST/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + +[.uma2Configuration] +==== uma2Configuration + +`GET /uma2-configuration` + +Gets UMA configuration data. + +===== Description + +Gets UMA configuration data. + + +// markup not found, no include::{specDir}uma2-configuration/GET/spec.adoc[opts=optional] + + + +===== Parameters + + + + + + + +===== Return Type + +<> + + +===== Content Type + +* application/json + +===== Responses + +.http response codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| OK +| <> + + +| 500 +| Invalid parameters provided to endpoint. +| <> + +|=== + +===== Samples + + +// markup not found, no include::{snippetDir}uma2-configuration/GET/http-request.adoc[opts=optional] + + +// markup not found, no include::{snippetDir}uma2-configuration/GET/http-response.adoc[opts=optional] + + + +// file not found, no * wiremock data link :uma2-configuration/GET/GET.json[] + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}uma2-configuration/GET/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + +[.Registration] +=== Registration + + +[.deleteRegister] +==== DELETE Register + +`DELETE /register` + +Deletes the client info for a previously registered client. + +===== Description + +The Client Registration Endpoint removes the Client Metadata for a previously registered client. + + +// markup not found, no include::{specDir}register/DELETE/spec.adoc[opts=optional] + + + +===== Parameters + + + + +====== Header Parameters + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| Authorization +| Authorization header carrying \\\"registration_access_token\\\" issued before as a Bearer token +| X + + +|=== + +====== Query Parameters + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| client_id +| Client ID that identifies client. +| X + + +|=== + + +===== Return Type + + + +- + +===== Content Type + +* application/json + +===== Responses + +.http response codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 204 +| OK +| <<>> + + +| 400 +| Invalid parameters provided to endpoint. +| <> + + +| 401 +| Invalid parameters provided to endpoint. +| <> + + +| 500 +| Internal error occured. Please check log file for details. +| <> + +|=== + +===== Samples + + +// markup not found, no include::{snippetDir}register/DELETE/http-request.adoc[opts=optional] + + +// markup not found, no include::{snippetDir}register/DELETE/http-response.adoc[opts=optional] + + + +// file not found, no * wiremock data link :register/DELETE/DELETE.json[] + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}register/DELETE/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + +[.getRegister] +==== GET Register + +`GET /register` + +Get client information for a previously registered client. + +===== Description + +Get client information for a previously registered client. + + +// markup not found, no include::{specDir}register/GET/spec.adoc[opts=optional] + + + +===== Parameters + + + + +====== Header Parameters + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| Authorization +| Authorization header carrying \\\"registration_access_token\\\" issued before as a Bearer token +| X + + +|=== + +====== Query Parameters + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| client_id +| Client ID that identifies client. +| X + + +|=== + + +===== Return Type + +<> + + +===== Content Type + +* application/json + +===== Responses + +.http response codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| OK +| <> + + +| 400 +| Invalid parameters provided to endpoint. +| <> + + +| 401 +| Invalid parameters are provided to endpoint. +| <> + + +| 500 +| Internal error occured. Please check log file for details. +| <> + +|=== + +===== Samples + + +// markup not found, no include::{snippetDir}register/GET/http-request.adoc[opts=optional] + + +// markup not found, no include::{snippetDir}register/GET/http-response.adoc[opts=optional] + + + +// file not found, no * wiremock data link :register/GET/GET.json[] + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}register/GET/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + +[.postRegister] +==== POST Register + +`POST /register` + +Registers new client dynamically. + +===== Description + +The Client Registration Endpoint is an OAuth 2.0 Protected Resource through which a new Client registration can be requested. + + +// markup not found, no include::{specDir}register/POST/spec.adoc[opts=optional] + + + +===== Parameters + + +===== Body Parameter + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| RegisterParams1 +| <> +| - +| +| + +|=== + + + + + +===== Return Type + +<> + + +===== Content Type + +* application/json + +===== Responses + +.http response codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| OK +| <> + + +| 400 +| Invalid parameters provided to endpoint. +| <> + + +| 500 +| Internal error occured. Please check log file for details. +| <> + +|=== + +===== Samples + + +// markup not found, no include::{snippetDir}register/POST/http-request.adoc[opts=optional] + + +// markup not found, no include::{snippetDir}register/POST/http-response.adoc[opts=optional] + + + +// file not found, no * wiremock data link :register/POST/POST.json[] + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}register/POST/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + +[.putRegister] +==== PUT Register + +`PUT /register` + +Updates Client Metadata for a registered client. + +===== Description + +Updates Client Metadata for a registered client. + + +// markup not found, no include::{specDir}register/PUT/spec.adoc[opts=optional] + + + +===== Parameters + + +===== Body Parameter + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| RegisterParams +| <> +| - +| +| + +|=== + + +====== Header Parameters + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| Authorization +| Authorization header carrying \\\"registration_access_token\\\" issued before as a Bearer token +| X + + +|=== + +====== Query Parameters + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| client_id +| Client ID that identifies client that must be updated by this request. +| X + + +|=== + + +===== Return Type + +<> + + +===== Content Type + +* application/json + +===== Responses + +.http response codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| OK +| <> + + +| 400 +| Invalid parameters provided to endpoint. +| <> + + +| 500 +| Internal error occured. Please check log file for details. +| <> + +|=== + +===== Samples + + +// markup not found, no include::{snippetDir}register/PUT/http-request.adoc[opts=optional] + + +// markup not found, no include::{snippetDir}register/PUT/http-response.adoc[opts=optional] + + + +// file not found, no * wiremock data link :register/PUT/PUT.json[] + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}register/PUT/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + +[#models] +== Models + + +[#AuthorizeError] +=== _AuthorizeError_ + + + +[.fields-AuthorizeError] +[cols="2,1,2,4,1"] +|=== +| Field Name| Required| Type| Description| Format + +| error +| X +| String +| +| enum + +| error_description +| X +| String +| +| + +| details +| +| String +| +| + +|=== + + +[#ClientInfoResponse] +=== _ClientInfoResponse_ + +Client details in response. + +[.fields-ClientInfoResponse] +[cols="2,1,2,4,1"] +|=== +| Field Name| Required| Type| Description| Format + +| displayName +| +| String +| +| + +| inum +| +| String +| XRI i-number +| + +| oxAuthAppType +| +| String +| oxAuth Appication type +| + +| oxAuthIdTokenSignedResponseAlg +| +| String +| oxAuth ID Token Signed Response Algorithm +| + +| oxAuthRedirectURI +| +| List of <> +| Array of redirect URIs values used in the Authorization +| + +| oxId +| +| String +| oxAuth Attribute Scope Id +| + +| custom_attributes +| +| List of <> +| +| + +|=== + + +[#ClientResponse] +=== _ClientResponse_ + + + +[.fields-ClientResponse] +[cols="2,1,2,4,1"] +|=== +| Field Name| Required| Type| Description| Format + +| redirect_uris +| +| List of <> +| Redirection URI values used by the Client. One of these registered Redirection URI values must exactly match the redirect_uri parameter value used in each Authorization Request +| + +| claims_redirect_uri +| +| List of <> +| Array of The Claims Redirect URIs to which the client wishes the authorization server to direct the requesting party's user agent after completing its interaction. +| + +| response_types +| +| List of <> +| A list of the OAuth 2.0 response_type values that the Client is declaring that it will restrict itself to using. If omitted, the default is that the Client will use only the code Response Type. Allowed values are code, token, id_token. +| + +| grant_types +| +| List of <> +| A list of the OAuth 2.0 Grant Types that the Client is declaring that it will restrict itself to using. +| + +| contacts +| +| List of <> +| e-mail addresses of people responsible for this Client. +| + +| client_name +| +| String +| Name of the Client to be presented to the user. +| + +| logo_uri +| +| String +| URL that references a logo for the Client application +| + +| client_uri +| +| String +| URL of the home page of the Client. The value of this field must point to a valid Web page. +| + +| policy_uri +| +| String +| URL that the Relying Party Client provides to the End-User to read about the how the profile data will be used. +| + +| tos_uri +| +| String +| URL that the Relying Party Client provides to the End-User to read about the Relying Party's terms of service. +| + +| jwks_uri +| +| String +| URL for the Client's JSON Web Key Set (JWK) document containing key(s) that are used for signing requests to the OP. The JWK Set may also contain the Client's encryption keys(s) that are used by the OP to encrypt the responses to the Client. When both signing and encryption keys are made available, a use (Key Use) parameter value is required for all keys in the document to indicate each key's intended usage . +| + +| jwks +| +| String +| Client's JSON Web Key Set (JWK) document, passed by value. The semantics of the jwks parameter are the same as the jwks_uri parameter, other than that the JWK Set is passed by value, rather than by reference. This parameter is intended only to be used by Clients that, for some reason, are unable to use the jwks_uri parameter, for instance, by native applications that might not have a location to host the contents of the JWK Set. If a Client can use jwks_uri, it must not use jwks. One significant downside of jwks is that it does not enable key rotation. The jwks_uri and jwks parameters must not be used together. +| + +| sector_identifier_uri +| +| String +| URL using the https scheme to be used in calculating Pseudonymous Identifiers by the OP. +| + +| subject_type +| +| String +| Subject type requested for the Client ID. Valid types include pairwise and public. +| + +| rpt_as_jwt +| +| Boolean +| Specifies whether RPT should be return as signed JWT. +| + +| access_token_as_jwt +| +| Boolean +| Specifies whether access token as signed JWT. +| + +| access_token_signing_alg +| +| String +| Specifies signing algorithm that has to be used during JWT signing. If it's not specified, then the default OP signing algorithm will be used . +| + +| id_token_signed_response_alg +| +| String +| JWS alg algorithm (JWA) required for signing the ID Token issued to this Client. +| + +| id_token_encrypted_response_alg +| +| String +| JWE alg algorithm (JWA) required for encrypting the ID Token issued to this Client. +| + +| id_token_encrypted_response_enc +| +| String +| JWE enc algorithm (JWA) required for encrypting the ID Token issued to this Client. +| + +| userinfo_signed_response_alg +| +| String +| JWS alg algorithm (JWA) required for signing UserInfo Responses. +| + +| userinfo_encrypted_response_alg +| +| String +| JWE alg algorithm (JWA) required for encrypting UserInfo Responses. +| + +| userinfo_encrypted_response_enc +| +| String +| JWE enc algorithm (JWA) required for encrypting UserInfo Responses. +| + +| request_object_signing_alg +| +| String +| JWS alg algorithm (JWA) that must be used for signing Request Objects sent to the OP. +| + +| request_object_encryption_alg +| +| String +| JWE alg algorithm (JWA) the RP is declaring that it may use for encrypting Request Objects sent to the OP. +| + +| request_object_encryption_enc +| +| String +| JWE enc algorithm (JWA) the RP is declaring that it may use for encrypting Request Objects sent to the OP. +| + +| token_endpoint_auth_method +| +| String +| Requested Client Authentication method for the Token Endpoint. +| + +| token_endpoint_auth_signing_alg +| +| String +| JWS alg algorithm (JWA) that must be used for signing the JWT used to authenticate the Client at the Token Endpoint for the private_key_jwt and client_secret_jwt authentication methods. +| + +| default_max_age +| +| Integer +| Specifies the Default Maximum Authentication Age. +| + +| require_auth_time +| +| Boolean +| Boolean value specifying whether the auth_time Claim in the ID Token is required. It is required when the value is true. +| + +| default_acr_values +| +| List of <> +| Array of default requested Authentication Context Class Reference values that the Authorization Server must use for processing requests from the Client. +| + +| initiate_login_uri +| +| String +| Specifies the URI using the https scheme that the authorization server can call to initiate a login at the client. +| + +| post_logout_redirect_uris +| +| List of <> +| Provide the URLs supplied by the RP to request that the user be redirected to this location after a logout has been performed. +| + +| frontchannel_logout_uri +| +| String +| RP URL that will cause the RP to log itself out when rendered in an iframe by the OP. +| + +| frontchannel_logout_session_required +| +| Boolean +| Boolean value specifying whether the RP requires that a session ID query parameter be included to identify the RP session at the OP when the logout_uri is used. If omitted, the default value is false. +| + +| backchannel_logout_uri +| +| String +| RP URL that will cause the RP to log itself out when sent a Logout Token by the OP. +| + +| backchannel_logout_session_required +| +| Boolean +| Boolean value specifying whether the RP requires that a session ID Claim be included in the Logout Token to identify the RP session with the OP when the backchannel_logout_uri is used. If omitted, the default value is false. +| + +| request_uris +| +| List of <> +| Provide a list of request_uri values that are pre-registered by the Client for use at the Authorization Server. +| + +| scopes +| +| String +| This param will be removed in a future version because the correct is 'scope' not 'scopes', see (rfc7591). +| + +| claims +| +| String +| String containing a space-separated list of claims that can be requested individually. +| + +| id_token_token_binding_cnf +| +| String +| Specifies the JWT Confirmation Method member name (e.g. tbh) that the Relying Party expects when receiving Token Bound ID Tokens. The presence of this parameter indicates that the Relying Party supports Token Binding of ID Tokens. If omitted, the default is that the Relying Party does not support Token Binding of ID Tokens. +| + +| tls_client_auth_subject_dn +| +| String +| An string representation of the expected subject distinguished name of the certificate, which the OAuth client will use in mutual TLS authentication. +| + +| allow_spontaneous_scopes +| +| Boolean +| Specifies whether to allow spontaneous scopes for client. The default value is false. +| + +| spontaneous_scopes +| +| List of <> +| List of spontaneous scopes +| + +| run_introspection_script_before_access_token_as_jwt_creation_and_include_claims +| +| Boolean +| Boolean value with default value false. If true and access_token_as_jwt\=true then run introspection script and transfer claims into JWT. +| + +| keep_client_authorization_after_expiration +| +| Boolean +| Boolean value indicating if the client authorization will not be removed afer expiration (expiration date is same as client's expiration that created it). The default value is false. +| + +| scope +| +| List of <> +| Provide list of scope which are used during authentication to authorize access to resource. +| + +| authorized_origins +| +| List of <> +| specifies authorized JavaScript origins. +| + +| access_token_lifetime +| +| Integer +| Specifies the Client-specific access token expiration. +| + +| software_id +| +| String +| Specifies a unique identifier string (UUID) assigned by the client developer or software publisher used by registration endpoints to identify the client software to be dynamically registered. +| + +| software_version +| +| String +| Specifies a version identifier string for the client software identified by 'software_id'. The value of the 'software_version' should change on any update to the client software identified by the same 'software_id'. +| + +| software_statement +| +| String +| specifies a software statement containing client metadata values about the client software as claims. This is a string value containing the entire signed JWT. +| + +| backchannel_token_delivery_mode +| +| String +| specifies how backchannel token will be deliveried. +| + +| backchannel_client_notification_endpoint +| +| String +| Client Initiated Backchannel Authentication (CIBA) enables a Client to initiate the authentication of an end-user by means of out-of-band mechanisms. Upon receipt of the notification, the Client makes a request to the token endpoint to obtain the tokens. +| + +| backchannel_authentication_request_signing_alg +| +| String +| The JWS algorithm alg value that the Client will use for signing authentication request, as described in Section 7.1.1. of OAuth 2.0 [RFC6749]. When omitted, the Client will not send signed authentication requests. +| + +| backchannel_user_code_parameter +| +| Boolean +| Boolean value specifying whether the Client supports the user_code parameter. If omitted, the default value is false. +| + +|=== + + +[#EndSessionError] +=== _EndSessionError_ + + + +[.fields-EndSessionError] +[cols="2,1,2,4,1"] +|=== +| Field Name| Required| Type| Description| Format + +| error +| X +| String +| +| enum + +| error_description +| X +| String +| +| + +| details +| +| String +| +| + +|=== + + +[#ErrorResponse] +=== _ErrorResponse_ + + + +[.fields-ErrorResponse] +[cols="2,1,2,4,1"] +|=== +| Field Name| Required| Type| Description| Format + +| error +| X +| String +| +| + +| error_description +| X +| String +| +| + +| details +| +| String +| +| + +|=== + + +[#InlineResponse200] +=== _InlineResponse200_ + +AccessTokenResponse. + +[.fields-InlineResponse200] +[cols="2,1,2,4,1"] +|=== +| Field Name| Required| Type| Description| Format + +| access_token +| X +| String +| The access token issued by the authorization server. +| + +| token_type +| X +| String +| The access token type provides the client with the information required to successfully utilize the access token to make a protected resource request (along with type-specific attributes). +| + +| expires_in +| +| Integer +| The lifetime in seconds of the access token. For example, the value \\\"3600\\\" denotes that the access token will expire in one hour from the time the response was generated. +| + +| refresh_token +| +| String +| The refresh token, which can be used to obtain new access tokens using the same authorization grant +| + +| scope +| +| List of <> +| +| + +| id_token +| +| String +| +| + +|=== + + +[#InlineResponse2001] +=== _InlineResponse2001_ + +UmaMetadata + +[.fields-InlineResponse2001] +[cols="2,1,2,4,1"] +|=== +| Field Name| Required| Type| Description| Format + +| issuer +| X +| String +| The authorization server's issuer identifier +| + +| authorization_endpoint +| X +| String +| URL of the authorization server +| + +| uma_profiles_supported +| +| List of <> +| UMA profiles supported by this authorization server. The value is an array of string values, where each string value is a URI identifying an UMA profile +| + +| permission_endpoint +| +| String +| The endpoint URI at which the resource server requests permissions on the client's behalf. +| + +| resource_registration_endpoint +| +| String +| The endpoint URI at which the resource server registers resources to put them under authorization manager protection. +| + +| scope_endpoint +| +| String +| The Scope endpoint URI. +| + +|=== + + +[#InlineResponse201] +=== _InlineResponse201_ + + + +[.fields-InlineResponse201] +[cols="2,1,2,4,1"] +|=== +| Field Name| Required| Type| Description| Format + +| resource_id +| X +| String +| The identifier for a resource to which this client is seeking access. The identifier MUST correspond to a resource that was previously registered. +| + +| resource_scopes +| X +| List of <> +| An array referencing zero or more strings representing scopes to which access was granted for this resource. Each string MUST correspond to a scope that was registered by this resource server for the referenced resource. +| + +| params +| +| Map of <> +| A key/value map that can contain custom parameters. +| + +| exp +| +| Long +| Number of seconds since January 1 1970 UTC, indicating when this token will expire. +| int64 + +|=== + + +[#InlineResponse400] +=== _InlineResponse400_ + + + +[.fields-InlineResponse400] +[cols="2,1,2,4,1"] +|=== +| Field Name| Required| Type| Description| Format + +| error +| X +| String +| +| enum + +| error_description +| X +| String +| +| + +| details +| +| String +| +| + +|=== + + +[#InlineResponse4001] +=== _InlineResponse4001_ + + + +[.fields-InlineResponse4001] +[cols="2,1,2,4,1"] +|=== +| Field Name| Required| Type| Description| Format + +| error +| X +| String +| +| enum + +| error_description +| X +| String +| +| + +| details +| +| String +| +| + +|=== + + +[#InlineResponse4002] +=== _InlineResponse4002_ + + + +[.fields-InlineResponse4002] +[cols="2,1,2,4,1"] +|=== +| Field Name| Required| Type| Description| Format + +| error +| X +| String +| +| enum + +| error_description +| X +| String +| +| + +| details +| +| String +| +| + +|=== + + +[#InlineResponse4003] +=== _InlineResponse4003_ + + + +[.fields-InlineResponse4003] +[cols="2,1,2,4,1"] +|=== +| Field Name| Required| Type| Description| Format + +| error +| X +| String +| +| enum + +| error_description +| X +| String +| +| + +| details +| +| String +| +| + +|=== + + +[#InlineResponse4004] +=== _InlineResponse4004_ + + + +[.fields-InlineResponse4004] +[cols="2,1,2,4,1"] +|=== +| Field Name| Required| Type| Description| Format + +| error +| X +| String +| +| enum + +| error_description +| X +| String +| +| + +| details +| +| String +| +| + +|=== + + +[#InlineResponse4005] +=== _InlineResponse4005_ + + + +[.fields-InlineResponse4005] +[cols="2,1,2,4,1"] +|=== +| Field Name| Required| Type| Description| Format + +| error +| X +| String +| +| enum + +| error_description +| X +| String +| +| + +| details +| +| String +| +| + +|=== + + +[#InlineResponse4006] +=== _InlineResponse4006_ + + + +[.fields-InlineResponse4006] +[cols="2,1,2,4,1"] +|=== +| Field Name| Required| Type| Description| Format + +| error +| X +| String +| +| enum + +| error_description +| X +| String +| +| + +| details +| +| String +| +| + +|=== + + +[#InlineResponse401] +=== _InlineResponse401_ + + + +[.fields-InlineResponse401] +[cols="2,1,2,4,1"] +|=== +| Field Name| Required| Type| Description| Format + +| error +| X +| String +| +| enum + +| error_description +| X +| String +| +| + +| details +| +| String +| +| + +|=== + + +[#InlineResponse403] +=== _InlineResponse403_ + + + +[.fields-InlineResponse403] +[cols="2,1,2,4,1"] +|=== +| Field Name| Required| Type| Description| Format + +| error +| X +| String +| +| enum + +| error_description +| X +| String +| +| + +| details +| +| String +| +| + +|=== + + +[#InlineResponse404] +=== _InlineResponse404_ + + + +[.fields-InlineResponse404] +[cols="2,1,2,4,1"] +|=== +| Field Name| Required| Type| Description| Format + +| error +| X +| String +| +| enum + +| error_description +| X +| String +| +| + +| details +| +| String +| +| + +|=== + + +[#InlineResponse500] +=== _InlineResponse500_ + + + +[.fields-InlineResponse500] +[cols="2,1,2,4,1"] +|=== +| Field Name| Required| Type| Description| Format + +| error +| X +| String +| +| enum + +| error_description +| X +| String +| +| + +| details +| +| String +| +| + +|=== + + +[#IntrospectionResponse] +=== _IntrospectionResponse_ + +meta-information about token + +[.fields-IntrospectionResponse] +[cols="2,1,2,4,1"] +|=== +| Field Name| Required| Type| Description| Format + +| active +| X +| Boolean +| Boolean indicator of whether or not the presented token is currently active. +| + +| scope +| +| List of <> +| Provide list of scopes to which access was granted for this resource. +| + +| client_id +| +| String +| Client identifier for the OAuth 2.0 client that requested this token. +| + +| username +| +| String +| Human-readable identifier for the resource owner who authorized this token. +| + +| token_type +| +| String +| Type of the token as defined in Section 5.1 of OAuth 2.0 [RFC6749]. +| + +| exp +| +| Integer +| Integer timestamp, measured in the number of seconds since January 1 1970 UTC, indicating when this permission will expire. +| + +| iat +| +| Integer +| +| + +| sub +| +| String +| Subject of the token, as defined in JWT [RFC7519]. +| + +| aud +| +| String +| Service-specific string identifier or list of string identifiers representing the intended audience for this token, as defined in JWT [RFC7519]. +| + +| iss +| +| String +| String representing the issuer of this token, as defined in JWT [RFC7519]. +| + +| acr_values +| +| String +| Authentication Context Class Reference values. +| + +| jti +| +| String +| String identifier for the token, as defined in JWT. +| + +|=== + + +[#JsonWebKey] +=== _JsonWebKey_ + + + +[.fields-JsonWebKey] +[cols="2,1,2,4,1"] +|=== +| Field Name| Required| Type| Description| Format + +| kid +| X +| String +| +| + +| kty +| X +| String +| +| + +| use +| X +| String +| +| + +| alg +| X +| String +| +| + +| crv +| +| String +| +| + +| exp +| X +| Long +| +| int64 + +| x5c +| X +| List of <> +| +| + +| n +| +| String +| +| + +| e +| +| String +| +| + +| x +| +| String +| +| + +| y +| +| String +| +| + +|=== + + +[#RegisterParams] +=== _RegisterParams_ RegisterParams + + + +[.fields-RegisterParams] +[cols="2,1,2,4,1"] +|=== +| Field Name| Required| Type| Description| Format + +| redirect_uris +| X +| List of <> +| Redirection URI values used by the Client. One of these registered Redirection URI values must exactly match the redirect_uri parameter value used in each Authorization Request +| + +| claims_redirect_uri +| +| List of <> +| Array of The Claims Redirect URIs to which the client wishes the authorization server to direct the requesting party's user agent after completing its interaction. +| + +| response_types +| +| List of <> +| A list of the OAuth 2.0 response_type values that the Client is declaring that it will restrict itself to using. If omitted, the default is that the Client will use only the code Response Type. Allowed values are code, token, id_token. +| + +| grant_types +| +| List of <> +| A list of the OAuth 2.0 Grant Types that the Client is declaring that it will restrict itself to using. +| + +| contacts +| +| List of <> +| e-mail addresses of people responsible for this Client. +| + +| client_name +| +| String +| Name of the Client to be presented to the user. +| + +| logo_uri +| +| String +| URL that references a logo for the Client application +| + +| client_uri +| +| String +| URL of the home page of the Client. The value of this field must point to a valid Web page. +| + +| policy_uri +| +| String +| URL that the Relying Party Client provides to the End-User to read about the how the profile data will be used. +| + +| tos_uri +| +| String +| URL that the Relying Party Client provides to the End-User to read about the Relying Party's terms of service. +| + +| jwks_uri +| +| String +| URL for the Client's JSON Web Key Set (JWK) document containing key(s) that are used for signing requests to the OP. The JWK Set may also contain the Client's encryption keys(s) that are used by the OP to encrypt the responses to the Client. When both signing and encryption keys are made available, a use (Key Use) parameter value is required for all keys in the document to indicate each key's intended usage . +| + +| jwks +| +| List of <> +| List of JSON Web Key (JWK) - A JSON object that represents a cryptographic key. The members of the object represent properties of the key, including its value. +| + +| sector_identifier_uri +| +| String +| URL using the https scheme to be used in calculating Pseudonymous Identifiers by the OP. +| + +| subject_type +| +| String +| Subject type requested for the Client ID. Valid types include pairwise and public. +| + +| rpt_as_jwt +| +| Boolean +| Specifies whether RPT should be return as signed JWT. +| + +| access_token_as_jwt +| +| Boolean +| Specifies whether access token as signed JWT. +| + +| access_token_signing_alg +| +| String +| Specifies signing algorithm that has to be used during JWT signing. If it's not specified, then the default OP signing algorithm will be used . +| + +| id_token_signed_response_alg +| +| String +| JWS alg algorithm (JWA) required for signing the ID Token issued to this Client. +| + +| id_token_encrypted_response_alg +| +| String +| JWE alg algorithm (JWA) required for encrypting the ID Token issued to this Client. +| + +| id_token_encrypted_response_enc +| +| String +| JWE enc algorithm (JWA) required for encrypting the ID Token issued to this Client. +| + +| userinfo_signed_response_alg +| +| String +| JWS alg algorithm (JWA) required for signing UserInfo Responses. +| + +| userinfo_encrypted_response_alg +| +| String +| JWE alg algorithm (JWA) required for encrypting UserInfo Responses. +| + +| userinfo_encrypted_response_enc +| +| String +| JWE enc algorithm (JWA) required for encrypting UserInfo Responses. +| + +| request_object_signing_alg +| +| String +| JWS alg algorithm (JWA) that must be used for signing Request Objects sent to the OP. +| + +| request_object_encryption_alg +| +| String +| JWE alg algorithm (JWA) the RP is declaring that it may use for encrypting Request Objects sent to the OP. +| + +| request_object_encryption_enc +| +| String +| JWE enc algorithm (JWA) the RP is declaring that it may use for encrypting Request Objects sent to the OP. +| + +| token_endpoint_auth_method +| +| String +| Requested Client Authentication method for the Token Endpoint. +| + +| token_endpoint_auth_signing_alg +| +| String +| JWS alg algorithm (JWA) that must be used for signing the JWT used to authenticate the Client at the Token Endpoint for the private_key_jwt and client_secret_jwt authentication methods. +| + +| default_max_age +| +| Integer +| Specifies the Default Maximum Authentication Age. +| + +| require_auth_time +| +| Boolean +| Boolean value specifying whether the auth_time Claim in the ID Token is required. It is required when the value is true. +| + +| default_acr_values +| +| List of <> +| Array of default requested Authentication Context Class Reference values that the Authorization Server must use for processing requests from the Client. +| + +| initiate_login_uri +| +| String +| Specifies the URI using the https scheme that the authorization server can call to initiate a login at the client. +| + +| post_logout_redirect_uris +| +| List of <> +| Provide the URLs supplied by the RP to request that the user be redirected to this location after a logout has been performed. +| + +| frontchannel_logout_uri +| +| String +| RP URL that will cause the RP to log itself out when rendered in an iframe by the OP. +| + +| frontchannel_logout_session_required +| +| Boolean +| Boolean value specifying whether the RP requires that a session ID query parameter be included to identify the RP session at the OP when the logout_uri is used. If omitted, the default value is false. +| + +| backchannel_logout_uri +| +| String +| RP URL that will cause the RP to log itself out when sent a Logout Token by the OP. +| + +| backchannel_logout_session_required +| +| Boolean +| Boolean value specifying whether the RP requires that a session ID Claim be included in the Logout Token to identify the RP session with the OP when the backchannel_logout_uri is used. If omitted, the default value is false. +| + +| request_uris +| +| List of <> +| Provide a list of request_uri values that are pre-registered by the Client for use at the Authorization Server. +| + +| scopes +| +| String +| This param will be removed in a future version because the correct is 'scope' not 'scopes', see (rfc7591). +| + +| claims +| +| String +| String containing a space-separated list of claims that can be requested individually. +| + +| id_token_token_binding_cnf +| +| String +| Specifies the JWT Confirmation Method member name (e.g. tbh) that the Relying Party expects when receiving Token Bound ID Tokens. The presence of this parameter indicates that the Relying Party supports Token Binding of ID Tokens. If omitted, the default is that the Relying Party does not support Token Binding of ID Tokens. +| + +| tls_client_auth_subject_dn +| +| String +| An string representation of the expected subject distinguished name of the certificate, which the OAuth client will use in mutual TLS authentication. +| + +| allow_spontaneous_scopes +| +| Boolean +| Specifies whether to allow spontaneous scopes for client. The default value is false. +| + +| spontaneous_scopes +| +| List of <> +| List of spontaneous scopes +| + +| run_introspection_script_before_access_token_as_jwt_creation_and_include_claims +| +| Boolean +| Boolean value with default value false. If true and access_token_as_jwt\=true then run introspection script and transfer claims into JWT. +| + +| keep_client_authorization_after_expiration +| +| Boolean +| Boolean value indicating if the client authorization will not be removed afer expiration (expiration date is same as client's expiration that created it). The default value is false. +| + +| scope +| +| List of <> +| Provide list of scope which are used during authentication to authorize access to resource. +| + +| authorized_origins +| +| List of <> +| specifies authorized JavaScript origins. +| + +| access_token_lifetime +| +| Integer +| Specifies the Client-specific access token expiration. +| + +| software_id +| +| String +| Specifies a unique identifier string (UUID) assigned by the client developer or software publisher used by registration endpoints to identify the client software to be dynamically registered. +| + +| software_version +| +| String +| Specifies a version identifier string for the client software identified by 'software_id'. The value of the 'software_version' should change on any update to the client software identified by the same 'software_id'. +| + +| software_statement +| +| String +| specifies a software statement containing client metadata values about the client software as claims. This is a string value containing the entire signed JWT. +| + +| backchannel_token_delivery_mode +| +| String +| specifies how backchannel token will be deliveried. +| + +| backchannel_client_notification_endpoint +| +| String +| Client Initiated Backchannel Authentication (CIBA) enables a Client to initiate the authentication of an end-user by means of out-of-band mechanisms. Upon receipt of the notification, the Client makes a request to the token endpoint to obtain the tokens. +| + +| backchannel_authentication_request_signing_alg +| +| String +| The JWS algorithm alg value that the Client will use for signing authentication request, as described in Section 7.1.1. of OAuth 2.0 [RFC6749]. When omitted, the Client will not send signed authentication requests. +| + +| backchannel_user_code_parameter +| +| Boolean +| Boolean value specifying whether the Client supports the user_code parameter. If omitted, the default value is false. +| + +| additional_audience +| +| List of <> +| Additional audiences. +| + +|=== + + +[#RegisterParams1] +=== _RegisterParams1_ RegisterParams + + + +[.fields-RegisterParams1] +[cols="2,1,2,4,1"] +|=== +| Field Name| Required| Type| Description| Format + +| redirect_uris +| X +| List of <> +| Redirection URI values used by the Client. One of these registered Redirection URI values must exactly match the redirect_uri parameter value used in each Authorization Request +| + +| claims_redirect_uri +| +| List of <> +| Array of The Claims Redirect URIs to which the client wishes the authorization server to direct the requesting party's user agent after completing its interaction. +| + +| response_types +| +| List of <> +| A list of the OAuth 2.0 response_type values that the Client is declaring that it will restrict itself to using. If omitted, the default is that the Client will use only the code Response Type. Allowed values are code, token, id_token. +| + +| grant_types +| +| List of <> +| A list of the OAuth 2.0 Grant Types that the Client is declaring that it will restrict itself to using. +| + +| contacts +| +| List of <> +| e-mail addresses of people responsible for this Client. +| + +| client_name +| +| String +| Name of the Client to be presented to the user. +| + +| logo_uri +| +| String +| URL that references a logo for the Client application +| + +| client_uri +| +| String +| URL of the home page of the Client. The value of this field must point to a valid Web page. +| + +| policy_uri +| +| String +| URL that the Relying Party Client provides to the End-User to read about the how the profile data will be used. +| + +| tos_uri +| +| String +| URL that the Relying Party Client provides to the End-User to read about the Relying Party's terms of service. +| + +| jwks_uri +| +| String +| URL for the Client's JSON Web Key Set (JWK) document containing key(s) that are used for signing requests to the OP. The JWK Set may also contain the Client's encryption keys(s) that are used by the OP to encrypt the responses to the Client. When both signing and encryption keys are made available, a use (Key Use) parameter value is required for all keys in the document to indicate each key's intended usage . +| + +| jwks +| +| List of <> +| List of JSON Web Key (JWK) - A JSON object that represents a cryptographic key. The members of the object represent properties of the key, including its value. +| + +| sector_identifier_uri +| +| String +| URL using the https scheme to be used in calculating Pseudonymous Identifiers by the OP. +| + +| subject_type +| +| String +| Subject type requested for the Client ID. Valid types include pairwise and public. +| + +| rpt_as_jwt +| +| Boolean +| Specifies whether RPT should be return as signed JWT. +| + +| access_token_as_jwt +| +| Boolean +| Specifies whether access token as signed JWT. +| + +| access_token_signing_alg +| +| String +| Specifies signing algorithm that has to be used during JWT signing. If it's not specified, then the default OP signing algorithm will be used . +| + +| id_token_signed_response_alg +| +| String +| JWS alg algorithm (JWA) required for signing the ID Token issued to this Client. +| + +| id_token_encrypted_response_alg +| +| String +| JWE alg algorithm (JWA) required for encrypting the ID Token issued to this Client. +| + +| id_token_encrypted_response_enc +| +| String +| JWE enc algorithm (JWA) required for encrypting the ID Token issued to this Client. +| + +| userinfo_signed_response_alg +| +| String +| JWS alg algorithm (JWA) required for signing UserInfo Responses. +| + +| userinfo_encrypted_response_alg +| +| String +| JWE alg algorithm (JWA) required for encrypting UserInfo Responses. +| + +| userinfo_encrypted_response_enc +| +| String +| JWE enc algorithm (JWA) required for encrypting UserInfo Responses. +| + +| request_object_signing_alg +| +| String +| JWS alg algorithm (JWA) that must be used for signing Request Objects sent to the OP. +| + +| request_object_encryption_alg +| +| String +| JWE alg algorithm (JWA) the RP is declaring that it may use for encrypting Request Objects sent to the OP. +| + +| request_object_encryption_enc +| +| String +| JWE enc algorithm (JWA) the RP is declaring that it may use for encrypting Request Objects sent to the OP. +| + +| token_endpoint_auth_method +| +| String +| Requested Client Authentication method for the Token Endpoint. +| + +| token_endpoint_auth_signing_alg +| +| String +| JWS alg algorithm (JWA) that must be used for signing the JWT used to authenticate the Client at the Token Endpoint for the private_key_jwt and client_secret_jwt authentication methods. +| + +| default_max_age +| +| Integer +| Specifies the Default Maximum Authentication Age. +| + +| require_auth_time +| +| Boolean +| Boolean value specifying whether the auth_time Claim in the ID Token is required. It is required when the value is true. +| + +| default_acr_values +| +| List of <> +| Array of default requested Authentication Context Class Reference values that the Authorization Server must use for processing requests from the Client. +| + +| initiate_login_uri +| +| String +| Specifies the URI using the https scheme that the authorization server can call to initiate a login at the client. +| + +| post_logout_redirect_uris +| +| List of <> +| Provide the URLs supplied by the RP to request that the user be redirected to this location after a logout has been performed. +| + +| frontchannel_logout_uri +| +| String +| RP URL that will cause the RP to log itself out when rendered in an iframe by the OP. +| + +| frontchannel_logout_session_required +| +| Boolean +| Boolean value specifying whether the RP requires that a session ID query parameter be included to identify the RP session at the OP when the logout_uri is used. If omitted, the default value is false. +| + +| backchannel_logout_uri +| +| String +| RP URL that will cause the RP to log itself out when sent a Logout Token by the OP. +| + +| backchannel_logout_session_required +| +| Boolean +| Boolean value specifying whether the RP requires that a session ID Claim be included in the Logout Token to identify the RP session with the OP when the backchannel_logout_uri is used. If omitted, the default value is false. +| + +| request_uris +| +| List of <> +| Provide a list of request_uri values that are pre-registered by the Client for use at the Authorization Server. +| + +| scopes +| +| String +| This param will be removed in a future version because the correct is 'scope' not 'scopes', see (rfc7591). +| + +| claims +| +| String +| String containing a space-separated list of claims that can be requested individually. +| + +| id_token_token_binding_cnf +| +| String +| Specifies the JWT Confirmation Method member name (e.g. tbh) that the Relying Party expects when receiving Token Bound ID Tokens. The presence of this parameter indicates that the Relying Party supports Token Binding of ID Tokens. If omitted, the default is that the Relying Party does not support Token Binding of ID Tokens. +| + +| tls_client_auth_subject_dn +| +| String +| An string representation of the expected subject distinguished name of the certificate, which the OAuth client will use in mutual TLS authentication. +| + +| allow_spontaneous_scopes +| +| Boolean +| Specifies whether to allow spontaneous scopes for client. The default value is false. +| + +| spontaneous_scopes +| +| List of <> +| List of spontaneous scopes +| + +| run_introspection_script_before_access_token_as_jwt_creation_and_include_claims +| +| Boolean +| Boolean value with default value false. If true and access_token_as_jwt\=true then run introspection script and transfer claims into JWT. +| + +| keep_client_authorization_after_expiration +| +| Boolean +| Boolean value indicating if the client authorization will not be removed afer expiration (expiration date is same as client's expiration that created it). The default value is false. +| + +| scope +| +| List of <> +| Provide list of scope which are used during authentication to authorize access to resource. +| + +| authorized_origins +| +| List of <> +| specifies authorized JavaScript origins. +| + +| access_token_lifetime +| +| Integer +| Specifies the Client-specific access token expiration. +| + +| software_id +| +| String +| Specifies a unique identifier string (UUID) assigned by the client developer or software publisher used by registration endpoints to identify the client software to be dynamically registered. +| + +| software_version +| +| String +| Specifies a version identifier string for the client software identified by 'software_id'. The value of the 'software_version' should change on any update to the client software identified by the same 'software_id'. +| + +| software_statement +| +| String +| specifies a software statement containing client metadata values about the client software as claims. This is a string value containing the entire signed JWT. +| + +| backchannel_token_delivery_mode +| +| String +| specifies how backchannel token will be deliveried. +| + +| backchannel_client_notification_endpoint +| +| String +| Client Initiated Backchannel Authentication (CIBA) enables a Client to initiate the authentication of an end-user by means of out-of-band mechanisms. Upon receipt of the notification, the Client makes a request to the token endpoint to obtain the tokens. +| + +| backchannel_authentication_request_signing_alg +| +| String +| The JWS algorithm alg value that the Client will use for signing authentication request, as described in Section 7.1.1. of OAuth 2.0 [RFC6749]. When omitted, the Client will not send signed authentication requests. +| + +| backchannel_user_code_parameter +| +| Boolean +| Boolean value specifying whether the Client supports the user_code parameter. If omitted, the default value is false. +| + +| additional_audience +| +| List of <> +| Additional audiences. +| + +|=== + + +[#RegisterResponseParam] +=== _RegisterResponseParam_ + + + +[.fields-RegisterResponseParam] +[cols="2,1,2,4,1"] +|=== +| Field Name| Required| Type| Description| Format + +| client_id +| X +| String +| Unique Client Identifier. It MUST NOT be currently valid for any other registered Client. +| + +| client_secret +| +| String +| This value is used by Confidential Clients to authenticate to the Token Endpoint +| + +| registration_access_token +| +| String +| Registration Access Token that can be used at the Client Configuration Endpoint to perform subsequent operations upon the Client registration. +| + +| registration_client_uri +| +| String +| Location of the Client Configuration Endpoint where the Registration Access Token can be used to perform subsequent operations upon the resulting Client registration. +| + +| client_id_issued_at +| +| Integer +| Time at which the Client Identifier was issued. +| + +| client_secret_expires_at +| +| Integer +| Time at which the client_secret will expire or 0 if it will not expire. +| + +|=== + + +[#RptIntrospectionResponse] +=== _RptIntrospectionResponse_ + + + +[.fields-RptIntrospectionResponse] +[cols="2,1,2,4,1"] +|=== +| Field Name| Required| Type| Description| Format + +| active +| X +| Boolean +| Boolean indicator of whether or not the presented token is currently active. +| + +| exp +| +| Long +| Integer timestamp, in seconds since January 1 1970 UTC, indicating when this token will expire. +| int64 + +| iat +| +| Integer +| Integer timestamp, measured in the number of seconds since January 1 1970 UTC, indicating when this permission was originally issued. +| + +| clientId +| +| String +| Client id used to obtain RPT. +| + +| sub +| +| String +| Subject of the token. Usually a machine-readable identifier of the resource owner who authorized this token. +| + +| aud +| +| String +| Service-specific string identifier or list of string identifiers representing the intended audience for this token. +| + +| permissions +| X +| List of <> +| +| + +| pct_claims +| +| Map of <> +| PCT token claims. +| + +| iss +| +| String +| String representing the issuer of this token, as defined in JWT [RFC7519]. +| + +| jti +| +| String +| String identifier for the token, as defined in JWT [RFC7519]. +| + +| nbf +| +| Integer +| Integer timestamp, measured in the number of seconds since January 1 1970 UTC, indicating the time before which this permission is not valid. +| + +| resource_id +| X +| String +| Resource ID. +| + +| resource_scopes +| X +| List of <> +| +| + +|=== + + +[#RptIntrospectionResponse1] +=== _RptIntrospectionResponse1_ + + + +[.fields-RptIntrospectionResponse1] +[cols="2,1,2,4,1"] +|=== +| Field Name| Required| Type| Description| Format + +| active +| X +| Boolean +| Boolean indicator of whether or not the presented token is currently active. +| + +| exp +| +| Long +| Integer timestamp, in seconds since January 1 1970 UTC, indicating when this token will expire. +| int64 + +| iat +| +| Integer +| Integer timestamp, measured in the number of seconds since January 1 1970 UTC, indicating when this permission was originally issued. +| + +| clientId +| +| String +| Client id used to obtain RPT. +| + +| sub +| +| String +| Subject of the token. Usually a machine-readable identifier of the resource owner who authorized this token. +| + +| aud +| +| String +| Service-specific string identifier or list of string identifiers representing the intended audience for this token. +| + +| permissions +| X +| List of <> +| +| + +| pct_claims +| +| Map of <> +| +| + +| iss +| +| String +| String representing the issuer of this token, as defined in JWT [RFC7519]. +| + +| jti +| +| String +| String identifier for the token, as defined in JWT [RFC7519]. +| + +| nbf +| +| Integer +| Integer timestamp, measured in the number of seconds since January 1 1970 UTC, indicating the time before which this permission is not valid. +| + +| resource_id +| X +| String +| Resource ID. +| + +| resource_scopes +| X +| List of <> +| +| + +|=== + + +[#RptIntrospectionResponsePermissions] +=== _RptIntrospectionResponsePermissions_ + +List of UmaPermission granted to RPT. A permission is (requested or granted) authorized access to a particular resource with some number of scopes bound to that resource. + +[.fields-RptIntrospectionResponsePermissions] +[cols="2,1,2,4,1"] +|=== +| Field Name| Required| Type| Description| Format + +| resource_id +| X +| String +| A string that uniquely identifies the protected resource, access to which has been granted to this client on behalf of this requesting party. The identifier MUST correspond to a resource that was previously registered as protected. +| + +| resource_scopes +| X +| List of <> +| An array referencing zero or more strings representing scopes to which access was granted for this resource. Each string MUST correspond to a scope that was registered by this resource server for the referenced resource. +| + +| exp +| +| Integer +| Integer timestamp, measured in the number of seconds since January 1 1970 UTC, indicating when this permission will expire. If the token-level exp value pre-dates a permission-level exp value, the token-level value takes precedence. +| + +| iat +| +| Integer +| Integer timestamp, measured in the number of seconds since January 1 1970 UTC, indicating when this permission was originally issued. If the token-level iat value post-dates a permission-level iat value, the token-level value takes precedence. +| + +| nbf +| +| Integer +| Integer timestamp, measured in the number of seconds since January 1 1970 UTC, indicating the time before which this permission is not valid. If the token-level nbf value post-dates a permission-level nbf value, the token-level value takes precedence. +| + +|=== + + +[#SessionStateObject] +=== _SessionStateObject_ + + + +[.fields-SessionStateObject] +[cols="2,1,2,4,1"] +|=== +| Field Name| Required| Type| Description| Format + +| state +| +| String +| String that represents the End-User's login state at the OP. It MUST NOT contain the space (\\\" \\\") character. +| + +| auth_time +| +| date +| specifies the time at which session was authenticated. +| date + +| custom_state +| +| String +| +| + +|=== + + +[#UmaPermissiona] +=== _UmaPermissiona_ UmaPermissiona + +A permission is (requested or granted) authorized access to a particular resource with some number of scopes bound to that resource. + +[.fields-UmaPermissiona] +[cols="2,1,2,4,1"] +|=== +| Field Name| Required| Type| Description| Format + +| resource_id +| X +| String +| The identifier for a resource to which this client is seeking access. The identifier MUST correspond to a resource that was previously registered. +| + +| resource_scopes +| X +| List of <> +| An array referencing zero or more strings representing scopes to which access was granted for this resource. Each string MUST correspond to a scope that was registered by this resource server for the referenced resource. +| + +| params +| +| Map of <> +| A key/value map that can contain custom parameters. +| + +|=== + + +[#UmaResource] +=== _UmaResource_ UmaResource + +Resource description + +[.fields-UmaResource] +[cols="2,1,2,4,1"] +|=== +| Field Name| Required| Type| Description| Format + +| name +| +| String +| A human-readable string describing a set of one or more resources. This name MAY be used by the authorization server in its resource owner user interface for the resource owner. +| + +| icon_uri +| +| String +| A URI for a graphic icon representing the resource set. The referenced icon MAY be used by the authorization server in its resource owner user interface for the resource owner. +| + +| type +| +| String +| A string uniquely identifying the semantics of the resource set. For example, if the resource set consists of a single resource that is an identity claim that leverages standardized claim semantics for \\\"verified email address\\\", the value of this property could be an identifying URI for this claim. +| + +| resource_scopes +| X +| List of <> +| An array of strings, any of which MAY be a URI, indicating the available scopes for this resource set. URIs MUST resolve to scope descriptions as defined in Section 2.1. Published scope descriptions MAY reside anywhere on the web; a resource server is not required to self-host scope descriptions and may wish to point to standardized scope descriptions residing elsewhere. It is the resource server's responsibility to ensure that scope description documents are accessible to authorization servers through GET calls to support any user interface requirements. The resource server and authorization server are presumed to have separately negotiated any required interpretation of scope handling not conveyed through scope descriptions. +| + +| scope_expression +| +| String +| +| + +| description +| +| String +| A human-readable string describing the resource +| + +| iat +| +| Long +| number of seconds since January 1 1970 UTC, indicating when the token was issued at +| int64 + +| exp +| +| Long +| number of seconds since January 1 1970 UTC, indicating when this token will expire. +| int64 + +|=== + + +[#UmaResource1] +=== _UmaResource1_ UmaResource + +Resource description + +[.fields-UmaResource1] +[cols="2,1,2,4,1"] +|=== +| Field Name| Required| Type| Description| Format + +| name +| +| String +| A human-readable string describing a set of one or more resources. This name MAY be used by the authorization server in its resource owner user interface for the resource owner. +| + +| icon_uri +| +| String +| A URI for a graphic icon representing the resource set. The referenced icon MAY be used by the authorization server in its resource owner user interface for the resource owner. +| + +| type +| +| String +| A string uniquely identifying the semantics of the resource set. For example, if the resource set consists of a single resource that is an identity claim that leverages standardized claim semantics for \\\"verified email address\\\", the value of this property could be an identifying URI for this claim. +| + +| resource_scopes +| X +| List of <> +| An array of strings, any of which MAY be a URI, indicating the available scopes for this resource set. URIs MUST resolve to scope descriptions as defined in Section 2.1. Published scope descriptions MAY reside anywhere on the web; a resource server is not required to self-host scope descriptions and may wish to point to standardized scope descriptions residing elsewhere. It is the resource server's responsibility to ensure that scope description documents are accessible to authorization servers through GET calls to support any user interface requirements. The resource server and authorization server are presumed to have separately negotiated any required interpretation of scope handling not conveyed through scope descriptions. +| + +| scope_expression +| +| String +| +| + +| description +| +| String +| A human-readable string describing the resource +| + +| iat +| +| Long +| number of seconds since January 1 1970 UTC, indicating when the token was issued at +| int64 + +| exp +| +| Long +| number of seconds since January 1 1970 UTC, indicating when this token will expire. +| int64 + +|=== + + +[#UmaResourceResponse] +=== _UmaResourceResponse_ + +UmaResourceResponse Resource created. + +[.fields-UmaResourceResponse] +[cols="2,1,2,4,1"] +|=== +| Field Name| Required| Type| Description| Format + +| _id +| X +| String +| UMA Resource identifier +| + +| user_access_policy_uri +| +| String +| +| + +|=== + + +[#UmaResourceWithId] +=== _UmaResourceWithId_ + +Uma Resource details + +[.fields-UmaResourceWithId] +[cols="2,1,2,4,1"] +|=== +| Field Name| Required| Type| Description| Format + +| _id +| X +| String +| UMA Resource identifier +| + +| name +| +| String +| A human-readable string describing a set of one or more resources. This name MAY be used by the authorization server in its resource owner user interface for the resource owner. +| + +| uri +| +| String +| A human-readable string describing the resource +| + +| type +| +| String +| A string uniquely identifying the semantics of the resource set. For example, if the resource set consists of a single resource that is an identity claim that leverages standardized claim semantics for \\\"verified email address\\\", the value of this property could be an identifying URI for this claim. +| + +| scopes +| +| List of <> +| An array of strings, any of which MAY be a URI, indicating the available scopes for this resource set. URIs MUST resolve to scope descriptions as defined in Section 2.1. Published scope descriptions MAY reside anywhere on the web; a resource server is not required to self-host scope descriptions and may wish to point to standardized scope descriptions residing elsewhere. It is the resource server's responsibility to ensure that scope description documents are accessible to authorization servers through GET calls to support any user interface requirements. The resource server and authorization server are presumed to have separately negotiated any required interpretation of scope handling not conveyed through scope descriptions. +| + +| scope_expression +| +| String +| +| + +| description +| +| String +| A human-readable string describing the resource +| + +| icon_uri +| +| String +| A URI for a graphic icon representing the resource set. The referenced icon MAY be used by the authorization server in its resource owner user interface for the resource owner. +| + +| iat +| X +| Long +| number of seconds since January 1 1970 UTC, indicating when the token was issued at +| int64 + +| exp +| X +| Long +| number of seconds since January 1 1970 UTC, indicating when this token will expire. +| int64 + +|=== + + +[#WebKeysConfiguration] +=== _WebKeysConfiguration_ + +JSON Web Key Set (JWKS) - A JSON object that represents a set of JWKs. The JSON object MUST have a keys member, which is an array of JWKs. + +[.fields-WebKeysConfiguration] +[cols="2,1,2,4,1"] +|=== +| Field Name| Required| Type| Description| Format + +| keys +| X +| List of <> +| List of JSON Web Key (JWK) - A JSON object that represents a cryptographic key. The members of the object represent properties of the key, including its value. +| + +|=== + + diff --git a/docs/ICD/README.adoc b/docs/ICD/README.adoc new file mode 100644 index 0000000..e0bfeb9 --- /dev/null +++ b/docs/ICD/README.adoc @@ -0,0 +1,32 @@ += Documentation +:component-name: Login Service +:component-github-name: um-login-service + +The documentation is written as AsciiDoc and auto-generated by the Travis job. + +The output of the auto-generation is published via GitHub pages and so available at the URL https://eoepca.github.io/{component-github-name}. + +== Setup Pre-requisites + +This section documents the pre-requisites for the job. + +=== gh-pages branch + +The travis job pushes the generated doc files (html/images + pdf) to trhe 'gh-pages' branch of the repository. This should be created if it doesn't already exist. + +=== Travis settings + +The Travis settings for the project must include the following environment variables... + +GH_USER_NAME:: +the name used in the commit message + +GH_USER_EMAIL:: +the email address used in the commit message + +GH_REPOS_NAME:: +the name of the repos in GitHub, i.e. the last part of the GitHub URL https://github.com/EOEPCA/ + +GH_TOKEN:: +the access token that allows Travis to push to the GitHub repos 'gh-pages' branch. This must be generated as a `Personal Access Token` within the settings of the `EOEPCA-CI` GitHub account (https://github.com/settings/tokens) and pasted in to set the environment variable in Travis. + +*In doing so, ensure that the 'display value' option is disabled*. diff --git a/docs/amendment-history.adoc b/docs/ICD/amendment-history.adoc similarity index 100% rename from docs/amendment-history.adoc rename to docs/ICD/amendment-history.adoc diff --git a/docs/end-of-document.adoc b/docs/ICD/end-of-document.adoc similarity index 100% rename from docs/end-of-document.adoc rename to docs/ICD/end-of-document.adoc diff --git a/docs/ICD/gh-page-README.adoc b/docs/ICD/gh-page-README.adoc new file mode 100644 index 0000000..1ee3b0d --- /dev/null +++ b/docs/ICD/gh-page-README.adoc @@ -0,0 +1,15 @@ += EOEPCA - {component-name} +:component-name: Login Service +:component-github-name: um-login-service + +EO Exploitation Platform Common Architecture (EOEPCA). + +This is the latest published version of the *{component-name} Component Design* document. + +The latest document https://eoepca.github.io/{component-github-name}/current/[can be viewed here]. It is published here for community review and feedback. + +It is also available in https://eoepca.github.io/{component-github-name}/current/EOEPCA-{component-github-name}.pdf[PDF form here]. + +Older document releases https://eoepca.github.io/{component-github-name}[can be accessed from the landing page here]. + +Comments are invited and should be made by raising an issue on the link:../../issues[Issues Page]. diff --git a/docs/ICD/gh-page-root.html b/docs/ICD/gh-page-root.html new file mode 100644 index 0000000..6e624f3 --- /dev/null +++ b/docs/ICD/gh-page-root.html @@ -0,0 +1,18 @@ + + + + + + + Document + + +

um-login-service documentation

+

Releases...

+
+ +
+ + diff --git a/docs/ICD/images/client.png b/docs/ICD/images/client.png new file mode 100644 index 0000000000000000000000000000000000000000..b5e66dcd423ee3c7f829972d1d6ec8c4af64b92c GIT binary patch literal 34327 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
  • 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 aiT5EXQ%9_o@XZtOPg-1FtV~4T?|%Uk_CARK literal 0 HcmV?d00001 diff --git a/docs/ICD/images/gluu-dashboard.png b/docs/ICD/images/gluu-dashboard.png new file mode 100644 index 0000000000000000000000000000000000000000..99693aa8b8d2df57f9f3bc0be5b8c698b0226316 GIT binary patch literal 63038 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

    >}r}C6Qii5_0fg_ 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 I68!G{Um3Rlk^lez literal 0 HcmV?d00001 diff --git a/docs/images/static.png b/docs/ICD/images/static.PNG similarity index 100% rename from docs/images/static.png rename to docs/ICD/images/static.PNG diff --git a/docs/ICD/images/workflow.png b/docs/ICD/images/workflow.png new file mode 100644 index 0000000000000000000000000000000000000000..93cb69cfaa2053df6492569d0b176e64ef9fd6b5 GIT binary patch literal 17608 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

    *;9kV+ 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 kCDU +:project: EOEPCA +:project-name: EO Exploitation Platform Common Architecture +:component-name: Login Service +:component-github-name: um-login-service +:doc-title: {component-name} Interface Control Document +:doc-num: {project}.ICD.xxx +:revnumber: 1.0 +:revdate: 30/11/2020 +:revremark: +:category: Interface Control Document +:copyrightYear: 2020 +// attributes +:hardbreaks: +:sectnums: +:sectnumlevels: 5 +:toc: left +:toclevels: 5 +:toc-title: {doc-title} +//:toc-title: +:description: {doc-title} for the Common Architecture +:keywords: common architecture design +:imagesdir: ./images +:linkcss: +:stylesdir: stylesheets +:stylesheet: eoepca.css +:icons: font +:source-highlighter: coderay +// pdf +:pdf-stylesdir: resources/themes +:pdf-style: eoepca +:media: screen +:title-logo-image: image::logo.png[top=5%, align=right, pdfwidth=6.5in] + += {doc-title}: {doc-num} + +:leveloffset: +1 + +include::preface.adoc[] + +<<< + +include::01.introduction/00.introduction.adoc[] + +<<< + +include::02.overview/00.overview.adoc[] + +<<< + +include::03.interfaces/00.interfaces.adoc[] + +''' + +include::end-of-document.adoc[] + +:leveloffset: -1 diff --git a/docs/preface.adoc b/docs/ICD/preface.adoc similarity index 100% rename from docs/preface.adoc rename to docs/ICD/preface.adoc diff --git a/docs/resources/themes/eoepca-theme.yml b/docs/ICD/resources/themes/eoepca-theme.yml similarity index 100% rename from docs/resources/themes/eoepca-theme.yml rename to docs/ICD/resources/themes/eoepca-theme.yml diff --git a/docs/resources/themes/origdefault-theme.yml b/docs/ICD/resources/themes/origdefault-theme.yml similarity index 100% rename from docs/resources/themes/origdefault-theme.yml rename to docs/ICD/resources/themes/origdefault-theme.yml diff --git a/docs/stylesheets/asciidoctor.css b/docs/ICD/stylesheets/asciidoctor.css similarity index 100% rename from docs/stylesheets/asciidoctor.css rename to docs/ICD/stylesheets/asciidoctor.css diff --git a/docs/stylesheets/eoepca.css b/docs/ICD/stylesheets/eoepca.css similarity index 100% rename from docs/stylesheets/eoepca.css rename to docs/ICD/stylesheets/eoepca.css diff --git a/docs/01.introduction/00.introduction.adoc b/docs/SDD/01.introduction/00.introduction.adoc similarity index 100% rename from docs/01.introduction/00.introduction.adoc rename to docs/SDD/01.introduction/00.introduction.adoc diff --git a/docs/SDD/01.introduction/03.reference-docs.adoc b/docs/SDD/01.introduction/03.reference-docs.adoc new file mode 100644 index 0000000..eae80fb --- /dev/null +++ b/docs/SDD/01.introduction/03.reference-docs.adoc @@ -0,0 +1,162 @@ + += 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/SDD/01.introduction/04.terminology.adoc b/docs/SDD/01.introduction/04.terminology.adoc new file mode 100644 index 0000000..7d2d906 --- /dev/null +++ b/docs/SDD/01.introduction/04.terminology.adoc @@ -0,0 +1,187 @@ + += 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/SDD/01.introduction/05.glossary.adoc b/docs/SDD/01.introduction/05.glossary.adoc new file mode 100644 index 0000000..18031f4 --- /dev/null +++ b/docs/SDD/01.introduction/05.glossary.adoc @@ -0,0 +1,49 @@ + += 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/SDD/02.overview/00.overview.adoc similarity index 100% rename from docs/02.overview/00.overview.adoc rename to docs/SDD/02.overview/00.overview.adoc diff --git a/docs/03.design/00.design.adoc b/docs/SDD/03.design/00.design.adoc similarity index 100% rename from docs/03.design/00.design.adoc rename to docs/SDD/03.design/00.design.adoc diff --git a/docs/README.adoc b/docs/SDD/README.adoc similarity index 100% rename from docs/README.adoc rename to docs/SDD/README.adoc diff --git a/docs/SDD/amendment-history.adoc b/docs/SDD/amendment-history.adoc new file mode 100644 index 0000000..c38f813 --- /dev/null +++ b/docs/SDD/amendment-history.adoc @@ -0,0 +1,16 @@ + +''' + +AMENDMENT HISTORY:: +This document shall be amended by releasing a new edition of the document in its entirety. + +The Amendment Record Sheet below records the history and issue status of this document. ++ +.Amendment Record Sheet +[cols="^1h,^2,<5"] +|=== +| ISSUE | DATE | REASON + +| 0.1 | dd/mm/yyyy | Initial in-progress draft +|=== + +''' diff --git a/docs/SDD/end-of-document.adoc b/docs/SDD/end-of-document.adoc new file mode 100644 index 0000000..946622b --- /dev/null +++ b/docs/SDD/end-of-document.adoc @@ -0,0 +1,3 @@ + +[.large] +<< End of Document >> diff --git a/docs/gh-page-README.adoc b/docs/SDD/gh-page-README.adoc similarity index 100% rename from docs/gh-page-README.adoc rename to docs/SDD/gh-page-README.adoc diff --git a/docs/gh-page-root.html b/docs/SDD/gh-page-root.html similarity index 100% rename from docs/gh-page-root.html rename to docs/SDD/gh-page-root.html diff --git a/docs/images/MongoFlow.png b/docs/SDD/images/MongoFlow.png similarity index 100% rename from docs/images/MongoFlow.png rename to docs/SDD/images/MongoFlow.png diff --git a/docs/images/PEPFlow2.png b/docs/SDD/images/PEPFlow2.png similarity index 100% rename from docs/images/PEPFlow2.png rename to docs/SDD/images/PEPFlow2.png diff --git a/docs/images/PEPflow.png b/docs/SDD/images/PEPflow.png similarity index 100% rename from docs/images/PEPflow.png rename to docs/SDD/images/PEPflow.png diff --git a/docs/images/init_flow.png b/docs/SDD/images/init_flow.png similarity index 100% rename from docs/images/init_flow.png rename to docs/SDD/images/init_flow.png diff --git a/docs/images/init_flow2.png b/docs/SDD/images/init_flow2.png similarity index 100% rename from docs/images/init_flow2.png rename to docs/SDD/images/init_flow2.png diff --git a/docs/SDD/images/logo.png b/docs/SDD/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..6e9ff5c431c2da2d276bc2d5708cf9303d99ca80 GIT binary patch literal 72755 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

    _4rKTh(j92)oVxZhTyi#?BDWn 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 A1^@s6 literal 0 HcmV?d00001 diff --git a/docs/SDD/images/static.png b/docs/SDD/images/static.png new file mode 100644 index 0000000000000000000000000000000000000000..28068eb96f7d658d4415769ff466378e8c0cde5e GIT binary patch literal 28404 zcmce;cT`hb*DtJsh!BcO3sMZdmmp0#5u_848U&;_5l}!n(gH~DDj-BaKswT-O0P=q zO`7y72!g;};hgiF=e_SY?w_AyFw~Ijz1LcE%{6~zCKRdmfQW#a;L@c_M2ZUcH7{Md zg1B_)vJ*ZYc*W5yKOOva`MKr;*-M4rXjZ|0aIIuiWiDMRi6Z=Cauxjlx}$>r^GlbA zo3THa+Z}SuFI{>Xs(4>U%gtyr^{She)dV_s%k_NPx6-$An)^W0I*cs4s-~vKZg+kQ z{J~ASa~*ky>9Q<-<1}HGG&>o`cG@y?9C4?g3QJr{r)ckU!^4Ed6oI%-l^-ddlI{)W zO?;4MK8V=@5>16Rp+c9qupz<=_E!2C-S-w17uB)9L_kfS#7mtWA#@5KvA0Lk4sJui zTk$Xu{)*1ilfx|iPnK}cUr&CY9?mjW!oeq8Ni0H2*wdo zYKunTj&0w-Efaf-8qPQ(ZVM+0&$uCse12QFFD2+Mf)7-xynzRq1!ITNMQV16d+nNX zS3JARj&o-$b9oKKSUG`%nUdEiT>KSwoOWQGSpjclZwoz%_CEP#Q>hn4Ct-f_YxCK` zn)bBM(T>UQ&B|GZC`)j?4eSN;M9f3L2Tf?O<0i1c{yUTo`})Pqyl{pf-p|4KG9UjCN4v^8B5&*&-!S)VDjk`anCNCZztjF@3$$xfQK#z#o_P-BO+ivGS z^65*Lsg9iZ{c}0%(dTC#Qe(pId*<9UoeVKxn%`kpNBbrM-B%sQ{*dLfaqQgePW#D<1n>Z>^(Tkh%H4_FOC2$c#{wYA7CvLcjJX64nUR;LmU<6v(u8Y2 z$8K0%hKq;y5m&UE6i?Ai%6cF(G~cjB$HDjK4^>*^bftswbnv)24j!b>8yk&Q*ym1Y ziG`JIrPtUU@2_@s#<9Ab^h%v`<>?d*6s^FYZ(I|5u{Fg0J94VtTg-OW|0?0W5cZ?X z|9liX{?$P23dX~*|MtM{E?KB4SlRc%|JTbmjiE~K#6O#VOVMAy?Dykq@_J9v*iACB z%un4n_7GynSTV>kSfS-0>}=+vGSA1ysj-tHlG`|c8AS9G2eVKew4LBQ(U{j>eM zIae1K78vX6>)~c?;W_#y>hHw+*?)}**-zZfee}7MC)@ApqticsoKG49u4Sx2=A29> z(L}eM6-kPpEh&-bd`%X9FntczXDK_)C-$wN-CF!F*Bgp3ns|_$1+|tMB}5``otwzH`h{SAud-8w`;{9I*}K)C+)2>!sdv8K8Ja zE9R<@z@e@6=#I!WVw%1Y#{ByYZo8(dgV|BxG@_b5zn3WOMUQ_ZJM5pOSm1LbUTn`K zOqAQ%IkhRqv8MdL~>`ifmZ**HV)4 z8dW-oycLvrG+i0NT*-9J3-z{$+u76B)^kpB=pE1g0`z0A8>qtKL@0JYps-;F;_G;{ z;Vf&~EQpX&r`V0doVgm}eK`GsgT?xH=No}+tpb$xM5RL!JRWYLw7J%F>byRpN+;=+ zV-{{Zlmm1B`D1v}ESDHk;S&R~pD0t5JYHecuY8(%*Sb%do@DVOn7f#$L~Lj||m|wb)jIl4!d$C-S733bQ_1WG4)5xjxCi(8He$F*JxYb29$o zbRK!qQX;Z7YV13l<|JPK;tyTBH}*^^yqJ#N1k4fM^kY45I%Dh^w2hBlik?L5-fBC+ zMg@5nDH!mri`c|K_`wJJFVfHd4=?j{lIk=O6BDyAbq*GrAl`_(m(EGbDh#kPF){6R zwel6VGeeKJYPJ{iit1x6zrMW@o7wrAKO>$)yUURt#=oc3|XFAXvu;x=3Lo;2f!n6Y1^)i=S!Ho%n3ABZlfcu+VX-i+pvWJTDN9 zrru&@f`QAL+JkTrb5cgX6?S;ahEPu8NdRl#2~L8gc6#4<5O^UC4Gm9!{`?tNU{KjL zoTuB`-u?h2Xim?=tpsp1cY#FcJej4OWIDwPdlv{_bl;k(;{*xePH0b#Mh3)~qy_=? z@bNjCyH@vZ0Ttn4XE$1yke+_$uOqkEL7WBQ_N;#%?0XuU>FV+syw;+LGMfzeP=SH4 z&G5&BnL=+uPR6QcR_g7*n@aBG3yR0WdXhe{AL$e#2?qOPgO9}YoyIUkD`)==q1kl#U-S!2j^`xQ(^TlCR=X(6_QIa;AI z1ALto_LFPzp2%!9+FVfisVxPFU{)A-r1f0QI-+6EK=D8R`FZyKN+{<`9bxk*aS`XKMAc@GTto7CRY< zsP9JCd!Hz|xm88-mOcmNrL|?ipYK*>__X(&71Yses}a=*wv!0k!30j-L{KE>?MwUj zm7^`IK~cWC0ay9-VEv|iky)k~>NPSC-_Sv?id>G`i6)$)T{K_tSV|J+3 zBNZ<=8L;#{mmYFhH0gwlOlfea>U~qJeBtQKzHt_uXxa?svr@vxjVL^$WW6 z-H|xi%-_cjs7~JBqH{Xoo~4aLWN&x)?Ia)9^?Q4O-OV4584Iwp>3=WB=xa(^*#?n8 z_LZS2Zin=P!o_7rdvzeoMFNh@)nuL~hBnO?SdaUC&qtLjh4G@%X!%Z3RB)eH@JZVd zOpn8-Ena#`7u3gDV8@^;J9eq?ovwkf+AV_=%(^R-TJ|z7Sn&u0W)nNYlW2y7$yO4Gt zJ$aSaoG`0xDBZf^MQEFnSg=`wnP2%PyG~U9Kz9TzeI}S*55GoPhZUjcq%<@%%m}K^ z$fbLZ{IUtkWlYdc(w9##tHfcI_qGV9pXaNo&J%(~c0-}>@YUf(B)l44m$Ntc#I5iu z@#CEWv0t%KlD}0ftcMn_4!jt<&#v*IbjW@{{sXrjXM)|`=;{cA?!>yeo6dhcb4T}Y zkKb@qx_3aumFh{QpQBD_dPpui#%a7u(ZTbLA+S2y$F6B_DA zQ*lsU^j7LLlWJ`qYfRPC&_S5o?oZ@deYW;=VOhI8u!)AS_I!yCbL3uC4 z$N>6@H=_XkIWll4EhZA+GP|Vbwtj8sA4;nhN&Qfi2hZSvhosaLs!$~Wy=2dj-LO+FW*Do9k&z0m) zx+~ZUKVh%C78Msa>MQ8QV;TTA4>z>120$y8*I2UiorGt;`PaAFXTN)Wska20N=R!G z7IcK6nO7;v`8@Vla=Hdc8QNXB1_e5boKXRIS2s>0{#5oC1DEmTz&XepXDXfzFRr zh$B1qT(;PtYyG}uDOJaM1|g`jO%@RJU0-4zW@&^Gmo9wCwKB(Na9Ny^u%DATOhTY5 zHpO7%c_Z4pjlT|jV8=By)8+z{4)IgJ!<2S9CvUpVd+OI6ynNceo92VZFLrZSRmkMW zvjo_mZ$mZ$3Ol)z$FIA!3%M7pxU`{b0_L?ebw|=3#59cDlZSoVscAU67zngE5ii!K3k zYBJg?b(GSj*uI66<+iFcPw6~@ME6a^7a^~&_SvVGtxp(Bo{<^4@>^IRj7TS75LWL9 z9tGi#Q;XU2Td>_ohqTglA$ySMkb-o`=irQ~pngg9PUQF9}fk}n%-kB8{a9Ji{2+*R_G`=2f~e7paf{du)~7Z;jUdLOF|YeV++3VEoJn(L)5rm zCKn=yigvOT`VJqYA5aVEAkkAepTzkqNDSxtdCT6#!p;Mhvz+qOGy-0c1kNHG8;?@# zZ%hJMgt4V1e?{wOrRCA4YWxw7>(|svH7MXxuO8^A5-kBs%Jgd4t@ms`5VEV3O0}HL z>gA&)t1~ZXnDOlkog5I8MLM28p3f)07isk;+~WA>qG%oiQ$gJ~zUBr0bQ900%Nbcu zGe+(O#)6Dr7jlq4-<>f|g zP=OWQ(&1>gBlk`-ROo^SI+f(i9fI<)gZuief2aOh+aJsb<*PCiot&TMcq)IO*OYL4XrlM=U%_@@5f09@=Yna0O;lbb@gcIjT~EL zDEwM5WKQtQ{dkaaKvwk0`Q(et<33)uqKrL$4U3O=KiRBgez7%$$`OH4sAWM~Ar}Qp z-$P_KCl?o&T6)jbSNZk7ChYdQIEy;Owrd&5Ikfu9BcCnuET_V8AS^M!-U8I)LCgFWx5{x$!$E#iP9@Ts4Tk*QDzs3|j{}E@h zupAQQkgi##C3pJDTMt6wPtL_H>UV^Kr?*#OZ#K@NBHf!jF}qQx*N^O6Z<(X1iZg_atT~%{!pAYFHM@ z_v{bV$%d?EBF3yeva{Cxr!#kNAmraQpI7T9RqK;Ksnti@j@a?gR+DHUshkcmVW{W$ zVHNMC87X{Pn+kGp0uMU>kh~kLmRS|>E@&m~FAa1VxWb_OfnL}PX_kA>(f{B`w+b|7*_1D zDhv{mdt15rQ9C=MyKjb3H&Z%B6Ye=~IbJRNC}}Gl;w}a1!T)-uF>Umh{h;;CM+pA=p=0;^FG?Of_tvAup8tiI@9x^st@UzjV*du0v@PGk!CN&!+6nf zGEctmbQaJZFMfbru^4`zOss>n?XrnCUxv5de))D^=!7AK#~CYmdhXBBm3;na7#vnu2+rr?`}M70^%+#jqRe5|NG(MqhK+N{)AO%%{Z1CDbbNs?q1QeQ{rU4J zQzJvZ`VLnqpa^ZoOS5=tpB(;m;7A0)H?Zt_$il+jLf~lj>!N0)+<-8WSX|V0syA7gPZ^7(XQDW3D1f$`^k|C>?UUV_NdBH zhWf5`G?QYQWF604+p^xt)jmwX-1;1GJSG$1K`aFR>~ZkXd3(mt%gf8u@CS}^6&9|4 zP*0=#+-ih@#E3lKU(x6^uFA|m6jQkwQv^4Mdr3_B{WAw1)Xt$qw}n*NqUg8xx(-2R zjse{2+7YbAB}oIVkm3hPD9{?*!BxE(*`wbe^glnUl3Zj|=aEuV*VNRMsg)q)uoo&{6>W4I@f@MQ$PR9VMj7sMf7ye{)iIcG49vJ}uA-Vjx5S6uKKc z2)!2&989JCB*kaut;T&%=MvSocNLd9i2vs`!&5C*uw*Z|duKEZvHv+lz3|cJ_v=Zm z4?f0fVyEr`uhEPl^rM5if`kBtK&}FzwY!{8_MnA9wGX_?#+R~U@#m!Tj94n zG>VHk*FG6G_((hp69l1%9R~3X7vlW)c1Gw7a6?U3Azyy%&$I6qT@9?uM8L z_u8)qypqwK&vjSpF=kes-@XX+PBn}p(fFaXU8DK9P-Z^}8*&-x*tfU$u;dO)rTmb9A`sgULFogFNOcbZ9h^R=Q@cgN7)zJGRu#ohPr`~2B`AVD{~ zpto_26c*nN2tRbbQ5`C(o#1FJSbKBV^GluQAsLNB(8Xd1K__l|$Yojn+{);E(yLn) z*z>4%?lT|h^gWXNYu_t|Rh@3S!Fg3v)cd#$tI61o}KQ0?2ps>+76ASk>0> zf2^8%Pun42Z5XTWfWy~Dcnat;7UyTDni$!uSe1cB+zm-U$-@N*DNX1EAlMc&6psH6 z3o_SLEB1#a=6~iLGD*lYeo~fcmmBr+Zj-S%#xCl69YJx zWhuu%wdf*MQTOwkSZ@a@%D=ehnoiM!N-9*G^N`)lnO=?F7M75q>%>I{$j|rt^H_-KXP< z6Rul%hk^j2vFm=W*tqB12)0aPm^pshR(ORJ38;xUbO5#wTl$);Yv9ZG^qN21>&OwX zHDM`$q14P)D_97*BZ4hIfz+iuT4-3#NwgdS7-wrc(&|PUIBp{lkK>fco`#(`CSo=x zEAuW**Lxcn1izx>Eu3UWv`{=wr+F*8`y-S`ZiWr<<;AvBu2E$!+;sktsLuiA^}3z7 zeR_Vm3NK2Q+ka%={|Zo(2wsWzY~RFbc39(rZ{*FE735F*?7TSA#BBQ-lO5+ug_c{eA#bhCSt(SdWL!YeF z>*&3LxHQlq++S1zfU>!s{SpLNE{*6fcMu`8Ai;Oj;_(CAt}DfEDxGPT#tDvkZIF*J zg7{`Hu=*>|v_>glxbQ%oRB}$8mHJOsy=$(xSAy(cyP!#s!cV9AbQsI-z^D*`r$AR9 zG%J>sK!p|)efr9mh(F7VDpM#TI(ivC!tR``4+NvVNB;i)&cD9+p8ce+`_5xr|42$M zo!K8Ae`MYjt17jMdE*Jng?oB?UxjCf)X^fJp>^oqfQ!kpSaJ9JLdJ%a&TbWO0w*{_ zx)@;^zBthb{d$0S_6C%BI{`!}RLw)6A^mT~bGFT#`&q6xC*=-yeQyk4*8Zqf*~?b!i{RizKGytoHNVD0!qrr_^w-*ZktJ@@-nuN0~EJcT!W5s2lss%KM*&t>Qq_OWu?2$R-c zrNfc$K7w{H2F7)mv_Wkln=eq=J-20qp@6))mFcoR!jTO)>{4tYXXQavhYlPg`sHWt@NLcXBcZpeQUM;fvuMPD7)kes z=?6>FN2;k3=(jSLOatDSi{Yp%18nTFR=Yj36Ec~SxE=Xugh!Ih&Y32S-Hv$NWvBM6 z9`LU&x7TinMov=gQjKhiFqtILt=+R8i(361+&dGo>voHv{gQ1h9#vJy<;Wb~SfDeW zN*>!OgwXz_TbbVgM6R`sv`JxKA965z1cBb8DIcM=TwiUKz z?iSjJaS}*W-1;Mz1;|g$avd}Ma%Sm|L$*u?4Pq>(Xw_H(?e(1d2P;yjtJyluiGYC7 zwizlli-(v5ojm$YV0WV(Mf5h0)MZ0b0WQB$G@PEL6up%6~z_2*~>6Jfc+>?8|f75ZJ+Vzch%rU2^>ndjE zgUlz_0pBC@RZ++A3IcL&eHnlCsmNFu*R)2K4uTe!IG0_otgO%|jm6ti5zqtX9Vv#A zXLqwGECV4XS3U8OUrKHd)Y|Jxuoy(vmfn#?-?Y=24l@rS-6Q)a?AG9mQ>s6{=%e}Y z@0kUDd9p(i6Oip25}gdMzS~}W{W+88))kJs557%D#lki?HQ(13%AxrmSVQ;5so4PQ zIk*My7y%frm3L)B0FDctEM-;km8$M3##K;Yh`9a!->Qzwf7$|HDx*9bxm!9B5%jbK zXHM9B7v=kV@sn0*NBcGu+H`3h@X*SiyUO?NcHs$H{vW;>XP8Ag(o`?p5&s6iYxSGr zbTYYYEuVAdkXC5*O?!Owf0cO@ja_hOES0$HwYz!w9PD^`icK=g6Dq7MMp`KO5Gx?<4*fZ2blPzs>Ec(Wk_TR@R~ z5ew4LQptad;ApvYh(56Gz)Jp7ggY3J=zQP;iXQuC+p!@=g0V&4Uqy5&emra)6gmIT z+k-kB6fMe#6?z5n!&&j4RR*!eyn$kCj0p74^IRzEyJ(sokcaE{tYAyki zOtaip8!GNrJk{VkyF+yo=*fR;hE_mbW3Tf#c&6_Nr0=YSkc$UMhtyF)AT8M$SYmE( zx1GKlu!lrZj86Y0z7Aw5U%;^#1EUmB^sY$Tiv6_)ym)ER9NHh5it3L))`A1`UOK72 zxieSj?F-}tc!FaAtStt3yt)DDZ@S6S?DHV$EiD4fT|9~ zdN+mtePTWVh)duHwNz?5TG$Y^?N~7;MZ}5WS=hzw$77ZQJg9-wCkE^1GYB&BJ$3mF zY%0vaS-3m~BLPY&(8z!aXBBXrH#&v!LFE0Xp-?jO6e0V!BMir@*=Y=w6PzIZfj8nA znt=Kywpa2*WhonYrr^L=m#bU!PUkVeP<%Dr^Vk1TdqHRf18kr9xY57V_uS_cYG_Kl z^AkwznZR9!5@N2{x|{dc36>cfMp)B2O`_BQ-UioC`pj$N6q@^5UccmWcHspbK_M`8CVyR zY3UgZ_9K@)oK5!3lp~~2Ffa8H36u<&gR=6yfKLEPLG<3fGYtz{MD}@6_3rUZU z^S(Wbo}QEK-U+B3skQ`6!3U2eE1$6;K(1M&j)(PLkP*@R8!TZG;0F*6Ajy2j!>WXF z5POj5zdx)bI&2NPHTtdtz~)kcm2e*i=WqZifL-BlsREnE1lbTuqa z`vf?gLXRu1OXH)Vu%8gy;7~tdCKG z^1g8s6MF};vYs&uSppv@JE(m2eT--tufMSZtE2*)rnnPopwUiW^9L!fYDC&7J6?8@ zqg61iwnu40RVSTe5;v=r`#KF`f8?onZ0uZr zzLcS^*-_uesZmK-b0m#&Sz*Ttsm$xHo$YBKVxeIp-SO^{v4RhbKz->nRAM<74Xec) zEiz654hyBv&z2Uk^m~}){%9YMo<91H3}6Ei0sQU&MvU@O$;81B;BW<$H8z)zf|coA z>Q2h?IX&J3drOUwe|VCFirsn{`Ix z<(}+z8u5dGOvNM(p$8oUWmng=B6S>y+~m>EmU%fESsl6oYZ80U9#53?tjwiT1#W(Z zDm1G5NZE?D=FFb`685$)u5Bu}8-*An#2z2;Be1w(l$ua2iA_T`k=K|9YiNuk(?Fa4 zwc`peLpk(IGXeP?Saq4_uHJ7c5W#zEc|~2UXIp%||hW z9KsO>F0n7ogqenuY%IQ3JSI;ctM07TKo3}TeJ~4nw*0N20GC*%RW`+~AlwKc7A<6t z5_MjQwrC{SciWyjXj?x6Cb89zKdpM-xwuF0U&uxmyJ$@Hhos(|l-0KsEWKfG*!(jD zYB_V)GRJ4IV$Y@Uk4Abo;pF6t+c{FBHbbZ@O@-@1;g_2#K9*A}cJ)3~?%UnY*jmOa z=H?qJGcTRv;_*`)t?pvUm0M6Ox!8H@U#= z8&S-Cx!P>GkMEY#6%kb{6Vr)PnSn8sLgr3`M=7^gj{^UbTOtBuVHt6^D&_`{fsu!? zvW1(i5Y0oXV)BAnq;XC!Lp_X2`SE_0;nl%PzyA76HiXU>1vHKLc6lidc-oX(dd~bN z*dhaLLQo`Njb9^+Ae__}6RWPWN^sSVVGzL`+`Qkwk)H+p%qTK`58&ekB!D?FZ!ApA z3~ZOb_0x;M3xy#>`uDeIjRBTtX4lM&23LFq%y`b56WaG)Y~@d@zV1g)PJe#>0FRg^ zXZkiUg6X`mnJCNcc`K-#g(ncihXmgKj6Gn&U6FjL&y2r?a~O2UYUwfei!DUNy6;Y% z`hyK7#JVe8?3&Bl`@5>ylBJA0WpgFpXe2##@gm;x)SpD&-XaMdo2{R`v@7NI&C%EN zf}|${^!`8TMxlcr%lm5P>+yqU45`3N6>*Zq@+RGSD#=2s1Y~S?CMz9A4{udu5JFl9 z;FdjX%e`qyz>gMIR8-Uzj&Q4fv0WHM%x{+`6MzSIn!nFonkeP#Lr$YoAm3Pr_0$1< zPz#L=aoC(F*UUk}xVzA43u2Zla&E~G)zk+0R|y~4lt(vp_}75q?z|#|l>Igxx1j6O zU-Z3Yxc%AQ#4TE>K%1kp#^ucWZ#b|5Pr)Nv*WKkDvu0kY*e?sh<+cZJ?Pbsn!X+1~ z#w&>}?OL|u;5uxlHpEyICz9@wB z0Q$vzvci7iX5fjVb6ikxurgrDUFfAn&j4Sdkgb+#-WCp>fCoYEi%@VY9ZBQ}2E1hg z{YFsqaMWmDrWiJ{%m{tqq$VE>Xp1;_!) zPt9?g(ZbZ8WMTDdszr}JYo1G%u>pcwo&MSxE8pu8wQ2KcOqsF8NY|i!!zSnO`rU;7 zhm@Tob#t*2FD~y+zmNs!G#2vaQ}?`}4+r9wRY6E}R>-J26p|L#N z`9}}5Gl4TnEoK_jSXi={kXrUJ9}j+ex-x{&;hs7EByxq1i%Ugn9HYDe7)xxaXy|{5 zRaD_7D#@|oDAnyzKtM1+fdPw` zbAPVhu?CRY>)S2Y3jysjVgG~5En@bDglimZ`6>L``YG}H!W6x$YWro2?l;Dk@*S}; z5F%h5|B)^S(c181J$1jZH>#cu0q8v(L*_-S_UD& zAOdFK5~!0td;Ko;(Q7K7VgshPVQ7vk1;c-pipdZYow0ervzxA~CgI*xd=id}T37Aj zX+>Wc*9`oU=NYIr9h97=1e9-ha*0-hagBr?pr$hkKdZg18{nj{xq9K`>c#UPEVWQJ z{J7*UBP>?9`+WJ^@SAMczYAhU45%GH)X{=7;d-qqdIJPJR^n?6zWeAl-#Xd!Rxe?7 zH!+P!y2U*{ihdv|V$rr8Q#@q+FBJ`IuO-60=JJ&B59S$I+{)dDM=N{CXcK+km+i z5KI8b{C90%(@olCpUxpcz+pOn|51R=A+`a_pgS zaeR_8GJ0gbau(iV|6lK`2Bz3oec%L{vL$~;{7#uHNbVFS*5V2eT&s=b_@d+gKMH-< z1PrVCyA_YX!4Il~g+TV}zJ#fPtkZdTa+e>q!Qp{|sYr{TL%9!uv;2mk=u~g7dN#rP zDG{ERdKxqVO(4oMq#QukfS0$oOD@iz2sYiwR>2Z~5sH##DgXZMJ$aB*v5+Sk!#kKc zIv>Qed&D)I^tT>3cRT__qmDFj${wZW;}EI`=1WY9fT1RCxfVsZE@~3wsB*Ps#r=p`uL0`KL7GTVstDfo5TChp0Y2VcBY(Pk zrf7?Gy1t^|<^b0C;q8dXUCZL*s(xVP-nhWb*WvZoxYer8^u0Hbv9TBsV5_q6AXCRZ zTk2Z9l0bSXa z=*-~Mc3|&s&$GbibkFD}^iBsEH%T)~$%Yg8_;Gq7Mez48C=D9{*oX5u9~kmTf|ON( zV&p}xVNHcrzMcje0%fRqv7N}NTliX%IP_w1=PxbXIonHWV9Ob@bA~8-VELc<1bCp_ zLf?P@@cIHpAi^i;lDjSPHS4@}R?|;^o@(tp9h>gh{@FMxkaht9e@RkhsWa|@k^2(4 z`)XEFrfd*_(ddNoQ~*|hEsBFd0_gyef$ibq-Jv4gERajvsp4}ZGke81gP*a5kd;+| zx~XDgp7b&hHIf=mHyvWR^x;bdl{2R6SX9Zs)naFkf5opvgo@;@We?S6bCg4BU<;&; z*5oeMzTkpnqvQcKpxg&9f=wbEamFpGb+(s}2DF(!t5^yb*(Mz!}FLuUxXr=aksS$OOzX_s|r;bfBZ$ z*g~Na!3z*fxW372ja9Gd>2kXyGF~I`J=T*JPIG*VW-*tw%)zmR>YNl|ynuR*HkyFE zh_ji%mlq4<88Dvpn*Y^!#u2oje`TWW@HM_4i2yJvL=0Q70mD7Sh(flE$=P)1i?$}LxR$J5KaXHQfA0cj^8sk zi=r_g=54-0^6q}cblyz6r_qehs+Z9)KKR%2hLo3NKrrDDRG4Eb8ehf{ctl{JtQ?`E zeA0Zt#sd#rcN+ev?Ggbm`=C=~WC+?o@(x4Cr^6_?2Ta4Ow!ZIro&8?EH+c-C*qFok zvUze~oRfXcZJ8y4szF|#Tygky_X45bu463d*whMEEuw2e373;z|9 z8JcQcIO(Dzb<~hO&L>r}tbpa;6{UpdxhW$f#U#yLDw8Njr!N zPa~)WV?j1ud#0W7ASw~q!u858Z=<%mxh)`*MB8nqKiOa2dP~4At!*J#*&|x$)zAU(_42 zfL4=i^|V6ekaQAq@A7!qHjqS9#%tX_5mO0bA0I2ifLmZ}h`Bjd9PXb>9P`p@E-G25 zehRR3RUmx}D{wz*q6kqYYp}(yyJKvKnQImyA^}~4p$Bx# zp*UEs-G~*@A25OeZ8_FXtV5eJPV(W756Zs^+KsH3J=cnF%{g3IFKiJcg=%fKta$Y} z65`|c-S|z_d-vI*!s={8gA%d)6hiXP>1p|uY>mHe@K@hf=xBE|BWWsV+Q~~vSG!`F zW}-w-pe7TG#3h)vyJY5iM|CVphdE>r+l6ph`)(NGNwF){7S8)19oq27b}IUzhthUr z^|EfrS6)Xub8yiAoBB3{S%~l1OhUB+nA3}hfM2oR{Lhp!L7}hc^1p;lz8|^Vw()p8 z45Som6LuB52X@5O6L(~yVI7c6tDWzS_Y589TSDQWlhlxeC>C(@_sCO$!(kH-5pgx# zM3(Tz2k859wnD?_7q>BY=zA6R9M87o`*QBX>@Hw~*!T4M(kMx@;B`B;44d7VIsxg8 zZ_#m*4jI3VPcnjP3TE%d!YXsvz)?Q?~L zfjqmwU8MwELd>AawR-AhzrDhM--Y`Jc?I;_cmtuNx=y2d!S`(0chSN3B)N9C?e<>7 z`ANg^S)*jeWOU}mQi3ao$3RTPui)9w-&z5Rf7&=D{Y6p2aA~`aMDj@5$YYfm+m&8e zwb^;d$PY{gSPRhg*DLVAT>`!Ks}@(VsGB@5H7)q)qKYGRdU@DwS5(JRRJ-@;8;9UM zG?T<%iVpK*8hGG`7x}-!mia)I+v|ToZ&M-TB$c1(k>GfM?fuAE-9h?(yW^l~1N@2%z#Pc_G`|SA0jF02kE< z!7e1wYDVPSnJjpWYORkv^k@O`l8<`%jEIdOe2=%qyD!ea@JYV(joJAwc?^X4@f`$c zRgVKaI+yK}D=(=Mc#UTk+^WkvUm?*q>B$sTWA+Hyb*kTwYGj(bvr}V<50#jkOH3DZ zTJZ2L1ynZm-cV_P<7l3)I)`o{7wEuccnaLba=E|^bk||#k<@;(;+?nefPeHd4iwek zQ|C{~5591KLk_g8k8+I2_g+=hV?%_JnCG~(8(}-^jM{heqL%LC%Adr-AMlC;7M(0# z9ZEyPWmxST1GpyZ8;{BG)Cj^NJ?)S?R7Kn<$NtHI_i}^<L2X=@brag226$ z#=!YIa<}pB%=KF=RFafTCSbslz=x(21UEPOmMX^TvNqHoRBNbT5gKYxOpQB2RG|Oq z13^r-DLZrmczuPkF%CEUUOtv{5AWq?B!)iwlN!wc`5*}lQbK8_<0Q+=eoW%=n3iDR zTmZP>I*J3^Q_Da$x|IC7>Z;j|+rm%t1S+J^x_D3I^yTp$!LTZLX; zPAFC@`<{MZDl$y{eRs6LQOlRo^H9*jvx|LDAPAI#WN%O2^fpnY32!~lbl2HgPBwi0 zwm}Ed+Bzq465Aii1>6&Me<0L%6{4_7=ItNg6?93bAYd>(@bHQg`CGm z3J_{=m%Z}6+nLn*YW&w-u#L_Xhi2^1y@P|{8z=JcK=O@U;m6Wc}k1CC?)yd!ybR+-aMY(V)iWMU8ZbcO;c?Ab4thVDeaQESrmxQe~e$ ziDC%txJK_|%O?903`E?8+w2hu3eAS-_kUUdXm(o+V#+FpL9Q(8nr46wqn1zGFHrO5 zvcYn$#jvPS1J_MlB)e>u@*_TW#0a+g^$tSxCmFn!EiRA|@nZu$32Mm{u#H~{MH4pml+uVkTj?nVl=f=ZF1Br7-&D^w z!*Q+& zD&Oh@7NP?Yb==l;(0MC+asL0?68h^^mU#}4+@66J!;aY>pYJEZaOEgUy131A02PmV z=>%+Z*liq@?=X+=!^%_3wr{;JeCxRa_}T%}hMDkwvtLaAP8}CBLeE90KI}bps{*?C z(mK%Nw9z{s8W{?XCi{ZWP6+G%-TQZRQt!FMf(niL>}v}R@SE%!Ht_J9?pI0ttquYK z%#jFmCt8zHUGYEs@VuC7_!T*)>60i2fMf0elhd!G)9Rbboqd4+FSEc4cq{P+8~|Iz zKveAl?Fa8xS(>TdgWs)5^#Y}8f9wc1tql~+JfPx^25_(C$TQTPDG?%*9}I!s{QYn$ zASiIM%;whbhZ}xFX55k)R1#0sWr6DX!UCU?hchkdCJOE7<7?Ctj{#?=60rTb3|}n< zhpwS;pJ-%j_bzNYuCZJYV1b(y6u{xT3zp>=D8Mc^w97KZw*$A-?jS&c9tY^_brDzx zF%P=xr3@N;qr-=DGVFJ7fV0!~g}zri&s>CmD|^XfuFUS0ZRQ^_(ZJUwm+y29Ydr-Z zCl2sf>*trxikzChFf<*}H=aX%+E@j9Oj}(4M7;x+y2AxuI7kT|d$K#^Cr(oi4W}O( z2rFHloB-v<*-*kODlGMRI9(;^=Y4~CWR8D+>!_in)fSW!r6BOEj1ZhLeeggGIarvH zLynpAZ*|u#yZb;sX7u%--bd~OZgF&(2KctM3Loi%b`4)eAPOu29uw~PT^dA57#TN8 zOYNOFJCH_(mq|udAMwJ}fE#Pr44A#sw_TJ*rq1AM z4pzknPWW6T#^3ALxE0)PgiA4rC$)gV>^?wd1eN2ml&pzmB2cEJx!j`LP)qT}a zK!l%@P$l8PnzA3Hi1gnJ-9j-!{lzf}H(7b`1)qk+5mJ!0Weej|s97I*AJJCjm^8@% zMyQ;o1^5&WUVKO3`m3LUg%~BFtSSOu=hE&r1->`HoVcVDxi$6qU_NN>5^1`wk-OZC z+X7J?lDaNZ2Zpi0r}x^Wng8r{FsysvP6xWEQ|`Ga87TJMfb1KywTWv7={No=iiPGe&>C9?t~=l;HbrT{ z=JzG^cA6ug@bg#*JzM3P`USspH)i963(CI0qvQPWqq)#zoX;|&A>wsZwgvuqk;pif z<}k3ivwIX@1&|czK-}9Chc*RYs%>y&;09U&rpH} z8x*3Ozm2F85hrrSs-epXD%pvfui}hl&!sfcJuo4_bupYBI`;bjhJ^{MhXN^jU8wBN z_wwd*Bfl$GnT+eb?zQ2bCs8^0pUXM_oG-5fT2tb6KXd68+RE-UsRsYUTTd%Ngy&3R zJgF)-r#^o=n|+^{yx@8?+p@Vs*LZRb$Ah-AS&wCkm{Mel*i6((tW4#5Q}`q%nV_=S zlcoERzZTfNcIX6PM(>ZZQ@aCNSfgam8rg|(*;7K+BD?G*?&D0?=W|{6{SSN} z-^Zh0GW&bZd7sDeI$p2mS?4?6h&IeTg`+^35bwm8)ro4aRV1#@4_@7SB?JqHI`5<3x$0;q8iYGk`E!qaX$cU8dJ;qoPFJWlY<7%nzoUag?a_Grv z=Wy32OU|tKnKMXe1Rcrq9VCwBwc7aNLO*^ivRJPS9Xbz|_R&#^(bOU8Nk$juPKyc_ zJQZ7iA@v8#+p9e8_9t=U}!72G)e=>v_dK>u50)~&(VKxMN~-VW8_!mGwTzVYI-aDn5_bywmYBrs{& zLoQV_E`Dz)6qQtBeLFfVsq!3>yEjYl^Rpg>Jo_`>4@C5L zW;aBKjK2(-;8MJpD+<61!^Aq*v4~@QW)kCs+VrBlVnv=BNEf5m%|~4HR;73QYzaLD zIAD$I-N?Ey%6(p2ge0uxhmRKt%wyrh(5DtCPtl{3h*sk#uJs? z!t|_Sd*eZDH}f!eq?C#|9__MUWGV^IQVNSOEw&Ed2uhzjXd&d>qxgZjnkE%kkkZC&)O6|0&s-%Kl@XJ(xreQ6pefs}`R-B)CwymY{x1UjrIm ztBkwBcOG)Yfqm$lktZeN=^R0gcCkA=%^H_uU&Yy}gU>WmeXf&FJ7Say?H?=Ym*4lI z^!(H5ldJlUG3i|VU^;#oY1M@Hnt+tQQ}wS1z3lfolnng*PP?NQBM`Fu#l<)%ql@@nSPq&wm1(SmAS*A zg`ba&o^^Z72a(9=b-$oe6&{O$C$E0houUY%4U-{x@a{W#noDc&dLUDwPD#OzNRcMmm%Ds~$Rt;Z{HdvIh)7`c@BKh=Hv9Zq;dS0~XawsL%P`&2V zJc+k^AAiVoYRA`h)ZyNly{w$vk0lo_`X#Fx1(eNG^bO=02X)R_43+KS_40cZ4mLQ+ zta`ltZS2GN1Y)N^)AKomZkVSClRxQl$YN=EryAnQlv&3tb||b<5$*(kfT~tPv)vA- z@ehGsyD@`>f$t+e*W|XAFEJ1;cixRrV-^>L?$Y6Mp#j<<@M@K;gi`Q;@;jZ`5_Ac_ z!19*XHlKz5RX|p>te<~UeR0WWeyy2+?a$cby;?Py>R+TV^Zok@b-#YvT2f8?ru>eC z24Pvcn{p}&bGs1&FSEz)fD6p_dF z%hI{nhqcb?6mnjgN++LLefj$9mHfORnAM-?ED!`QD+tTYs9;NMue><(Gb?lrja5|Y z8vXDyI1dzDhHS{Q19^j!LefKgLatKo3c-1&B+w4=#ecGzz9YW^+6KyMrfY0lVSy|5 zXWbc}H$fsBH1%OuVpr<64CWg*E5tYwb?^hGVJ(5YijNLhhkF_Qe(M}fvdcB~6bNrH zVQ)KSFB%zm^&9nL)MrF+qqi3`CVkoE4ykF!w4FV1uJP+art)ChE4QT;UUUQd7a?DQ zdi#c~9=Qo84)O!-PppjeJRhkIA2W8$Z>1&G$gs-;uE&px2XXn&BCS@A@9_fQuPWan zmb>Fxe!$DV8*SV^k=MMw`XTg@-!t3Q6&bWr4jmFQ|Vnd|VSp&ZV>oLnAbj2O?<{jM+ORMw@ndO%vqe3(1JlfMl<=cwd zX{^8#)CR@`-QxB#KacZDU3;8|RL{qU6zj`WkM!ic@}47d`0MJM0n-T8V3cDd(E!s@ zK$4A3PZG9EjZDpBNnB&8YsIH0uI%)L9J9RiJ-xq-Yc5-#9?Fe0gOt@LzQED@N!Ak9 zpD9gQ^s2o!b+a$mdWHQAKK*-geV4SWWz~{gv6?;hMZNvJ$7e6Kg2+Uxa2lx>Z1`T$ z7rEhs{fUxuyzApIhkp$MXjDbO{oWTcp@v3G(o@%N@MA1njfEGgZ|14ePjO!CI@IgG zq8jx8l~fylv-CtxHxtX8kWZvZG=g9WQ< zn|0r0RGL!Fq-XGv__5`(|N9_C@dOJ=KE1!*(qBFdO;qa87&B4f4|$ zsOvvH1WD_zGHZR)SmMH>zOuaD7|EL)Xb{8kBv%b2~OLQ}bTOu&F0^nz$3nIk*-pntW)>YOa+ zG+DmpG1INc!u9W6OAu#GO2Wzy=?;WK0AZOn_%6OQ1X4*Ao@mxJ0ytc8Az=Q}xegT4 zjNLeI+_I9GA7M8IW_DUPANyOeWOcsMjzLM+cv+C~&DEMaUXU#wPPGcgPLaa8{Jpj6 z5cuAKt(6PF0=+MEPo4s)0GTMm^oiTlZ#&cdi}4#z-5jY&Z-Z0?A_T}_!ieCQA$i%O z6b&rVXc))N{bD$EQJIWi-g8-D_CvvG5EFZb#&=hCpQE@GA>8B%hO7OJK{hT8vML%N zL~udeqIa6uEwa*mfv@lRg%1sZQ5+?#$aDdr&w)#d58+q7pQghikyi)Z9~(nXRy$iO z-ZC2&*Ypew5)p6UU66VxAa2!EyuX`5sg#Yf>PXbBD(}vV1CTKVCFO&MX+T+RN3=WN zH|(IqNhOnG!Qmn1%L>@p@l_yZqd}c?FG#D(Gp+UWe%z5Hir76*;naaiib;kFY(FVd z)J~g~{sA<$s%ixoyh9pU2y%aJ4lQA?>*^E$N+D~-oBgjh(`rCqS1_14Isvs8gW%!8 z8egAGkVRauR-kso&knx75E_6(XGHL9f0|Itb8}mlFZ^2NJE^Cz>cEy}m#D`~f7`bup%> z@Sn{qTTd%!L}9=HScU(5pm_ltVzkrx$7@1U0)d?0W!RsyD_CY>gn?87=Z0N6q(nYV z=V6aRz5{`nVJ$m_zKL4Ju#jobZ6FQfl9G^mR<5L*Y4X8l%gr<@=qfJs)@a^MDz%J3 zv5Mj}V)Fz}{ED1$7_g^_-%-(M+xn)tzR_9RA^>G(mrvfjf>L{TX#9}yC4!F1aZN{w zx|d+_+ZgwpF6acCh&a9=qOI>Up#2^r2|8){Uu!Wi08yqYQK{|r&Oim4`f(!O&K1#w zZ(T^p^ysPD*nsDb?QL2=xsarzmq+5FQKet`RHv!lMv!p~6k6?x*`d$uM(Mt%>~Sa1 z9r@a}!e2X6=E32|inY$V(oSo_gYs@Zei_0ff%tfg0Pg-oKo7}5kxK-Pc!BIoBdcH9 zdB7nO17E7jVhDPN8ca=nN@2Ve!S}m62TN>Y&e(s{(N2-eV|cz&vk6$R{gqTSj)rNJ z2JE*b2L&G#KyW()#NNB6P)>6$RBa?ws|n4OB_C>eu=Ls4c0-HNwlBM!x%P*|?AG|? zv`sr;Aruq6QA~nFE%|c}wViRJU@V(1g$b-Cc%Up{B7=DDL5@gFH+k)91uqq$ZS3KA z7B*+$KC1fpeEY#ywmCFeZIb~gLvLXt6xB;}QYKb;>jX()m8vIl5o}k%MWTaNTAxJ& zx@T_yc34XwHDiHFgM^auH3|ABI}95Uv}!A4n{NMA-1O&As%QaU{2KAkcRVTQV86A3 z{J7!EQKPDNVvCeLM?D2 z5rAPySuFn*ZBXv{C0!ZUj;kL4At~Nht>EPjG2S@gy%EjKR%-ZeT5r08IAl;A0d90v zVJrN`@A>#epeiR-ndii28{mwR-l!9V4^z_Pslegz%gI-3`4a-N`4HZ)eOSUZmG&4VDD{G8>H#WkoqUL> z337XGy%jE#OxO~$M$&}`4xit=mLsF(xm`Z)RS!8pPU1&*^d36zH{!0C^n@M| zs#&VttD)50xVG)~B%()U2nC3#81qd22c~0a$o3fzx=zKE4h0~s-o+Y?1#+Uw{GaYD zm8q{Liv^d_=pR|POutFnr^oV{YE`|H+F+WxjEw~d=Px}tHM;uBG8a%7F6qcJO;;I} zJF3HJx^xbeGE+ote9}{Jfz{jN=q^KEKB@X`Gc73zhIPftF_j_*gp=>0MOh`2!!R#Iae`L0~cLg5*b z%6rS3W+!&JiOX?ctb>Da1jMwm4Q3blyw5wG(n~Wg)=jUi$gA0+INoSO7al)YH+mKl z77Qm!ew4vfk$^IjPM(B#$+tOan9G0b&yLyECO-+amATilEo;g#i0`^>x5)3NO$Nma z8fB&AR~J3D|33U42$UHnqNfaBy(#%sR=^An?!!-OPLBKxe@Jh|H&;V%mD37@6!Y6L ziiCKlVScu|k1mp3^?3|G_?^-kZl+qIkh*4c`h#0%Us9c@Cb+YUSjQlkB4ps>5E6jD z9(}3gJlEQipDu9PE&RI;Nxpp>0PzDd3r2O_zpOZMP`gQu9ba-H{|X{Ab=A9c_)*8Q zPenO~oq7h1oB(wtuk%EsL{2~?PLsAI~m9w*GD1(qy$;WJh07ANnKL`da%3U*uRm)Pj~ zhJ%fqcJgcq6@?(DjqQ)E$;eWY!yFH$KZ_K1bco=XT8LfhEflDP42zcyM9Rn-aotZJ z=$AqfXQycdbFR|N!#|zFgPKwq4&P;bOPsDkXVKNY5PU2P%;7oq9aHGuTH{7 z?&&AuPC8=K1I=X;U08O5J@$ZCgMU84GAd&3iou{#o1U9^1c(b*OII8}!zfL9Sv9g} zvh0uzxrpQnljsHGRnDSm}Rs9Jup<)@~ zBZnrhhrxJ#0aCEcaE5k&+uu1}GZA`vt)`Hz@9m28vib=T?_u{Ad-QiW6_nlaIIpPNJQh<<9qB{IM1~(S zyVgzd{M2UM>6u*Z_a2|WQvYr~WuHR&3M*1T1b?YJao8e2r~IH&36g)_yA_Y=h*z+S zh(>My_;GXO=o511J5XD%T?)q$oFBtv`KD|XTyJEygN(V{Wm5H&ey)LD`%!HUU>@Jr zEup0bJlX{m#63V`Nx7S(UA7>fJ<0~LQAF*9#D)N<=6&CmGq_K8`wD(?i@E9csI@g@ zm1_-3yS&kJ4(o%g&KE!3p63DzNn7i?3!7=IbP)@m@5X|>emNH9wkWAwp5gEIv6<{u z{b}Xnr8B9TcgFmfBTtE>{s!6c;Sv`k#}2V(4O&!I7o*GsxH|(5OzW$4b3{5wFiN-6 zb`~V2M=q&BV!j2*%5@c46R^qxK{(a!i-+j^kHSev)FPP&=b4c~l>-c@AJ)y461>R- zM=`zYD&xZEsocP!8mW5^bx9+@-+xVRg`rPVgzG3ti?HDP9xVc8jszyv3;pLX)7bVp zezknHQ2S|1|JECl(02dJ(wuIeP=<^)o8wfyuRrM64!?})FdolY56tRZjRsETOc&s7 zp|E+RUTYRpvZwgPsWS(r<$*&JwoF7ntzVLyu}&_y8pT}V4@Ny8 zbjQ7LS!^OxJxA8r&dYNzJwc*n;@T&0qP33R1Y2X1tAr)5_cB4botn5ACW&%MsTx9t z5y0+o8UwrQ%yWj|9w5=Lg@=ort6+IX&R``C7xe)Ql(&!GwPy7LXU{m#gUfi`vW=)e z-=YllYtD3!C+XGKSU@_f-*4cu0z8Mq%6%|8b1ye-UQ9iAN-#BeQrDS|gsDhk?jGHx zXWAM1*wac60F&Z4jZu#g$|%E*Z-rfTZ3Q9s+3oF{pDv!cF?h`OV|qZp(||spVmZ91 zj>e2#a$$RZbYG=UZlkUIDdI@ac3fnEg>42bmt5I}m9mEK=}O`)==AS2lbt92+1}c8$7_1UB65FSmzSfHq{OFHQ4cvA#V&db3n8P_k(DfnW_4om znG)X__%L>5B;RU22ehcqMaWv!zg+mFp$8Ew_x_iBHW2131bQ8{R}PIVMqz9iE+ksZrIp1+zPwJ*_mLFVGn(8m#ha5ByE~p6SoPjXTf5!=vf6Vj~D0rIk6Zy9M6n- zF2l=#==zN3?Qf0MkeG6&*O22UWDe57tNHUXw{fal-(tr1t1tTv-yNqaKg@hqtE%w_ z^;6E1Jbxv;yQQy7c!O%|F2>?%f|=M}gBgbxT?rZ7DFnmS-ZK&y?-rJ)%wSuxmAcL% z_{D(gFbwGxk8Fl4Vslx71~8V8E;20r)G=s{EIl*7iVsZgh8AI4349R8us3$A%YNwA z)sghqIVGwD)9$7j_ZALfV@QY=5ylFe9lTG=) z>b$OP+%?~wB;B>M+XQYu04tWfCvCSGTN&GSE}6Z~ z+ymChb*}~~3to|f-6MgkLtt3RgwCgulg#jraKJHW4c_R2P8%Vr)E=F2hvJwmK{*K# zN}KqFjR^C=#)mB#TzMRLPYA04i4bfIh{+F6TL3TjM>$3ecwzh?6XnZx9L|DbL!cE`yUZ@2bx~u?V@t-o6k}O+4)`Fgg_fhDeOD99g$p5;%LC>w@ZNqJM zZnFTAvZ7;LH`%??myvSrZygn+GNo!3P=3t2^Mwd)6^sH+;m6f@QQL^#=IAE>Z}p2D z$O8}ZLxR8D`%OcMThUuS-l(p_POo{^3$AhWUrDVP&S+0`iqLsDBqjwm&Q|col^6xN zoc7cfG57&j%c*4LiYKc7ejaqZ2pNvueJ$8>X2c^#GfTiUMtSPjro8YS+4+b;@a*%+ zS=7%!YhauC719T`EF2T82Di(=IaZm)Y1!Aug+f+TNgx2)wwMxRwg11Da}%t1A=}Yg z=3oSRbd#NW64q)eF4XK=q$U;`K3P!XXh5Bwc#jyL_OB|#iG$1Gz(0LBM35_)F&Tj> zj{G4k55#rk4{rwm2!!0pVh;3c_`~n(!To;JM+uHSn&*m5<{Mc`H{nWk2h>%xmGhOZ G`~MHoivJD( literal 0 HcmV?d00001 diff --git a/docs/index.adoc b/docs/SDD/index.adoc similarity index 100% rename from docs/index.adoc rename to docs/SDD/index.adoc diff --git a/docs/SDD/preface.adoc b/docs/SDD/preface.adoc new file mode 100644 index 0000000..0600e6e --- /dev/null +++ b/docs/SDD/preface.adoc @@ -0,0 +1,22 @@ + +{project-name} +_{doc-title}_ +{doc-num} + +[cols="^1,^1"] +|=== +| *COMMENTS and ISSUES* + +If you would like to raise comments or issues on this document, please do so by raising an Issue at the following URL https://github.com/EOEPCA/{component-github-name}/issues. +| *PDF* + +This document is available in PDF format link:EOEPCA-{component-github-name}.pdf[here^]. +| *EUROPEAN SPACE AGENCY CONTRACT REPORT* + +The work described in this report was done under ESA contract. Responsibility for the contents resides in the author or organisation that prepared it. +| *TELESPAZIO VEGA UK Ltd* + +350 Capability Green, Luton, Bedfordshire, LU1 3LU, United Kingdom. + +Tel: +44 (0)1582 399000 + +http://telespazio-vega.com/[www.telespazio-vega.com] +|=== + +include::amendment-history.adoc[] + +<<< diff --git a/docs/SDD/resources/themes/eoepca-theme.yml b/docs/SDD/resources/themes/eoepca-theme.yml new file mode 100644 index 0000000..9bb3156 --- /dev/null +++ b/docs/SDD/resources/themes/eoepca-theme.yml @@ -0,0 +1,28 @@ +extends: origdefault +base: + font_color: 000000 +running_content: + start_at: title +footer: + height: $base_line_height_length * 4 + columns: "<35% =30% >35%" + # odd + recto: + left: + content: | + {project} + + {doc-title} + center: + content: '{page-number}' + right: + content: | + {doc-num} + + Issue {revnumber} + # even + verso: + left: + content: $footer_recto_right_content + center: + content: $footer_recto_center_content + right: + content: $footer_recto_left_content diff --git a/docs/SDD/resources/themes/origdefault-theme.yml b/docs/SDD/resources/themes/origdefault-theme.yml new file mode 100644 index 0000000..b762948 --- /dev/null +++ b/docs/SDD/resources/themes/origdefault-theme.yml @@ -0,0 +1,274 @@ +font: + catalog: + # Noto Serif supports Latin, Latin-1 Supplement, Latin Extended-A, Greek, Cyrillic, Vietnamese & an assortment of symbols + Noto Serif: + normal: notoserif-regular-subset.ttf + bold: notoserif-bold-subset.ttf + italic: notoserif-italic-subset.ttf + bold_italic: notoserif-bold_italic-subset.ttf + # M+ 1mn supports ASCII and the circled numbers used for conums + M+ 1mn: + normal: mplus1mn-regular-ascii-conums.ttf + bold: mplus1mn-bold-ascii.ttf + italic: mplus1mn-italic-ascii.ttf + bold_italic: mplus1mn-bold_italic-ascii.ttf + # M+ 1p supports Latin, Latin-1 Supplement, Latin Extended, Greek, Cyrillic, Vietnamese, Japanese & an assortment of symbols + # It also provides arrows for ->, <-, => and <= replacements in case these glyphs are missing from font + M+ 1p Fallback: + normal: mplus1p-regular-fallback.ttf + bold: mplus1p-regular-fallback.ttf + italic: mplus1p-regular-fallback.ttf + bold_italic: mplus1p-regular-fallback.ttf + fallbacks: + - M+ 1p Fallback +page: + background_color: ffffff + layout: portrait + margin: [0.5in, 0.67in, 0.67in, 0.67in] + # margin_inner and margin_outer keys are used for recto/verso print margins when media=prepress + margin_inner: 0.75in + margin_outer: 0.59in + size: A4 +base: + align: justify + # color as hex string (leading # is optional) + font_color: 333333 + # color as RGB array + #font_color: [51, 51, 51] + # color as CMYK array (approximated) + #font_color: [0, 0, 0, 0.92] + #font_color: [0, 0, 0, 92%] + font_family: Noto Serif + # choose one of these font_size/line_height_length combinations + #font_size: 14 + #line_height_length: 20 + #font_size: 11.25 + #line_height_length: 18 + #font_size: 11.2 + #line_height_length: 16 + font_size: 10.5 + #line_height_length: 15 + # correct line height for Noto Serif metrics + line_height_length: 12 + #font_size: 11.25 + #line_height_length: 18 + line_height: $base_line_height_length / $base_font_size + font_size_large: round($base_font_size * 1.25) + font_size_small: round($base_font_size * 0.85) + font_size_min: $base_font_size * 0.75 + font_style: normal + border_color: eeeeee + border_radius: 4 + border_width: 0.5 +# FIXME vertical_rhythm is weird; we should think in terms of ems +#vertical_rhythm: $base_line_height_length * 2 / 3 +# correct line height for Noto Serif metrics (comes with built-in line height) +vertical_rhythm: $base_line_height_length +horizontal_rhythm: $base_line_height_length +# QUESTION should vertical_spacing be block_spacing instead? +vertical_spacing: $vertical_rhythm +link: + font_color: 428bca +# literal is currently used for inline monospaced in prose and table cells +literal: + font_color: b12146 + font_family: M+ 1mn +menu_caret_content: " \u203a " +heading: + align: left + #font_color: 181818 + font_color: $base_font_color + font_family: $base_font_family + font_style: bold + # h1 is used for part titles (book doctype) or the doctitle (article doctype) + h1_font_size: floor($base_font_size * 2.6) + # h2 is used for chapter titles (book doctype only) + h2_font_size: floor($base_font_size * 2.15) + h3_font_size: round($base_font_size * 1.7) + h4_font_size: $base_font_size_large + h5_font_size: $base_font_size + h6_font_size: $base_font_size_small + #line_height: 1.4 + # correct line height for Noto Serif metrics (comes with built-in line height) + line_height: 1 + margin_top: $vertical_rhythm * 0.4 + margin_bottom: $vertical_rhythm * 0.9 +title_page: + align: right + logo: + top: 10% + title: + top: 55% + font_size: $heading_h1_font_size + font_color: 999999 + line_height: 0.9 + subtitle: + font_size: $heading_h3_font_size + font_style: bold_italic + line_height: 1 + authors: + margin_top: $base_font_size * 1.25 + font_size: $base_font_size_large + font_color: 181818 + revision: + margin_top: $base_font_size * 1.25 +block: + margin_top: 0 + margin_bottom: $vertical_rhythm +caption: + align: left + font_size: $base_font_size * 0.95 + font_style: italic + # FIXME perhaps set line_height instead of / in addition to margins? + margin_inside: $vertical_rhythm / 3 + #margin_inside: $vertical_rhythm / 4 + margin_outside: 0 +lead: + font_size: $base_font_size_large + line_height: 1.4 +abstract: + font_color: 5c6266 + font_size: $lead_font_size + line_height: $lead_line_height + font_style: italic + first_line_font_style: bold + title: + align: center + font_color: $heading_font_color + font_family: $heading_font_family + font_size: $heading_h4_font_size + font_style: $heading_font_style +admonition: + column_rule_color: $base_border_color + column_rule_width: $base_border_width + padding: [0, $horizontal_rhythm, 0, $horizontal_rhythm] + #icon: + # tip: + # name: far-lightbulb + # stroke_color: 111111 + # size: 24 + label: + text_transform: uppercase + font_style: bold +blockquote: + font_color: $base_font_color + font_size: $base_font_size_large + border_color: $base_border_color + border_width: 5 + # FIXME disable negative padding bottom once margin collapsing is implemented + padding: [0, $horizontal_rhythm, $block_margin_bottom * -0.75, $horizontal_rhythm + $blockquote_border_width / 2] + cite_font_size: $base_font_size_small + cite_font_color: 999999 +# code is used for source blocks (perhaps change to source or listing?) +code: + font_color: $base_font_color + font_family: $literal_font_family + font_size: ceil($base_font_size) + padding: $code_font_size + line_height: 1.25 + # line_gap is an experimental property to control how a background color is applied to an inline block element + line_gap: 3.8 + background_color: f5f5f5 + border_color: cccccc + border_radius: $base_border_radius + border_width: 0.75 +conum: + font_family: M+ 1mn + font_color: $literal_font_color + font_size: $base_font_size + line_height: 4 / 3 +example: + border_color: $base_border_color + border_radius: $base_border_radius + border_width: 0.75 + background_color: ffffff + # FIXME reenable padding bottom once margin collapsing is implemented + padding: [$vertical_rhythm, $horizontal_rhythm, 0, $horizontal_rhythm] +image: + align: left +prose: + margin_top: $block_margin_top + margin_bottom: $block_margin_bottom +sidebar: + background_color: eeeeee + border_color: e1e1e1 + border_radius: $base_border_radius + border_width: $base_border_width + # FIXME reenable padding bottom once margin collapsing is implemented + padding: [$vertical_rhythm, $vertical_rhythm * 1.25, 0, $vertical_rhythm * 1.25] + title: + align: center + font_color: $heading_font_color + font_family: $heading_font_family + font_size: $heading_h4_font_size + font_style: $heading_font_style +thematic_break: + border_color: $base_border_color + border_style: solid + border_width: $base_border_width + margin_top: $vertical_rhythm * 0.5 + margin_bottom: $vertical_rhythm * 1.5 +description_list: + term_font_style: bold + term_spacing: $vertical_rhythm / 4 + description_indent: $horizontal_rhythm * 1.25 +outline_list: + indent: $horizontal_rhythm * 1.5 + #marker_font_color: 404040 + # NOTE outline_list_item_spacing applies to list items that do not have complex content + item_spacing: $vertical_rhythm / 2 +table: + background_color: $page_background_color + #head_background_color: + #head_font_color: $base_font_color + head_font_style: bold + #body_background_color: + body_stripe_background_color: f9f9f9 + foot_background_color: f0f0f0 + border_color: dddddd + border_width: $base_border_width + cell_padding: 3 +toc: + indent: $horizontal_rhythm + line_height: 1.4 + dot_leader: + #content: ". " + font_color: a9a9a9 + #levels: 2 3 +footnotes: + font_size: round($base_font_size * 0.75) + item_spacing: $outline_list_item_spacing / 2 +# NOTE in addition to footer, header is also supported +footer: + font_size: $base_font_size_small + # NOTE if background_color is set, background and border will span width of page + border_color: dddddd + border_width: 0.25 + height: $base_line_height_length * 2.5 + line_height: 1 + padding: [$base_line_height_length / 2, 1, 0, 1] + vertical_align: top + #image_vertical_align: or + # additional attributes for content: + # * {page-count} + # * {page-number} + # * {document-title} + # * {document-subtitle} + # * {chapter-title} + # * {section-title} + # * {section-or-chapter-title} + recto: + #columns: "<50% =0% >50%" + right: + content: '{page-number}' + #content: '{section-or-chapter-title} | {page-number}' + #content: '{document-title} | {page-number}' + #center: + # content: '{page-number}' + verso: + #columns: $footer_recto_columns + left: + content: $footer_recto_right_content + #content: '{page-number} | {chapter-title}' + #center: + # content: '{page-number}' diff --git a/docs/SDD/stylesheets/asciidoctor.css b/docs/SDD/stylesheets/asciidoctor.css new file mode 100644 index 0000000..37a53c3 --- /dev/null +++ b/docs/SDD/stylesheets/asciidoctor.css @@ -0,0 +1,420 @@ +/* Asciidoctor default stylesheet | MIT License | http://asciidoctor.org */ +/* Uncomment @import statement below to use as custom stylesheet */ +@import "https://fonts.googleapis.com/css?family=Open+Sans:300,300italic,400,400italic,600,600italic%7CNoto+Serif:400,400italic,700,700italic%7CDroid+Sans+Mono:400,700"; +article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block} +audio,canvas,video{display:inline-block} +audio:not([controls]){display:none;height:0} +script{display:none!important} +html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%} +a{background:transparent} +a:focus{outline:thin dotted} +a:active,a:hover{outline:0} +h1{font-size:2em;margin:.67em 0} +abbr[title]{border-bottom:1px dotted} +b,strong{font-weight:bold} +dfn{font-style:italic} +hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0} +mark{background:#ff0;color:#000} +code,kbd,pre,samp{font-family:monospace;font-size:1em} +pre{white-space:pre-wrap} +q{quotes:"\201C" "\201D" "\2018" "\2019"} +small{font-size:80%} +sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline} +sup{top:-.5em} +sub{bottom:-.25em} +img{border:0} +svg:not(:root){overflow:hidden} +figure{margin:0} +fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em} +legend{border:0;padding:0} +button,input,select,textarea{font-family:inherit;font-size:100%;margin:0} +button,input{line-height:normal} +button,select{text-transform:none} +button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer} +button[disabled],html input[disabled]{cursor:default} +input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0} +button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0} +textarea{overflow:auto;vertical-align:top} +table{border-collapse:collapse;border-spacing:0} +*,*::before,*::after{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box} +html,body{font-size:100%} +body{background:#fff;color:rgba(0,0,0,.8);padding:0;margin:0;font-family:"Noto Serif","DejaVu Serif",serif;font-weight:400;font-style:normal;line-height:1;position:relative;cursor:auto;tab-size:4;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased} +a:hover{cursor:pointer} +img,object,embed{max-width:100%;height:auto} +object,embed{height:100%} +img{-ms-interpolation-mode:bicubic} +.left{float:left!important} +.right{float:right!important} +.text-left{text-align:left!important} +.text-right{text-align:right!important} +.text-center{text-align:center!important} +.text-justify{text-align:justify!important} +.hide{display:none} +img,object,svg{display:inline-block;vertical-align:middle} +textarea{height:auto;min-height:50px} +select{width:100%} +.center{margin-left:auto;margin-right:auto} +.stretch{width:100%} +.subheader,.admonitionblock td.content>.title,.audioblock>.title,.exampleblock>.title,.imageblock>.title,.listingblock>.title,.literalblock>.title,.stemblock>.title,.openblock>.title,.paragraph>.title,.quoteblock>.title,table.tableblock>.title,.verseblock>.title,.videoblock>.title,.dlist>.title,.olist>.title,.ulist>.title,.qlist>.title,.hdlist>.title{line-height:1.45;color:#7a2518;font-weight:400;margin-top:0;margin-bottom:.25em} +div,dl,dt,dd,ul,ol,li,h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6,pre,form,p,blockquote,th,td{margin:0;padding:0;direction:ltr} +a{color:#2156a5;text-decoration:underline;line-height:inherit} +a:hover,a:focus{color:#1d4b8f} +a img{border:none} +p{font-family:inherit;font-weight:400;font-size:1em;line-height:1.6;margin-bottom:1.25em;text-rendering:optimizeLegibility} +p aside{font-size:.875em;line-height:1.35;font-style:italic} +h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{font-family:"Open Sans","DejaVu Sans",sans-serif;font-weight:300;font-style:normal;color:#ba3925;text-rendering:optimizeLegibility;margin-top:1em;margin-bottom:.5em;line-height:1.0125em} +h1 small,h2 small,h3 small,#toctitle small,.sidebarblock>.content>.title small,h4 small,h5 small,h6 small{font-size:60%;color:#e99b8f;line-height:0} +h1{font-size:2.125em} +h2{font-size:1.6875em} +h3,#toctitle,.sidebarblock>.content>.title{font-size:1.375em} +h4,h5{font-size:1.125em} +h6{font-size:1em} +hr{border:solid #dddddf;border-width:1px 0 0;clear:both;margin:1.25em 0 1.1875em;height:0} +em,i{font-style:italic;line-height:inherit} +strong,b{font-weight:bold;line-height:inherit} +small{font-size:60%;line-height:inherit} +code{font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;font-weight:400;color:rgba(0,0,0,.9)} +ul,ol,dl{font-size:1em;line-height:1.6;margin-bottom:1.25em;list-style-position:outside;font-family:inherit} +ul,ol{margin-left:1.5em} +ul li ul,ul li ol{margin-left:1.25em;margin-bottom:0;font-size:1em} +ul.square li ul,ul.circle li ul,ul.disc li ul{list-style:inherit} +ul.square{list-style-type:square} +ul.circle{list-style-type:circle} +ul.disc{list-style-type:disc} +ol li ul,ol li ol{margin-left:1.25em;margin-bottom:0} +dl dt{margin-bottom:.3125em;font-weight:bold} +dl dd{margin-bottom:1.25em} +abbr,acronym{text-transform:uppercase;font-size:90%;color:rgba(0,0,0,.8);border-bottom:1px dotted #ddd;cursor:help} +abbr{text-transform:none} +blockquote{margin:0 0 1.25em;padding:.5625em 1.25em 0 1.1875em;border-left:1px solid #ddd} +blockquote cite{display:block;font-size:.9375em;color:rgba(0,0,0,.6)} +blockquote cite::before{content:"\2014 \0020"} +blockquote cite a,blockquote cite a:visited{color:rgba(0,0,0,.6)} +blockquote,blockquote p{line-height:1.6;color:rgba(0,0,0,.85)} +@media screen and (min-width:768px){h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{line-height:1.2} +h1{font-size:2.75em} +h2{font-size:2.3125em} +h3,#toctitle,.sidebarblock>.content>.title{font-size:1.6875em} +h4{font-size:1.4375em}} +table{background:#fff;margin-bottom:1.25em;border:solid 1px #dedede} +table thead,table tfoot{background:#f7f8f7} +table thead tr th,table thead tr td,table tfoot tr th,table tfoot tr td{padding:.5em .625em .625em;font-size:inherit;color:rgba(0,0,0,.8);text-align:left} +table tr th,table tr td{padding:.5625em .625em;font-size:inherit;color:rgba(0,0,0,.8)} +table tr.even,table tr.alt,table tr:nth-of-type(even){background:#f8f8f7} +table thead tr th,table tfoot tr th,table tbody tr td,table tr td,table tfoot tr td{display:table-cell;line-height:1.6} +h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{line-height:1.2;word-spacing:-.05em} +h1 strong,h2 strong,h3 strong,#toctitle strong,.sidebarblock>.content>.title strong,h4 strong,h5 strong,h6 strong{font-weight:400} +.clearfix::before,.clearfix::after,.float-group::before,.float-group::after{content:" ";display:table} +.clearfix::after,.float-group::after{clear:both} +*:not(pre)>code{font-size:.9375em;font-style:normal!important;letter-spacing:0;padding:.1em .5ex;word-spacing:-.15em;background-color:#f7f7f8;-webkit-border-radius:4px;border-radius:4px;line-height:1.45;text-rendering:optimizeSpeed;word-wrap:break-word} +*:not(pre)>code.nobreak{word-wrap:normal} +*:not(pre)>code.nowrap{white-space:nowrap} +pre,pre>code{line-height:1.45;color:rgba(0,0,0,.9);font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;font-weight:400;text-rendering:optimizeSpeed} +em em{font-style:normal} +strong strong{font-weight:400} +.keyseq{color:rgba(51,51,51,.8)} +kbd{font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;display:inline-block;color:rgba(0,0,0,.8);font-size:.65em;line-height:1.45;background-color:#f7f7f7;border:1px solid #ccc;-webkit-border-radius:3px;border-radius:3px;-webkit-box-shadow:0 1px 0 rgba(0,0,0,.2),0 0 0 .1em white inset;box-shadow:0 1px 0 rgba(0,0,0,.2),0 0 0 .1em #fff inset;margin:0 .15em;padding:.2em .5em;vertical-align:middle;position:relative;top:-.1em;white-space:nowrap} +.keyseq kbd:first-child{margin-left:0} +.keyseq kbd:last-child{margin-right:0} +.menuseq,.menuref{color:#000} +.menuseq b:not(.caret),.menuref{font-weight:inherit} +.menuseq{word-spacing:-.02em} +.menuseq b.caret{font-size:1.25em;line-height:.8} +.menuseq i.caret{font-weight:bold;text-align:center;width:.45em} +b.button::before,b.button::after{position:relative;top:-1px;font-weight:400} +b.button::before{content:"[";padding:0 3px 0 2px} +b.button::after{content:"]";padding:0 2px 0 3px} +p a>code:hover{color:rgba(0,0,0,.9)} +#header,#content,#footnotes,#footer{width:100%;margin-left:auto;margin-right:auto;margin-top:0;margin-bottom:0;max-width:62.5em;*zoom:1;position:relative;padding-left:.9375em;padding-right:.9375em} +#header::before,#header::after,#content::before,#content::after,#footnotes::before,#footnotes::after,#footer::before,#footer::after{content:" ";display:table} +#header::after,#content::after,#footnotes::after,#footer::after{clear:both} +#content{margin-top:1.25em} +#content::before{content:none} +#header>h1:first-child{color:rgba(0,0,0,.85);margin-top:2.25rem;margin-bottom:0} +#header>h1:first-child+#toc{margin-top:8px;border-top:1px solid #dddddf} +#header>h1:only-child,body.toc2 #header>h1:nth-last-child(2){border-bottom:1px solid #dddddf;padding-bottom:8px} +#header .details{border-bottom:1px solid #dddddf;line-height:1.45;padding-top:.25em;padding-bottom:.25em;padding-left:.25em;color:rgba(0,0,0,.6);display:-ms-flexbox;display:-webkit-flex;display:flex;-ms-flex-flow:row wrap;-webkit-flex-flow:row wrap;flex-flow:row wrap} +#header .details span:first-child{margin-left:-.125em} +#header .details span.email a{color:rgba(0,0,0,.85)} +#header .details br{display:none} +#header .details br+span::before{content:"\00a0\2013\00a0"} +#header .details br+span.author::before{content:"\00a0\22c5\00a0";color:rgba(0,0,0,.85)} +#header .details br+span#revremark::before{content:"\00a0|\00a0"} +#header #revnumber{text-transform:capitalize} +#header #revnumber::after{content:"\00a0"} +#content>h1:first-child:not([class]){color:rgba(0,0,0,.85);border-bottom:1px solid #dddddf;padding-bottom:8px;margin-top:0;padding-top:1rem;margin-bottom:1.25rem} +#toc{border-bottom:1px solid #e7e7e9;padding-bottom:.5em} +#toc>ul{margin-left:.125em} +#toc ul.sectlevel0>li>a{font-style:italic} +#toc ul.sectlevel0 ul.sectlevel1{margin:.5em 0} +#toc ul{font-family:"Open Sans","DejaVu Sans",sans-serif;list-style-type:none} +#toc li{line-height:1.3334;margin-top:.3334em} +#toc a{text-decoration:none} +#toc a:active{text-decoration:underline} +#toctitle{color:#7a2518;font-size:1.2em} +@media screen and (min-width:768px){#toctitle{font-size:1.375em} +body.toc2{padding-left:15em;padding-right:0} +#toc.toc2{margin-top:0!important;background-color:#f8f8f7;position:fixed;width:15em;left:0;top:0;border-right:1px solid #e7e7e9;border-top-width:0!important;border-bottom-width:0!important;z-index:1000;padding:1.25em 1em;height:100%;overflow:auto} +#toc.toc2 #toctitle{margin-top:0;margin-bottom:.8rem;font-size:1.2em} +#toc.toc2>ul{font-size:.9em;margin-bottom:0} +#toc.toc2 ul ul{margin-left:0;padding-left:1em} +#toc.toc2 ul.sectlevel0 ul.sectlevel1{padding-left:0;margin-top:.5em;margin-bottom:.5em} +body.toc2.toc-right{padding-left:0;padding-right:15em} +body.toc2.toc-right #toc.toc2{border-right-width:0;border-left:1px solid #e7e7e9;left:auto;right:0}} +@media screen and (min-width:1280px){body.toc2{padding-left:20em;padding-right:0} +#toc.toc2{width:20em} +#toc.toc2 #toctitle{font-size:1.375em} +#toc.toc2>ul{font-size:.95em} +#toc.toc2 ul ul{padding-left:1.25em} +body.toc2.toc-right{padding-left:0;padding-right:20em}} +#content #toc{border-style:solid;border-width:1px;border-color:#e0e0dc;margin-bottom:1.25em;padding:1.25em;background:#f8f8f7;-webkit-border-radius:4px;border-radius:4px} +#content #toc>:first-child{margin-top:0} +#content #toc>:last-child{margin-bottom:0} +#footer{max-width:100%;background-color:rgba(0,0,0,.8);padding:1.25em} +#footer-text{color:rgba(255,255,255,.8);line-height:1.44} +#content{margin-bottom:.625em} +.sect1{padding-bottom:.625em} +@media screen and (min-width:768px){#content{margin-bottom:1.25em} +.sect1{padding-bottom:1.25em}} +.sect1:last-child{padding-bottom:0} +.sect1+.sect1{border-top:1px solid #e7e7e9} +#content h1>a.anchor,h2>a.anchor,h3>a.anchor,#toctitle>a.anchor,.sidebarblock>.content>.title>a.anchor,h4>a.anchor,h5>a.anchor,h6>a.anchor{position:absolute;z-index:1001;width:1.5ex;margin-left:-1.5ex;display:block;text-decoration:none!important;visibility:hidden;text-align:center;font-weight:400} +#content h1>a.anchor::before,h2>a.anchor::before,h3>a.anchor::before,#toctitle>a.anchor::before,.sidebarblock>.content>.title>a.anchor::before,h4>a.anchor::before,h5>a.anchor::before,h6>a.anchor::before{content:"\00A7";font-size:.85em;display:block;padding-top:.1em} +#content h1:hover>a.anchor,#content h1>a.anchor:hover,h2:hover>a.anchor,h2>a.anchor:hover,h3:hover>a.anchor,#toctitle:hover>a.anchor,.sidebarblock>.content>.title:hover>a.anchor,h3>a.anchor:hover,#toctitle>a.anchor:hover,.sidebarblock>.content>.title>a.anchor:hover,h4:hover>a.anchor,h4>a.anchor:hover,h5:hover>a.anchor,h5>a.anchor:hover,h6:hover>a.anchor,h6>a.anchor:hover{visibility:visible} +#content h1>a.link,h2>a.link,h3>a.link,#toctitle>a.link,.sidebarblock>.content>.title>a.link,h4>a.link,h5>a.link,h6>a.link{color:#ba3925;text-decoration:none} +#content h1>a.link:hover,h2>a.link:hover,h3>a.link:hover,#toctitle>a.link:hover,.sidebarblock>.content>.title>a.link:hover,h4>a.link:hover,h5>a.link:hover,h6>a.link:hover{color:#a53221} +.audioblock,.imageblock,.literalblock,.listingblock,.stemblock,.videoblock{margin-bottom:1.25em} +.admonitionblock td.content>.title,.audioblock>.title,.exampleblock>.title,.imageblock>.title,.listingblock>.title,.literalblock>.title,.stemblock>.title,.openblock>.title,.paragraph>.title,.quoteblock>.title,table.tableblock>.title,.verseblock>.title,.videoblock>.title,.dlist>.title,.olist>.title,.ulist>.title,.qlist>.title,.hdlist>.title{text-rendering:optimizeLegibility;text-align:left;font-family:"Noto Serif","DejaVu Serif",serif;font-size:1rem;font-style:italic} +table.tableblock.fit-content>caption.title{white-space:nowrap;width:0} +.paragraph.lead>p,#preamble>.sectionbody>[class="paragraph"]:first-of-type p{font-size:1.21875em;line-height:1.6;color:rgba(0,0,0,.85)} +table.tableblock #preamble>.sectionbody>[class="paragraph"]:first-of-type p{font-size:inherit} +.admonitionblock>table{border-collapse:separate;border:0;background:none;width:100%} +.admonitionblock>table td.icon{text-align:center;width:80px} +.admonitionblock>table td.icon img{max-width:none} +.admonitionblock>table td.icon .title{font-weight:bold;font-family:"Open Sans","DejaVu Sans",sans-serif;text-transform:uppercase} +.admonitionblock>table td.content{padding-left:1.125em;padding-right:1.25em;border-left:1px solid #dddddf;color:rgba(0,0,0,.6)} +.admonitionblock>table td.content>:last-child>:last-child{margin-bottom:0} +.exampleblock>.content{border-style:solid;border-width:1px;border-color:#e6e6e6;margin-bottom:1.25em;padding:1.25em;background:#fff;-webkit-border-radius:4px;border-radius:4px} +.exampleblock>.content>:first-child{margin-top:0} +.exampleblock>.content>:last-child{margin-bottom:0} +.sidebarblock{border-style:solid;border-width:1px;border-color:#e0e0dc;margin-bottom:1.25em;padding:1.25em;background:#f8f8f7;-webkit-border-radius:4px;border-radius:4px} +.sidebarblock>:first-child{margin-top:0} +.sidebarblock>:last-child{margin-bottom:0} +.sidebarblock>.content>.title{color:#7a2518;margin-top:0;text-align:center} +.exampleblock>.content>:last-child>:last-child,.exampleblock>.content .olist>ol>li:last-child>:last-child,.exampleblock>.content .ulist>ul>li:last-child>:last-child,.exampleblock>.content .qlist>ol>li:last-child>:last-child,.sidebarblock>.content>:last-child>:last-child,.sidebarblock>.content .olist>ol>li:last-child>:last-child,.sidebarblock>.content .ulist>ul>li:last-child>:last-child,.sidebarblock>.content .qlist>ol>li:last-child>:last-child{margin-bottom:0} +.literalblock pre,.listingblock pre:not(.highlight),.listingblock pre[class="highlight"],.listingblock pre[class^="highlight "],.listingblock pre.CodeRay,.listingblock pre.prettyprint{background:#f7f7f8} +.sidebarblock .literalblock pre,.sidebarblock .listingblock pre:not(.highlight),.sidebarblock .listingblock pre[class="highlight"],.sidebarblock .listingblock pre[class^="highlight "],.sidebarblock .listingblock pre.CodeRay,.sidebarblock .listingblock pre.prettyprint{background:#f2f1f1} +.literalblock pre,.literalblock pre[class],.listingblock pre,.listingblock pre[class]{-webkit-border-radius:4px;border-radius:4px;word-wrap:break-word;overflow-x:auto;padding:1em;font-size:.8125em} +@media screen and (min-width:768px){.literalblock pre,.literalblock pre[class],.listingblock pre,.listingblock pre[class]{font-size:.90625em}} +@media screen and (min-width:1280px){.literalblock pre,.literalblock pre[class],.listingblock pre,.listingblock pre[class]{font-size:1em}} +.literalblock pre.nowrap,.literalblock pre.nowrap pre,.listingblock pre.nowrap,.listingblock pre.nowrap pre{white-space:pre;word-wrap:normal} +.literalblock.output pre{color:#f7f7f8;background-color:rgba(0,0,0,.9)} +.listingblock pre.highlightjs{padding:0} +.listingblock pre.highlightjs>code{padding:1em;-webkit-border-radius:4px;border-radius:4px} +.listingblock pre.prettyprint{border-width:0} +.listingblock>.content{position:relative} +.listingblock code[data-lang]::before{display:none;content:attr(data-lang);position:absolute;font-size:.75em;top:.425rem;right:.5rem;line-height:1;text-transform:uppercase;color:#999} +.listingblock:hover code[data-lang]::before{display:block} +.listingblock.terminal pre .command::before{content:attr(data-prompt);padding-right:.5em;color:#999} +.listingblock.terminal pre .command:not([data-prompt])::before{content:"$"} +table.pyhltable{border-collapse:separate;border:0;margin-bottom:0;background:none} +table.pyhltable td{vertical-align:top;padding-top:0;padding-bottom:0;line-height:1.45} +table.pyhltable td.code{padding-left:.75em;padding-right:0} +pre.pygments .lineno,table.pyhltable td:not(.code){color:#999;padding-left:0;padding-right:.5em;border-right:1px solid #dddddf} +pre.pygments .lineno{display:inline-block;margin-right:.25em} +table.pyhltable .linenodiv{background:none!important;padding-right:0!important} +.quoteblock{margin:0 1em 1.25em 1.5em;display:table} +.quoteblock>.title{margin-left:-1.5em;margin-bottom:.75em} +.quoteblock blockquote,.quoteblock p{color:rgba(0,0,0,.85);font-size:1.15rem;line-height:1.75;word-spacing:.1em;letter-spacing:0;font-style:italic;text-align:justify} +.quoteblock blockquote{margin:0;padding:0;border:0} +.quoteblock blockquote::before{content:"\201c";float:left;font-size:2.75em;font-weight:bold;line-height:.6em;margin-left:-.6em;color:#7a2518;text-shadow:0 1px 2px rgba(0,0,0,.1)} +.quoteblock blockquote>.paragraph:last-child p{margin-bottom:0} +.quoteblock .attribution{margin-top:.75em;margin-right:.5ex;text-align:right} +.verseblock{margin:0 1em 1.25em} +.verseblock pre{font-family:"Open Sans","DejaVu Sans",sans;font-size:1.15rem;color:rgba(0,0,0,.85);font-weight:300;text-rendering:optimizeLegibility} +.verseblock pre strong{font-weight:400} +.verseblock .attribution{margin-top:1.25rem;margin-left:.5ex} +.quoteblock .attribution,.verseblock .attribution{font-size:.9375em;line-height:1.45;font-style:italic} +.quoteblock .attribution br,.verseblock .attribution br{display:none} +.quoteblock .attribution cite,.verseblock .attribution cite{display:block;letter-spacing:-.025em;color:rgba(0,0,0,.6)} +.quoteblock.abstract blockquote::before,.quoteblock.excerpt blockquote::before,.quoteblock .quoteblock blockquote::before{display:none} +.quoteblock.abstract blockquote,.quoteblock.abstract p,.quoteblock.excerpt blockquote,.quoteblock.excerpt p,.quoteblock .quoteblock blockquote,.quoteblock .quoteblock p{line-height:1.6;word-spacing:0} +.quoteblock.abstract{margin:0 1em 1.25em;display:block} +.quoteblock.abstract>.title{margin:0 0 .375em;font-size:1.15em;text-align:center} +.quoteblock.excerpt,.quoteblock .quoteblock{margin:0 0 1.25em;padding:0 0 .25em 1em;border-left:.25em solid #dddddf} +.quoteblock.excerpt blockquote,.quoteblock.excerpt p,.quoteblock .quoteblock blockquote,.quoteblock .quoteblock p{color:inherit;font-size:1.0625rem} +.quoteblock.excerpt .attribution,.quoteblock .quoteblock .attribution{color:inherit;text-align:left;margin-right:0} +table.tableblock{max-width:100%;border-collapse:separate} +p.tableblock:last-child{margin-bottom:0} +td.tableblock>.content{margin-bottom:-1.25em} +table.tableblock,th.tableblock,td.tableblock{border:0 solid #dedede} +table.grid-all>thead>tr>.tableblock,table.grid-all>tbody>tr>.tableblock{border-width:0 1px 1px 0} +table.grid-all>tfoot>tr>.tableblock{border-width:1px 1px 0 0} +table.grid-cols>*>tr>.tableblock{border-width:0 1px 0 0} +table.grid-rows>thead>tr>.tableblock,table.grid-rows>tbody>tr>.tableblock{border-width:0 0 1px} +table.grid-rows>tfoot>tr>.tableblock{border-width:1px 0 0} +table.grid-all>*>tr>.tableblock:last-child,table.grid-cols>*>tr>.tableblock:last-child{border-right-width:0} +table.grid-all>tbody>tr:last-child>.tableblock,table.grid-all>thead:last-child>tr>.tableblock,table.grid-rows>tbody>tr:last-child>.tableblock,table.grid-rows>thead:last-child>tr>.tableblock{border-bottom-width:0} +table.frame-all{border-width:1px} +table.frame-sides{border-width:0 1px} +table.frame-topbot,table.frame-ends{border-width:1px 0} +table.stripes-all tr,table.stripes-odd tr:nth-of-type(odd){background:#f8f8f7} +table.stripes-none tr,table.stripes-odd tr:nth-of-type(even){background:none} +th.halign-left,td.halign-left{text-align:left} +th.halign-right,td.halign-right{text-align:right} +th.halign-center,td.halign-center{text-align:center} +th.valign-top,td.valign-top{vertical-align:top} +th.valign-bottom,td.valign-bottom{vertical-align:bottom} +th.valign-middle,td.valign-middle{vertical-align:middle} +table thead th,table tfoot th{font-weight:bold} +tbody tr th{display:table-cell;line-height:1.6;background:#f7f8f7} +tbody tr th,tbody tr th p,tfoot tr th,tfoot tr th p{color:rgba(0,0,0,.8);font-weight:bold} +p.tableblock>code:only-child{background:none;padding:0} +p.tableblock{font-size:1em} +td>div.verse{white-space:pre} +ol{margin-left:1.75em} +ul li ol{margin-left:1.5em} +dl dd{margin-left:1.125em} +dl dd:last-child,dl dd:last-child>:last-child{margin-bottom:0} +ol>li p,ul>li p,ul dd,ol dd,.olist .olist,.ulist .ulist,.ulist .olist,.olist .ulist{margin-bottom:.625em} +ul.checklist,ul.none,ol.none,ul.no-bullet,ol.no-bullet,ol.unnumbered,ul.unstyled,ol.unstyled{list-style-type:none} +ul.no-bullet,ol.no-bullet,ol.unnumbered{margin-left:.625em} +ul.unstyled,ol.unstyled{margin-left:0} +ul.checklist{margin-left:.625em} +ul.checklist li>p:first-child>.fa-square-o:first-child,ul.checklist li>p:first-child>.fa-check-square-o:first-child{width:1.25em;font-size:.8em;position:relative;bottom:.125em} +ul.checklist li>p:first-child>input[type="checkbox"]:first-child{margin-right:.25em} +ul.inline{display:-ms-flexbox;display:-webkit-box;display:flex;-ms-flex-flow:row wrap;-webkit-flex-flow:row wrap;flex-flow:row wrap;list-style:none;margin:0 0 .625em -1.25em} +ul.inline>li{margin-left:1.25em} +.unstyled dl dt{font-weight:400;font-style:normal} +ol.arabic{list-style-type:decimal} +ol.decimal{list-style-type:decimal-leading-zero} +ol.loweralpha{list-style-type:lower-alpha} +ol.upperalpha{list-style-type:upper-alpha} +ol.lowerroman{list-style-type:lower-roman} +ol.upperroman{list-style-type:upper-roman} +ol.lowergreek{list-style-type:lower-greek} +.hdlist>table,.colist>table{border:0;background:none} +.hdlist>table>tbody>tr,.colist>table>tbody>tr{background:none} +td.hdlist1,td.hdlist2{vertical-align:top;padding:0 .625em} +td.hdlist1{font-weight:bold;padding-bottom:1.25em} +.literalblock+.colist,.listingblock+.colist{margin-top:-.5em} +.colist td:not([class]):first-child{padding:.4em .75em 0;line-height:1;vertical-align:top} +.colist td:not([class]):first-child img{max-width:none} +.colist td:not([class]):last-child{padding:.25em 0} +.thumb,.th{line-height:0;display:inline-block;border:solid 4px #fff;-webkit-box-shadow:0 0 0 1px #ddd;box-shadow:0 0 0 1px #ddd} +.imageblock.left{margin:.25em .625em 1.25em 0} +.imageblock.right{margin:.25em 0 1.25em .625em} +.imageblock>.title{margin-bottom:0} +.imageblock.thumb,.imageblock.th{border-width:6px} +.imageblock.thumb>.title,.imageblock.th>.title{padding:0 .125em} +.image.left,.image.right{margin-top:.25em;margin-bottom:.25em;display:inline-block;line-height:0} +.image.left{margin-right:.625em} +.image.right{margin-left:.625em} +a.image{text-decoration:none;display:inline-block} +a.image object{pointer-events:none} +sup.footnote,sup.footnoteref{font-size:.875em;position:static;vertical-align:super} +sup.footnote a,sup.footnoteref a{text-decoration:none} +sup.footnote a:active,sup.footnoteref a:active{text-decoration:underline} +#footnotes{padding-top:.75em;padding-bottom:.75em;margin-bottom:.625em} +#footnotes hr{width:20%;min-width:6.25em;margin:-.25em 0 .75em;border-width:1px 0 0} +#footnotes .footnote{padding:0 .375em 0 .225em;line-height:1.3334;font-size:.875em;margin-left:1.2em;margin-bottom:.2em} +#footnotes .footnote a:first-of-type{font-weight:bold;text-decoration:none;margin-left:-1.05em} +#footnotes .footnote:last-of-type{margin-bottom:0} +#content #footnotes{margin-top:-.625em;margin-bottom:0;padding:.75em 0} +.gist .file-data>table{border:0;background:#fff;width:100%;margin-bottom:0} +.gist .file-data>table td.line-data{width:99%} +div.unbreakable{page-break-inside:avoid} +.big{font-size:larger} +.small{font-size:smaller} +.underline{text-decoration:underline} +.overline{text-decoration:overline} +.line-through{text-decoration:line-through} +.aqua{color:#00bfbf} +.aqua-background{background-color:#00fafa} +.black{color:#000} +.black-background{background-color:#000} +.blue{color:#0000bf} +.blue-background{background-color:#0000fa} +.fuchsia{color:#bf00bf} +.fuchsia-background{background-color:#fa00fa} +.gray{color:#606060} +.gray-background{background-color:#7d7d7d} +.green{color:#006000} +.green-background{background-color:#007d00} +.lime{color:#00bf00} +.lime-background{background-color:#00fa00} +.maroon{color:#600000} +.maroon-background{background-color:#7d0000} +.navy{color:#000060} +.navy-background{background-color:#00007d} +.olive{color:#606000} +.olive-background{background-color:#7d7d00} +.purple{color:#600060} +.purple-background{background-color:#7d007d} +.red{color:#bf0000} +.red-background{background-color:#fa0000} +.silver{color:#909090} +.silver-background{background-color:#bcbcbc} +.teal{color:#006060} +.teal-background{background-color:#007d7d} +.white{color:#bfbfbf} +.white-background{background-color:#fafafa} +.yellow{color:#bfbf00} +.yellow-background{background-color:#fafa00} +span.icon>.fa{cursor:default} +a span.icon>.fa{cursor:inherit} +.admonitionblock td.icon [class^="fa icon-"]{font-size:2.5em;text-shadow:1px 1px 2px rgba(0,0,0,.5);cursor:default} +.admonitionblock td.icon .icon-note::before{content:"\f05a";color:#19407c} +.admonitionblock td.icon .icon-tip::before{content:"\f0eb";text-shadow:1px 1px 2px rgba(155,155,0,.8);color:#111} +.admonitionblock td.icon .icon-warning::before{content:"\f071";color:#bf6900} +.admonitionblock td.icon .icon-caution::before{content:"\f06d";color:#bf3400} +.admonitionblock td.icon .icon-important::before{content:"\f06a";color:#bf0000} +.conum[data-value]{display:inline-block;color:#fff!important;background-color:rgba(0,0,0,.8);-webkit-border-radius:100px;border-radius:100px;text-align:center;font-size:.75em;width:1.67em;height:1.67em;line-height:1.67em;font-family:"Open Sans","DejaVu Sans",sans-serif;font-style:normal;font-weight:bold} +.conum[data-value] *{color:#fff!important} +.conum[data-value]+b{display:none} +.conum[data-value]::after{content:attr(data-value)} +pre .conum[data-value]{position:relative;top:-.125em} +b.conum *{color:inherit!important} +.conum:not([data-value]):empty{display:none} +dt,th.tableblock,td.content,div.footnote{text-rendering:optimizeLegibility} +h1,h2,p,td.content,span.alt{letter-spacing:-.01em} +p strong,td.content strong,div.footnote strong{letter-spacing:-.005em} +p,blockquote,dt,td.content,span.alt{font-size:1.0625rem} +p{margin-bottom:1.25rem} +.sidebarblock p,.sidebarblock dt,.sidebarblock td.content,p.tableblock{font-size:1em} +.exampleblock>.content{background-color:#fffef7;border-color:#e0e0dc;-webkit-box-shadow:0 1px 4px #e0e0dc;box-shadow:0 1px 4px #e0e0dc} +.print-only{display:none!important} +@page{margin:1.25cm .75cm} +@media print{*{-webkit-box-shadow:none!important;box-shadow:none!important;text-shadow:none!important} +html{font-size:80%} +a{color:inherit!important;text-decoration:underline!important} +a.bare,a[href^="#"],a[href^="mailto:"]{text-decoration:none!important} +a[href^="http:"]:not(.bare)::after,a[href^="https:"]:not(.bare)::after{content:"(" attr(href) ")";display:inline-block;font-size:.875em;padding-left:.25em} +abbr[title]::after{content:" (" attr(title) ")"} +pre,blockquote,tr,img,object,svg{page-break-inside:avoid} +thead{display:table-header-group} +svg{max-width:100%} +p,blockquote,dt,td.content{font-size:1em;orphans:3;widows:3} +h2,h3,#toctitle,.sidebarblock>.content>.title{page-break-after:avoid} +#toc,.sidebarblock,.exampleblock>.content{background:none!important} +#toc{border-bottom:1px solid #dddddf!important;padding-bottom:0!important} +body.book #header{text-align:center} +body.book #header>h1:first-child{border:0!important;margin:2.5em 0 1em} +body.book #header .details{border:0!important;display:block;padding:0!important} +body.book #header .details span:first-child{margin-left:0!important} +body.book #header .details br{display:block} +body.book #header .details br+span::before{content:none!important} +body.book #toc{border:0!important;text-align:left!important;padding:0!important;margin:0!important} +body.book #toc,body.book #preamble,body.book h1.sect0,body.book .sect1>h2{page-break-before:always} +.listingblock code[data-lang]::before{display:block} +#footer{padding:0 .9375em} +.hide-on-print{display:none!important} +.print-only{display:block!important} +.hide-for-print{display:none!important} +.show-for-print{display:inherit!important}} +@media print,amzn-kf8{#header>h1:first-child{margin-top:1.25rem} +.sect1{padding:0!important} +.sect1+.sect1{border:0} +#footer{background:none} +#footer-text{color:rgba(0,0,0,.6);font-size:.9em}} +@media amzn-kf8{#header,#content,#footnotes,#footer{padding:0}} \ No newline at end of file diff --git a/docs/SDD/stylesheets/eoepca.css b/docs/SDD/stylesheets/eoepca.css new file mode 100644 index 0000000..46a3ba5 --- /dev/null +++ b/docs/SDD/stylesheets/eoepca.css @@ -0,0 +1,25 @@ +@import "asciidoctor.css"; + +/* Centre align figure captions */ +.imageblock.text-center>.title { + text-align: center; +} + +/* Scale font size of CodeRay [source] elements, which seem a bit big */ +pre.CodeRay code { + font-size: 0.9em; +} + +/* Bold font for the numbers in an ordered list */ +div.strong > ol { + font-weight: bold; +} + +/* + * EXAMPLE for custom code-block scaling... + * Use an asciidoc source element like this '[source.src-scale05,python]' and then apply a custom style like below. + * This basically adds the custom CSS class 'src-scale05' to the root of the DOM for the code block, which we can then exploit for styling. + */ + .src-scale05 pre.CodeRay code { + font-size: 0.5em; +} diff --git a/docs/bin/generate-docs.sh b/docs/bin/generate-docs.sh index 1df07ee..9b25147 100755 --- a/docs/bin/generate-docs.sh +++ b/docs/bin/generate-docs.sh @@ -9,24 +9,29 @@ trap "cd '${ORIG_DIR}'" EXIT # Work in the docs/ directory cd "${BIN_DIR}/.." -# Set context dependant on whether this script has been invoked by Travis or not -if [ "${TRAVIS}" = "true" ] -then - echo "Running in Travis..." -else - # Running locally we want to remove the docker containers - export DOCKER_RM="--rm" -fi -# Prepare output/ directory -rm -rf output -mkdir -p output -cp -r images output -cp -r stylesheets output +for doc in SDD ICD; do + cd $doc + # Set context dependant on whether this script has been invoked by Travis or not + if [ "${TRAVIS}" = "true" ] + then + echo "Running in Travis..." + else + # Running locally we want to remove the docker containers + export DOCKER_RM="--rm" + fi + export DOCKER_RM="--rm" + # Prepare output/ directory + rm -rf output + mkdir -p output + cp -r images output + cp -r stylesheets output -# Docuemnt Generation - using asciidoctor docker image -# -# HTML version -docker run ${DOCKER_RM} -v $PWD:/documents/ --name asciidoc-to-html asciidoctor/docker-asciidoctor asciidoctor -r asciidoctor-diagram -D /documents/output index.adoc -# PDF version -docker run ${DOCKER_RM} -v $PWD:/documents/ --name asciidoc-to-pdf asciidoctor/docker-asciidoctor asciidoctor-pdf -r asciidoctor-diagram -D /documents/output index.adoc + # Docuemnt Generation - using asciidoctor docker image + # + # HTML version + docker run ${DOCKER_RM} -v $PWD:/documents/ --name asciidoc-to-html-um asciidoctor/docker-asciidoctor asciidoctor -r asciidoctor-diagram -D /documents/output index.adoc + # PDF version + docker run ${DOCKER_RM} -v $PWD:/documents/ --name asciidoc-to-pdf-um asciidoctor/docker-asciidoctor asciidoctor-pdf -r asciidoctor-diagram -D /documents/output index.adoc + cd "${BIN_DIR}/.." +done \ No newline at end of file diff --git a/docs/bin/publish-docs.sh b/docs/bin/publish-docs.sh index 15dbea3..f221e57 100755 --- a/docs/bin/publish-docs.sh +++ b/docs/bin/publish-docs.sh @@ -4,18 +4,11 @@ ORIG_DIR="$(pwd)" cd "$(dirname "$0")" BIN_DIR="$(pwd)" -trap "cd '${ORIG_DIR}'" EXIT +trap "cd '${ORIG_DIR}';" EXIT # Work in the docs/ directory cd "${BIN_DIR}/.." -# Check the output directory exists -if [ ! -d "output" ] -then - echo "ERROR: output directory is missing, generate-docs must be run first" >&2 - exit 1 -fi - # Deduce the name of the repository if [ -z "${GH_REPOS_NAME}" ] then @@ -25,16 +18,26 @@ fi # Clone the 'gh-pages' branch git clone --branch gh-pages --single-branch "https://${GH_TOKEN}@github.com/EOEPCA/${GH_REPOS_NAME}" repos -# Move generated doc outputs to the repos -cd repos -rm -rf current -mv ../output current -mv current/index.pdf current/EOEPCA-${GH_REPOS_NAME}.pdf - -# Prepare the root landing page/README - but don't overwrite if they already exist -if [ ! -e index.html ]; then cp ../gh-page-root.html index.html; fi -if [ ! -e README.adoc ]; then cp ../gh-page-README.adoc README.adoc; fi +for doc in SDD ICD; do + # Check the output directory exists + if [ ! -d "$doc/output" ] + then + echo "ERROR: output directory is missing, generate-docs must be run first" >&2 + exit 1 + fi + cd repos/$doc + rm -rf current + mv ../../$doc/output current + mv current/index.pdf current/EOEPCA-${GH_REPOS_NAME}.pdf + + # Prepare the root landing page/README - but don't overwrite if they already exist + #if [ ! -e index.html ]; then cp ../gh-page-root.html index.html; fi + #if [ ! -e README.adoc ]; then cp ../gh-page-README.adoc README.adoc; fi + cd ${BIN_DIR}/.. + +done +cd repos # Config git profile for commits if [ -n "${GH_USER_NAME}" ]; then git config user.name "${GH_USER_NAME}"; fi if [ -n "${GH_USER_EMAIL}" ]; then git config user.email "${GH_USER_EMAIL}"; fi From 7be08128ffbcd76c69408b5759efae8e495123b3 Mon Sep 17 00:00:00 2001 From: Hector Rodriguez Date: Thu, 5 Nov 2020 08:40:45 +0000 Subject: [PATCH 31/80] EOEPCA-179 #comment Added content to PEP ICD --- docs/ICD/03.interfaces/00.interfaces.adoc | 5897 +-------------------- 1 file changed, 291 insertions(+), 5606 deletions(-) diff --git a/docs/ICD/03.interfaces/00.interfaces.adoc b/docs/ICD/03.interfaces/00.interfaces.adoc index d41a437..f5c1b96 100644 --- a/docs/ICD/03.interfaces/00.interfaces.adoc +++ b/docs/ICD/03.interfaces/00.interfaces.adoc @@ -1,9 +1,8 @@ -[[Interfaces]] -= Login Service Interfaces += Policy Enforcement Point Interfaces [abstract] .Abstract -OpenID Connect Provider (OP) & UMA Authorization Server (AS) +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. // markup not found, no include::{specDir}intro.adoc[opts=optional] @@ -13,70 +12,64 @@ OpenID Connect Provider (OP) & UMA Authorization Server (AS) == Endpoints -[.Authentication] -=== OIDC - Authentication +[.PolicyEnforcement] +=== PolicyEnforcement -[.endSession] -==== endSession +[.proxyPathDelete] +==== Proxy DELETE -`GET /end_session` +`DELETE /proxy/{path}` -End current session. +Request to Back-End Service ===== Description -End current session. +This operation propagates all headers -// markup not found, no include::{specDir}end_session/GET/spec.adoc[opts=optional] +// markup not found, no include::{specDir}proxy/\{path\}/DELETE/spec.adoc[opts=optional] ===== Parameters - - - - -====== Query Parametersa +====== Path Parameters [cols="2,3,1"] |=== -|Name| Description| Required +|Name| Description| Required -| id_token_hint -| Previously issued ID Token (id_token) passed to the logout endpoint as a hint about the End-User's current authenticated session with the Client. This is used as an indication of the identity of the End-User that the RP is requesting be logged out by the OP. The OP need not be listed as an audience of the ID Token when it is used as an id_token_hint value. -| - +| path +| Path to the Back-End Service +| X -| post_logout_redirect_uri -| URL to which the RP is requesting that the End-User's User Agent be redirected after a logout has been performed. The value MUST have been previously registered with the OP, either using the post_logout_redirect_uris Registration parameter or via another mechanism. If supplied, the OP SHOULD honor this request following the logout. -| - - +|=== -| state -| Opaque value used by the RP to maintain state between the logout request and the callback to the endpoint specified by the post_logout_redirect_uri parameter. If included in the logout request, the OP passes this value back to the RP using the state query parameter when redirecting the User Agent back to the RP. -| - - -| session_id -| Session Id + +====== Header Parameters + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| Authorization +| RPT Token generated through UMA Flow | - |=== + ===== Return Type - -===== Content Type - -* application/json ===== Responses @@ -87,213 +80,92 @@ End current session. | 200 -| OK - User redirected to logout page +| OK | <<>> -| 302 -| Resource Found. +| 401 +| Unauthorized access request. | <<>> - -| 400 -| Error codes for end session endpoint. -| <> - - -| 500 -| Internal error occured. Please check log file for details. -| <> - |=== ===== Samples -// markup not found, no include::{snippetDir}end_session/GET/http-request.adoc[opts=optional] +// markup not found, no include::{snippetDir}proxy/\{path\}/DELETE/http-request.adoc[opts=optional] -// markup not found, no include::{snippetDir}end_session/GET/http-response.adoc[opts=optional] +// markup not found, no include::{snippetDir}proxy/\{path\}/DELETE/http-response.adoc[opts=optional] -// file not found, no * wiremock data link :end_session/GET/GET.json[] +// file not found, no * wiremock data link :proxy/{path}/DELETE/DELETE.json[] ifdef::internal-generation[] ===== Implementation -// markup not found, no include::{specDir}end_session/GET/implementation.adoc[opts=optional] +// markup not found, no include::{specDir}proxy/\{path\}/DELETE/implementation.adoc[opts=optional] endif::internal-generation[] -[.getAuthorize] -==== GET Authorize +[.proxyPathGet] +==== Proxy GET -`GET /authorize` +`GET /proxy/{path}` -The Authorization Endpoint performs Authentication of the End-User. +Request to Back-End Service ===== Description -End-User Authentication and Authorization done by sending the User Agent to the Authorization Endpoint using request parameters defined by OAuth 2.0 and OpenID Connect. +This operation propagates all headers and query parameters -// markup not found, no include::{specDir}authorize/GET/spec.adoc[opts=optional] +// markup not found, no include::{specDir}proxy/\{path\}/GET/spec.adoc[opts=optional] ===== Parameters - - - - -====== Query Parameters +====== Path Parameters [cols="2,3,1"] |=== |Name| Description| Required -| scope -| OpenID Connect requests MUST contain the openid scope value. If the openid scope value is not present, the behavior is entirely unspecified. Other scope values MAY be present. -| X - - -| response_type -| OAuth 2.0 Response Type value that determines the authorization processing flow to be used, including what parameters are returned from the endpoints used. +| path +| Path to the Back-End Service | X -| client_id -| OAuth 2.0 Client Identifier valid at the Authorization Server. -| X - - -| redirect_uri -| Redirection URI to which the response will be sent. This URI MUST exactly match one of the Redirection URI values for the Client pre-registered at the OpenID Provider. -| X - - -| state -| Opaque value used to maintain state between the request and the callback. -| - - - -| response_mode -| Informs the Authorization Server of the mechanism to be used for returning parameters from the Authorization Endpoint. -| - - - -| nonce -| String value used to associate a Client session with an ID Token, and to mitigate replay attacks. -| - - - -| display -| ASCII string value that specifies how the Authorization Server displays the authentication and consent user interface pages to the End-User. -| - - - -| prompt -| Space delimited, case sensitive list of ASCII string values that specifies whether the Authorization Server prompts the End-User for reauthentication and consent. The defined values are - none, login, consent, select_account. -| - - - -| max_age -| Maximum Authentication Age. Specifies the allowable elapsed time in seconds since the last time the End-User was actively authenticated by the OP. -| - - - -| ui_locales -| End-User's preferred languages and scripts for the user interface, represented as a space-separated list of BCP47 [RFC5646] language tag values, ordered by preference. -| - - - -| id_token_hint -| ID Token previously issued by the Authorization Server being passed as a hint about the End-User's current or past authenticated session with the Client. If the End-User identified by the ID Token is logged in or is logged in by the request, then the Authorization Server returns a positive response. -| - - - -| login_hint -| Hint to the Authorization Server about the login identifier the End-User might use to log in (if necessary). -| - - - -| acr_values -| Requested Authentication Context Class Reference values. Space-separated string that specifies the acr values that the Authorization Server is being requested to use for processing this Authentication Request, with the values appearing in order of preference. -| - - - -| amr_values -| AMR Values. -| - - - -| request -| This parameter enables OpenID Connect requests to be passed in a single, self-contained parameter and to be optionally signed and/or encrypted. The parameter value is a Request Object value. It represents the request as a JWT whose Claims are the request parameters. -| - - - -| request_uri -| This parameter enables OpenID Connect requests to be passed by reference, rather than by value. The request_uri value is a URL using the https scheme referencing a resource containing a Request Object value, which is a JWT containing the request parameters. -| - - - -| request_session_id -| Request session id. -| - - - -| session_id -| Session id of this call. -| - - - -| origin_headers -| Origin headers. Used in custom workflows. -| - - +|=== -| code_challenge -| PKCE code challenge. -| - - -| code_challenge_method -| PKCE code challenge method. -| - - -| custom_response_headers -| Custom Response Headers. -| - - +====== Header Parameters -| claims -| Requested Claims. -| - - +[cols="2,3,1"] +|=== +|Name| Description| Required -| auth_req_id -| CIBA authentication request Id. +| Authorization +| RPT Token generated through UMA Flow | - |=== + ===== Return Type - -===== Content Type - -* application/json ===== Responses @@ -308,105 +180,88 @@ End-User Authentication and Authorization done by sending the User Agent to the | <<>> -| 302 -| Error codes for authorization endpoint. -| <> - - -| 400 -| Invalid parameters are provided to endpoint. -| <> - - | 401 | Unauthorized access request. -| <> - - -| 500 -| Internal error occured. Please check log file for details. -| <> +| <<>> |=== ===== Samples -// markup not found, no include::{snippetDir}authorize/GET/http-request.adoc[opts=optional] +// markup not found, no include::{snippetDir}proxy/\{path\}/GET/http-request.adoc[opts=optional] -// markup not found, no include::{snippetDir}authorize/GET/http-response.adoc[opts=optional] +// markup not found, no include::{snippetDir}proxy/\{path\}/GET/http-response.adoc[opts=optional] -// file not found, no * wiremock data link :authorize/GET/GET.json[] +// file not found, no * wiremock data link :proxy/{path}/GET/GET.json[] ifdef::internal-generation[] ===== Implementation -// markup not found, no include::{specDir}authorize/GET/implementation.adoc[opts=optional] +// markup not found, no include::{specDir}proxy/\{path\}/GET/implementation.adoc[opts=optional] endif::internal-generation[] -[.getClientinfo] -==== GET Clientinfo +[.proxyPathPost] +==== Proxy POST -`GET /clientinfo` +`POST /proxy/{path}` -To get Claims details about the registered client. +Request to Back-End Service ===== Description -The ClientInfo Endpoint is an OAuth 2.0 Protected Resource that returns Claims about the registered client. +This operation propagates all headers, query parameters and body -// markup not found, no include::{specDir}clientinfo/GET/spec.adoc[opts=optional] +// markup not found, no include::{specDir}proxy/\{path\}/POST/spec.adoc[opts=optional] ===== Parameters - - - -====== Header Parameters +====== Path Parameters [cols="2,3,1"] |=== |Name| Description| Required -| Authorization -| -| - +| path +| Path to the Back-End Service +| X |=== -====== Query Parameters + + +====== Header Parameters [cols="2,3,1"] |=== |Name| Description| Required -| access_token -| +| Authorization +| RPT Token generated through UMA Flow | - |=== + ===== Return Type -<> -===== Content Type +- -* application/json ===== Responses @@ -418,103 +273,91 @@ The ClientInfo Endpoint is an OAuth 2.0 Protected Resource that returns Claims a | 200 | OK -| <> +| <<>> -| 400 -| Invalid Request are provided to endpoint. -| <> +| 401 +| Unauthorized access request. +| <<>> |=== ===== Samples -// markup not found, no include::{snippetDir}clientinfo/GET/http-request.adoc[opts=optional] +// markup not found, no include::{snippetDir}proxy/\{path\}/POST/http-request.adoc[opts=optional] -// markup not found, no include::{snippetDir}clientinfo/GET/http-response.adoc[opts=optional] +// markup not found, no include::{snippetDir}proxy/\{path\}/POST/http-response.adoc[opts=optional] -// file not found, no * wiremock data link :clientinfo/GET/GET.json[] +// file not found, no * wiremock data link :proxy/{path}/POST/POST.json[] ifdef::internal-generation[] ===== Implementation -// markup not found, no include::{specDir}clientinfo/GET/implementation.adoc[opts=optional] +// markup not found, no include::{specDir}proxy/\{path\}/POST/implementation.adoc[opts=optional] endif::internal-generation[] -[.getIntrospection] -==== GET Introspection +[.proxyPathPut] +==== Proxy PUT -`GET /introspection` +`PUT /proxy/{path}` -The Introspection OAuth 2 Endpoint. +Request to Back-End Service ===== Description -The Introspection OAuth 2 Endpoint. +This operation propagates all headers, query parameters and body -// markup not found, no include::{specDir}introspection/GET/spec.adoc[opts=optional] +// markup not found, no include::{specDir}proxy/\{path\}/PUT/spec.adoc[opts=optional] ===== Parameters - - - -====== Header Parameters +====== Path Parameters [cols="2,3,1"] |=== |Name| Description| Required -| Authorization -| Client Authorization details that contains the access token along with other details. +| path +| Path to the Back-End Service | X |=== -====== Query Parameters + + +====== Header Parameters [cols="2,3,1"] |=== |Name| Description| Required -| token -| -| X - - -| token_type_hint -| ID Token previously issued by the Authorization Server being passed as a hint about the End-User. -| - - - -| response_as_jwt -| OPTIONAL. Boolean value with default value false. If true, returns introspection response as JWT (signed based on client configuration used for authentication to Introspection Endpoint). +| Authorization +| RPT Token generated through UMA Flow | - |=== + ===== Return Type -<> -===== Content Type +- -* application/json ===== Responses @@ -526,60 +369,54 @@ The Introspection OAuth 2 Endpoint. | 200 | OK -| <> - - -| 400 -| Error codes for introspection endpoint. -| <> +| <<>> | 401 | Unauthorized access request. -| <> - - -| 500 -| Internal error occured. Please check log file for details. -| <> +| <<>> |=== ===== Samples -// markup not found, no include::{snippetDir}introspection/GET/http-request.adoc[opts=optional] +// markup not found, no include::{snippetDir}proxy/\{path\}/PUT/http-request.adoc[opts=optional] -// markup not found, no include::{snippetDir}introspection/GET/http-response.adoc[opts=optional] +// markup not found, no include::{snippetDir}proxy/\{path\}/PUT/http-response.adoc[opts=optional] -// file not found, no * wiremock data link :introspection/GET/GET.json[] +// file not found, no * wiremock data link :proxy/{path}/PUT/PUT.json[] ifdef::internal-generation[] ===== Implementation -// markup not found, no include::{specDir}introspection/GET/implementation.adoc[opts=optional] +// markup not found, no include::{specDir}proxy/\{path\}/PUT/implementation.adoc[opts=optional] endif::internal-generation[] -[.getUserinfo] -==== GET Userinfo +[.Resources] +=== Resources + + +[.resourcesGet] +==== Resources GET -`GET /userinfo` +`GET /resources` -Returns Claims about the authenticated End-User. +List all owned resources ===== Description -Returns Claims about the authenticated End-User. +This operation lists all resources filtered by ownership ID. Ownership ID is extracted from the OpenID Connect Token -// markup not found, no include::{specDir}userinfo/GET/spec.adoc[opts=optional] +// markup not found, no include::{specDir}resources/GET/spec.adoc[opts=optional] @@ -595,35 +432,21 @@ Returns Claims about the authenticated End-User. |Name| Description| Required | Authorization -| +| JWT or Bearer Token | - |=== -====== Query Parameters - -[cols="2,3,1"] -|=== -|Name| Description| Required - -| access_token -| OAuth 2.0 Access Token. -| X - - -|=== ===== Return Type - -<> +array[<>] ===== Content Type -* application/jwt * application/json ===== Responses @@ -636,79 +459,84 @@ Returns Claims about the authenticated End-User. | 200 | OK -| <> - - -| 400 -| Invalid parameters provided to endpoint. -| <> - - -| 401 -| Invalid parameters provided to endpoint. -| <> - - -| 403 -| Invalid parameters provided to endpoint. -| <> - - -| 500 -| Internal error occured. Please check log file for details. -| <> +| List[<>] |=== ===== Samples -// markup not found, no include::{snippetDir}userinfo/GET/http-request.adoc[opts=optional] +// markup not found, no include::{snippetDir}resources/GET/http-request.adoc[opts=optional] -// markup not found, no include::{snippetDir}userinfo/GET/http-response.adoc[opts=optional] +// markup not found, no include::{snippetDir}resources/GET/http-response.adoc[opts=optional] -// file not found, no * wiremock data link :userinfo/GET/GET.json[] +// file not found, no * wiremock data link :resources/GET/GET.json[] ifdef::internal-generation[] ===== Implementation -// markup not found, no include::{specDir}userinfo/GET/implementation.adoc[opts=optional] +// markup not found, no include::{specDir}resources/GET/implementation.adoc[opts=optional] endif::internal-generation[] -[.jwks] -==== jwks +[.resourcesPost] +==== Resources POST -`GET /jwks` +`POST /resources` -A JSON Web Key (JWK) used by server. JWK is a JSON data structure that represents a set of public keys as a JSON object [RFC4627]. +Creates a new Resource reference in the Platform ===== Description -Provides list of JWK used by server. +This operation generates a new resource reference object that can be protected. Ownership ID is set to the unique ID of the End-User -// markup not found, no include::{specDir}jwks/GET/spec.adoc[opts=optional] +// markup not found, no include::{specDir}resources/POST/spec.adoc[opts=optional] ===== Parameters +===== Body Parameter + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| NewResource +| <> +| X +| +| + +|=== + + +====== Header Parameters + +[cols="2,3,1"] +|=== +|Name| Description| Required +| Authorization +| JWT or Bearer Token +| - + +|=== ===== Return Type -<> +<> ===== Content Type @@ -725,199 +553,98 @@ Provides list of JWK used by server. | 200 | OK -| <> +| <> + +| 401 +| UNAUTHORIZED +| <<>> -| 500 -| Internal error occured. Please check log file for details. -| <> + +| 404 +| NOT FOUND +| <<>> |=== ===== Samples -// markup not found, no include::{snippetDir}jwks/GET/http-request.adoc[opts=optional] +// markup not found, no include::{snippetDir}resources/POST/http-request.adoc[opts=optional] -// markup not found, no include::{snippetDir}jwks/GET/http-response.adoc[opts=optional] +// markup not found, no include::{snippetDir}resources/POST/http-response.adoc[opts=optional] -// file not found, no * wiremock data link :jwks/GET/GET.json[] +// file not found, no * wiremock data link :resources/POST/POST.json[] ifdef::internal-generation[] ===== Implementation -// markup not found, no include::{specDir}jwks/GET/implementation.adoc[opts=optional] +// markup not found, no include::{specDir}resources/POST/implementation.adoc[opts=optional] endif::internal-generation[] -[.postAuthorize] -==== POST Authorize +[.resourcesResourceIdDelete] +==== Resources DELETE -`POST /authorize` +`DELETE /resources/{resource_id}` -The Authorization Endpoint performs Authentication of the End-User. +Deletes an owned Resource Reference from the Platform ===== Description -End-User Authentication and Authorization done by sending the User Agent to the Authorization Endpoint using request parameters defined by OAuth 2.0 and OpenID Connect. +This operation removes an existing Resource reference owned by the user. -// markup not found, no include::{specDir}authorize/POST/spec.adoc[opts=optional] +// markup not found, no include::{specDir}resources/\{resource_id\}/DELETE/spec.adoc[opts=optional] ===== Parameters - - -===== Form Parameter +====== Path Parameters [cols="2,3,1"] |=== |Name| Description| Required -| scope -| OpenID Connect requests MUST contain the openid scope value. If the openid scope value is not present, the behavior is entirely unspecified. Other scope values MAY be present. <> +| resource_id +| Unique Resource ID | X -| response_type -| OAuth 2.0 Response Type value that determines the authorization processing flow to be used, including what parameters are returned from the endpoints used. <> -| X - +|=== -| client_id -| OAuth 2.0 Client Identifier valid at the Authorization Server. <> -| X - -| redirect_uri -| Redirection URI to which the response will be sent. This URI MUST exactly match one of the Redirection URI values for the Client pre-registered at the OpenID Provider. <> -| X - -| state -| Opaque value used to maintain state between the request and the callback. <> -| - - +====== Header Parameters -| response_mode -| Informs the Authorization Server of the mechanism to be used for returning parameters from the Authorization Endpoint. <> -| - - +[cols="2,3,1"] +|=== +|Name| Description| Required -| nonce -| String value used to associate a Client session with an ID Token, and to mitigate replay attacks. <> +| Authorization +| JWT or Bearer Token | - -| display -| ASCII string value that specifies how the Authorization Server displays the authentication and consent user interface pages to the End-User. <> -| - - +|=== -| prompt -| Space delimited, case sensitive list of ASCII string values that specifies whether the Authorization Server prompts the End-User for reauthentication and consent. <> -| - - -| max_age -| Maximum Authentication Age. Specifies the allowable elapsed time in seconds since the last time the End-User was actively authenticated by the OP. <> -| - - -| ui_locales -| End-User's preferred languages and scripts for the user interface, represented as a space-separated list of BCP47 [RFC5646] language tag values, ordered by preference. <> -| - - +===== Return Type -| id_token_hint -| ID Token previously issued by the Authorization Server being passed as a hint about the End-User's current or past authenticated session with the Client. If the End-User identified by the ID Token is logged in or is logged in by the request, then the Authorization Server returns a positive response. <> -| - - -| login_hint -| Hint to the Authorization Server about the login identifier the End-User might use to log in (if necessary). <> -| - - -| acr_values -| Requested Authentication Context Class Reference values. Space-separated string that specifies the acr values that the Authorization Server is being requested to use for processing this Authentication Request, with the values appearing in order of preference. <> -| - - +- -| amr_values -| AMR Values. <> -| - - -| request -| This parameter enables OpenID Connect requests to be passed in a single, self-contained parameter and to be optionally signed and/or encrypted. The parameter value is a Request Object value. It represents the request as a JWT whose Claims are the request parameters. <> -| - - - -| request_uri -| This parameter enables OpenID Connect requests to be passed by reference, rather than by value. The request_uri value is a URL using the https scheme referencing a resource containing a Request Object value, which is a JWT containing the request parameters. <> -| - - - -| request_session_id -| Request session id. <> -| - - - -| session_id -| Session id of this call. <> -| - - - -| origin_headers -| Origin headers. Used in custom workflows. <> -| - - - -| code_challenge -| PKCE code challenge. <> -| - - - -| code_challenge_method -| PKCE code challenge method. <> -| - - - -| custom_response_headers -| Custom Response Headers. <> -| - - - -| claims -| Requested Claims. <> -| - - - -|=== - - - - -===== Return Type - - - -- - -===== Content Type - -* application/json - -===== Responses +===== Responses .http response codes [cols="2,3,1"] @@ -930,82 +657,72 @@ End-User Authentication and Authorization done by sending the User Agent to the | <<>> -| 302 -| Error codes for authorization endpoint. -| <> - - -| 400 -| Invalid parameters are provided to endpoint. -| <> - - | 401 -| Unauthorized access request. -| <> +| UNAUTHORIZED +| <<>> -| 500 -| Internal error occured. Please check log file for details. -| <> +| 404 +| NOT FOUND +| <<>> |=== ===== Samples -// markup not found, no include::{snippetDir}authorize/POST/http-request.adoc[opts=optional] +// markup not found, no include::{snippetDir}resources/\{resource_id\}/DELETE/http-request.adoc[opts=optional] -// markup not found, no include::{snippetDir}authorize/POST/http-response.adoc[opts=optional] +// markup not found, no include::{snippetDir}resources/\{resource_id\}/DELETE/http-response.adoc[opts=optional] -// file not found, no * wiremock data link :authorize/POST/POST.json[] +// file not found, no * wiremock data link :resources/{resource_id}/DELETE/DELETE.json[] ifdef::internal-generation[] ===== Implementation -// markup not found, no include::{specDir}authorize/POST/implementation.adoc[opts=optional] +// markup not found, no include::{specDir}resources/\{resource_id\}/DELETE/implementation.adoc[opts=optional] endif::internal-generation[] -[.postClientinfo] -==== POST Clientinfo +[.resourcesResourceIdGet] +==== Resource GET (ID) -`POST /clientinfo` +`GET /resources/{resource_id}` -To get Claims details about the registered client. +Retrieve a specific owned resource ===== Description -The ClientInfo Endpoint is an OAuth 2.0 Protected Resource that returns Claims about the registered client. +This operation retrieves information about an owned resource. -// markup not found, no include::{specDir}clientinfo/POST/spec.adoc[opts=optional] +// markup not found, no include::{specDir}resources/\{resource_id\}/GET/spec.adoc[opts=optional] ===== Parameters - - -===== Form Parameter +====== Path Parameters [cols="2,3,1"] |=== |Name| Description| Required -| access_token -| Client-specific access token. <> +| resource_id +| Unique Resource ID | X |=== + + ====== Header Parameters [cols="2,3,1"] @@ -1013,7 +730,7 @@ The ClientInfo Endpoint is an OAuth 2.0 Protected Resource that returns Claims a |Name| Description| Required | Authorization -| +| JWT or Bearer Token | - @@ -1023,7 +740,7 @@ The ClientInfo Endpoint is an OAuth 2.0 Protected Resource that returns Claims a ===== Return Type -<> +<> ===== Content Type @@ -1040,248 +757,91 @@ The ClientInfo Endpoint is an OAuth 2.0 Protected Resource that returns Claims a | 200 | OK -| <> +| <> -| 400 -| Invalid Request are provided to endpoint. -| <> +| 404 +| NOT FOUND +| <<>> |=== ===== Samples -// markup not found, no include::{snippetDir}clientinfo/POST/http-request.adoc[opts=optional] +// markup not found, no include::{snippetDir}resources/\{resource_id\}/GET/http-request.adoc[opts=optional] -// markup not found, no include::{snippetDir}clientinfo/POST/http-response.adoc[opts=optional] +// markup not found, no include::{snippetDir}resources/\{resource_id\}/GET/http-response.adoc[opts=optional] -// file not found, no * wiremock data link :clientinfo/POST/POST.json[] +// file not found, no * wiremock data link :resources/{resource_id}/GET/GET.json[] ifdef::internal-generation[] ===== Implementation -// markup not found, no include::{specDir}clientinfo/POST/implementation.adoc[opts=optional] +// markup not found, no include::{specDir}resources/\{resource_id\}/GET/implementation.adoc[opts=optional] endif::internal-generation[] -[.postIntrospection] -==== POST Introspection +[.resourcesResourceIdPut] +==== Resource PUT (ID) -`POST /introspection` +`PUT /resources/{resource_id}` -The Introspection OAuth 2 Endpoint. +Updates an existing Resource reference in the Platform ===== Description -The Introspection OAuth 2 Endpoint. +This operation updates an existing 'owned' resource reference. -// markup not found, no include::{specDir}introspection/POST/spec.adoc[opts=optional] +// markup not found, no include::{specDir}resources/\{resource_id\}/PUT/spec.adoc[opts=optional] ===== Parameters - - -===== Form Parameter +====== Path Parameters [cols="2,3,1"] |=== |Name| Description| Required -| token -| Client access token. <> +| resource_id +| Unique Resource ID | X |=== -====== Header Parameters +===== Body Parameter [cols="2,3,1"] |=== |Name| Description| Required -| Authorization -| Client Authorization details that contains the access token along with other details. +| Resource +| <> | X - - -|=== - - - -===== Return Type - -<> - - -===== Content Type - -* application/json - -===== Responses - -.http response codes -[cols="2,3,1"] -|=== -| Code | Message | Datatype - - -| 200 -| OK -| <> - - -| 400 -| Error codes for introspection endpoint. -| <> - - -| 401 -| Unauthorized access request. -| <> - - -| 500 -| Internal error occured. Please check log file for details. -| <> +| +| |=== -===== Samples - - -// markup not found, no include::{snippetDir}introspection/POST/http-request.adoc[opts=optional] - - -// markup not found, no include::{snippetDir}introspection/POST/http-response.adoc[opts=optional] - - - -// file not found, no * wiremock data link :introspection/POST/POST.json[] - - -ifdef::internal-generation[] -===== Implementation - -// markup not found, no include::{specDir}introspection/POST/implementation.adoc[opts=optional] - - -endif::internal-generation[] - - -[.postToken] -==== POST Token - -`POST /token` - -To obtain an Access Token, an ID Token, and optionally a Refresh Token, the RP (Client). - -===== Description - -To obtain an Access Token, an ID Token, and optionally a Refresh Token, the RP (Client). - - -// markup not found, no include::{specDir}token/POST/spec.adoc[opts=optional] - - - -===== Parameters - - -===== Form Parameter +====== Header Parameters [cols="2,3,1"] |=== |Name| Description| Required -| grant_type -| Provide a list of the OAuth 2.0 grant types that the Client is declaring that it will restrict itself to using. <> -| X - - -| code -| Code which is returned by authorization endpoint. (For grant_type\=authorization_code) <> -| - - - -| redirect_uri -| Redirection URI to which the response will be sent. This URI MUST exactly match one of the Redirection URI values for the Client pre-registered at the OpenID Provider. <> -| - - - -| username -| End-User username. <> -| - - - -| password -| End-User password. <> -| - - - -| scope -| OpenID Connect requests MUST contain the openid scope value. If the openid scope value is not present, the behavior is entirely unspecified. Other scope values MAY be present. Scope values used that are not understood by an implementation SHOULD be ignored. <> -| - - - -| assertion -| Assertion. <> -| - - - -| refresh_token -| Refresh token. <> -| - - - -| client_id -| OAuth 2.0 Client Identifier valid at the Authorization Server. <> -| - - - -| client_secret -| The client secret. The client MAY omit the parameter if the client secret is an empty string. <> -| - - - -| code_verifier -| The client's PKCE code verifier. <> -| - - - -| ticket -| <> -| - - - -| claim_token -| <> -| - - - -| claim_token_format -| <> -| - - - -| pct -| <> -| - - - -| rpt -| <> +| Authorization +| JWT or Bearer Token | - @@ -1289,15 +849,12 @@ To obtain an Access Token, an ID Token, and optionally a Refresh Token, the RP ( - ===== Return Type -<> -===== Content Type +- -* application/json ===== Responses @@ -1309,4987 +866,115 @@ To obtain an Access Token, an ID Token, and optionally a Refresh Token, the RP ( | 200 | OK -| <> - - -| 400 -| Invalid parameters provided to endpoint. -| <> +| <<>> | 401 -| Unauthorized access request. -| <> - - -| 403 -| Invalid details provided hence access denied. -| <> +| UNAUTHORIZED +| <<>> -| 500 -| Internal error occured. Please check log file for details. -| <> +| 404 +| NOT FOUND +| <<>> |=== ===== Samples -// markup not found, no include::{snippetDir}token/POST/http-request.adoc[opts=optional] +// markup not found, no include::{snippetDir}resources/\{resource_id\}/PUT/http-request.adoc[opts=optional] -// markup not found, no include::{snippetDir}token/POST/http-response.adoc[opts=optional] +// markup not found, no include::{snippetDir}resources/\{resource_id\}/PUT/http-response.adoc[opts=optional] -// file not found, no * wiremock data link :token/POST/POST.json[] +// file not found, no * wiremock data link :resources/{resource_id}/PUT/PUT.json[] ifdef::internal-generation[] ===== Implementation -// markup not found, no include::{specDir}token/POST/implementation.adoc[opts=optional] +// markup not found, no include::{specDir}resources/\{resource_id\}/PUT/implementation.adoc[opts=optional] endif::internal-generation[] -[.postUserinfo] -==== POST Userinfo - -`POST /userinfo` - -Returns Claims about the authenticated End-User. - -===== Description - -Returns Claims about the authenticated End-User. - - -// markup not found, no include::{specDir}userinfo/POST/spec.adoc[opts=optional] - - - -===== Parameters - - - -===== Form Parameter - -[cols="2,3,1"] -|=== -|Name| Description| Required - -| access_token -| OAuth 2.0 Access Token. <> -| X - +[#models] +== Models -|=== -====== Header Parameters +[#NewResource] +=== _NewResource_ -[cols="2,3,1"] -|=== -|Name| Description| Required -| Authorization -| Client Authorization details that contains the access token along with other details. -| - - +[.fields-NewResource] +[cols="2,1,2,4,1"] |=== +| Field Name| Required| Type| Description| Format +| name +| +| String +| Human readable name for the resource +| +| icon_uri +| +| String +| Protected uri of the resource. +| -===== Return Type - +| scopes +| +| List of <> +| List of scopes associated with the resource +| -<> +|=== -===== Content Type +[#Resource] +=== _Resource_ -* application/jwt -* application/json -===== Responses -.http response codes -[cols="2,3,1"] +[.fields-Resource] +[cols="2,1,2,4,1"] |=== -| Code | Message | Datatype +| Field Name| Required| Type| Description| Format +| ownership_id +| +| UUID +| UUID of the Owner End-User +| uuid -| 200 -| OK -| <> +| id +| +| UUID +| UUID of the resource +| uuid +| name +| +| String +| Human readable name for the resource +| -| 400 -| Invalid parameters provided to endpoint. -| <> - - -| 401 -| Invalid parameters provided to endpoint. -| <> - - -| 403 -| Invalid parameters provided to endpoint. -| <> - - -| 500 -| Internal error occured. Please check log file for details. -| <> - -|=== - -===== Samples - - -// markup not found, no include::{snippetDir}userinfo/POST/http-request.adoc[opts=optional] - - -// markup not found, no include::{snippetDir}userinfo/POST/http-response.adoc[opts=optional] - - - -// file not found, no * wiremock data link :userinfo/POST/POST.json[] - - -ifdef::internal-generation[] -===== Implementation - -// markup not found, no include::{specDir}userinfo/POST/implementation.adoc[opts=optional] - - -endif::internal-generation[] - - -[.revoke] -==== revoke - -`POST /revoke` - -Revoke an Access Token or a Refresh Token, the RP (Client). - -===== Description - -Revoke an Access Token or a Refresh Token, the RP (Client). - - -// markup not found, no include::{specDir}revoke/POST/spec.adoc[opts=optional] - - - -===== Parameters - - - -===== Form Parameter - -[cols="2,3,1"] -|=== -|Name| Description| Required - -| token -| The token that the client wants to get revoked. <> -| X - - -| token_type_hint -| A hint about the type of the token submitted for revocation. <> -| - - - -|=== - - - - -===== Return Type - - - -- - -===== Content Type - -* content -* application/json - -===== Responses - -.http response codes -[cols="2,3,1"] -|=== -| Code | Message | Datatype - - -| 200 -| OK -| <<>> - - -| 400 -| Invalid parameters provided to endpoint. -| <> - -|=== - -===== Samples - - -// markup not found, no include::{snippetDir}revoke/POST/http-request.adoc[opts=optional] - - -// markup not found, no include::{snippetDir}revoke/POST/http-response.adoc[opts=optional] - - - -// file not found, no * wiremock data link :revoke/POST/POST.json[] - - -ifdef::internal-generation[] -===== Implementation - -// markup not found, no include::{specDir}revoke/POST/implementation.adoc[opts=optional] - - -endif::internal-generation[] - - -[.revokeSession] -==== revokeSession - -`POST /revoke_session` - -Revoke all sessions for user. - -===== Description - -Revoke all sessions for user (requires revoke_session scope). - - -// markup not found, no include::{specDir}revoke_session/POST/spec.adoc[opts=optional] - - - -===== Parameters - - - -===== Form Parameter - -[cols="2,3,1"] -|=== -|Name| Description| Required - -| user_criterion_key -| user criterion key (e.g. uid) <> -| X - - -| user_criterion_value -| user criterion value (e.g. chris) <> -| X - - -|=== - - - - -===== Return Type - - - -- - -===== Content Type - -* application/json - -===== Responses - -.http response codes -[cols="2,3,1"] -|=== -| Code | Message | Datatype - - -| 200 -| OK - Returned if request was processed successfully. Means it will return in case sessions are found as well as in case sessions are not found (error is not returned to not disclose internal information). -| <<>> - - -| 401 -| Unauthorized access request. -| <> - - -| 500 -| Internal error occured. Please check log file for details. -| <> - -|=== - -===== Samples - - -// markup not found, no include::{snippetDir}revoke_session/POST/http-request.adoc[opts=optional] - - -// markup not found, no include::{snippetDir}revoke_session/POST/http-response.adoc[opts=optional] - - - -// file not found, no * wiremock data link :revoke_session/POST/POST.json[] - - -ifdef::internal-generation[] -===== Implementation - -// markup not found, no include::{specDir}revoke_session/POST/implementation.adoc[opts=optional] - - -endif::internal-generation[] - - -[.sessionStatus] -==== sessionStatus - -`GET /session_status` - -Determine current sesion status. - -===== Description - -Determine current sesion status. - - -// markup not found, no include::{specDir}session_status/GET/spec.adoc[opts=optional] - - - -===== Parameters - - - - - - - -===== Return Type - -<> - - -===== Content Type - -* application/json - -===== Responses - -.http response codes -[cols="2,3,1"] -|=== -| Code | Message | Datatype - - -| 200 -| OK -| <> - -|=== - -===== Samples - - -// markup not found, no include::{snippetDir}session_status/GET/http-request.adoc[opts=optional] - - -// markup not found, no include::{snippetDir}session_status/GET/http-response.adoc[opts=optional] - - - -// file not found, no * wiremock data link :session_status/GET/GET.json[] - - -ifdef::internal-generation[] -===== Implementation - -// markup not found, no include::{specDir}session_status/GET/implementation.adoc[opts=optional] - - -endif::internal-generation[] - - -[.Authorization] -=== UMA - Authorization - - -[.deleteHostRsrcResourceSet] -==== DELETE HostRsrcResourceSet - -`DELETE /host/rsrc/resource_set/{rsid}` - -Deletes a previously registered resource. - -===== Description - -Deletes a previously registered resource. - - -// markup not found, no include::{specDir}host/rsrc/resource_set/\{rsid\}/DELETE/spec.adoc[opts=optional] - - - -===== Parameters - -====== Path Parameters - -[cols="2,3,1"] -|=== -|Name| Description| Required - -| rsid -| Resource ID -| X - - -|=== - - - -====== Header Parameters - -[cols="2,3,1"] -|=== -|Name| Description| Required - -| Authorization -| -| X - - -|=== - - - -===== Return Type - - - -- - -===== Content Type - -* application/json - -===== Responses - -.http response codes -[cols="2,3,1"] -|=== -| Code | Message | Datatype - - -| 204 -| OK -| <<>> - - -| 500 -| Invalid parameters provided to endpoint. -| <> - -|=== - -===== Samples - - -// markup not found, no include::{snippetDir}host/rsrc/resource_set/\{rsid\}/DELETE/http-request.adoc[opts=optional] - - -// markup not found, no include::{snippetDir}host/rsrc/resource_set/\{rsid\}/DELETE/http-response.adoc[opts=optional] - - - -// file not found, no * wiremock data link :host/rsrc/resource_set/{rsid}/DELETE/DELETE.json[] - - -ifdef::internal-generation[] -===== Implementation - -// markup not found, no include::{specDir}host/rsrc/resource_set/\{rsid\}/DELETE/implementation.adoc[opts=optional] - - -endif::internal-generation[] - - -[.getHostRsrcResourceSet] -==== GET HostRsrcResourceSet - -`GET /host/rsrc/resource_set` - -Lists all previously registered resource. - -===== Description - -Lists all previously registered resource. - - -// markup not found, no include::{specDir}host/rsrc/resource_set/GET/spec.adoc[opts=optional] - - - -===== Parameters - - - - -====== Header Parameters - -[cols="2,3,1"] -|=== -|Name| Description| Required - -| Authorization -| -| X - - -|=== - -====== Query Parameters - -[cols="2,3,1"] -|=== -|Name| Description| Required - -| scope -| Scope uri. -| - - - -|=== - - -===== Return Type - - -<> - - -===== Content Type - -* application/json - -===== Responses - -.http response codes -[cols="2,3,1"] -|=== -| Code | Message | Datatype - - -| 200 -| OK -| List[<>] - - -| 500 -| Invalid parameters provided to endpoint. -| <> - -|=== - -===== Samples - - -// markup not found, no include::{snippetDir}host/rsrc/resource_set/GET/http-request.adoc[opts=optional] - - -// markup not found, no include::{snippetDir}host/rsrc/resource_set/GET/http-response.adoc[opts=optional] - - - -// file not found, no * wiremock data link :host/rsrc/resource_set/GET/GET.json[] - - -ifdef::internal-generation[] -===== Implementation - -// markup not found, no include::{specDir}host/rsrc/resource_set/GET/implementation.adoc[opts=optional] - - -endif::internal-generation[] - - -[.getHostRsrcResourceSet/{rsid}] -==== GET HostRsrcResourceSet/{rsid} - -`GET /host/rsrc/resource_set/{rsid}` - -Reads a previously registered resource. - -===== Description - -Reads a previously registered resource. - - -// markup not found, no include::{specDir}host/rsrc/resource_set/\{rsid\}/GET/spec.adoc[opts=optional] - - - -===== Parameters - -====== Path Parameters - -[cols="2,3,1"] -|=== -|Name| Description| Required - -| rsid -| Resource description ID. -| X - - -|=== - - - -====== Header Parameters - -[cols="2,3,1"] -|=== -|Name| Description| Required - -| Authorization -| Client Authorization details that contains the access token along with other details. -| X - - -|=== - - - -===== Return Type - -<> - - -===== Content Type - -* application/json - -===== Responses - -.http response codes -[cols="2,3,1"] -|=== -| Code | Message | Datatype - - -| 200 -| OK -| <> - - -| 500 -| Invalid parameters provided to endpoint. -| <> - -|=== - -===== Samples - - -// markup not found, no include::{snippetDir}host/rsrc/resource_set/\{rsid\}/GET/http-request.adoc[opts=optional] - - -// markup not found, no include::{snippetDir}host/rsrc/resource_set/\{rsid\}/GET/http-response.adoc[opts=optional] - - - -// file not found, no * wiremock data link :host/rsrc/resource_set/{rsid}/GET/GET.json[] - - -ifdef::internal-generation[] -===== Implementation - -// markup not found, no include::{specDir}host/rsrc/resource_set/\{rsid\}/GET/implementation.adoc[opts=optional] - - -endif::internal-generation[] - - -[.getIntrospection] -==== GET Introspection - -`GET /introspection` - -The Introspection OAuth 2 Endpoint. - -===== Description - -The Introspection OAuth 2 Endpoint. - - -// markup not found, no include::{specDir}introspection/GET/spec.adoc[opts=optional] - - - -===== Parameters - - - - -====== Header Parameters - -[cols="2,3,1"] -|=== -|Name| Description| Required - -| Authorization -| Client Authorization details that contains the access token along with other details. -| X - - -|=== - -====== Query Parameters - -[cols="2,3,1"] -|=== -|Name| Description| Required - -| token -| -| X - - -| token_type_hint -| ID Token previously issued by the Authorization Server being passed as a hint about the End-User. -| - - - -| response_as_jwt -| OPTIONAL. Boolean value with default value false. If true, returns introspection response as JWT (signed based on client configuration used for authentication to Introspection Endpoint). -| - - - -|=== - - -===== Return Type - -<> - - -===== Content Type - -* application/json - -===== Responses - -.http response codes -[cols="2,3,1"] -|=== -| Code | Message | Datatype - - -| 200 -| OK -| <> - - -| 400 -| Error codes for introspection endpoint. -| <> - - -| 401 -| Unauthorized access request. -| <> - - -| 500 -| Internal error occured. Please check log file for details. -| <> - -|=== - -===== Samples - - -// markup not found, no include::{snippetDir}introspection/GET/http-request.adoc[opts=optional] - - -// markup not found, no include::{snippetDir}introspection/GET/http-response.adoc[opts=optional] - - - -// file not found, no * wiremock data link :introspection/GET/GET.json[] - - -ifdef::internal-generation[] -===== Implementation - -// markup not found, no include::{specDir}introspection/GET/implementation.adoc[opts=optional] - - -endif::internal-generation[] - - -[.getRptStatus] -==== GET RptStatus - -`GET /rpt/status` - -The Introspection OAuth 2 Endpoint for RPT. - -===== Description - -The Introspection OAuth 2 Endpoint for RPT. - - -// markup not found, no include::{specDir}rpt/status/GET/spec.adoc[opts=optional] - - - -===== Parameters - - - - -====== Header Parameters - -[cols="2,3,1"] -|=== -|Name| Description| Required - -| Authorization -| -| X - - -|=== - -====== Query Parameters - -[cols="2,3,1"] -|=== -|Name| Description| Required - -| token -| -| X - - -| token_type_hint -| -| - - - -|=== - - -===== Return Type - -<> - - -===== Content Type - -* application/json - -===== Responses - -.http response codes -[cols="2,3,1"] -|=== -| Code | Message | Datatype - - -| 200 -| OK -| <> - - -| 405 -| Introspection of RPT is not allowed. -| <> - - -| 500 -| Invalid parameters provided to endpoint. -| <> - -|=== - -===== Samples - - -// markup not found, no include::{snippetDir}rpt/status/GET/http-request.adoc[opts=optional] - - -// markup not found, no include::{snippetDir}rpt/status/GET/http-response.adoc[opts=optional] - - - -// file not found, no * wiremock data link :rpt/status/GET/GET.json[] - - -ifdef::internal-generation[] -===== Implementation - -// markup not found, no include::{specDir}rpt/status/GET/implementation.adoc[opts=optional] - - -endif::internal-generation[] - - -[.getUmaGatherClaims] -==== GET UmaGatherClaims - -`GET /uma/gather_claims` - -UMA Claims Gathering Endpoint. - -===== Description - -UMA Claims Gathering Endpoint. - - -// markup not found, no include::{specDir}uma/gather_claims/GET/spec.adoc[opts=optional] - - - -===== Parameters - - - - - -====== Query Parameters - -[cols="2,3,1"] -|=== -|Name| Description| Required - -| client_id -| OAuth 2.0 Client Identifier valid at the Authorization Server. -| - - - -| ticket -| -| - - - -| claims_redirect_uri -| -| - - - -| state -| -| - - - -| reset -| -| - - - -| authentication -| -| - - - -|=== - - -===== Return Type - - - -- - -===== Content Type - -* application/json - -===== Responses - -.http response codes -[cols="2,3,1"] -|=== -| Code | Message | Datatype - - -| 302 -| Resource Found. -| <<>> - - -| 400 -| Invalid parameters provided to endpoint. -| <> - - -| 500 -| Invalid parameters provided to endpoint. -| <> - -|=== - -===== Samples - - -// markup not found, no include::{snippetDir}uma/gather_claims/GET/http-request.adoc[opts=optional] - - -// markup not found, no include::{snippetDir}uma/gather_claims/GET/http-response.adoc[opts=optional] - - - -// file not found, no * wiremock data link :uma/gather_claims/GET/GET.json[] - - -ifdef::internal-generation[] -===== Implementation - -// markup not found, no include::{specDir}uma/gather_claims/GET/implementation.adoc[opts=optional] - - -endif::internal-generation[] - - -[.hostRsrcPr] -==== hostRsrcPr - -`POST /host/rsrc_pr` - -Registers permission. - -===== Description - -Registers permission. - - -// markup not found, no include::{specDir}host/rsrc_pr/POST/spec.adoc[opts=optional] - - - -===== Parameters - - - -===== Form Parameter - -[cols="2,3,1"] -|=== -|Name| Description| Required - -| resource_id -| The identifier for a resource to which this client is seeking access. The identifier MUST correspond to a resource that was previously registered. <> -| X - - -| resource_scopes -| An array referencing zero or more strings representing scopes to which access was granted for this resource. Each string MUST correspond to a scope that was registered by this resource server for the referenced resource. <> -| X - - -|=== - -====== Header Parameters - -[cols="2,3,1"] -|=== -|Name| Description| Required - -| Authorization -| Client Authorization details that contains the access token along with other details. -| X - - -|=== - - - -===== Return Type - -array[<>] - - -===== Content Type - -* application/json - -===== Responses - -.http response codes -[cols="2,3,1"] -|=== -| Code | Message | Datatype - - -| 201 -| OK -| List[<>] - - -| 500 -| Invalid parameters provided to endpoint. -| <> - -|=== - -===== Samples - - -// markup not found, no include::{snippetDir}host/rsrc_pr/POST/http-request.adoc[opts=optional] - - -// markup not found, no include::{snippetDir}host/rsrc_pr/POST/http-response.adoc[opts=optional] - - - -// file not found, no * wiremock data link :host/rsrc_pr/POST/POST.json[] - - -ifdef::internal-generation[] -===== Implementation - -// markup not found, no include::{specDir}host/rsrc_pr/POST/implementation.adoc[opts=optional] - - -endif::internal-generation[] - - -[.jwks] -==== jwks - -`GET /jwks` - -A JSON Web Key (JWK) used by server. JWK is a JSON data structure that represents a set of public keys as a JSON object [RFC4627]. - -===== Description - -Provides list of JWK used by server. - - -// markup not found, no include::{specDir}jwks/GET/spec.adoc[opts=optional] - - - -===== Parameters - - - - - - - -===== Return Type - -<> - - -===== Content Type - -* application/json - -===== Responses - -.http response codes -[cols="2,3,1"] -|=== -| Code | Message | Datatype - - -| 200 -| OK -| <> - - -| 500 -| Internal error occured. Please check log file for details. -| <> - -|=== - -===== Samples - - -// markup not found, no include::{snippetDir}jwks/GET/http-request.adoc[opts=optional] - - -// markup not found, no include::{snippetDir}jwks/GET/http-response.adoc[opts=optional] - - - -// file not found, no * wiremock data link :jwks/GET/GET.json[] - - -ifdef::internal-generation[] -===== Implementation - -// markup not found, no include::{specDir}jwks/GET/implementation.adoc[opts=optional] - - -endif::internal-generation[] - - -[.postHostRsrcResourceSet] -==== POST HostRsrcResourceSet - -`POST /host/rsrc/resource_set` - -Adds a new resource description. - -===== Description - -Adds a new resource description. - - -// markup not found, no include::{specDir}host/rsrc/resource_set/POST/spec.adoc[opts=optional] - - - -===== Parameters - - -===== Body Parameter - -[cols="2,3,1"] -|=== -|Name| Description| Required - -| UmaResource -| <> -| - -| -| - -|=== - - -====== Header Parameters - -[cols="2,3,1"] -|=== -|Name| Description| Required - -| Authorization -| Client Authorization details that contains the access token along with other details. -| X - - -|=== - - - -===== Return Type - -<> - - -===== Content Type - -* application/json - -===== Responses - -.http response codes -[cols="2,3,1"] -|=== -| Code | Message | Datatype - - -| 201 -| OK -| <> - - -| 500 -| Invalid parameters provided to endpoint. -| <> - -|=== - -===== Samples - - -// markup not found, no include::{snippetDir}host/rsrc/resource_set/POST/http-request.adoc[opts=optional] - - -// markup not found, no include::{snippetDir}host/rsrc/resource_set/POST/http-response.adoc[opts=optional] - - - -// file not found, no * wiremock data link :host/rsrc/resource_set/POST/POST.json[] - - -ifdef::internal-generation[] -===== Implementation - -// markup not found, no include::{specDir}host/rsrc/resource_set/POST/implementation.adoc[opts=optional] - - -endif::internal-generation[] - - -[.postIntrospection] -==== POST Introspection - -`POST /introspection` - -The Introspection OAuth 2 Endpoint. - -===== Description - -The Introspection OAuth 2 Endpoint. - - -// markup not found, no include::{specDir}introspection/POST/spec.adoc[opts=optional] - - - -===== Parameters - - - -===== Form Parameter - -[cols="2,3,1"] -|=== -|Name| Description| Required - -| token -| Client access token. <> -| X - - -|=== - -====== Header Parameters - -[cols="2,3,1"] -|=== -|Name| Description| Required - -| Authorization -| Client Authorization details that contains the access token along with other details. -| X - - -|=== - - - -===== Return Type - -<> - - -===== Content Type - -* application/json - -===== Responses - -.http response codes -[cols="2,3,1"] -|=== -| Code | Message | Datatype - - -| 200 -| OK -| <> - - -| 400 -| Error codes for introspection endpoint. -| <> - - -| 401 -| Unauthorized access request. -| <> - - -| 500 -| Internal error occured. Please check log file for details. -| <> - -|=== - -===== Samples - - -// markup not found, no include::{snippetDir}introspection/POST/http-request.adoc[opts=optional] - - -// markup not found, no include::{snippetDir}introspection/POST/http-response.adoc[opts=optional] - - - -// file not found, no * wiremock data link :introspection/POST/POST.json[] - - -ifdef::internal-generation[] -===== Implementation - -// markup not found, no include::{specDir}introspection/POST/implementation.adoc[opts=optional] - - -endif::internal-generation[] - - -[.postRptStatus] -==== POST RptStatus - -`POST /rpt/status` - -The Introspection OAuth 2 Endpoint for RPT. - -===== Description - -The Introspection OAuth 2 Endpoint for RPT. - - -// markup not found, no include::{specDir}rpt/status/POST/spec.adoc[opts=optional] - - - -===== Parameters - - - -===== Form Parameter - -[cols="2,3,1"] -|=== -|Name| Description| Required - -| token -| Client access token. <> -| X - - -| token_type_hint -| ID Token previously issued by the Authorization Server being passed as a hint about the End-User. <> -| - - - -|=== - -====== Header Parameters - -[cols="2,3,1"] -|=== -|Name| Description| Required - -| Authorization -| Client Authorization details that contains the access token along with other details. -| X - - -|=== - - - -===== Return Type - -<> - - -===== Content Type - -* application/json - -===== Responses - -.http response codes -[cols="2,3,1"] -|=== -| Code | Message | Datatype - - -| 200 -| OK -| <> - - -| 405 -| Introspection of RPT is not allowed. -| <> - - -| 500 -| Invalid parameters provided to endpoint. -| <> - -|=== - -===== Samples - - -// markup not found, no include::{snippetDir}rpt/status/POST/http-request.adoc[opts=optional] - - -// markup not found, no include::{snippetDir}rpt/status/POST/http-response.adoc[opts=optional] - - - -// file not found, no * wiremock data link :rpt/status/POST/POST.json[] - - -ifdef::internal-generation[] -===== Implementation - -// markup not found, no include::{specDir}rpt/status/POST/implementation.adoc[opts=optional] - - -endif::internal-generation[] - - -[.postToken] -==== POST Token - -`POST /token` - -To obtain an Access Token, an ID Token, and optionally a Refresh Token, the RP (Client). - -===== Description - -To obtain an Access Token, an ID Token, and optionally a Refresh Token, the RP (Client). - - -// markup not found, no include::{specDir}token/POST/spec.adoc[opts=optional] - - - -===== Parameters - - - -===== Form Parameter - -[cols="2,3,1"] -|=== -|Name| Description| Required - -| grant_type -| Provide a list of the OAuth 2.0 grant types that the Client is declaring that it will restrict itself to using. <> -| X - - -| code -| Code which is returned by authorization endpoint. (For grant_type\=authorization_code) <> -| - - - -| redirect_uri -| Redirection URI to which the response will be sent. This URI MUST exactly match one of the Redirection URI values for the Client pre-registered at the OpenID Provider. <> -| - - - -| username -| End-User username. <> -| - - - -| password -| End-User password. <> -| - - - -| scope -| OpenID Connect requests MUST contain the openid scope value. If the openid scope value is not present, the behavior is entirely unspecified. Other scope values MAY be present. Scope values used that are not understood by an implementation SHOULD be ignored. <> -| - - - -| assertion -| Assertion. <> -| - - - -| refresh_token -| Refresh token. <> -| - - - -| client_id -| OAuth 2.0 Client Identifier valid at the Authorization Server. <> -| - - - -| client_secret -| The client secret. The client MAY omit the parameter if the client secret is an empty string. <> -| - - - -| code_verifier -| The client's PKCE code verifier. <> -| - - - -| ticket -| <> -| - - - -| claim_token -| <> -| - - - -| claim_token_format -| <> -| - - - -| pct -| <> -| - - - -| rpt -| <> -| - - - -|=== - - - - -===== Return Type - -<> - - -===== Content Type - -* application/json - -===== Responses - -.http response codes -[cols="2,3,1"] -|=== -| Code | Message | Datatype - - -| 200 -| OK -| <> - - -| 400 -| Invalid parameters provided to endpoint. -| <> - - -| 401 -| Unauthorized access request. -| <> - - -| 403 -| Invalid details provided hence access denied. -| <> - - -| 500 -| Internal error occured. Please check log file for details. -| <> - -|=== - -===== Samples - - -// markup not found, no include::{snippetDir}token/POST/http-request.adoc[opts=optional] - - -// markup not found, no include::{snippetDir}token/POST/http-response.adoc[opts=optional] - - - -// file not found, no * wiremock data link :token/POST/POST.json[] - - -ifdef::internal-generation[] -===== Implementation - -// markup not found, no include::{specDir}token/POST/implementation.adoc[opts=optional] - - -endif::internal-generation[] - - -[.postUmaGatherClaims] -==== POST UmaGatherClaims - -`POST /uma/gather_claims` - -UMA Claims Gathering Endpoint - -===== Description - -UMA Claims Gathering Endpoint - - -// markup not found, no include::{specDir}uma/gather_claims/POST/spec.adoc[opts=optional] - - - -===== Parameters - - - -===== Form Parameter - -[cols="2,3,1"] -|=== -|Name| Description| Required - -| client_id -| OAuth 2.0 Client Identifier valid at the Authorization Server. <> -| - - - -| ticket -| <> -| - - - -| claims_redirect_uri -| <> -| - - - -| state -| <> -| - - - -| reset -| <> -| - - - -| authentication -| <> -| - - - -|=== - - - - -===== Return Type - - - -- - -===== Content Type - -* application/json - -===== Responses - -.http response codes -[cols="2,3,1"] -|=== -| Code | Message | Datatype - - -| 302 -| Resource Found. -| <<>> - - -| 400 -| Invalid parameters provided to endpoint. -| <> - - -| 500 -| Invalid parameters provided to endpoint. -| <> - -|=== - -===== Samples - - -// markup not found, no include::{snippetDir}uma/gather_claims/POST/http-request.adoc[opts=optional] - - -// markup not found, no include::{snippetDir}uma/gather_claims/POST/http-response.adoc[opts=optional] - - - -// file not found, no * wiremock data link :uma/gather_claims/POST/POST.json[] - - -ifdef::internal-generation[] -===== Implementation - -// markup not found, no include::{specDir}uma/gather_claims/POST/implementation.adoc[opts=optional] - - -endif::internal-generation[] - - -[.putHostRsrcResourceSet{rsid}] -==== PUT HostRsrcResourceSet{rsid} - -`PUT /host/rsrc/resource_set/{rsid}` - -Updates a previously registered resource. - -===== Description - -Updates a previously registered resource. - - -// markup not found, no include::{specDir}host/rsrc/resource_set/\{rsid\}/PUT/spec.adoc[opts=optional] - - - -===== Parameters - -====== Path Parameters - -[cols="2,3,1"] -|=== -|Name| Description| Required - -| rsid -| Resource ID. -| X - - -|=== - -===== Body Parameter - -[cols="2,3,1"] -|=== -|Name| Description| Required - -| UmaResource1 -| <> -| - -| -| - -|=== - - -====== Header Parameters - -[cols="2,3,1"] -|=== -|Name| Description| Required - -| Authorization -| -| X - - -|=== - - - -===== Return Type - -<> - - -===== Content Type - -* application/json - -===== Responses - -.http response codes -[cols="2,3,1"] -|=== -| Code | Message | Datatype - - -| 200 -| OK -| <> - - -| 404 -| Invalid parameters provided to endpoint. -| <> - - -| 500 -| Invalid parameters provided to endpoint. -| <> - -|=== - -===== Samples - - -// markup not found, no include::{snippetDir}host/rsrc/resource_set/\{rsid\}/PUT/http-request.adoc[opts=optional] - - -// markup not found, no include::{snippetDir}host/rsrc/resource_set/\{rsid\}/PUT/http-response.adoc[opts=optional] - - - -// file not found, no * wiremock data link :host/rsrc/resource_set/{rsid}/PUT/PUT.json[] - - -ifdef::internal-generation[] -===== Implementation - -// markup not found, no include::{specDir}host/rsrc/resource_set/\{rsid\}/PUT/implementation.adoc[opts=optional] - - -endif::internal-generation[] - - -[.revoke] -==== revoke - -`POST /revoke` - -Revoke an Access Token or a Refresh Token, the RP (Client). - -===== Description - -Revoke an Access Token or a Refresh Token, the RP (Client). - - -// markup not found, no include::{specDir}revoke/POST/spec.adoc[opts=optional] - - - -===== Parameters - - - -===== Form Parameter - -[cols="2,3,1"] -|=== -|Name| Description| Required - -| token -| The token that the client wants to get revoked. <> -| X - - -| token_type_hint -| A hint about the type of the token submitted for revocation. <> -| - - - -|=== - - - - -===== Return Type - - - -- - -===== Content Type - -* content -* application/json - -===== Responses - -.http response codes -[cols="2,3,1"] -|=== -| Code | Message | Datatype - - -| 200 -| OK -| <<>> - - -| 400 -| Invalid parameters provided to endpoint. -| <> - -|=== - -===== Samples - - -// markup not found, no include::{snippetDir}revoke/POST/http-request.adoc[opts=optional] - - -// markup not found, no include::{snippetDir}revoke/POST/http-response.adoc[opts=optional] - - - -// file not found, no * wiremock data link :revoke/POST/POST.json[] - - -ifdef::internal-generation[] -===== Implementation - -// markup not found, no include::{specDir}revoke/POST/implementation.adoc[opts=optional] - - -endif::internal-generation[] - - -[.uma2Configuration] -==== uma2Configuration - -`GET /uma2-configuration` - -Gets UMA configuration data. - -===== Description - -Gets UMA configuration data. - - -// markup not found, no include::{specDir}uma2-configuration/GET/spec.adoc[opts=optional] - - - -===== Parameters - - - - - - - -===== Return Type - -<> - - -===== Content Type - -* application/json - -===== Responses - -.http response codes -[cols="2,3,1"] -|=== -| Code | Message | Datatype - - -| 200 -| OK -| <> - - -| 500 -| Invalid parameters provided to endpoint. -| <> - -|=== - -===== Samples - - -// markup not found, no include::{snippetDir}uma2-configuration/GET/http-request.adoc[opts=optional] - - -// markup not found, no include::{snippetDir}uma2-configuration/GET/http-response.adoc[opts=optional] - - - -// file not found, no * wiremock data link :uma2-configuration/GET/GET.json[] - - -ifdef::internal-generation[] -===== Implementation - -// markup not found, no include::{specDir}uma2-configuration/GET/implementation.adoc[opts=optional] - - -endif::internal-generation[] - - -[.Registration] -=== Registration - - -[.deleteRegister] -==== DELETE Register - -`DELETE /register` - -Deletes the client info for a previously registered client. - -===== Description - -The Client Registration Endpoint removes the Client Metadata for a previously registered client. - - -// markup not found, no include::{specDir}register/DELETE/spec.adoc[opts=optional] - - - -===== Parameters - - - - -====== Header Parameters - -[cols="2,3,1"] -|=== -|Name| Description| Required - -| Authorization -| Authorization header carrying \\\"registration_access_token\\\" issued before as a Bearer token -| X - - -|=== - -====== Query Parameters - -[cols="2,3,1"] -|=== -|Name| Description| Required - -| client_id -| Client ID that identifies client. -| X - - -|=== - - -===== Return Type - - - -- - -===== Content Type - -* application/json - -===== Responses - -.http response codes -[cols="2,3,1"] -|=== -| Code | Message | Datatype - - -| 204 -| OK -| <<>> - - -| 400 -| Invalid parameters provided to endpoint. -| <> - - -| 401 -| Invalid parameters provided to endpoint. -| <> - - -| 500 -| Internal error occured. Please check log file for details. -| <> - -|=== - -===== Samples - - -// markup not found, no include::{snippetDir}register/DELETE/http-request.adoc[opts=optional] - - -// markup not found, no include::{snippetDir}register/DELETE/http-response.adoc[opts=optional] - - - -// file not found, no * wiremock data link :register/DELETE/DELETE.json[] - - -ifdef::internal-generation[] -===== Implementation - -// markup not found, no include::{specDir}register/DELETE/implementation.adoc[opts=optional] - - -endif::internal-generation[] - - -[.getRegister] -==== GET Register - -`GET /register` - -Get client information for a previously registered client. - -===== Description - -Get client information for a previously registered client. - - -// markup not found, no include::{specDir}register/GET/spec.adoc[opts=optional] - - - -===== Parameters - - - - -====== Header Parameters - -[cols="2,3,1"] -|=== -|Name| Description| Required - -| Authorization -| Authorization header carrying \\\"registration_access_token\\\" issued before as a Bearer token -| X - - -|=== - -====== Query Parameters - -[cols="2,3,1"] -|=== -|Name| Description| Required - -| client_id -| Client ID that identifies client. -| X - - -|=== - - -===== Return Type - -<> - - -===== Content Type - -* application/json - -===== Responses - -.http response codes -[cols="2,3,1"] -|=== -| Code | Message | Datatype - - -| 200 -| OK -| <> - - -| 400 -| Invalid parameters provided to endpoint. -| <> - - -| 401 -| Invalid parameters are provided to endpoint. -| <> - - -| 500 -| Internal error occured. Please check log file for details. -| <> - -|=== - -===== Samples - - -// markup not found, no include::{snippetDir}register/GET/http-request.adoc[opts=optional] - - -// markup not found, no include::{snippetDir}register/GET/http-response.adoc[opts=optional] - - - -// file not found, no * wiremock data link :register/GET/GET.json[] - - -ifdef::internal-generation[] -===== Implementation - -// markup not found, no include::{specDir}register/GET/implementation.adoc[opts=optional] - - -endif::internal-generation[] - - -[.postRegister] -==== POST Register - -`POST /register` - -Registers new client dynamically. - -===== Description - -The Client Registration Endpoint is an OAuth 2.0 Protected Resource through which a new Client registration can be requested. - - -// markup not found, no include::{specDir}register/POST/spec.adoc[opts=optional] - - - -===== Parameters - - -===== Body Parameter - -[cols="2,3,1"] -|=== -|Name| Description| Required - -| RegisterParams1 -| <> -| - -| -| - -|=== - - - - - -===== Return Type - -<> - - -===== Content Type - -* application/json - -===== Responses - -.http response codes -[cols="2,3,1"] -|=== -| Code | Message | Datatype - - -| 200 -| OK -| <> - - -| 400 -| Invalid parameters provided to endpoint. -| <> - - -| 500 -| Internal error occured. Please check log file for details. -| <> - -|=== - -===== Samples - - -// markup not found, no include::{snippetDir}register/POST/http-request.adoc[opts=optional] - - -// markup not found, no include::{snippetDir}register/POST/http-response.adoc[opts=optional] - - - -// file not found, no * wiremock data link :register/POST/POST.json[] - - -ifdef::internal-generation[] -===== Implementation - -// markup not found, no include::{specDir}register/POST/implementation.adoc[opts=optional] - - -endif::internal-generation[] - - -[.putRegister] -==== PUT Register - -`PUT /register` - -Updates Client Metadata for a registered client. - -===== Description - -Updates Client Metadata for a registered client. - - -// markup not found, no include::{specDir}register/PUT/spec.adoc[opts=optional] - - - -===== Parameters - - -===== Body Parameter - -[cols="2,3,1"] -|=== -|Name| Description| Required - -| RegisterParams -| <> -| - -| -| - -|=== - - -====== Header Parameters - -[cols="2,3,1"] -|=== -|Name| Description| Required - -| Authorization -| Authorization header carrying \\\"registration_access_token\\\" issued before as a Bearer token -| X - - -|=== - -====== Query Parameters - -[cols="2,3,1"] -|=== -|Name| Description| Required - -| client_id -| Client ID that identifies client that must be updated by this request. -| X - - -|=== - - -===== Return Type - -<> - - -===== Content Type - -* application/json - -===== Responses - -.http response codes -[cols="2,3,1"] -|=== -| Code | Message | Datatype - - -| 200 -| OK -| <> - - -| 400 -| Invalid parameters provided to endpoint. -| <> - - -| 500 -| Internal error occured. Please check log file for details. -| <> - -|=== - -===== Samples - - -// markup not found, no include::{snippetDir}register/PUT/http-request.adoc[opts=optional] - - -// markup not found, no include::{snippetDir}register/PUT/http-response.adoc[opts=optional] - - - -// file not found, no * wiremock data link :register/PUT/PUT.json[] - - -ifdef::internal-generation[] -===== Implementation - -// markup not found, no include::{specDir}register/PUT/implementation.adoc[opts=optional] - - -endif::internal-generation[] - - -[#models] -== Models - - -[#AuthorizeError] -=== _AuthorizeError_ - - - -[.fields-AuthorizeError] -[cols="2,1,2,4,1"] -|=== -| Field Name| Required| Type| Description| Format - -| error -| X -| String -| -| enum - -| error_description -| X -| String -| -| - -| details -| -| String -| -| - -|=== - - -[#ClientInfoResponse] -=== _ClientInfoResponse_ - -Client details in response. - -[.fields-ClientInfoResponse] -[cols="2,1,2,4,1"] -|=== -| Field Name| Required| Type| Description| Format - -| displayName -| -| String -| -| - -| inum -| -| String -| XRI i-number -| - -| oxAuthAppType -| -| String -| oxAuth Appication type -| - -| oxAuthIdTokenSignedResponseAlg -| -| String -| oxAuth ID Token Signed Response Algorithm -| - -| oxAuthRedirectURI -| -| List of <> -| Array of redirect URIs values used in the Authorization -| - -| oxId -| -| String -| oxAuth Attribute Scope Id -| - -| custom_attributes -| -| List of <> -| -| - -|=== - - -[#ClientResponse] -=== _ClientResponse_ - - - -[.fields-ClientResponse] -[cols="2,1,2,4,1"] -|=== -| Field Name| Required| Type| Description| Format - -| redirect_uris -| -| List of <> -| Redirection URI values used by the Client. One of these registered Redirection URI values must exactly match the redirect_uri parameter value used in each Authorization Request -| - -| claims_redirect_uri -| -| List of <> -| Array of The Claims Redirect URIs to which the client wishes the authorization server to direct the requesting party's user agent after completing its interaction. -| - -| response_types -| -| List of <> -| A list of the OAuth 2.0 response_type values that the Client is declaring that it will restrict itself to using. If omitted, the default is that the Client will use only the code Response Type. Allowed values are code, token, id_token. -| - -| grant_types -| -| List of <> -| A list of the OAuth 2.0 Grant Types that the Client is declaring that it will restrict itself to using. -| - -| contacts -| -| List of <> -| e-mail addresses of people responsible for this Client. -| - -| client_name -| -| String -| Name of the Client to be presented to the user. -| - -| logo_uri -| -| String -| URL that references a logo for the Client application -| - -| client_uri -| -| String -| URL of the home page of the Client. The value of this field must point to a valid Web page. -| - -| policy_uri -| -| String -| URL that the Relying Party Client provides to the End-User to read about the how the profile data will be used. -| - -| tos_uri -| -| String -| URL that the Relying Party Client provides to the End-User to read about the Relying Party's terms of service. -| - -| jwks_uri -| -| String -| URL for the Client's JSON Web Key Set (JWK) document containing key(s) that are used for signing requests to the OP. The JWK Set may also contain the Client's encryption keys(s) that are used by the OP to encrypt the responses to the Client. When both signing and encryption keys are made available, a use (Key Use) parameter value is required for all keys in the document to indicate each key's intended usage . -| - -| jwks -| -| String -| Client's JSON Web Key Set (JWK) document, passed by value. The semantics of the jwks parameter are the same as the jwks_uri parameter, other than that the JWK Set is passed by value, rather than by reference. This parameter is intended only to be used by Clients that, for some reason, are unable to use the jwks_uri parameter, for instance, by native applications that might not have a location to host the contents of the JWK Set. If a Client can use jwks_uri, it must not use jwks. One significant downside of jwks is that it does not enable key rotation. The jwks_uri and jwks parameters must not be used together. -| - -| sector_identifier_uri -| -| String -| URL using the https scheme to be used in calculating Pseudonymous Identifiers by the OP. -| - -| subject_type -| -| String -| Subject type requested for the Client ID. Valid types include pairwise and public. -| - -| rpt_as_jwt -| -| Boolean -| Specifies whether RPT should be return as signed JWT. -| - -| access_token_as_jwt -| -| Boolean -| Specifies whether access token as signed JWT. -| - -| access_token_signing_alg -| -| String -| Specifies signing algorithm that has to be used during JWT signing. If it's not specified, then the default OP signing algorithm will be used . -| - -| id_token_signed_response_alg -| -| String -| JWS alg algorithm (JWA) required for signing the ID Token issued to this Client. -| - -| id_token_encrypted_response_alg -| -| String -| JWE alg algorithm (JWA) required for encrypting the ID Token issued to this Client. -| - -| id_token_encrypted_response_enc -| -| String -| JWE enc algorithm (JWA) required for encrypting the ID Token issued to this Client. -| - -| userinfo_signed_response_alg -| -| String -| JWS alg algorithm (JWA) required for signing UserInfo Responses. -| - -| userinfo_encrypted_response_alg -| -| String -| JWE alg algorithm (JWA) required for encrypting UserInfo Responses. -| - -| userinfo_encrypted_response_enc -| -| String -| JWE enc algorithm (JWA) required for encrypting UserInfo Responses. -| - -| request_object_signing_alg -| -| String -| JWS alg algorithm (JWA) that must be used for signing Request Objects sent to the OP. -| - -| request_object_encryption_alg -| -| String -| JWE alg algorithm (JWA) the RP is declaring that it may use for encrypting Request Objects sent to the OP. -| - -| request_object_encryption_enc -| -| String -| JWE enc algorithm (JWA) the RP is declaring that it may use for encrypting Request Objects sent to the OP. -| - -| token_endpoint_auth_method -| -| String -| Requested Client Authentication method for the Token Endpoint. -| - -| token_endpoint_auth_signing_alg -| -| String -| JWS alg algorithm (JWA) that must be used for signing the JWT used to authenticate the Client at the Token Endpoint for the private_key_jwt and client_secret_jwt authentication methods. -| - -| default_max_age -| -| Integer -| Specifies the Default Maximum Authentication Age. -| - -| require_auth_time -| -| Boolean -| Boolean value specifying whether the auth_time Claim in the ID Token is required. It is required when the value is true. -| - -| default_acr_values -| -| List of <> -| Array of default requested Authentication Context Class Reference values that the Authorization Server must use for processing requests from the Client. -| - -| initiate_login_uri -| -| String -| Specifies the URI using the https scheme that the authorization server can call to initiate a login at the client. -| - -| post_logout_redirect_uris -| -| List of <> -| Provide the URLs supplied by the RP to request that the user be redirected to this location after a logout has been performed. -| - -| frontchannel_logout_uri -| -| String -| RP URL that will cause the RP to log itself out when rendered in an iframe by the OP. -| - -| frontchannel_logout_session_required -| -| Boolean -| Boolean value specifying whether the RP requires that a session ID query parameter be included to identify the RP session at the OP when the logout_uri is used. If omitted, the default value is false. -| - -| backchannel_logout_uri -| -| String -| RP URL that will cause the RP to log itself out when sent a Logout Token by the OP. -| - -| backchannel_logout_session_required -| -| Boolean -| Boolean value specifying whether the RP requires that a session ID Claim be included in the Logout Token to identify the RP session with the OP when the backchannel_logout_uri is used. If omitted, the default value is false. -| - -| request_uris -| -| List of <> -| Provide a list of request_uri values that are pre-registered by the Client for use at the Authorization Server. -| - -| scopes -| -| String -| This param will be removed in a future version because the correct is 'scope' not 'scopes', see (rfc7591). -| - -| claims -| -| String -| String containing a space-separated list of claims that can be requested individually. -| - -| id_token_token_binding_cnf -| -| String -| Specifies the JWT Confirmation Method member name (e.g. tbh) that the Relying Party expects when receiving Token Bound ID Tokens. The presence of this parameter indicates that the Relying Party supports Token Binding of ID Tokens. If omitted, the default is that the Relying Party does not support Token Binding of ID Tokens. -| - -| tls_client_auth_subject_dn -| -| String -| An string representation of the expected subject distinguished name of the certificate, which the OAuth client will use in mutual TLS authentication. -| - -| allow_spontaneous_scopes -| -| Boolean -| Specifies whether to allow spontaneous scopes for client. The default value is false. -| - -| spontaneous_scopes -| -| List of <> -| List of spontaneous scopes -| - -| run_introspection_script_before_access_token_as_jwt_creation_and_include_claims -| -| Boolean -| Boolean value with default value false. If true and access_token_as_jwt\=true then run introspection script and transfer claims into JWT. -| - -| keep_client_authorization_after_expiration -| -| Boolean -| Boolean value indicating if the client authorization will not be removed afer expiration (expiration date is same as client's expiration that created it). The default value is false. -| - -| scope -| -| List of <> -| Provide list of scope which are used during authentication to authorize access to resource. -| - -| authorized_origins -| -| List of <> -| specifies authorized JavaScript origins. -| - -| access_token_lifetime -| -| Integer -| Specifies the Client-specific access token expiration. -| - -| software_id -| -| String -| Specifies a unique identifier string (UUID) assigned by the client developer or software publisher used by registration endpoints to identify the client software to be dynamically registered. -| - -| software_version -| -| String -| Specifies a version identifier string for the client software identified by 'software_id'. The value of the 'software_version' should change on any update to the client software identified by the same 'software_id'. -| - -| software_statement -| -| String -| specifies a software statement containing client metadata values about the client software as claims. This is a string value containing the entire signed JWT. -| - -| backchannel_token_delivery_mode -| -| String -| specifies how backchannel token will be deliveried. -| - -| backchannel_client_notification_endpoint -| -| String -| Client Initiated Backchannel Authentication (CIBA) enables a Client to initiate the authentication of an end-user by means of out-of-band mechanisms. Upon receipt of the notification, the Client makes a request to the token endpoint to obtain the tokens. -| - -| backchannel_authentication_request_signing_alg -| -| String -| The JWS algorithm alg value that the Client will use for signing authentication request, as described in Section 7.1.1. of OAuth 2.0 [RFC6749]. When omitted, the Client will not send signed authentication requests. -| - -| backchannel_user_code_parameter -| -| Boolean -| Boolean value specifying whether the Client supports the user_code parameter. If omitted, the default value is false. -| - -|=== - - -[#EndSessionError] -=== _EndSessionError_ - - - -[.fields-EndSessionError] -[cols="2,1,2,4,1"] -|=== -| Field Name| Required| Type| Description| Format - -| error -| X -| String -| -| enum - -| error_description -| X -| String -| -| - -| details -| -| String -| -| - -|=== - - -[#ErrorResponse] -=== _ErrorResponse_ - - - -[.fields-ErrorResponse] -[cols="2,1,2,4,1"] -|=== -| Field Name| Required| Type| Description| Format - -| error -| X -| String -| -| - -| error_description -| X -| String -| -| - -| details -| -| String -| -| - -|=== - - -[#InlineResponse200] -=== _InlineResponse200_ - -AccessTokenResponse. - -[.fields-InlineResponse200] -[cols="2,1,2,4,1"] -|=== -| Field Name| Required| Type| Description| Format - -| access_token -| X -| String -| The access token issued by the authorization server. -| - -| token_type -| X -| String -| The access token type provides the client with the information required to successfully utilize the access token to make a protected resource request (along with type-specific attributes). -| - -| expires_in -| -| Integer -| The lifetime in seconds of the access token. For example, the value \\\"3600\\\" denotes that the access token will expire in one hour from the time the response was generated. -| - -| refresh_token -| -| String -| The refresh token, which can be used to obtain new access tokens using the same authorization grant -| - -| scope -| -| List of <> -| -| - -| id_token -| -| String -| -| - -|=== - - -[#InlineResponse2001] -=== _InlineResponse2001_ - -UmaMetadata - -[.fields-InlineResponse2001] -[cols="2,1,2,4,1"] -|=== -| Field Name| Required| Type| Description| Format - -| issuer -| X -| String -| The authorization server's issuer identifier -| - -| authorization_endpoint -| X -| String -| URL of the authorization server -| - -| uma_profiles_supported -| -| List of <> -| UMA profiles supported by this authorization server. The value is an array of string values, where each string value is a URI identifying an UMA profile -| - -| permission_endpoint -| -| String -| The endpoint URI at which the resource server requests permissions on the client's behalf. -| - -| resource_registration_endpoint -| -| String -| The endpoint URI at which the resource server registers resources to put them under authorization manager protection. -| - -| scope_endpoint -| -| String -| The Scope endpoint URI. -| - -|=== - - -[#InlineResponse201] -=== _InlineResponse201_ - - - -[.fields-InlineResponse201] -[cols="2,1,2,4,1"] -|=== -| Field Name| Required| Type| Description| Format - -| resource_id -| X -| String -| The identifier for a resource to which this client is seeking access. The identifier MUST correspond to a resource that was previously registered. -| - -| resource_scopes -| X -| List of <> -| An array referencing zero or more strings representing scopes to which access was granted for this resource. Each string MUST correspond to a scope that was registered by this resource server for the referenced resource. -| - -| params -| -| Map of <> -| A key/value map that can contain custom parameters. -| - -| exp -| -| Long -| Number of seconds since January 1 1970 UTC, indicating when this token will expire. -| int64 - -|=== - - -[#InlineResponse400] -=== _InlineResponse400_ - - - -[.fields-InlineResponse400] -[cols="2,1,2,4,1"] -|=== -| Field Name| Required| Type| Description| Format - -| error -| X -| String -| -| enum - -| error_description -| X -| String -| -| - -| details -| -| String -| -| - -|=== - - -[#InlineResponse4001] -=== _InlineResponse4001_ - - - -[.fields-InlineResponse4001] -[cols="2,1,2,4,1"] -|=== -| Field Name| Required| Type| Description| Format - -| error -| X -| String -| -| enum - -| error_description -| X -| String -| -| - -| details -| -| String -| -| - -|=== - - -[#InlineResponse4002] -=== _InlineResponse4002_ - - - -[.fields-InlineResponse4002] -[cols="2,1,2,4,1"] -|=== -| Field Name| Required| Type| Description| Format - -| error -| X -| String -| -| enum - -| error_description -| X -| String -| -| - -| details -| -| String -| -| - -|=== - - -[#InlineResponse4003] -=== _InlineResponse4003_ - - - -[.fields-InlineResponse4003] -[cols="2,1,2,4,1"] -|=== -| Field Name| Required| Type| Description| Format - -| error -| X -| String -| -| enum - -| error_description -| X -| String -| -| - -| details -| -| String -| -| - -|=== - - -[#InlineResponse4004] -=== _InlineResponse4004_ - - - -[.fields-InlineResponse4004] -[cols="2,1,2,4,1"] -|=== -| Field Name| Required| Type| Description| Format - -| error -| X -| String -| -| enum - -| error_description -| X -| String -| -| - -| details -| -| String -| -| - -|=== - - -[#InlineResponse4005] -=== _InlineResponse4005_ - - - -[.fields-InlineResponse4005] -[cols="2,1,2,4,1"] -|=== -| Field Name| Required| Type| Description| Format - -| error -| X -| String -| -| enum - -| error_description -| X -| String -| -| - -| details -| -| String -| -| - -|=== - - -[#InlineResponse4006] -=== _InlineResponse4006_ - - - -[.fields-InlineResponse4006] -[cols="2,1,2,4,1"] -|=== -| Field Name| Required| Type| Description| Format - -| error -| X -| String -| -| enum - -| error_description -| X -| String -| -| - -| details -| -| String -| -| - -|=== - - -[#InlineResponse401] -=== _InlineResponse401_ - - - -[.fields-InlineResponse401] -[cols="2,1,2,4,1"] -|=== -| Field Name| Required| Type| Description| Format - -| error -| X -| String -| -| enum - -| error_description -| X -| String -| -| - -| details -| -| String -| -| - -|=== - - -[#InlineResponse403] -=== _InlineResponse403_ - - - -[.fields-InlineResponse403] -[cols="2,1,2,4,1"] -|=== -| Field Name| Required| Type| Description| Format - -| error -| X -| String -| -| enum - -| error_description -| X -| String -| -| - -| details -| -| String -| -| - -|=== - - -[#InlineResponse404] -=== _InlineResponse404_ - - - -[.fields-InlineResponse404] -[cols="2,1,2,4,1"] -|=== -| Field Name| Required| Type| Description| Format - -| error -| X -| String -| -| enum - -| error_description -| X -| String -| -| - -| details -| -| String -| -| - -|=== - - -[#InlineResponse500] -=== _InlineResponse500_ - - - -[.fields-InlineResponse500] -[cols="2,1,2,4,1"] -|=== -| Field Name| Required| Type| Description| Format - -| error -| X -| String -| -| enum - -| error_description -| X -| String -| -| - -| details -| -| String -| -| - -|=== - - -[#IntrospectionResponse] -=== _IntrospectionResponse_ - -meta-information about token - -[.fields-IntrospectionResponse] -[cols="2,1,2,4,1"] -|=== -| Field Name| Required| Type| Description| Format - -| active -| X -| Boolean -| Boolean indicator of whether or not the presented token is currently active. -| - -| scope -| -| List of <> -| Provide list of scopes to which access was granted for this resource. -| - -| client_id -| -| String -| Client identifier for the OAuth 2.0 client that requested this token. -| - -| username -| -| String -| Human-readable identifier for the resource owner who authorized this token. -| - -| token_type -| -| String -| Type of the token as defined in Section 5.1 of OAuth 2.0 [RFC6749]. -| - -| exp -| -| Integer -| Integer timestamp, measured in the number of seconds since January 1 1970 UTC, indicating when this permission will expire. -| - -| iat -| -| Integer -| -| - -| sub -| -| String -| Subject of the token, as defined in JWT [RFC7519]. -| - -| aud -| -| String -| Service-specific string identifier or list of string identifiers representing the intended audience for this token, as defined in JWT [RFC7519]. -| - -| iss -| -| String -| String representing the issuer of this token, as defined in JWT [RFC7519]. -| - -| acr_values -| -| String -| Authentication Context Class Reference values. -| - -| jti -| -| String -| String identifier for the token, as defined in JWT. -| - -|=== - - -[#JsonWebKey] -=== _JsonWebKey_ - - - -[.fields-JsonWebKey] -[cols="2,1,2,4,1"] -|=== -| Field Name| Required| Type| Description| Format - -| kid -| X -| String -| -| - -| kty -| X -| String -| -| - -| use -| X -| String -| -| - -| alg -| X -| String -| -| - -| crv -| -| String -| -| - -| exp -| X -| Long -| -| int64 - -| x5c -| X -| List of <> -| -| - -| n -| -| String -| -| - -| e -| -| String -| -| - -| x -| -| String -| -| - -| y -| -| String -| -| - -|=== - - -[#RegisterParams] -=== _RegisterParams_ RegisterParams - - - -[.fields-RegisterParams] -[cols="2,1,2,4,1"] -|=== -| Field Name| Required| Type| Description| Format - -| redirect_uris -| X -| List of <> -| Redirection URI values used by the Client. One of these registered Redirection URI values must exactly match the redirect_uri parameter value used in each Authorization Request -| - -| claims_redirect_uri -| -| List of <> -| Array of The Claims Redirect URIs to which the client wishes the authorization server to direct the requesting party's user agent after completing its interaction. -| - -| response_types -| -| List of <> -| A list of the OAuth 2.0 response_type values that the Client is declaring that it will restrict itself to using. If omitted, the default is that the Client will use only the code Response Type. Allowed values are code, token, id_token. -| - -| grant_types -| -| List of <> -| A list of the OAuth 2.0 Grant Types that the Client is declaring that it will restrict itself to using. -| - -| contacts -| -| List of <> -| e-mail addresses of people responsible for this Client. -| - -| client_name -| -| String -| Name of the Client to be presented to the user. -| - -| logo_uri -| -| String -| URL that references a logo for the Client application -| - -| client_uri -| -| String -| URL of the home page of the Client. The value of this field must point to a valid Web page. -| - -| policy_uri -| -| String -| URL that the Relying Party Client provides to the End-User to read about the how the profile data will be used. -| - -| tos_uri -| -| String -| URL that the Relying Party Client provides to the End-User to read about the Relying Party's terms of service. -| - -| jwks_uri -| -| String -| URL for the Client's JSON Web Key Set (JWK) document containing key(s) that are used for signing requests to the OP. The JWK Set may also contain the Client's encryption keys(s) that are used by the OP to encrypt the responses to the Client. When both signing and encryption keys are made available, a use (Key Use) parameter value is required for all keys in the document to indicate each key's intended usage . -| - -| jwks -| -| List of <> -| List of JSON Web Key (JWK) - A JSON object that represents a cryptographic key. The members of the object represent properties of the key, including its value. -| - -| sector_identifier_uri -| -| String -| URL using the https scheme to be used in calculating Pseudonymous Identifiers by the OP. -| - -| subject_type -| -| String -| Subject type requested for the Client ID. Valid types include pairwise and public. -| - -| rpt_as_jwt -| -| Boolean -| Specifies whether RPT should be return as signed JWT. -| - -| access_token_as_jwt -| -| Boolean -| Specifies whether access token as signed JWT. -| - -| access_token_signing_alg -| -| String -| Specifies signing algorithm that has to be used during JWT signing. If it's not specified, then the default OP signing algorithm will be used . -| - -| id_token_signed_response_alg -| -| String -| JWS alg algorithm (JWA) required for signing the ID Token issued to this Client. -| - -| id_token_encrypted_response_alg -| -| String -| JWE alg algorithm (JWA) required for encrypting the ID Token issued to this Client. -| - -| id_token_encrypted_response_enc -| -| String -| JWE enc algorithm (JWA) required for encrypting the ID Token issued to this Client. -| - -| userinfo_signed_response_alg -| -| String -| JWS alg algorithm (JWA) required for signing UserInfo Responses. -| - -| userinfo_encrypted_response_alg -| -| String -| JWE alg algorithm (JWA) required for encrypting UserInfo Responses. -| - -| userinfo_encrypted_response_enc -| -| String -| JWE enc algorithm (JWA) required for encrypting UserInfo Responses. -| - -| request_object_signing_alg -| -| String -| JWS alg algorithm (JWA) that must be used for signing Request Objects sent to the OP. -| - -| request_object_encryption_alg -| -| String -| JWE alg algorithm (JWA) the RP is declaring that it may use for encrypting Request Objects sent to the OP. -| - -| request_object_encryption_enc -| -| String -| JWE enc algorithm (JWA) the RP is declaring that it may use for encrypting Request Objects sent to the OP. -| - -| token_endpoint_auth_method -| -| String -| Requested Client Authentication method for the Token Endpoint. -| - -| token_endpoint_auth_signing_alg -| -| String -| JWS alg algorithm (JWA) that must be used for signing the JWT used to authenticate the Client at the Token Endpoint for the private_key_jwt and client_secret_jwt authentication methods. -| - -| default_max_age -| -| Integer -| Specifies the Default Maximum Authentication Age. -| - -| require_auth_time -| -| Boolean -| Boolean value specifying whether the auth_time Claim in the ID Token is required. It is required when the value is true. -| - -| default_acr_values -| -| List of <> -| Array of default requested Authentication Context Class Reference values that the Authorization Server must use for processing requests from the Client. -| - -| initiate_login_uri -| -| String -| Specifies the URI using the https scheme that the authorization server can call to initiate a login at the client. -| - -| post_logout_redirect_uris -| -| List of <> -| Provide the URLs supplied by the RP to request that the user be redirected to this location after a logout has been performed. -| - -| frontchannel_logout_uri -| -| String -| RP URL that will cause the RP to log itself out when rendered in an iframe by the OP. -| - -| frontchannel_logout_session_required -| -| Boolean -| Boolean value specifying whether the RP requires that a session ID query parameter be included to identify the RP session at the OP when the logout_uri is used. If omitted, the default value is false. -| - -| backchannel_logout_uri -| -| String -| RP URL that will cause the RP to log itself out when sent a Logout Token by the OP. -| - -| backchannel_logout_session_required -| -| Boolean -| Boolean value specifying whether the RP requires that a session ID Claim be included in the Logout Token to identify the RP session with the OP when the backchannel_logout_uri is used. If omitted, the default value is false. -| - -| request_uris -| -| List of <> -| Provide a list of request_uri values that are pre-registered by the Client for use at the Authorization Server. -| - -| scopes -| -| String -| This param will be removed in a future version because the correct is 'scope' not 'scopes', see (rfc7591). -| - -| claims -| -| String -| String containing a space-separated list of claims that can be requested individually. -| - -| id_token_token_binding_cnf -| -| String -| Specifies the JWT Confirmation Method member name (e.g. tbh) that the Relying Party expects when receiving Token Bound ID Tokens. The presence of this parameter indicates that the Relying Party supports Token Binding of ID Tokens. If omitted, the default is that the Relying Party does not support Token Binding of ID Tokens. -| - -| tls_client_auth_subject_dn -| -| String -| An string representation of the expected subject distinguished name of the certificate, which the OAuth client will use in mutual TLS authentication. -| - -| allow_spontaneous_scopes -| -| Boolean -| Specifies whether to allow spontaneous scopes for client. The default value is false. -| - -| spontaneous_scopes -| -| List of <> -| List of spontaneous scopes -| - -| run_introspection_script_before_access_token_as_jwt_creation_and_include_claims -| -| Boolean -| Boolean value with default value false. If true and access_token_as_jwt\=true then run introspection script and transfer claims into JWT. -| - -| keep_client_authorization_after_expiration -| -| Boolean -| Boolean value indicating if the client authorization will not be removed afer expiration (expiration date is same as client's expiration that created it). The default value is false. -| - -| scope -| -| List of <> -| Provide list of scope which are used during authentication to authorize access to resource. -| - -| authorized_origins -| -| List of <> -| specifies authorized JavaScript origins. -| - -| access_token_lifetime -| -| Integer -| Specifies the Client-specific access token expiration. -| - -| software_id -| -| String -| Specifies a unique identifier string (UUID) assigned by the client developer or software publisher used by registration endpoints to identify the client software to be dynamically registered. -| - -| software_version -| -| String -| Specifies a version identifier string for the client software identified by 'software_id'. The value of the 'software_version' should change on any update to the client software identified by the same 'software_id'. -| - -| software_statement -| -| String -| specifies a software statement containing client metadata values about the client software as claims. This is a string value containing the entire signed JWT. -| - -| backchannel_token_delivery_mode -| -| String -| specifies how backchannel token will be deliveried. -| - -| backchannel_client_notification_endpoint -| -| String -| Client Initiated Backchannel Authentication (CIBA) enables a Client to initiate the authentication of an end-user by means of out-of-band mechanisms. Upon receipt of the notification, the Client makes a request to the token endpoint to obtain the tokens. -| - -| backchannel_authentication_request_signing_alg -| -| String -| The JWS algorithm alg value that the Client will use for signing authentication request, as described in Section 7.1.1. of OAuth 2.0 [RFC6749]. When omitted, the Client will not send signed authentication requests. -| - -| backchannel_user_code_parameter -| -| Boolean -| Boolean value specifying whether the Client supports the user_code parameter. If omitted, the default value is false. -| - -| additional_audience -| -| List of <> -| Additional audiences. -| - -|=== - - -[#RegisterParams1] -=== _RegisterParams1_ RegisterParams - - - -[.fields-RegisterParams1] -[cols="2,1,2,4,1"] -|=== -| Field Name| Required| Type| Description| Format - -| redirect_uris -| X -| List of <> -| Redirection URI values used by the Client. One of these registered Redirection URI values must exactly match the redirect_uri parameter value used in each Authorization Request -| - -| claims_redirect_uri -| -| List of <> -| Array of The Claims Redirect URIs to which the client wishes the authorization server to direct the requesting party's user agent after completing its interaction. -| - -| response_types -| -| List of <> -| A list of the OAuth 2.0 response_type values that the Client is declaring that it will restrict itself to using. If omitted, the default is that the Client will use only the code Response Type. Allowed values are code, token, id_token. -| - -| grant_types -| -| List of <> -| A list of the OAuth 2.0 Grant Types that the Client is declaring that it will restrict itself to using. -| - -| contacts -| -| List of <> -| e-mail addresses of people responsible for this Client. -| - -| client_name -| -| String -| Name of the Client to be presented to the user. -| - -| logo_uri -| -| String -| URL that references a logo for the Client application -| - -| client_uri -| -| String -| URL of the home page of the Client. The value of this field must point to a valid Web page. -| - -| policy_uri -| -| String -| URL that the Relying Party Client provides to the End-User to read about the how the profile data will be used. -| - -| tos_uri -| -| String -| URL that the Relying Party Client provides to the End-User to read about the Relying Party's terms of service. -| - -| jwks_uri -| -| String -| URL for the Client's JSON Web Key Set (JWK) document containing key(s) that are used for signing requests to the OP. The JWK Set may also contain the Client's encryption keys(s) that are used by the OP to encrypt the responses to the Client. When both signing and encryption keys are made available, a use (Key Use) parameter value is required for all keys in the document to indicate each key's intended usage . -| - -| jwks -| -| List of <> -| List of JSON Web Key (JWK) - A JSON object that represents a cryptographic key. The members of the object represent properties of the key, including its value. -| - -| sector_identifier_uri -| -| String -| URL using the https scheme to be used in calculating Pseudonymous Identifiers by the OP. -| - -| subject_type -| -| String -| Subject type requested for the Client ID. Valid types include pairwise and public. -| - -| rpt_as_jwt -| -| Boolean -| Specifies whether RPT should be return as signed JWT. -| - -| access_token_as_jwt -| -| Boolean -| Specifies whether access token as signed JWT. -| - -| access_token_signing_alg -| -| String -| Specifies signing algorithm that has to be used during JWT signing. If it's not specified, then the default OP signing algorithm will be used . -| - -| id_token_signed_response_alg -| -| String -| JWS alg algorithm (JWA) required for signing the ID Token issued to this Client. -| - -| id_token_encrypted_response_alg -| -| String -| JWE alg algorithm (JWA) required for encrypting the ID Token issued to this Client. -| - -| id_token_encrypted_response_enc -| -| String -| JWE enc algorithm (JWA) required for encrypting the ID Token issued to this Client. -| - -| userinfo_signed_response_alg -| -| String -| JWS alg algorithm (JWA) required for signing UserInfo Responses. -| - -| userinfo_encrypted_response_alg -| -| String -| JWE alg algorithm (JWA) required for encrypting UserInfo Responses. -| - -| userinfo_encrypted_response_enc -| -| String -| JWE enc algorithm (JWA) required for encrypting UserInfo Responses. -| - -| request_object_signing_alg -| -| String -| JWS alg algorithm (JWA) that must be used for signing Request Objects sent to the OP. -| - -| request_object_encryption_alg -| -| String -| JWE alg algorithm (JWA) the RP is declaring that it may use for encrypting Request Objects sent to the OP. -| - -| request_object_encryption_enc -| -| String -| JWE enc algorithm (JWA) the RP is declaring that it may use for encrypting Request Objects sent to the OP. -| - -| token_endpoint_auth_method -| -| String -| Requested Client Authentication method for the Token Endpoint. -| - -| token_endpoint_auth_signing_alg -| -| String -| JWS alg algorithm (JWA) that must be used for signing the JWT used to authenticate the Client at the Token Endpoint for the private_key_jwt and client_secret_jwt authentication methods. -| - -| default_max_age -| -| Integer -| Specifies the Default Maximum Authentication Age. -| - -| require_auth_time -| -| Boolean -| Boolean value specifying whether the auth_time Claim in the ID Token is required. It is required when the value is true. -| - -| default_acr_values -| -| List of <> -| Array of default requested Authentication Context Class Reference values that the Authorization Server must use for processing requests from the Client. -| - -| initiate_login_uri -| -| String -| Specifies the URI using the https scheme that the authorization server can call to initiate a login at the client. -| - -| post_logout_redirect_uris -| -| List of <> -| Provide the URLs supplied by the RP to request that the user be redirected to this location after a logout has been performed. -| - -| frontchannel_logout_uri -| -| String -| RP URL that will cause the RP to log itself out when rendered in an iframe by the OP. -| - -| frontchannel_logout_session_required -| -| Boolean -| Boolean value specifying whether the RP requires that a session ID query parameter be included to identify the RP session at the OP when the logout_uri is used. If omitted, the default value is false. -| - -| backchannel_logout_uri -| -| String -| RP URL that will cause the RP to log itself out when sent a Logout Token by the OP. -| - -| backchannel_logout_session_required -| -| Boolean -| Boolean value specifying whether the RP requires that a session ID Claim be included in the Logout Token to identify the RP session with the OP when the backchannel_logout_uri is used. If omitted, the default value is false. -| - -| request_uris -| -| List of <> -| Provide a list of request_uri values that are pre-registered by the Client for use at the Authorization Server. -| - -| scopes -| -| String -| This param will be removed in a future version because the correct is 'scope' not 'scopes', see (rfc7591). -| - -| claims -| -| String -| String containing a space-separated list of claims that can be requested individually. -| - -| id_token_token_binding_cnf -| -| String -| Specifies the JWT Confirmation Method member name (e.g. tbh) that the Relying Party expects when receiving Token Bound ID Tokens. The presence of this parameter indicates that the Relying Party supports Token Binding of ID Tokens. If omitted, the default is that the Relying Party does not support Token Binding of ID Tokens. -| - -| tls_client_auth_subject_dn -| -| String -| An string representation of the expected subject distinguished name of the certificate, which the OAuth client will use in mutual TLS authentication. -| - -| allow_spontaneous_scopes -| -| Boolean -| Specifies whether to allow spontaneous scopes for client. The default value is false. -| - -| spontaneous_scopes -| -| List of <> -| List of spontaneous scopes -| - -| run_introspection_script_before_access_token_as_jwt_creation_and_include_claims -| -| Boolean -| Boolean value with default value false. If true and access_token_as_jwt\=true then run introspection script and transfer claims into JWT. -| - -| keep_client_authorization_after_expiration -| -| Boolean -| Boolean value indicating if the client authorization will not be removed afer expiration (expiration date is same as client's expiration that created it). The default value is false. -| - -| scope -| -| List of <> -| Provide list of scope which are used during authentication to authorize access to resource. -| - -| authorized_origins -| -| List of <> -| specifies authorized JavaScript origins. -| - -| access_token_lifetime -| -| Integer -| Specifies the Client-specific access token expiration. -| - -| software_id -| -| String -| Specifies a unique identifier string (UUID) assigned by the client developer or software publisher used by registration endpoints to identify the client software to be dynamically registered. -| - -| software_version -| -| String -| Specifies a version identifier string for the client software identified by 'software_id'. The value of the 'software_version' should change on any update to the client software identified by the same 'software_id'. -| - -| software_statement -| -| String -| specifies a software statement containing client metadata values about the client software as claims. This is a string value containing the entire signed JWT. -| - -| backchannel_token_delivery_mode -| -| String -| specifies how backchannel token will be deliveried. -| - -| backchannel_client_notification_endpoint -| -| String -| Client Initiated Backchannel Authentication (CIBA) enables a Client to initiate the authentication of an end-user by means of out-of-band mechanisms. Upon receipt of the notification, the Client makes a request to the token endpoint to obtain the tokens. -| - -| backchannel_authentication_request_signing_alg -| -| String -| The JWS algorithm alg value that the Client will use for signing authentication request, as described in Section 7.1.1. of OAuth 2.0 [RFC6749]. When omitted, the Client will not send signed authentication requests. -| - -| backchannel_user_code_parameter -| -| Boolean -| Boolean value specifying whether the Client supports the user_code parameter. If omitted, the default value is false. -| - -| additional_audience -| -| List of <> -| Additional audiences. -| - -|=== - - -[#RegisterResponseParam] -=== _RegisterResponseParam_ - - - -[.fields-RegisterResponseParam] -[cols="2,1,2,4,1"] -|=== -| Field Name| Required| Type| Description| Format - -| client_id -| X -| String -| Unique Client Identifier. It MUST NOT be currently valid for any other registered Client. -| - -| client_secret -| -| String -| This value is used by Confidential Clients to authenticate to the Token Endpoint -| - -| registration_access_token -| -| String -| Registration Access Token that can be used at the Client Configuration Endpoint to perform subsequent operations upon the Client registration. -| - -| registration_client_uri -| -| String -| Location of the Client Configuration Endpoint where the Registration Access Token can be used to perform subsequent operations upon the resulting Client registration. -| - -| client_id_issued_at -| -| Integer -| Time at which the Client Identifier was issued. -| - -| client_secret_expires_at -| -| Integer -| Time at which the client_secret will expire or 0 if it will not expire. -| - -|=== - - -[#RptIntrospectionResponse] -=== _RptIntrospectionResponse_ - - - -[.fields-RptIntrospectionResponse] -[cols="2,1,2,4,1"] -|=== -| Field Name| Required| Type| Description| Format - -| active -| X -| Boolean -| Boolean indicator of whether or not the presented token is currently active. -| - -| exp -| -| Long -| Integer timestamp, in seconds since January 1 1970 UTC, indicating when this token will expire. -| int64 - -| iat -| -| Integer -| Integer timestamp, measured in the number of seconds since January 1 1970 UTC, indicating when this permission was originally issued. -| - -| clientId -| -| String -| Client id used to obtain RPT. -| - -| sub -| -| String -| Subject of the token. Usually a machine-readable identifier of the resource owner who authorized this token. -| - -| aud -| -| String -| Service-specific string identifier or list of string identifiers representing the intended audience for this token. -| - -| permissions -| X -| List of <> -| -| - -| pct_claims -| -| Map of <> -| PCT token claims. -| - -| iss -| -| String -| String representing the issuer of this token, as defined in JWT [RFC7519]. -| - -| jti -| -| String -| String identifier for the token, as defined in JWT [RFC7519]. -| - -| nbf -| -| Integer -| Integer timestamp, measured in the number of seconds since January 1 1970 UTC, indicating the time before which this permission is not valid. -| - -| resource_id -| X -| String -| Resource ID. -| - -| resource_scopes -| X -| List of <> -| -| - -|=== - - -[#RptIntrospectionResponse1] -=== _RptIntrospectionResponse1_ - - - -[.fields-RptIntrospectionResponse1] -[cols="2,1,2,4,1"] -|=== -| Field Name| Required| Type| Description| Format - -| active -| X -| Boolean -| Boolean indicator of whether or not the presented token is currently active. -| - -| exp -| -| Long -| Integer timestamp, in seconds since January 1 1970 UTC, indicating when this token will expire. -| int64 - -| iat -| -| Integer -| Integer timestamp, measured in the number of seconds since January 1 1970 UTC, indicating when this permission was originally issued. -| - -| clientId -| -| String -| Client id used to obtain RPT. -| - -| sub -| -| String -| Subject of the token. Usually a machine-readable identifier of the resource owner who authorized this token. -| - -| aud -| -| String -| Service-specific string identifier or list of string identifiers representing the intended audience for this token. -| - -| permissions -| X -| List of <> -| -| - -| pct_claims -| -| Map of <> -| -| - -| iss -| -| String -| String representing the issuer of this token, as defined in JWT [RFC7519]. -| - -| jti -| -| String -| String identifier for the token, as defined in JWT [RFC7519]. -| - -| nbf -| -| Integer -| Integer timestamp, measured in the number of seconds since January 1 1970 UTC, indicating the time before which this permission is not valid. -| - -| resource_id -| X -| String -| Resource ID. -| - -| resource_scopes -| X -| List of <> -| -| - -|=== - - -[#RptIntrospectionResponsePermissions] -=== _RptIntrospectionResponsePermissions_ - -List of UmaPermission granted to RPT. A permission is (requested or granted) authorized access to a particular resource with some number of scopes bound to that resource. - -[.fields-RptIntrospectionResponsePermissions] -[cols="2,1,2,4,1"] -|=== -| Field Name| Required| Type| Description| Format - -| resource_id -| X -| String -| A string that uniquely identifies the protected resource, access to which has been granted to this client on behalf of this requesting party. The identifier MUST correspond to a resource that was previously registered as protected. -| - -| resource_scopes -| X -| List of <> -| An array referencing zero or more strings representing scopes to which access was granted for this resource. Each string MUST correspond to a scope that was registered by this resource server for the referenced resource. -| - -| exp -| -| Integer -| Integer timestamp, measured in the number of seconds since January 1 1970 UTC, indicating when this permission will expire. If the token-level exp value pre-dates a permission-level exp value, the token-level value takes precedence. -| - -| iat -| -| Integer -| Integer timestamp, measured in the number of seconds since January 1 1970 UTC, indicating when this permission was originally issued. If the token-level iat value post-dates a permission-level iat value, the token-level value takes precedence. -| - -| nbf -| -| Integer -| Integer timestamp, measured in the number of seconds since January 1 1970 UTC, indicating the time before which this permission is not valid. If the token-level nbf value post-dates a permission-level nbf value, the token-level value takes precedence. -| - -|=== - - -[#SessionStateObject] -=== _SessionStateObject_ - - - -[.fields-SessionStateObject] -[cols="2,1,2,4,1"] -|=== -| Field Name| Required| Type| Description| Format - -| state -| -| String -| String that represents the End-User's login state at the OP. It MUST NOT contain the space (\\\" \\\") character. -| - -| auth_time -| -| date -| specifies the time at which session was authenticated. -| date - -| custom_state -| -| String -| -| - -|=== - - -[#UmaPermissiona] -=== _UmaPermissiona_ UmaPermissiona - -A permission is (requested or granted) authorized access to a particular resource with some number of scopes bound to that resource. - -[.fields-UmaPermissiona] -[cols="2,1,2,4,1"] -|=== -| Field Name| Required| Type| Description| Format - -| resource_id -| X -| String -| The identifier for a resource to which this client is seeking access. The identifier MUST correspond to a resource that was previously registered. -| - -| resource_scopes -| X -| List of <> -| An array referencing zero or more strings representing scopes to which access was granted for this resource. Each string MUST correspond to a scope that was registered by this resource server for the referenced resource. -| - -| params -| -| Map of <> -| A key/value map that can contain custom parameters. -| - -|=== - - -[#UmaResource] -=== _UmaResource_ UmaResource - -Resource description - -[.fields-UmaResource] -[cols="2,1,2,4,1"] -|=== -| Field Name| Required| Type| Description| Format - -| name -| -| String -| A human-readable string describing a set of one or more resources. This name MAY be used by the authorization server in its resource owner user interface for the resource owner. -| - -| icon_uri -| -| String -| A URI for a graphic icon representing the resource set. The referenced icon MAY be used by the authorization server in its resource owner user interface for the resource owner. -| - -| type -| -| String -| A string uniquely identifying the semantics of the resource set. For example, if the resource set consists of a single resource that is an identity claim that leverages standardized claim semantics for \\\"verified email address\\\", the value of this property could be an identifying URI for this claim. -| - -| resource_scopes -| X -| List of <> -| An array of strings, any of which MAY be a URI, indicating the available scopes for this resource set. URIs MUST resolve to scope descriptions as defined in Section 2.1. Published scope descriptions MAY reside anywhere on the web; a resource server is not required to self-host scope descriptions and may wish to point to standardized scope descriptions residing elsewhere. It is the resource server's responsibility to ensure that scope description documents are accessible to authorization servers through GET calls to support any user interface requirements. The resource server and authorization server are presumed to have separately negotiated any required interpretation of scope handling not conveyed through scope descriptions. -| - -| scope_expression -| -| String -| -| - -| description -| -| String -| A human-readable string describing the resource -| - -| iat -| -| Long -| number of seconds since January 1 1970 UTC, indicating when the token was issued at -| int64 - -| exp -| -| Long -| number of seconds since January 1 1970 UTC, indicating when this token will expire. -| int64 - -|=== - - -[#UmaResource1] -=== _UmaResource1_ UmaResource - -Resource description - -[.fields-UmaResource1] -[cols="2,1,2,4,1"] -|=== -| Field Name| Required| Type| Description| Format - -| name -| -| String -| A human-readable string describing a set of one or more resources. This name MAY be used by the authorization server in its resource owner user interface for the resource owner. -| - -| icon_uri -| -| String -| A URI for a graphic icon representing the resource set. The referenced icon MAY be used by the authorization server in its resource owner user interface for the resource owner. -| - -| type -| -| String -| A string uniquely identifying the semantics of the resource set. For example, if the resource set consists of a single resource that is an identity claim that leverages standardized claim semantics for \\\"verified email address\\\", the value of this property could be an identifying URI for this claim. -| - -| resource_scopes -| X -| List of <> -| An array of strings, any of which MAY be a URI, indicating the available scopes for this resource set. URIs MUST resolve to scope descriptions as defined in Section 2.1. Published scope descriptions MAY reside anywhere on the web; a resource server is not required to self-host scope descriptions and may wish to point to standardized scope descriptions residing elsewhere. It is the resource server's responsibility to ensure that scope description documents are accessible to authorization servers through GET calls to support any user interface requirements. The resource server and authorization server are presumed to have separately negotiated any required interpretation of scope handling not conveyed through scope descriptions. -| - -| scope_expression -| -| String -| -| - -| description -| -| String -| A human-readable string describing the resource -| - -| iat -| -| Long -| number of seconds since January 1 1970 UTC, indicating when the token was issued at -| int64 - -| exp -| -| Long -| number of seconds since January 1 1970 UTC, indicating when this token will expire. -| int64 - -|=== - - -[#UmaResourceResponse] -=== _UmaResourceResponse_ - -UmaResourceResponse Resource created. - -[.fields-UmaResourceResponse] -[cols="2,1,2,4,1"] -|=== -| Field Name| Required| Type| Description| Format - -| _id -| X -| String -| UMA Resource identifier -| - -| user_access_policy_uri -| -| String -| -| - -|=== - - -[#UmaResourceWithId] -=== _UmaResourceWithId_ - -Uma Resource details - -[.fields-UmaResourceWithId] -[cols="2,1,2,4,1"] -|=== -| Field Name| Required| Type| Description| Format - -| _id -| X -| String -| UMA Resource identifier -| - -| name -| -| String -| A human-readable string describing a set of one or more resources. This name MAY be used by the authorization server in its resource owner user interface for the resource owner. -| - -| uri -| -| String -| A human-readable string describing the resource -| - -| type -| -| String -| A string uniquely identifying the semantics of the resource set. For example, if the resource set consists of a single resource that is an identity claim that leverages standardized claim semantics for \\\"verified email address\\\", the value of this property could be an identifying URI for this claim. -| +| icon_uri +| +| String +| Protected uri of the resource. +| | scopes | | List of <> -| An array of strings, any of which MAY be a URI, indicating the available scopes for this resource set. URIs MUST resolve to scope descriptions as defined in Section 2.1. Published scope descriptions MAY reside anywhere on the web; a resource server is not required to self-host scope descriptions and may wish to point to standardized scope descriptions residing elsewhere. It is the resource server's responsibility to ensure that scope description documents are accessible to authorization servers through GET calls to support any user interface requirements. The resource server and authorization server are presumed to have separately negotiated any required interpretation of scope handling not conveyed through scope descriptions. -| - -| scope_expression -| -| String -| -| - -| description -| -| String -| A human-readable string describing the resource -| - -| icon_uri -| -| String -| A URI for a graphic icon representing the resource set. The referenced icon MAY be used by the authorization server in its resource owner user interface for the resource owner. -| - -| iat -| X -| Long -| number of seconds since January 1 1970 UTC, indicating when the token was issued at -| int64 - -| exp -| X -| Long -| number of seconds since January 1 1970 UTC, indicating when this token will expire. -| int64 - -|=== - - -[#WebKeysConfiguration] -=== _WebKeysConfiguration_ - -JSON Web Key Set (JWKS) - A JSON object that represents a set of JWKs. The JSON object MUST have a keys member, which is an array of JWKs. - -[.fields-WebKeysConfiguration] -[cols="2,1,2,4,1"] -|=== -| Field Name| Required| Type| Description| Format - -| keys -| X -| List of <> -| List of JSON Web Key (JWK) - A JSON object that represents a cryptographic key. The members of the object represent properties of the key, including its value. +| List of scopes associated with the resource | |=== From c17113b212d9ac20e4a43500aac1a8b03e927963 Mon Sep 17 00:00:00 2001 From: Hector Rodriguez Date: Thu, 5 Nov 2020 08:43:54 +0000 Subject: [PATCH 32/80] EOEPCA-179 Fixed references to wrong Building Block --- docs/ICD/02.overview/00.overview.adoc | 2 +- docs/ICD/index.adoc | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/ICD/02.overview/00.overview.adoc b/docs/ICD/02.overview/00.overview.adoc index d98d483..c4b24a9 100644 --- a/docs/ICD/02.overview/00.overview.adoc +++ b/docs/ICD/02.overview/00.overview.adoc @@ -1,7 +1,7 @@ [[mainOverview]] = Overview -This Interface Control Document (ICD) is a companion to the System Design Document for the Login Service <>. The ICD provides a Building Block level specification of the interfaces exposed by the Login Service to the rest of EOEPCA components. +This Interface Control Document (ICD) is a companion to the System Design Document for the Policy Enforcement Point. The ICD provides a Building Block level specification of the interfaces exposed by the PEP to the rest of EOEPCA components. Section <>:: Provides the interface specification of the Building Block. \ No newline at end of file diff --git a/docs/ICD/index.adoc b/docs/ICD/index.adoc index 4dc36e5..24426e1 100644 --- a/docs/ICD/index.adoc +++ b/docs/ICD/index.adoc @@ -3,8 +3,8 @@ :email: :project: EOEPCA :project-name: EO Exploitation Platform Common Architecture -:component-name: Login Service -:component-github-name: um-login-service +:component-name: Policy Enforcement Point +:component-github-name: um-pep-engine :doc-title: {component-name} Interface Control Document :doc-num: {project}.ICD.xxx :revnumber: 1.0 From 0b3fdc3096696715d57a676f01cf32c9a576ae93 Mon Sep 17 00:00:00 2001 From: mamuniz Date: Thu, 5 Nov 2020 16:48:54 +0000 Subject: [PATCH 33/80] EOEPCA-187: #comment Added verification for JWT --- .../signature_verification.py | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 src/jwt_verification/signature_verification.py diff --git a/src/jwt_verification/signature_verification.py b/src/jwt_verification/signature_verification.py new file mode 100644 index 0000000..7c2432e --- /dev/null +++ b/src/jwt_verification/signature_verification.py @@ -0,0 +1,62 @@ +import json +import os.path +import requests +from hashlib import md5 +import urllib3 +import base64 +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +from Cryptodome.PublicKey import RSA +from jwkest.ecc import P256 +from jwkest.ecc import P384 +from jwkest.ecc import P521 + +import jwkest +from jwkest import jws, b64d_enc_dec +from jwkest import b64d, b64e + +from jwkest.jwk import SYMKey, KEYS +from jwkest.jwk import ECKey +from jwkest.jwk import import_rsa_key_from_file +from jwkest.jwk import RSAKey +from jwkest.jws import SIGNER_ALGS, factory +from jwkest.jws import JWSig +from jwkest.jws import JWS +from config import load_config + + +class JWT_Verification(): + + def __init__(self): + self.SIGKEYS = KEYS() + keys_json = self.getKeys_JWT() + self.SIGKEYS.load_dict(keys_json) + + def verify_signature_JWT(self, jwt): + symkeys = [k for k in self.SIGKEYS if k.alg == "RS256"] + + _rj = JWS() + info = _rj.verify_compact(jwt, symkeys) + decoded_json = self.decode_JWT(jwt) + + if info == decoded_json: + return True + else: + return False + + def getKeys_JWT(self): + g_config = load_config("./config/config.json") + + headers = { 'content-type': "application/json", "cache-control": "no-cache" } + res = requests.get(g_config["auth_server_url"]+"/oxauth/restv1/jwks", headers=headers, verify=False) + + json_dict = json.loads(res.text) + return json_dict + + def decode_JWT(self, jwt): + payload = str(jwt).split(".")[1] + paddedPayload = payload + '=' * (4 - len(payload) % 4) + decoded = base64.b64decode(paddedPayload) + decoded_json = json.loads(decoded) + + return decoded_json \ No newline at end of file From ef878da39fa582df3879c2bbd7a4d040b740f066 Mon Sep 17 00:00:00 2001 From: mamuniz Date: Thu, 5 Nov 2020 17:39:27 +0000 Subject: [PATCH 34/80] EOEPCA-187: #comment Updated file to verify JWT --- src/custom_oidc.py | 12 ++++++++++++ src/main.py | 15 ++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/custom_oidc.py b/src/custom_oidc.py index 3c7415c..4da2722 100644 --- a/src/custom_oidc.py +++ b/src/custom_oidc.py @@ -13,6 +13,7 @@ from jwkest.jwk import RSAKey, import_rsa_key_from_file, load_jwks_from_url, import_rsa_key from jwkest.jwk import load_jwks from Crypto.PublicKey import RSA +from jwt_verification.signature_verification import JWT_Verification from requests import post @@ -67,6 +68,17 @@ def verify_JWT_token(self, token, key): decoded = base64.b64decode(paddedPayload) #to remove byte-code decoded = decoded.decode('utf-8') + decoded_str = json.loads(decoded) + + verificator = JWT_Verification() + result = verificator.verify_signature_JWT(token) + + if result == False: + print("Verification of the signature for the JWT failed!") + raise Exception + else: + print("Signature verification is correct") + user_value = json.loads(decoded)[key] return user_value except Exception as e: diff --git a/src/main.py b/src/main.py index a060c53..fb6debd 100644 --- a/src/main.py +++ b/src/main.py @@ -25,6 +25,7 @@ from jwkest.jwk import RSAKey, import_rsa_key_from_file, load_jwks_from_url, import_rsa_key from jwkest.jwk import load_jwks from Crypto.PublicKey import RSA +from jwt_verification.signature_verification import JWT_Verification import logging logging.getLogger().setLevel(logging.INFO) ### INITIAL SETUP @@ -211,6 +212,19 @@ def resource_request(path): if rpt: print("Token found: "+rpt) rpt = rpt.replace("Bearer ","").strip() + + if len(str(rpt))>40 and len(str(rpt)) != 76: + verificator = JWT_Verification() + result = verificator.verify_signature_JWT(str(rpt)) + + if result == False: + print("Verification of the signature for the JWT failed!") + response.status_code = 403 + return response + else: + print("Signature verification is correct") + + # Validate for a specific resource if uma_handler.validate_rpt(rpt, [{"resource_id": resource_id, "resource_scopes": scopes }], int(g_config["s_margin_rpt_valid"]), int(g_config["rpt_limit_uses"])) or not api_rpt_uma_validation: print("RPT valid, accesing ") @@ -218,7 +232,6 @@ def resource_request(path): pat = oidc_client.get_new_pat() rpt_class = class_rpt.introspect(rpt=rpt, pat=pat, introspection_endpoint=introspection_endpoint, secure=False) jwt_rpt_response = create_jwt(rpt_class, private_key) - headers_splitted = split_headers(str(request.headers)) headers_splitted['Authorization'] = "Bearer "+str(jwt_rpt_response) From 77d7f0c343089af603dd196b47b3d16cfd8e1e94 Mon Sep 17 00:00:00 2001 From: Tiago Fernandes Date: Thu, 5 Nov 2020 18:15:57 +0000 Subject: [PATCH 35/80] EOEPCA-178 changed data type to json --- src/config/config.json | 2 +- src/handlers/policy_handler.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/config/config.json b/src/config/config.json index b5b087e..2ee0e0f 100644 --- a/src/config/config.json +++ b/src/config/config.json @@ -1 +1 @@ -{"realm": "eoepca", "auth_server_url": "https://test.eoepca.org", "proxy_endpoint": "/", "service_host": "0.0.0.0", "service_port": 5566, "s_margin_rpt_valid": 5, "check_ssl_certs": false, "use_threads": true, "debug_mode": true, "resource_server_endpoint": "http://eoepca-ades-core", "api_rpt_uma_validation": true, "rpt_limit_uses": 5, "pdp_url": "https://test.eoepca.org", "pdp_port": 5567, "pdp_policy_endpoint": "/policy/"} +{"realm": "eoepca", "auth_server_url": "https://test.eoepca.org", "proxy_endpoint": "/", "service_host": "0.0.0.0", "service_port": 5566, "s_margin_rpt_valid": 5, "check_ssl_certs": false, "use_threads": true, "debug_mode": true, "resource_server_endpoint": "http://eoepca-ades-core", "api_rpt_uma_validation": true, "rpt_limit_uses": 5, "pdp_url": "http://test.eoepca.org", "pdp_port": 5567, "pdp_policy_endpoint": "/policy/"} diff --git a/src/handlers/policy_handler.py b/src/handlers/policy_handler.py index 843d538..78476d0 100644 --- a/src/handlers/policy_handler.py +++ b/src/handlers/policy_handler.py @@ -22,5 +22,5 @@ def __init__(self, pdp_url: str, pdp_port: int, pdp_policy_endpoint: str): ''' def create_policy(self, policy_body, input_headers): headers = input_headers - data = policy_body - return post(self.url+':'+str(self.port)+self.endpoint, headers=headers, data=data) \ No newline at end of file + json = policy_body + return post(self.url+':'+str(self.port)+self.endpoint, headers=headers, json=json) \ No newline at end of file From 92949ad4bd360c2c2c221b6c221c263146522dd5 Mon Sep 17 00:00:00 2001 From: Tiago Fernandes Date: Fri, 6 Nov 2020 15:27:24 +0000 Subject: [PATCH 36/80] EOEPCA-178 default scope change --- src/resources/resources.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/resources/resources.py b/src/resources/resources.py index e572c6d..56bd63e 100644 --- a/src/resources/resources.py +++ b/src/resources/resources.py @@ -241,7 +241,7 @@ def get_default_ownership_policy_body(resource_id, user_name): name = "Default Ownership Policy" description = "This is the default ownership policy for created resources through PEP" policy_cfg = get_default_ownership_policy_cfg(resource_id, user_name) - scopes = ["Authenticated"] - return {"name": name, "description": description, "policy_cfg": policy_cfg, "scopes": scopes} + scopes = ["protected_access"] + return {"name": name, "description": description, "config": policy_cfg, "scopes": scopes} return resources_bp From 5bf41f88acf8195c34ceb7ff5e67563144edf575 Mon Sep 17 00:00:00 2001 From: vagrant Date: Fri, 6 Nov 2020 15:54:15 +0000 Subject: [PATCH 37/80] EOEPCA-187: #comment Added documentation --- docs/SDD/02.overview/00.overview.adoc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/SDD/02.overview/00.overview.adoc b/docs/SDD/02.overview/00.overview.adoc index 3e175ed..4776fbe 100644 --- a/docs/SDD/02.overview/00.overview.adoc +++ b/docs/SDD/02.overview/00.overview.adoc @@ -94,7 +94,9 @@ The endpoints used for SCIM are: The Resource API Endpoints offered by the PEP component are protected based on the unique identifier of the Resource Owner that is adding/removing/editing resources. The Resource API is protected with OAuth/OIDC in the PEP, expecting any of these tokens: -* JWT id_tokens: in this case the PEP extracts the necessary claims from the JWT uniquely identifying the user (“sub” parameter) +* JWT id_tokens: in this case the PEP extracts the necessary claims from the JWT uniquely identifying the user (“sub” parameter) and the signature of the JWT will also be verified. + +* RPT Token: in this case the PEP extracts the necessary claims from the RPT uniquely from the "pct_claims" parameter * OAuth Access Token: in this case the PEP performs a query against the User-Info endpoint, uniquely identifying the user. From 451ae6e5f308d8bcbf3d6e409fcecd501e86c705 Mon Sep 17 00:00:00 2001 From: Hector Rodriguez Date: Fri, 6 Nov 2020 17:02:43 +0100 Subject: [PATCH 38/80] Update README.md --- README.md | 111 +----------------------------------------------------- 1 file changed, 2 insertions(+), 109 deletions(-) diff --git a/README.md b/README.md index 8a3e057..e6a0dbe 100644 --- a/README.md +++ b/README.md @@ -69,116 +69,9 @@ This is an example of how to list things you need to use the software and how to - [Docker](https://www.docker.com/) - [Python](https://www.python.org//) -### Installation +### Usage, Deployment and Configuration -1. Get into EOEPCA's development environment - -```sh -vagrant ssh -``` - -3. Clone the repo - -```sh -git clone https://github.com/EOEPCA/um-pep-engine.git -``` - -4. Change local directory - -```sh -cd um-pep-engine -``` -## Dependencies -The PEP is written and tested for python 3.6.9, and has all dependencies listed in src/requirements.txt - -## Configuration - -The PEP gets all its configuration from the file located under `config/config.json`. -The parameters that are accepted, and their meaning, are as follows: -- **realm**: 'realm' parameter answered for each UMA ticket. Default is "eoepca" -- **auth_server_url**: complete url (with "https") of the Authorization server. -- **proxy_endpoint**: "/path"-formatted string to indicate where the reverse proxy should listen. The proxy will catch any request that starts with that path. Default is "/pep" -- **service_host**: Host for the proxy to listen on. For example, "0.0.0.0" will listen on all interfaces -- **service_port**: Port for the proxy to listen on. By default, **5566**. Keep in mind you will have to edit the docker file and/or kubernetes yaml file in order for all the prot forwarding to work. -- **s_margin_rpt_valid**: An integer representing how many seconds of "margin" do we want when checking RPT. For example, using **5** will make sure the provided RPT is valid now AND AT LEAST in the next 5 seconds. -- **check_ssl_certs**: Toggle on/off (bool) to check certificates in all requests. This should be forced to True in a production environment -- **use_threads**: Toggle on/off (bool) the usage of threads for the proxy. Recommended to be left as True. -- **debug_mode**: Toggle on/off (bool) a debug mode of Flask. In a production environment, this should be false. -- **resource_server_endpoint**: Complete url (with "https" and any port) of the Resource Server to protect with this PEP. -- **rpt_limit_uses**: Number of uses for each of the RPTs. -- **client_id**: string indicating a client_id for an already registered and configured client. **This parameter is optional**. When not supplied, the PEP will generate a new client for itself and store it in this key inside the JSON. -- **client_secret**: string indicating the client secret for the client_id. **This parameter is optional**. When not supplied, the PEP will generate a new client for itself and store it in this key inside the JSON. - -## Usage & functionality - -Use directly from docker with -```sh -docker run --publish : -``` -Where **configured-port** is the port configured inside the config.json file inside the image. The default image is called **eoepca/um-pep-engine:latest**. - -If this is running in a development environment without proper DNS setup, add the following to your docker run command: -```sh ---add-host : -``` - -When launched, the PEP will answer to all requests that start with the configured path. These answers will come in the form of UUMA tickets (if there are no RPT provided, or an invalid one is used). -In case the request is accompained by an "Authorization: Bearer ", the PEP will make a request to the resource server, for the resource located exactly at the path requested (minus the configured at config), and return the resource's server answer. - -Examples, given the example values of: -- path configured: "/pep" -- PEP is at pep.domain.com/pep -- Resource server is at remote.server.com - -| Token | Request to PEP | PEP Action | PEP answer | -|-------|---------|------------|--------------| -| No RPT | pep.domain.com | None (request does not get to PEP endpoint) | None (the PEP doesn't see this request) | -| No RPT | pep.domain.com/pep/thing | Generate ticket for "/thing" | 401 + ticket | -| Valid RPT for "/thing" | pep.domain.com/pep/thing | Request to remote.server.com/thing | Contents of remote.server.com/thing | -| Valid RPT for "/thing" | pep.domain.com/pep/different | Generate ticket for "/different" | 401 + ticket | -| INVALID RPT for "/thing" | pep.domain.com/pep/thing | Generate ticket for "/thing" | 401 + ticket | -| No RPT | pep.domain.com/pep/thing/with/large/path | Generate ticket for "/thing/with/large/path" | 401 + ticket | -| Valid RPT for "/thing/with/large/path" | pep.domain.com/pep/thing/with/large/path | Request to remote.server.com/thing/with/large/path | Contents of remote.server.com/thing/with/large/path | - -## Developer documentation - -The API will expose an endpoint to interact with the resources. -The main endpoints for the resource operations exposed by the API are now secured with OAuth/OIDC, it would accept both OAuth and JWT in order to authorize the user and both are expected on the header. -This check will retrieve the UUID for the user and insert it on the data model of the resource storage, so when any call is made against a resource, the API will double check if the UUID of the requester matches the one associated to the resource in order to operate against it. - --------- - -Testing and Demo for the validation with OAuth/OIDC: - -Execute the `test_validation_token.py` in `um-pep-engine/tests/` - -### Demo functionality - -At the moment, the PEP will auto register a resource for the sake of demoing it's capabilities, using the `create` function of the UMA handler. This can be deleted if unwanted, or expanded to dinamically register resources. Note that the UMA library used allows for full control over resources (create, delete, etc) and could be used to help in that functionality expansion. - -### Test functionality - -In order to test the PEP engine at the moment first you have reach this prerequisites: - -- Register a client and a user inside the gluu instance and update the test_settings.json -- Disable current UMA Policies and set inside JSONConfig > OxAuth umaGrantAccessIfNoPolicies to true - -### Endpoints - -The PEP uses the following endpoints from a "Well Known Handler", which parses the Auth server's "well-known" endpoints: - -- OIDC_TOKEN_ENDPOINT -- UMA_V2_RESOURCE_REGISTRATION_ENDPOINT -- UMA_V2_PERMISSION_ENDPOINT -- UMA_V2_INTROSPECTION_ENDPOIN - -### Resources Repository - - -When a resource is registered, the name and id are stored as a document into a Mongodb database as a sidecar container sharing data through a persistent storage volume. -The pod runs the pep-engine image and the mongo image exposing the default mongo port (27017) where communicates the service and keeps it alive for the pep-engine container to query the database. - -A local MongoDB service can be used to test the repo since the main script would listen the port 27017 +The full getting started guide starts in the Wiki home page ## Roadmap From 9a73f40dd07e980d8b4d2aa6dca9249c012b072f Mon Sep 17 00:00:00 2001 From: Hector Rodriguez Date: Fri, 6 Nov 2020 17:03:22 +0100 Subject: [PATCH 39/80] Update README.md --- README.md | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/README.md b/README.md index e6a0dbe..3ca9884 100644 --- a/README.md +++ b/README.md @@ -30,16 +30,6 @@ - [Table of Contents](#table-of-contents) - [Built With](#built-with) - [Getting Started](#getting-started) - - [Prerequisites](#prerequisites) - - [Installation](#installation) -- [Dependencies](#dependencies) -- [Configuration](#configuration) -- [Usage & functionality](#usage--functionality) -- [Developer documentation](#developer-documentation) - - [Demo functionality](#demo-functionality) - - [Test functionality](#test-functionality) - - [Endpoints](#endpoints) - - [Resources Repository](#resources-repository) - [Roadmap](#roadmap) - [Contributing](#contributing) - [License](#license) From 760e966c1911600ad9870e0cc9eef6bb24ff502a Mon Sep 17 00:00:00 2001 From: Tiago Fernandes Date: Tue, 10 Nov 2020 08:49:18 +0000 Subject: [PATCH 40/80] EOEPCA-178 endpoint sanitation fix --- src/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.py b/src/main.py index 8151df6..e6cb74d 100644 --- a/src/main.py +++ b/src/main.py @@ -81,7 +81,7 @@ if g_config["pdp_policy_endpoint"][0] is not "/": g_config["pdp_policy_endpoint"] = "/" + g_config["pdp_policy_endpoint"] if g_config["pdp_policy_endpoint"][-1] is not "/": - g_config["pdp_policy_endpoint"] = "/" + g_config["pdp_policy_endpoint"] + g_config["pdp_policy_endpoint"] = g_config["pdp_policy_endpoint"] + "/" # Global handlers g_wkh = WellKnownHandler(g_config["auth_server_url"], secure=False) From d5965750d2fd3f8fc034b343ecf2de9d2bd69b56 Mon Sep 17 00:00:00 2001 From: mamuniz Date: Tue, 10 Nov 2020 09:04:06 +0000 Subject: [PATCH 41/80] EOEPCA-187: Added import get --- src/custom_oidc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/custom_oidc.py b/src/custom_oidc.py index 4da2722..279f775 100644 --- a/src/custom_oidc.py +++ b/src/custom_oidc.py @@ -15,7 +15,7 @@ from Crypto.PublicKey import RSA from jwt_verification.signature_verification import JWT_Verification -from requests import post +from requests import post, get class OIDCHandler: From 26cacdd512ef8aeacd71505750c47e676bcb916b Mon Sep 17 00:00:00 2001 From: Tiago Fernandes Date: Tue, 10 Nov 2020 10:14:52 +0000 Subject: [PATCH 42/80] EOEPCA-178 unique policy name --- src/resources/resources.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/resources/resources.py b/src/resources/resources.py index 56bd63e..c1cad63 100644 --- a/src/resources/resources.py +++ b/src/resources/resources.py @@ -238,7 +238,7 @@ def get_default_ownership_policy_cfg(resource_id, user_name): return { "resource_id": resource_id, "rules": [{ "AND": [ {"EQUAL": {"user_name" : user_name } }] }] } def get_default_ownership_policy_body(resource_id, user_name): - name = "Default Ownership Policy" + name = "Default Ownership Policy of " + str(resource_id) description = "This is the default ownership policy for created resources through PEP" policy_cfg = get_default_ownership_policy_cfg(resource_id, user_name) scopes = ["protected_access"] From 03fe1d09fdcf060a4f3d74e8fd3a2ab683f249f3 Mon Sep 17 00:00:00 2001 From: Tiago Fernandes Date: Tue, 10 Nov 2020 11:56:18 +0000 Subject: [PATCH 43/80] Disable JWT RSA1 verification --- src/handlers/oidc_handler.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/handlers/oidc_handler.py b/src/handlers/oidc_handler.py index 279f775..53e27ed 100644 --- a/src/handlers/oidc_handler.py +++ b/src/handlers/oidc_handler.py @@ -4,8 +4,8 @@ from WellKnownHandler import TYPE_OIDC, KEY_OIDC_TOKEN_ENDPOINT, KEY_OIDC_USERINFO_ENDPOINT from WellKnownHandler import TYPE_UMA_V2, KEY_UMA_V2_RESOURCE_REGISTRATION_ENDPOINT, KEY_UMA_V2_PERMISSION_ENDPOINT, KEY_UMA_V2_INTROSPECTION_ENDPOINT from base64 import b64encode -from custom_uma import UMA_Handler, resource -from custom_uma import rpt as class_rpt +from handlers.uma_handler import UMA_Handler, resource +from handlers.uma_handler import rpt as class_rpt import logging import base64 import json @@ -70,14 +70,14 @@ def verify_JWT_token(self, token, key): decoded = decoded.decode('utf-8') decoded_str = json.loads(decoded) - verificator = JWT_Verification() - result = verificator.verify_signature_JWT(token) + # verificator = JWT_Verification() + # result = verificator.verify_signature_JWT(token) - if result == False: - print("Verification of the signature for the JWT failed!") - raise Exception - else: - print("Signature verification is correct") + # if result == False: + # print("Verification of the signature for the JWT failed!") + # raise Exception + # else: + # print("Signature verification is correct") user_value = json.loads(decoded)[key] return user_value From 7b124c02024c0c498d02bbd40e2c10634854da28 Mon Sep 17 00:00:00 2001 From: Tiago Fernandes Date: Tue, 10 Nov 2020 12:01:32 +0000 Subject: [PATCH 44/80] JWT RSA1 disabled in main.py --- src/main.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/main.py b/src/main.py index 3241ebf..e98aa8a 100644 --- a/src/main.py +++ b/src/main.py @@ -226,16 +226,16 @@ def resource_request(path): print("Token found: "+rpt) rpt = rpt.replace("Bearer ","").strip() - if len(str(rpt))>40 and len(str(rpt)) != 76: - verificator = JWT_Verification() - result = verificator.verify_signature_JWT(str(rpt)) + # if len(str(rpt))>40 and len(str(rpt)) != 76: + # verificator = JWT_Verification() + # result = verificator.verify_signature_JWT(str(rpt)) - if result == False: - print("Verification of the signature for the JWT failed!") - response.status_code = 403 - return response - else: - print("Signature verification is correct") + # if result == False: + # print("Verification of the signature for the JWT failed!") + # response.status_code = 403 + # return response + # else: + # print("Signature verification is correct") # Validate for a specific resource From 1d22f20d7195fc749271fade1ae2caae3e066b8c Mon Sep 17 00:00:00 2001 From: Hector Rodriguez Date: Tue, 10 Nov 2020 15:44:05 +0000 Subject: [PATCH 45/80] Added Docker-Compose version of the PEP --- docker-compose.yml | 27 +++++++++++++++++++++++++++ src/config/config.json | 2 +- 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 docker-compose.yml diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..229f752 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,27 @@ +version: "3.5" +services: + um-pep-engine: + build: + context: . + dockerfile: Dockerfile + image: eoepca/um-pep-engine:build + container_name: um-pep-engine + networks: + - eoepca_network + ports: + - '5566:5566' + volumes: + - ./src/config:/config + + mongo: + image: 'mongo' + container_name: 'mongo' + networks: + - eoepca_network + ports: + - '27017-27019:27017-27019' + +networks: + eoepca_network: + driver: bridge + name: eoepcanetwork diff --git a/src/config/config.json b/src/config/config.json index 2ee0e0f..8ebf200 100644 --- a/src/config/config.json +++ b/src/config/config.json @@ -1 +1 @@ -{"realm": "eoepca", "auth_server_url": "https://test.eoepca.org", "proxy_endpoint": "/", "service_host": "0.0.0.0", "service_port": 5566, "s_margin_rpt_valid": 5, "check_ssl_certs": false, "use_threads": true, "debug_mode": true, "resource_server_endpoint": "http://eoepca-ades-core", "api_rpt_uma_validation": true, "rpt_limit_uses": 5, "pdp_url": "http://test.eoepca.org", "pdp_port": 5567, "pdp_policy_endpoint": "/policy/"} +{"realm": "eoepca_dev", "auth_server_url": "https://test.185.52.193.87.nip.io", "proxy_endpoint": "/ades", "service_host": "0.0.0.0", "service_port": 5566, "s_margin_rpt_valid": 5, "check_ssl_certs": false, "use_threads": true, "debug_mode": true, "resource_server_endpoint": "http://eoepca-ades-core", "api_rpt_uma_validation": true, "rpt_limit_uses": 5, "pdp_url": "http://pdp", "pdp_port": 5567, "pdp_policy_endpoint": "/policy/", "client_id": "7ce60aeb-cc78-49ec-baca-33c1085dcae6", "client_secret": "67f73f06-1d8d-4b8b-9b4f-e5b465222859"} \ No newline at end of file From d313cfbf7d77b05c7a301d476723d983199fbf24 Mon Sep 17 00:00:00 2001 From: Tiago Fernandes Date: Fri, 13 Nov 2020 12:56:08 +0000 Subject: [PATCH 46/80] EOEPCA-195 commit to develop --- src/main.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main.py b/src/main.py index e98aa8a..057829e 100644 --- a/src/main.py +++ b/src/main.py @@ -197,6 +197,8 @@ def proxy_request(request, new_header): excluded_headers = ['transfer-encoding'] headers = [(name, value) for (name, value) in res.raw.headers.items() if name.lower() not in excluded_headers] response = Response(res.content, res.status_code, headers) + response.autocorrect_location_header = False + response.headers["Location"] = g_config["proxy_endpoint"] + response.headers["Location"].replace(g_config["resource_server_endpoint"], '') return response except Exception as e: response = Response() From abeb4a839d4a7328778e9c7b5cfae03122466f0a Mon Sep 17 00:00:00 2001 From: Tiago Fernandes Date: Mon, 16 Nov 2020 15:16:04 +0000 Subject: [PATCH 47/80] EOEPCA-195 develop "if" for Location --- src/main.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main.py b/src/main.py index 057829e..6f24add 100644 --- a/src/main.py +++ b/src/main.py @@ -197,8 +197,9 @@ def proxy_request(request, new_header): excluded_headers = ['transfer-encoding'] headers = [(name, value) for (name, value) in res.raw.headers.items() if name.lower() not in excluded_headers] response = Response(res.content, res.status_code, headers) - response.autocorrect_location_header = False - response.headers["Location"] = g_config["proxy_endpoint"] + response.headers["Location"].replace(g_config["resource_server_endpoint"], '') + if "Location" in response.headers: + response.autocorrect_location_header = False + response.headers["Location"] = g_config["proxy_endpoint"] + response.headers["Location"].replace(g_config["resource_server_endpoint"], '') return response except Exception as e: response = Response() From 793f2de77a5ceef388f0a86e1cb2d7cb2af11a1d Mon Sep 17 00:00:00 2001 From: mamuniz Date: Tue, 17 Nov 2020 10:45:06 +0000 Subject: [PATCH 48/80] EOEPCA-189: #comment Added verification signatures for JWT --- src/handlers/oidc_handler.py | 79 ++++++++++++++++++++++-------------- src/handlers/uma_handler.py | 27 +++++++++++- src/main.py | 31 ++++++-------- 3 files changed, 87 insertions(+), 50 deletions(-) diff --git a/src/handlers/oidc_handler.py b/src/handlers/oidc_handler.py index 53e27ed..b397dab 100644 --- a/src/handlers/oidc_handler.py +++ b/src/handlers/oidc_handler.py @@ -6,12 +6,15 @@ from base64 import b64encode from handlers.uma_handler import UMA_Handler, resource from handlers.uma_handler import rpt as class_rpt +from config import load_config import logging import base64 import json from jwkest.jws import JWS +from jwkest.jwk import SYMKey, KEYS from jwkest.jwk import RSAKey, import_rsa_key_from_file, load_jwks_from_url, import_rsa_key from jwkest.jwk import load_jwks +from jwkest.jwk import rsa_load from Crypto.PublicKey import RSA from jwt_verification.signature_verification import JWT_Verification @@ -44,25 +47,15 @@ def get_new_pat(self): return access_token - def verify_RPT_token(self, token, key): - try: - introspection_endpoint = self.wkh.get(TYPE_UMA_V2, KEY_UMA_V2_INTROSPECTION_ENDPOINT) - pat = self.get_new_pat() - rpt_class = class_rpt.introspect(rpt=token, pat=pat, introspection_endpoint=introspection_endpoint, secure=False) - - if rpt_class[key] == None: - if rpt_class['pct_claims'][key][0] == None: - raise Exception - else: - return rpt_class['pct_claims'][key][0] - else: - return rpt_class[key] - except Exception as e: - print("Authenticated RPT Resource. No Valid RPT token passed! " +str(e)) - return None - def verify_JWT_token(self, token, key): try: + header = str(token).split(".")[0] + paddedHeader = header + '=' * (4 - len(header) % 4) + decodedHeader = base64.b64decode(paddedHeader) + #to remove byte-code + decodedHeader_format = decodedHeader.decode('utf-8') + decoded_str_header = json.loads(decodedHeader_format) + payload = str(token).split(".")[1] paddedPayload = payload + '=' * (4 - len(payload) % 4) decoded = base64.b64decode(paddedPayload) @@ -70,16 +63,39 @@ def verify_JWT_token(self, token, key): decoded = decoded.decode('utf-8') decoded_str = json.loads(decoded) - # verificator = JWT_Verification() - # result = verificator.verify_signature_JWT(token) - - # if result == False: - # print("Verification of the signature for the JWT failed!") - # raise Exception - # else: - # print("Signature verification is correct") + verification_signature = self.getVerificationConfig() + + if verification_signature == True: + if decoded_str_header['kid'] != "RSA1": + verificator = JWT_Verification() + result = verificator.verify_signature_JWT(token) + else: + #validate signature for rpt + rsajwk = RSAKey(kid="RSA1", key=import_rsa_key_from_file("config/public.pem")) + dict_rpt_values = JWS().verify_compact(token, keys=[rsajwk], sigalg="RS256") + + if dict_rpt_values == decoded_str: + result = True + else: + result = False + + if result == False: + print("Verification of the signature for the JWT failed!") + raise Exception + else: + print("Signature verification is correct!") + + if decoded_str_header['kid'] != "RSA1": + user_value = decoded_str['pct_claims'][key] + else: + if decoded_str[key] == None: + if decoded_str['pct_claims'][key][0] == None: + raise Exception + else: + user_value = decoded_str['pct_claims'][key][0] + else: + user_value = decoded_str[key] - user_value = json.loads(decoded)[key] return user_value except Exception as e: print("Authenticated RPT Resource. No Valid JWT id token passed! " +str(e)) @@ -106,13 +122,16 @@ def verify_uid_headers(self, headers_protected, key): token_protected = inputToken_protected if token_protected: #Compares between JWT id_token and OAuth access token to retrieve the requested key-value - if len(str(token_protected)) == 76: - value=self.verify_RPT_token(token_protected, key) - elif len(str(token_protected))>40: + if len(str(token_protected))>40: value=self.verify_JWT_token(token_protected, key) else: value=self.verify_OAuth_token(token_protected, key) return value else: - return 'NO TOKEN FOUND' \ No newline at end of file + return 'NO TOKEN FOUND' + + def getVerificationConfig(self): + g_config = load_config("config/config.json") + + return g_config['verify_signature'] \ No newline at end of file diff --git a/src/handlers/uma_handler.py b/src/handlers/uma_handler.py index c913be8..a4fea87 100644 --- a/src/handlers/uma_handler.py +++ b/src/handlers/uma_handler.py @@ -2,7 +2,10 @@ from eoepca_uma import rpt, resource from handlers.mongo_handler import Mongo_Handler from WellKnownHandler import TYPE_UMA_V2, KEY_UMA_V2_RESOURCE_REGISTRATION_ENDPOINT, KEY_UMA_V2_PERMISSION_ENDPOINT, KEY_UMA_V2_INTROSPECTION_ENDPOINT +from jwt_verification.signature_verification import JWT_Verification from typing import List +import base64 +import json import pymongo from datetime import datetime @@ -69,15 +72,35 @@ def delete(self, resource_id: str): # Usage of Python library for query mongodb instance - def validate_rpt(self, user_rpt: str, resources: List[dict], margin_time_rpt_valid: float, rpt_limit_uses: int) -> bool: + def validate_rpt(self, user_rpt: str, resources: List[dict], margin_time_rpt_valid: float, rpt_limit_uses: int, verify_signature: bool) -> bool: """ Returns True/False, if the RPT is valid for the resource(s) they are trying to access """ results = [] + rpt_splitted = user_rpt.split('.') + introspection_endpoint = self.wkh.get(TYPE_UMA_V2, KEY_UMA_V2_INTROSPECTION_ENDPOINT) pat = self.oidch.get_new_pat() - rpt_class = rpt.introspect(rpt=user_rpt, pat=pat, introspection_endpoint=introspection_endpoint, secure=False) + + if len(rpt_splitted) == 3: + if verify_signature == True: + test = JWT_Verification() + result = test.verify_signature_JWT(user_rpt) + + if result == False: + print("Verification of the signature for the JWT failed!") + return False + else: + print("Signature verification is correct!") + + payload = str(user_rpt).split(".")[1] + paddedPayload = payload + '=' * (4 - len(payload) % 4) + decoded = base64.b64decode(paddedPayload) + decoded = decoded.decode('utf-8') + rpt_class = json.loads(decoded) + else: + rpt_class = rpt.introspect(rpt=user_rpt, pat=pat, introspection_endpoint=introspection_endpoint, secure=False) result = rpt.is_valid_now(user_rpt, pat, introspection_endpoint, resources, time_margin= margin_time_rpt_valid ,secure= self.verify ) diff --git a/src/main.py b/src/main.py index e98aa8a..0fd6dac 100644 --- a/src/main.py +++ b/src/main.py @@ -26,7 +26,6 @@ from jwkest.jwk import RSAKey, import_rsa_key_from_file, load_jwks_from_url, import_rsa_key from jwkest.jwk import load_jwks from Crypto.PublicKey import RSA -from jwt_verification.signature_verification import JWT_Verification import logging logging.getLogger().setLevel(logging.INFO) ### INITIAL SETUP @@ -226,31 +225,27 @@ def resource_request(path): print("Token found: "+rpt) rpt = rpt.replace("Bearer ","").strip() - # if len(str(rpt))>40 and len(str(rpt)) != 76: - # verificator = JWT_Verification() - # result = verificator.verify_signature_JWT(str(rpt)) - - # if result == False: - # print("Verification of the signature for the JWT failed!") - # response.status_code = 403 - # return response - # else: - # print("Signature verification is correct") - - # Validate for a specific resource - if uma_handler.validate_rpt(rpt, [{"resource_id": resource_id, "resource_scopes": scopes }], int(g_config["s_margin_rpt_valid"]), int(g_config["rpt_limit_uses"])) or not api_rpt_uma_validation: + if uma_handler.validate_rpt(rpt, [{"resource_id": resource_id, "resource_scopes": scopes }], int(g_config["s_margin_rpt_valid"]), int(g_config["rpt_limit_uses"]), g_config["verify_signature"]) or not api_rpt_uma_validation: print("RPT valid, accesing ") - introspection_endpoint=g_wkh.get(TYPE_UMA_V2, KEY_UMA_V2_INTROSPECTION_ENDPOINT) - pat = oidc_client.get_new_pat() - rpt_class = class_rpt.introspect(rpt=rpt, pat=pat, introspection_endpoint=introspection_endpoint, secure=False) - jwt_rpt_response = create_jwt(rpt_class, private_key) + + rpt_splitted = rpt.split('.') + + if len(rpt_splitted) == 3: + jwt_rpt_response = rpt + else: + introspection_endpoint=g_wkh.get(TYPE_UMA_V2, KEY_UMA_V2_INTROSPECTION_ENDPOINT) + pat = oidc_client.get_new_pat() + rpt_class = class_rpt.introspect(rpt=rpt, pat=pat, introspection_endpoint=introspection_endpoint, secure=False) + jwt_rpt_response = create_jwt(rpt_class, private_key) + headers_splitted = split_headers(str(request.headers)) headers_splitted['Authorization'] = "Bearer "+str(jwt_rpt_response) new_header = Headers() for key, value in headers_splitted.items(): new_header.add(key, value) + # redirect to resource return proxy_request(request, new_header) print("Invalid RPT!, sending ticket") From ec5478a7f8c28241a09b383b49161fa2930a0c39 Mon Sep 17 00:00:00 2001 From: mamuniz Date: Tue, 17 Nov 2020 13:57:30 +0000 Subject: [PATCH 49/80] EOEPCA-189: #comment Updated config file --- src/config/config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/config.json b/src/config/config.json index 8ebf200..c492d56 100644 --- a/src/config/config.json +++ b/src/config/config.json @@ -1 +1 @@ -{"realm": "eoepca_dev", "auth_server_url": "https://test.185.52.193.87.nip.io", "proxy_endpoint": "/ades", "service_host": "0.0.0.0", "service_port": 5566, "s_margin_rpt_valid": 5, "check_ssl_certs": false, "use_threads": true, "debug_mode": true, "resource_server_endpoint": "http://eoepca-ades-core", "api_rpt_uma_validation": true, "rpt_limit_uses": 5, "pdp_url": "http://pdp", "pdp_port": 5567, "pdp_policy_endpoint": "/policy/", "client_id": "7ce60aeb-cc78-49ec-baca-33c1085dcae6", "client_secret": "67f73f06-1d8d-4b8b-9b4f-e5b465222859"} \ No newline at end of file +{"realm": "eoepca_dev", "auth_server_url": "https://test.185.52.193.87.nip.io", "proxy_endpoint": "/ades", "service_host": "0.0.0.0", "service_port": 5566, "s_margin_rpt_valid": 5, "check_ssl_certs": false, "use_threads": true, "debug_mode": true, "resource_server_endpoint": "http://eoepca-ades-core", "api_rpt_uma_validation": true, "rpt_limit_uses": 5, "pdp_url": "http://pdp", "pdp_port": 5567, "pdp_policy_endpoint": "/policy/", "verify_signature": false, "client_id": "7ce60aeb-cc78-49ec-baca-33c1085dcae6", "client_secret": "67f73f06-1d8d-4b8b-9b4f-e5b465222859"} \ No newline at end of file From 61c712b71173bd89189170427450f6184d753e69 Mon Sep 17 00:00:00 2001 From: mamuniz Date: Tue, 17 Nov 2020 15:59:52 +0000 Subject: [PATCH 50/80] EOEPCA-189: #comment Fixed bug when the bearer is a JWT --- src/handlers/oidc_handler.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/handlers/oidc_handler.py b/src/handlers/oidc_handler.py index b397dab..ee72227 100644 --- a/src/handlers/oidc_handler.py +++ b/src/handlers/oidc_handler.py @@ -86,7 +86,13 @@ def verify_JWT_token(self, token, key): print("Signature verification is correct!") if decoded_str_header['kid'] != "RSA1": - user_value = decoded_str['pct_claims'][key] + if key in decoded_str.keys(): + if decoded_str[key] != None: + user_value = decoded_str[key] + else: + raise Exception + else: + user_value = decoded_str['pct_claims'][key] else: if decoded_str[key] == None: if decoded_str['pct_claims'][key][0] == None: From 350a060ef6b4800011a7d720cd803ca24623c6b1 Mon Sep 17 00:00:00 2001 From: vagrant Date: Tue, 17 Nov 2020 17:00:48 +0000 Subject: [PATCH 51/80] EOEPCA-189: #comment Added initial documentation --- docs/SDD/02.overview/00.overview.adoc | 11 ++++++----- docs/SDD/03.design/00.design.adoc | 1 + 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/SDD/02.overview/00.overview.adoc b/docs/SDD/02.overview/00.overview.adoc index 4776fbe..aaa6e2e 100644 --- a/docs/SDD/02.overview/00.overview.adoc +++ b/docs/SDD/02.overview/00.overview.adoc @@ -94,9 +94,10 @@ The endpoints used for SCIM are: The Resource API Endpoints offered by the PEP component are protected based on the unique identifier of the Resource Owner that is adding/removing/editing resources. The Resource API is protected with OAuth/OIDC in the PEP, expecting any of these tokens: -* JWT id_tokens: in this case the PEP extracts the necessary claims from the JWT uniquely identifying the user (“sub” parameter) and the signature of the JWT will also be verified. - -* RPT Token: in this case the PEP extracts the necessary claims from the RPT uniquely from the "pct_claims" parameter +* JWT id_tokens: in this case the PEP extracts the necessary claims from the JWT uniquely identifying the user (“sub” parameter). +The signature of this token will be verified if the signature verification is enabled in the environment variables. +In case it is enabled, it will be distinguished if the JWT obtained in the header is signed with the internal keys of the platform or the building block. +If the platform signature has been used, it will be verified with the platform endpoint. If the signature is from the PEP block, it will be verified with the public key from the PEP. * OAuth Access Token: in this case the PEP performs a query against the User-Info endpoint, uniquely identifying the user. @@ -177,6 +178,6 @@ On the other hand, this baseline functionality is desirable to allow PEP-chainin === Request Forwarding with JWT header -After validating the RPT we proceed to make a call to the introspection endpoint (/oxauth/restv1/rpt/status) passing through parameters the RPT and the pat. Returning a JSON with the information for that token, called claims, where the user name can appear, for example. - +After validating the RPT we proceed to verify the signature of the JWT if the signature verification is enabled in the environment variables and the we pass the JWT to the request header in the request to the resource server. +If the header has a RPT as token we make a call to the introspection endpoint (/oxauth/restv1/rpt/status) passing through parameters the RPT and the pat. Returning a JSON with the information for that token, called claims, where the user name can appear, for example. Then we proceed to generate this JSON to the format of JWT using an asymmetric cryptography, in this case using RSA with a private key. And then pass this JWT as a header in the request to the resource server. diff --git a/docs/SDD/03.design/00.design.adoc b/docs/SDD/03.design/00.design.adoc index 48b532f..954b1de 100644 --- a/docs/SDD/03.design/00.design.adoc +++ b/docs/SDD/03.design/00.design.adoc @@ -60,6 +60,7 @@ The parameters that are accepted, and their meaning, are as follows: - **use_threads**: Toggle on/off (bool) the usage of threads for the proxy. Recommended to be left as True. - **debug_mode**: Toggle on/off (bool) a debug mode of Flask. In a production environment, this should be false. - **resource_server_endpoint**: Complete url (with "https" and any port) of the Resource Server to protect with this PEP. +- **verify_signature**: Toggle on/off (bool) the usage of signature validation for the JWT. - **client_id**: string indicating a client_id for an already registered and configured client. **This parameter is optional**. When not supplied, the PEP will generate a new client for itself and store it in this key inside the JSON. - **client_secret**: string indicating the client secret for the client_id. **This parameter is optional**. When not supplied, the PEP will generate a new client for itself and store it in this key inside the JSON. From 99cf94273484b1196666e72c024d4dbc68f15de7 Mon Sep 17 00:00:00 2001 From: mamuniz Date: Thu, 19 Nov 2020 11:54:05 +0000 Subject: [PATCH 52/80] EOEPCA-189: Added env var in main --- src/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main.py b/src/main.py index 0fd6dac..fc16855 100644 --- a/src/main.py +++ b/src/main.py @@ -45,7 +45,8 @@ "PEP_RPT_LIMIT_USES", "PEP_PDP_URL", "PEP_PDP_PORT", -"PEP_PDP_POLICY_ENDPOINT"] +"PEP_PDP_POLICY_ENDPOINT", +"PEP_VERIFY_SIGNATURE"] use_env_var = True From 3512ec43e978ee67ef62eed821e940aaeded7cef Mon Sep 17 00:00:00 2001 From: mamuniz Date: Thu, 19 Nov 2020 12:15:33 +0000 Subject: [PATCH 53/80] EOEPCA-189: Save config values in config file --- src/main.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main.py b/src/main.py index fc16855..2712e9b 100644 --- a/src/main.py +++ b/src/main.py @@ -116,6 +116,8 @@ else: print("Client found in config, using: "+g_config["client_id"]) +save_config("config/config.json", g_config) + oidc_client = OIDCHandler(g_wkh, client_id = g_config["client_id"], client_secret = g_config["client_secret"], From c05338f9ab9592047e5c1bcce03a597c882265d6 Mon Sep 17 00:00:00 2001 From: mamuniz Date: Thu, 19 Nov 2020 15:36:26 +0000 Subject: [PATCH 54/80] EOEPCA-189: Updated documentation to clarify if the verification is disabled --- docs/SDD/02.overview/00.overview.adoc | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/SDD/02.overview/00.overview.adoc b/docs/SDD/02.overview/00.overview.adoc index aaa6e2e..fa7cef7 100644 --- a/docs/SDD/02.overview/00.overview.adoc +++ b/docs/SDD/02.overview/00.overview.adoc @@ -98,6 +98,7 @@ The Resource API is protected with OAuth/OIDC in the PEP, expecting any of these The signature of this token will be verified if the signature verification is enabled in the environment variables. In case it is enabled, it will be distinguished if the JWT obtained in the header is signed with the internal keys of the platform or the building block. If the platform signature has been used, it will be verified with the platform endpoint. If the signature is from the PEP block, it will be verified with the public key from the PEP. +In case it is disabled, the signature will not be verified but the other steps above will be performed. The PEP extracts the necessary claims from "sub" parameter. * OAuth Access Token: in this case the PEP performs a query against the User-Info endpoint, uniquely identifying the user. From ec1cccb805dbfac2ace0c4ec2e47c874e3b024c8 Mon Sep 17 00:00:00 2001 From: mamuniz Date: Thu, 19 Nov 2020 16:30:30 +0000 Subject: [PATCH 55/80] EOEPCA-189: Updated documentation to clarify if the verification is disabled part 2 --- docs/SDD/02.overview/00.overview.adoc | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/SDD/02.overview/00.overview.adoc b/docs/SDD/02.overview/00.overview.adoc index fa7cef7..ab5a6f8 100644 --- a/docs/SDD/02.overview/00.overview.adoc +++ b/docs/SDD/02.overview/00.overview.adoc @@ -182,3 +182,4 @@ On the other hand, this baseline functionality is desirable to allow PEP-chainin After validating the RPT we proceed to verify the signature of the JWT if the signature verification is enabled in the environment variables and the we pass the JWT to the request header in the request to the resource server. If the header has a RPT as token we make a call to the introspection endpoint (/oxauth/restv1/rpt/status) passing through parameters the RPT and the pat. Returning a JSON with the information for that token, called claims, where the user name can appear, for example. Then we proceed to generate this JSON to the format of JWT using an asymmetric cryptography, in this case using RSA with a private key. And then pass this JWT as a header in the request to the resource server. +If the verification of the signature for the JWT is disabled, the code will do the introspection steps in the case of a RPT, and then will add the JWT to the request header without verifying the token signature. From 826c0fb61e06b5d014215902c6edfc2eba8ba4c0 Mon Sep 17 00:00:00 2001 From: mamuniz Date: Fri, 20 Nov 2020 07:13:59 +0000 Subject: [PATCH 56/80] EOEPCA-189: Updated oidc_handler --- src/handlers/oidc_handler.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/handlers/oidc_handler.py b/src/handlers/oidc_handler.py index ee72227..d353edf 100644 --- a/src/handlers/oidc_handler.py +++ b/src/handlers/oidc_handler.py @@ -63,9 +63,7 @@ def verify_JWT_token(self, token, key): decoded = decoded.decode('utf-8') decoded_str = json.loads(decoded) - verification_signature = self.getVerificationConfig() - - if verification_signature == True: + if self.getVerificationConfig() == True: if decoded_str_header['kid'] != "RSA1": verificator = JWT_Verification() result = verificator.verify_signature_JWT(token) From 85231e88eca3210117105af869a63201860bceb1 Mon Sep 17 00:00:00 2001 From: Tiago Fernandes Date: Mon, 23 Nov 2020 11:55:27 +0000 Subject: [PATCH 57/80] config file fix --- src/config/config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/config.json b/src/config/config.json index c492d56..ea1c155 100644 --- a/src/config/config.json +++ b/src/config/config.json @@ -1 +1 @@ -{"realm": "eoepca_dev", "auth_server_url": "https://test.185.52.193.87.nip.io", "proxy_endpoint": "/ades", "service_host": "0.0.0.0", "service_port": 5566, "s_margin_rpt_valid": 5, "check_ssl_certs": false, "use_threads": true, "debug_mode": true, "resource_server_endpoint": "http://eoepca-ades-core", "api_rpt_uma_validation": true, "rpt_limit_uses": 5, "pdp_url": "http://pdp", "pdp_port": 5567, "pdp_policy_endpoint": "/policy/", "verify_signature": false, "client_id": "7ce60aeb-cc78-49ec-baca-33c1085dcae6", "client_secret": "67f73f06-1d8d-4b8b-9b4f-e5b465222859"} \ No newline at end of file +{"realm": "eoepca", "auth_server_url": "https://test.eoepca.org", "proxy_endpoint": "/ades", "service_host": "0.0.0.0", "service_port": 5566, "s_margin_rpt_valid": 5, "check_ssl_certs": false, "use_threads": true, "debug_mode": true, "resource_server_endpoint": "http://eoepca-ades-core", "api_rpt_uma_validation": true, "rpt_limit_uses": 5, "pdp_url": "http://test.eoepca.org", "pdp_port": 5567, "pdp_policy_endpoint": "/policy/", "verify_signature": false} From ac2d0187dcde10d0fa51139012c9b913ae37697a Mon Sep 17 00:00:00 2001 From: Tiago Fernandes Date: Tue, 24 Nov 2020 16:04:33 +0000 Subject: [PATCH 58/80] EOEPCA-205 swagger first impl --- src/main.py | 17 ++ src/requirements.txt | 1 + src/static/swagger_pep_ui.json | 305 +++++++++++++++++++++++++++++++++ 3 files changed, 323 insertions(+) create mode 100644 src/static/swagger_pep_ui.json diff --git a/src/main.py b/src/main.py index ba14eba..e6e17f0 100644 --- a/src/main.py +++ b/src/main.py @@ -4,6 +4,7 @@ from WellKnownHandler import TYPE_UMA_V2, KEY_UMA_V2_RESOURCE_REGISTRATION_ENDPOINT, KEY_UMA_V2_PERMISSION_ENDPOINT, KEY_UMA_V2_INTROSPECTION_ENDPOINT from flask import Flask, request, Response +from flask_swagger_ui import get_swaggerui_blueprint from werkzeug.datastructures import Headers from random import choice from string import ascii_lowercase @@ -142,8 +143,24 @@ app = Flask(__name__) app.secret_key = ''.join(choice(ascii_lowercase) for i in range(30)) # Random key +# SWAGGER initiation +SWAGGER_URL = '/swagger-ui' # URL for exposing Swagger UI (without trailing '/') +API_URL = "" # Our local swagger resource for PEP. Not used here as 'spec' parameter is used in config +SWAGGER_SPEC = json.load(open("./static/swagger_pep_ui.json")) +SWAGGER_APP_NAME = "Policy Enforcement Point Interfaces" + +swaggerui_blueprint = get_swaggerui_blueprint( + SWAGGER_URL, + API_URL, + config={ # Swagger UI config overrides + 'app_name': SWAGGER_APP_NAME, + 'spec': SWAGGER_SPEC + }, +) + # Register api blueprints (module endpoints) app.register_blueprint(resources.construct_blueprint(oidc_client, uma_handler, pdp_policy_handler, g_config)) +app.register_blueprint(swaggerui_blueprint) def generateRSAKeyPair(): _rsakey = RSA.generate(2048) diff --git a/src/requirements.txt b/src/requirements.txt index 85bdf8f..5a39fab 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -8,3 +8,4 @@ pyjwkest==1.4.2 pycrypto==2.6.1 pymongo mock +flask-swagger-ui==3.36.0 \ No newline at end of file diff --git a/src/static/swagger_pep_ui.json b/src/static/swagger_pep_ui.json new file mode 100644 index 0000000..0f1b8dc --- /dev/null +++ b/src/static/swagger_pep_ui.json @@ -0,0 +1,305 @@ +{ + "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" : "Policy Enforcement", + "description" : "Proxying functionality to enforce authorization policies" + }, { + "name" : "Resources", + "description" : "Operations to create, modify or delete resources" + } ], + "paths" : { + "/proxy/{path}" : { + "parameters" : [ { + "in" : "path", + "name" : "path", + "description" : "Path to the Back-End Service", + "required" : true, + "schema" : { + "type" : "string" + } + }, { + "in" : "header", + "name" : "Authorization", + "description" : "RPT Token generated through UMA Flow", + "schema" : { + "type" : "string" + } + } ], + "get" : { + "tags" : [ "Policy Enforcement" ], + "summary" : "Request to Back-End Service", + "description" : "This operation propagates all headers and query parameters", + "responses" : { + "200" : { + "description" : "OK" + }, + "401" : { + "$ref" : "#/components/responses/UMAUnauthorized" + } + } + }, + "post" : { + "tags" : [ "Policy Enforcement" ], + "summary" : "Request to Back-End Service", + "description" : "This operation propagates all headers, query parameters and body", + "responses" : { + "200" : { + "description" : "OK" + }, + "401" : { + "$ref" : "#/components/responses/UMAUnauthorized" + } + } + }, + "put" : { + "tags" : [ "Policy Enforcement" ], + "summary" : "Request to Back-End Service", + "description" : "This operation propagates all headers, query parameters and body", + "responses" : { + "200" : { + "description" : "OK" + }, + "401" : { + "$ref" : "#/components/responses/UMAUnauthorized" + } + } + }, + "delete" : { + "tags" : [ "Policy Enforcement" ], + "summary" : "Request to Back-End Service", + "description" : "This operation propagates all headers", + "responses" : { + "200" : { + "description" : "OK" + }, + "401" : { + "$ref" : "#/components/responses/UMAUnauthorized" + } + } + } + }, + "/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" ] + } + } + } + } + } +} \ No newline at end of file From 7b35765befded1645c6bccf77577b5d398320d4d Mon Sep 17 00:00:00 2001 From: AlvaroVillanueva Date: Wed, 25 Nov 2020 10:48:57 +0000 Subject: [PATCH 59/80] EOEPCA-210 Chart template --- charts/pep-engine/Chart.yaml | 24 ++++++++ charts/pep-engine/templates/_helpers.tpl | 32 +++++++++++ charts/pep-engine/templates/ingress.yaml | 20 +++++++ charts/pep-engine/templates/pep-cm.yml | 21 +++++++ .../pep-engine/templates/pep-deployment.yml | 56 +++++++++++++++++++ charts/pep-engine/templates/pep-service.yml | 21 +++++++ charts/pep-engine/templates/pv.yaml | 14 +++++ charts/pep-engine/templates/pvc.yaml | 21 +++++++ charts/pep-engine/values.yaml | 52 +++++++++++++++++ src/config/config.json | 2 +- 10 files changed, 262 insertions(+), 1 deletion(-) create mode 100644 charts/pep-engine/Chart.yaml create mode 100644 charts/pep-engine/templates/_helpers.tpl create mode 100644 charts/pep-engine/templates/ingress.yaml create mode 100755 charts/pep-engine/templates/pep-cm.yml create mode 100755 charts/pep-engine/templates/pep-deployment.yml create mode 100755 charts/pep-engine/templates/pep-service.yml create mode 100644 charts/pep-engine/templates/pv.yaml create mode 100644 charts/pep-engine/templates/pvc.yaml create mode 100644 charts/pep-engine/values.yaml diff --git a/charts/pep-engine/Chart.yaml b/charts/pep-engine/Chart.yaml new file mode 100644 index 0000000..e185b28 --- /dev/null +++ b/charts/pep-engine/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v1 +name: pep-engine +description: A Helm chart for PEP Engine +maintainers: + - name: eoepca +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +appVersion: 1.0 diff --git a/charts/pep-engine/templates/_helpers.tpl b/charts/pep-engine/templates/_helpers.tpl new file mode 100644 index 0000000..7b4094b --- /dev/null +++ b/charts/pep-engine/templates/_helpers.tpl @@ -0,0 +1,32 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "login-service.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "login-service.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "login-service.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + diff --git a/charts/pep-engine/templates/ingress.yaml b/charts/pep-engine/templates/ingress.yaml new file mode 100644 index 0000000..112ef44 --- /dev/null +++ b/charts/pep-engine/templates/ingress.yaml @@ -0,0 +1,20 @@ + +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: gluu-ingress-pep-engine + + annotations: + kubernetes.io/ingress.class: nginx + nginx.ingress.kubernetes.io/ssl-redirect: "false" + nginx.ingress.kubernetes.io/rewrite-target: /$2 +spec: + rules: + - host: {{ .Values.global.ep | quote }} + http: + paths: + - path: /secure(/|$)(.*) + backend: + serviceName: pep-engine + servicePort: 5566 + diff --git a/charts/pep-engine/templates/pep-cm.yml b/charts/pep-engine/templates/pep-cm.yml new file mode 100755 index 0000000..d852fad --- /dev/null +++ b/charts/pep-engine/templates/pep-cm.yml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: pep-cm +data: + PEP_REALM: {{ .Values.global.realm | quote }} + PEP_AUTH_SERVER_URL: {{ .Values.global.domain | quote }} + PEP_PROXY_ENDPOINT: {{ .Values.global.proxyEndpoint | quote }} + PEP_SERVICE_HOST: {{ .Values.global.serviceHost | quote }} + PEP_SERVICE_PORT: {{ .Values.global.servicePort | quote }} + PEP_S_MARGIN_RPT_VALID: {{ .Values.global.margin | quote }} + PEP_CHECK_SSL_CERTS: {{ .Values.global.sslCerts | quote }} + PEP_USE_THREADS: {{ .Values.global.useThreads | quote }} + PEP_DEBUG_MODE: {{ .Values.global.debugMode | quote }} + PEP_RESOURCE_SERVER_ENDPOINT: {{ .Values.global.resourceServer | quote }} + PEP_API_RPT_UMA_VALIDATION: {{ .Values.global.umaValidation | quote }} + PEP_RPT_LIMIT_USES: {{ .Values.global.limitUses | quote }} + PEP_PDP_URL: {{ .Values.global.pdpUrl | quote }} + PEP_PDP_PORT: {{ .Values.global.pdpPort | quote }} + PEP_PDP_POLICY_ENDPOINT: {{ .Values.global.pdpPolicy | quote }} + PEP_VERIFY_SIGNATURE: {{ .Values.global.verifySignature | quote }} \ No newline at end of file diff --git a/charts/pep-engine/templates/pep-deployment.yml b/charts/pep-engine/templates/pep-deployment.yml new file mode 100755 index 0000000..7c9c0bb --- /dev/null +++ b/charts/pep-engine/templates/pep-deployment.yml @@ -0,0 +1,56 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Values.global.pep }} + labels: + app: {{ .Values.global.pep }} +spec: + replicas: 1 + selector: + matchLabels: + app: {{ .Values.global.pep }} + template: + metadata: + labels: + app: {{ .Values.global.pep }} + spec: + containers: + - name: {{ .Values.global.pep }} + imagePullPolicy: {{ .Values.image.imagePullPolicy }} + image: {{ .Values.image.image }} + ports: + - name: http-pep + containerPort: 5566 + protocol: TCP + - name: https-pep + containerPort: 443 + protocol: TCP + envFrom: + - configMapRef: + name: pep-cm + volumeMounts: + - mountPath: /data/db/resource + sub_path: pep-engine/db/resource + name: eoepca-pep-pv-host + - name: mongo + imagePullPolicy: {{ .Values.image.imagePullPolicy }} + image: mongo + ports: + - name: http-rp + containerPort: 27017 + protocol: TCP + envFrom: + - configMapRef: + name: pep-cm + volumeMounts: + - mountPath: /data/db/resource + sub_path: pep-engine/db/resource + name: eoepca-pep-pv-host + hostAliases: + - ip: {{ .Values.global.nginxIp }} + hostnames: + - {{ .Values.global.ep }} + volumes: + - name: eoepca-pep-pv-host + persistentVolumeClaim: + claimName: eoepca-pep-pvc diff --git a/charts/pep-engine/templates/pep-service.yml b/charts/pep-engine/templates/pep-service.yml new file mode 100755 index 0000000..aff9142 --- /dev/null +++ b/charts/pep-engine/templates/pep-service.yml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Values.global.pep }} + labels: + app: {{ .Values.global.pep }} +spec: + type: NodePort + ports: + - port: 5566 + name: http-pep + targetPort: 5566 + protocol: TCP + nodePort: 31707 + - port: 1025 + name: https-pep + targetPort: 443 + protocol: TCP + selector: + app: {{ .Values.global.pep }} + \ No newline at end of file diff --git a/charts/pep-engine/templates/pv.yaml b/charts/pep-engine/templates/pv.yaml new file mode 100644 index 0000000..8b44f1a --- /dev/null +++ b/charts/pep-engine/templates/pv.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: PersistentVolume +metadata: + name: eoepca-pep-pv-host + labels: + eoepca_type: userman +spec: + capacity: + storage: {{ .Values.persistence.dbStorageSize }} + accessModes: + - {{ .Values.persistence.accessModes }} + hostPath: + path: "/kubedata/userman" + type: {{ .Values.persistence.type }} \ No newline at end of file diff --git a/charts/pep-engine/templates/pvc.yaml b/charts/pep-engine/templates/pvc.yaml new file mode 100644 index 0000000..ad4dfde --- /dev/null +++ b/charts/pep-engine/templates/pvc.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: eoepca-pep-pvc + namespace: {{ .Release.Namespace }} + labels: + eoepca_type: userman + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + accessModes: + - {{ .Values.persistence.accessModes }} + capacity: + storage: {{ .Values.persistence.dbStorageSize }} + resources: + requests: + storage: {{ .Values.persistence.dbStorageSize }} + selector: + matchLabels: + eoepca_type: userman diff --git a/charts/pep-engine/values.yaml b/charts/pep-engine/values.yaml new file mode 100644 index 0000000..338d90f --- /dev/null +++ b/charts/pep-engine/values.yaml @@ -0,0 +1,52 @@ +# Default values for login-service. + +global: + namespace: default + serviceName: opendj + ep: demoexample.gluu.org + domain: https://demoexample.gluu.org + pep: pep-engine + realm: eoepca + proxyEndpoint: /ades + serviceHost: 0.0.0.0 + servicePort: 5566 + margin: 5 + sslCerts: "'false'" + useThreads: "'true'" + debugMode: "'true'" + resourceServer: http://ades/ + umaValidation: "'true'" + limitUses: 1 + pdpUrl: http://demoexample.gluu.org + pdpPort: 5567 + pdpPolicy: /policy/ + verifySignature: "'false'" + nginxIp: 10.0.2.15 + + + +image: + statefulSetReplicas: 1 + imagePullPolicy: Always + image: eoepca/um-pep-engine:latest +persistence: + accessModes: ReadWriteMany + dbStorageSize: 5Gi + type: DirectoryOrCreate + +config: + enabled: true + + +ingress: + enabled: true + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + path: / + hosts: + - demoexample.gluu.org + tls: + - secretName: tls-certificate + hosts: + - demoexample.gluu.org diff --git a/src/config/config.json b/src/config/config.json index ea1c155..5bcf6f9 100644 --- a/src/config/config.json +++ b/src/config/config.json @@ -1 +1 @@ -{"realm": "eoepca", "auth_server_url": "https://test.eoepca.org", "proxy_endpoint": "/ades", "service_host": "0.0.0.0", "service_port": 5566, "s_margin_rpt_valid": 5, "check_ssl_certs": false, "use_threads": true, "debug_mode": true, "resource_server_endpoint": "http://eoepca-ades-core", "api_rpt_uma_validation": true, "rpt_limit_uses": 5, "pdp_url": "http://test.eoepca.org", "pdp_port": 5567, "pdp_policy_endpoint": "/policy/", "verify_signature": false} +{"realm": "eoepca", "auth_server_url": "https://demoexample.gluu.org", "proxy_endpoint": "/ades", "service_host": "0.0.0.0", "service_port": 5566, "s_margin_rpt_valid": 5, "check_ssl_certs": false, "use_threads": true, "debug_mode": true, "resource_server_endpoint": "http://eoepca-ades-core", "api_rpt_uma_validation": true, "rpt_limit_uses": 5, "pdp_url": "http://test.eoepca.org", "pdp_port": 5567, "pdp_policy_endpoint": "/policy/", "verify_signature": false} From 83a6b86bd2ccad36a88d9efcf94dfc413ff34d32 Mon Sep 17 00:00:00 2001 From: Tiago Fernandes Date: Wed, 25 Nov 2020 19:00:29 +0000 Subject: [PATCH 60/80] EOEPCA-203 proxy blueprint --- src/blueprints/proxy.py | 144 +++++++++++++++++++++ src/{resources => blueprints}/resources.py | 0 src/main.py | 139 ++------------------ 3 files changed, 153 insertions(+), 130 deletions(-) create mode 100644 src/blueprints/proxy.py rename src/{resources => blueprints}/resources.py (100%) diff --git a/src/blueprints/proxy.py b/src/blueprints/proxy.py new file mode 100644 index 0000000..e443960 --- /dev/null +++ b/src/blueprints/proxy.py @@ -0,0 +1,144 @@ +import json +from flask import Blueprint, request, Response, jsonify +from handlers.mongo_handler import Mongo_Handler +from handlers.uma_handler import UMA_Handler, resource +from handlers.uma_handler import rpt as class_rpt +from werkzeug.datastructures import Headers +from random import choice +from string import ascii_lowercase +from requests import get, post, put, delete +import json + +from WellKnownHandler import WellKnownHandler +from WellKnownHandler import TYPE_UMA_V2, KEY_UMA_V2_RESOURCE_REGISTRATION_ENDPOINT, KEY_UMA_V2_PERMISSION_ENDPOINT, KEY_UMA_V2_INTROSPECTION_ENDPOINT + +from jwkest.jws import JWS +from jwkest.jwk import RSAKey, import_rsa_key_from_file, load_jwks_from_url, import_rsa_key +from jwkest.jwk import load_jwks +from Crypto.PublicKey import RSA +import logging +logging.getLogger().setLevel(logging.INFO) + + +def construct_blueprint(oidc_client, uma_handler, g_config, private_key): + proxy_bp = Blueprint('proxy_bp', __name__) + + @proxy_bp.route(g_config["proxy_endpoint"], defaults={'path': ''}) + @proxy_bp.route(g_config["proxy_endpoint"]+"/", methods=["GET","POST","PUT","DELETE"]) + def resource_request(path): + # Check for token + print("Processing path: '"+path+"'") + custom_mongo = Mongo_Handler("resource_db", "resources") + rpt = request.headers.get('Authorization') + # Get resource + resource_id = custom_mongo.get_id_from_uri("/"+path) + scopes= None + if resource_id: + scopes = uma_handler.get_resource_scopes(resource_id) + + uid = None + + #If UUID exists and resource requested has same UUID + + if rpt: + print("Token found: "+rpt) + rpt = rpt.replace("Bearer ","").strip() + + # Validate for a specific resource + if uma_handler.validate_rpt(rpt, [{"resource_id": resource_id, "resource_scopes": scopes }], int(g_config["s_margin_rpt_valid"]), int(g_config["rpt_limit_uses"]), g_config["verify_signature"]) or not api_rpt_uma_validation: + print("RPT valid, accesing ") + + rpt_splitted = rpt.split('.') + + if len(rpt_splitted) == 3: + jwt_rpt_response = rpt + else: + introspection_endpoint=g_wkh.get(TYPE_UMA_V2, KEY_UMA_V2_INTROSPECTION_ENDPOINT) + pat = oidc_client.get_new_pat() + rpt_class = class_rpt.introspect(rpt=rpt, pat=pat, introspection_endpoint=introspection_endpoint, secure=False) + jwt_rpt_response = create_jwt(rpt_class, private_key) + + headers_splitted = split_headers(str(request.headers)) + headers_splitted['Authorization'] = "Bearer "+str(jwt_rpt_response) + + new_header = Headers() + for key, value in headers_splitted.items(): + new_header.add(key, value) + + # redirect to resource + return proxy_request(request, new_header) + print("Invalid RPT!, sending ticket") + # In any other case, we have an invalid RPT, so send a ticket. + # Fallthrough intentional + print("No auth token, or auth token is invalid") + response = Response() + if resource_id is not None: + print("Matched resource: "+str(resource_id)) + # Generate ticket if token is not present + ticket = uma_handler.request_access_ticket([{"resource_id": resource_id, "resource_scopes": scopes }]) + # Return ticket + response.headers["WWW-Authenticate"] = "UMA realm="+g_config["realm"]+",as_uri="+g_config["auth_server_url"]+",ticket="+ticket + response.status_code = 401 # Answer with "Unauthorized" as per the standard spec. + return response + else: + print("No matched resource, passing through to resource server to handle") + # In this case, the PEP doesn't have that resource handled, and just redirects to it. + try: + #Takes the full path, which contains query parameters, and removes the proxy_endpoint at the start + endpoint_path = request.full_path.replace(g_config["proxy_endpoint"], '', 1) + cont = get(g_config["resource_server_endpoint"]+endpoint_path, headers=request.headers).content + return cont + except Exception as e: + print("Error while redirecting to resource: "+str(e)) + response.status_code = 500 + return response + + def proxy_request(request, new_header): + try: + endpoint_path = request.full_path.replace(g_config["proxy_endpoint"], '', 1) + if request.method == 'POST': + res = post(g_config["resource_server_endpoint"]+endpoint_path, headers=new_header, data=request.data, stream=False) + elif request.method == 'GET': + res = get(g_config["resource_server_endpoint"]+endpoint_path, headers=new_header, stream=False) + elif request.method == 'PUT': + res = put(g_config["resource_server_endpoint"]+endpoint_path, headers=new_header, data=request.data, stream=False) + elif request.method == 'DELETE': + res = delete(g_config["resource_server_endpoint"]+endpoint_path, headers=new_header, stream=False) + else: + response = Response() + response.status_code = 501 + return response + excluded_headers = ['transfer-encoding'] + headers = [(name, value) for (name, value) in res.raw.headers.items() if name.lower() not in excluded_headers] + response = Response(res.content, res.status_code, headers) + if "Location" in response.headers: + response.autocorrect_location_header = False + response.headers["Location"] = g_config["proxy_endpoint"] + response.headers["Location"].replace(g_config["resource_server_endpoint"], '') + return response + except Exception as e: + response = Response() + print("Error while redirecting to resource: "+ traceback.format_exc(),file=sys.stderr) + response.status_code = 500 + response.content = "Error while redirecting to resource: "+str(e) + return response + + def create_jwt(payload, p_key): + rsajwk = RSAKey(kid="RSA1", key=import_rsa_key(p_key)) + jws = JWS(payload, alg="RS256") + return jws.sign_compact(keys=[rsajwk]) + + def split_headers(headers): + headers_tmp = headers.splitlines() + d = {} + + for h in headers_tmp: + h = h.split(': ') + if len(h) < 2: + continue + field=h[0] + value= h[1] + d[field] = value + + return d + + return proxy_bp \ No newline at end of file diff --git a/src/resources/resources.py b/src/blueprints/resources.py similarity index 100% rename from src/resources/resources.py rename to src/blueprints/resources.py diff --git a/src/main.py b/src/main.py index ba14eba..3aab1ec 100644 --- a/src/main.py +++ b/src/main.py @@ -17,7 +17,8 @@ from handlers.uma_handler import rpt as class_rpt from handlers.mongo_handler import Mongo_Handler from handlers.policy_handler import policy_handler -import resources.resources as resources +import blueprints.resources as resources +import blueprints.proxy as proxy import os import sys import traceback @@ -139,12 +140,6 @@ #PDP Policy Handler pdp_policy_handler = policy_handler(pdp_url=g_config["pdp_url"], pdp_port=g_config["pdp_port"], pdp_policy_endpoint=g_config["pdp_policy_endpoint"]) -app = Flask(__name__) -app.secret_key = ''.join(choice(ascii_lowercase) for i in range(30)) # Random key - -# Register api blueprints (module endpoints) -app.register_blueprint(resources.construct_blueprint(oidc_client, uma_handler, pdp_policy_handler, g_config)) - def generateRSAKeyPair(): _rsakey = RSA.generate(2048) private_key = _rsakey.exportKey() @@ -154,132 +149,16 @@ def generateRSAKeyPair(): file_out.write(private_key) file_out.close() - file_out = open("config/public.pem", "wb+") - file_out.write(public_key) - file_out.close() - - return private_key, public_key - -private_key, public_key = generateRSAKeyPair() - -def create_jwt(payload, p_key): - rsajwk = RSAKey(kid="RSA1", key=import_rsa_key(p_key)) - jws = JWS(payload, alg="RS256") - return jws.sign_compact(keys=[rsajwk]) - -def split_headers(headers): - headers_tmp = headers.splitlines() - d = {} + return private_key - for h in headers_tmp: - h = h.split(': ') - if len(h) < 2: - continue - field=h[0] - value= h[1] - d[field] = value +private_key = generateRSAKeyPair() - return d - -def proxy_request(request, new_header): - try: - endpoint_path = request.full_path.replace(g_config["proxy_endpoint"], '', 1) - if request.method == 'POST': - res = post(g_config["resource_server_endpoint"]+endpoint_path, headers=new_header, data=request.data, stream=False) - elif request.method == 'GET': - res = get(g_config["resource_server_endpoint"]+endpoint_path, headers=new_header, stream=False) - elif request.method == 'PUT': - res = put(g_config["resource_server_endpoint"]+endpoint_path, headers=new_header, data=request.data, stream=False) - elif request.method == 'DELETE': - res = delete(g_config["resource_server_endpoint"]+endpoint_path, headers=new_header, stream=False) - else: - response = Response() - response.status_code = 501 - return response - excluded_headers = ['transfer-encoding'] - headers = [(name, value) for (name, value) in res.raw.headers.items() if name.lower() not in excluded_headers] - response = Response(res.content, res.status_code, headers) - if "Location" in response.headers: - response.autocorrect_location_header = False - response.headers["Location"] = g_config["proxy_endpoint"] + response.headers["Location"].replace(g_config["resource_server_endpoint"], '') - return response - except Exception as e: - response = Response() - print("Error while redirecting to resource: "+ traceback.format_exc(),file=sys.stderr) - response.status_code = 500 - response.content = "Error while redirecting to resource: "+str(e) - return response - -@app.route(g_config["proxy_endpoint"], defaults={'path': ''}) -@app.route(g_config["proxy_endpoint"]+"/", methods=["GET","POST","PUT","DELETE"]) -def resource_request(path): - # Check for token - print("Processing path: '"+path+"'") - custom_mongo = Mongo_Handler("resource_db", "resources") - rpt = request.headers.get('Authorization') - # Get resource - resource_id = custom_mongo.get_id_from_uri("/"+path) - scopes= None - if resource_id: - scopes = uma_handler.get_resource_scopes(resource_id) - - uid = None - - #If UUID exists and resource requested has same UUID - - if rpt: - print("Token found: "+rpt) - rpt = rpt.replace("Bearer ","").strip() - - # Validate for a specific resource - if uma_handler.validate_rpt(rpt, [{"resource_id": resource_id, "resource_scopes": scopes }], int(g_config["s_margin_rpt_valid"]), int(g_config["rpt_limit_uses"]), g_config["verify_signature"]) or not api_rpt_uma_validation: - print("RPT valid, accesing ") - - rpt_splitted = rpt.split('.') - - if len(rpt_splitted) == 3: - jwt_rpt_response = rpt - else: - introspection_endpoint=g_wkh.get(TYPE_UMA_V2, KEY_UMA_V2_INTROSPECTION_ENDPOINT) - pat = oidc_client.get_new_pat() - rpt_class = class_rpt.introspect(rpt=rpt, pat=pat, introspection_endpoint=introspection_endpoint, secure=False) - jwt_rpt_response = create_jwt(rpt_class, private_key) - - headers_splitted = split_headers(str(request.headers)) - headers_splitted['Authorization'] = "Bearer "+str(jwt_rpt_response) - - new_header = Headers() - for key, value in headers_splitted.items(): - new_header.add(key, value) +app = Flask(__name__) +app.secret_key = ''.join(choice(ascii_lowercase) for i in range(30)) # Random key - # redirect to resource - return proxy_request(request, new_header) - print("Invalid RPT!, sending ticket") - # In any other case, we have an invalid RPT, so send a ticket. - # Fallthrough intentional - print("No auth token, or auth token is invalid") - response = Response() - if resource_id is not None: - print("Matched resource: "+str(resource_id)) - # Generate ticket if token is not present - ticket = uma_handler.request_access_ticket([{"resource_id": resource_id, "resource_scopes": scopes }]) - # Return ticket - response.headers["WWW-Authenticate"] = "UMA realm="+g_config["realm"]+",as_uri="+g_config["auth_server_url"]+",ticket="+ticket - response.status_code = 401 # Answer with "Unauthorized" as per the standard spec. - return response - else: - print("No matched resource, passing through to resource server to handle") - # In this case, the PEP doesn't have that resource handled, and just redirects to it. - try: - #Takes the full path, which contains query parameters, and removes the proxy_endpoint at the start - endpoint_path = request.full_path.replace(g_config["proxy_endpoint"], '', 1) - cont = get(g_config["resource_server_endpoint"]+endpoint_path, headers=request.headers).content - return cont - except Exception as e: - print("Error while redirecting to resource: "+str(e)) - response.status_code = 500 - return response - +# Register api blueprints (module endpoints) +app.register_blueprint(resources.construct_blueprint(oidc_client, uma_handler, pdp_policy_handler, g_config)) +app.register_blueprint(proxy.construct_blueprint(oidc_client, uma_handler, g_config, private_key)) # Start reverse proxy for x endpoint app.run( From f47b794c8a7d2f9c086047aaa33d9bd89f072244 Mon Sep 17 00:00:00 2001 From: Tiago Fernandes Date: Thu, 26 Nov 2020 16:51:48 +0000 Subject: [PATCH 61/80] EOEPCA-203 init config refactor --- src/config.py | 102 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.py | 100 ++----------------------------------------------- 2 files changed, 105 insertions(+), 97 deletions(-) diff --git a/src/config.py b/src/config.py index 21cecc6..88d6a1f 100644 --- a/src/config.py +++ b/src/config.py @@ -1,6 +1,12 @@ #!/usr/bin/env python3 +import os +import sys +import traceback from json import load, dump +from eoepca_scim import EOEPCA_Scim, ENDPOINT_AUTH_CLIENT_POST +from WellKnownHandler import WellKnownHandler +from WellKnownHandler import TYPE_UMA_V2, KEY_UMA_V2_RESOURCE_REGISTRATION_ENDPOINT, KEY_UMA_V2_PERMISSION_ENDPOINT, KEY_UMA_V2_INTROSPECTION_ENDPOINT def load_config(config_path: str) -> dict: """ @@ -21,3 +27,99 @@ def save_config(config_path: str, data: dict): """ with open(config_path, 'w') as j: dump(data,j) + +def get_config(config_path: str): + """ + Loads entire configuration onto memory + """ + env_vars = [ + "PEP_REALM", + "PEP_AUTH_SERVER_URL", + "PEP_PROXY_ENDPOINT", + "PEP_SERVICE_HOST", + "PEP_SERVICE_PORT", + "PEP_S_MARGIN_RPT_VALID", + "PEP_CHECK_SSL_CERTS", + "PEP_USE_THREADS", + "PEP_DEBUG_MODE", + "PEP_RESOURCE_SERVER_ENDPOINT", + "PEP_API_RPT_UMA_VALIDATION", + "PEP_RPT_LIMIT_USES", + "PEP_PDP_URL", + "PEP_PDP_PORT", + "PEP_PDP_POLICY_ENDPOINT", + "PEP_VERIFY_SIGNATURE"] + + use_env_var = True + + for env_var in env_vars: + if env_var not in os.environ: + use_env_var = False + + g_config = {} + # Global config objects + if use_env_var is False: + g_config = load_config(config_path) + else: + for env_var in env_vars: + env_var_config = env_var.replace('PEP_', '') + + if "true" in os.environ[env_var].replace('"', ''): + g_config[env_var_config.lower()] = True + elif "false" in os.environ[env_var].replace('"', ''): + g_config[env_var_config.lower()] = False + else: + g_config[env_var_config.lower()] = os.environ[env_var].replace('"', '') + + # Sanitize proxy endpoint config value, VERY IMPORTANT to ensure proper function of the endpoint + proxy_endpoint_reject_list = ["/", "/resources", "resources"] + if g_config["proxy_endpoint"] in proxy_endpoint_reject_list: + raise Exception("PROXY_ENDPOINT value contains one of invalid values: " + str(proxy_endpoint_reject_list)) + if g_config["proxy_endpoint"][0] is not "/": + g_config["proxy_endpoint"] = "/" + g_config["proxy_endpoint"] + if g_config["proxy_endpoint"][-1] is "/": + g_config["proxy_endpoint"] = g_config["proxy_endpoint"][:-1] + + # Sanitize PDP "policy" endpoint config value, VERY IMPORTANT to ensure proper function of the endpoint + if g_config["pdp_policy_endpoint"][0] is not "/": + g_config["pdp_policy_endpoint"] = "/" + g_config["pdp_policy_endpoint"] + if g_config["pdp_policy_endpoint"][-1] is not "/": + g_config["pdp_policy_endpoint"] = g_config["pdp_policy_endpoint"] + "/" + + # Global handlers + g_wkh = WellKnownHandler(g_config["auth_server_url"], secure=False) + + # Global setting to validate RPTs received at endpoints + api_rpt_uma_validation = g_config["api_rpt_uma_validation"] + if api_rpt_uma_validation: + print("UMA RPT validation is ON.") + else: + print("UMA RPT validation is OFF.") + + # Generate client dynamically if one is not configured. + if "client_id" not in g_config or "client_secret" not in g_config: + print ("NOTICE: Client not found, generating one... ") + scim_client = EOEPCA_Scim(g_config["auth_server_url"]) + new_client = scim_client.registerClient("PEP Dynamic Client", + grantTypes = ["client_credentials", "password"], + redirectURIs = [""], + logoutURI = "", + responseTypes = ["code","token","id_token"], + scopes = ['openid', 'uma_protection', 'permission', 'profile', 'is_operator'], + token_endpoint_auth_method = ENDPOINT_AUTH_CLIENT_POST) + print("NEW CLIENT created with ID '"+new_client["client_id"]+"', since no client config was found on config.json or environment") + + g_config["client_id"] = new_client["client_id"] + g_config["client_secret"] = new_client["client_secret"] + if use_env_var is False: + save_config("config/config.json", g_config) + else: + os.environ["PEP_CLIENT_ID"] = new_client["client_id"] + os.environ["PEP_CLIENT_SECRET"] = new_client["client_secret"] + print("New client saved to config!") + else: + print("Client found in config, using: "+g_config["client_id"]) + + save_config(config_path, g_config) + + return g_config, g_wkh diff --git a/src/main.py b/src/main.py index 3aab1ec..081697a 100644 --- a/src/main.py +++ b/src/main.py @@ -10,7 +10,7 @@ from requests import get, post, put, delete import json -from config import load_config, save_config +from config import get_config from eoepca_scim import EOEPCA_Scim, ENDPOINT_AUTH_CLIENT_POST from handlers.oidc_handler import OIDCHandler from handlers.uma_handler import UMA_Handler, resource @@ -29,95 +29,9 @@ from Crypto.PublicKey import RSA import logging logging.getLogger().setLevel(logging.INFO) -### INITIAL SETUP - -env_vars = [ -"PEP_REALM", -"PEP_AUTH_SERVER_URL", -"PEP_PROXY_ENDPOINT", -"PEP_SERVICE_HOST", -"PEP_SERVICE_PORT", -"PEP_S_MARGIN_RPT_VALID", -"PEP_CHECK_SSL_CERTS", -"PEP_USE_THREADS", -"PEP_DEBUG_MODE", -"PEP_RESOURCE_SERVER_ENDPOINT", -"PEP_API_RPT_UMA_VALIDATION", -"PEP_RPT_LIMIT_USES", -"PEP_PDP_URL", -"PEP_PDP_PORT", -"PEP_PDP_POLICY_ENDPOINT", -"PEP_VERIFY_SIGNATURE"] - -use_env_var = True - -for env_var in env_vars: - if env_var not in os.environ: - use_env_var = False - -g_config = {} -# Global config objects -if use_env_var is False: - g_config = load_config("config/config.json") -else: - for env_var in env_vars: - env_var_config = env_var.replace('PEP_', '') - - if "true" in os.environ[env_var].replace('"', ''): - g_config[env_var_config.lower()] = True - elif "false" in os.environ[env_var].replace('"', ''): - g_config[env_var_config.lower()] = False - else: - g_config[env_var_config.lower()] = os.environ[env_var].replace('"', '') - -# Sanitize proxy endpoint config value, VERY IMPORTANT to ensure proper function of the endpoint -proxy_endpoint_reject_list = ["/", "/resources", "resources"] -if g_config["proxy_endpoint"] in proxy_endpoint_reject_list: - raise Exception("PROXY_ENDPOINT value contains one of invalid values: " + str(proxy_endpoint_reject_list)) -if g_config["proxy_endpoint"][0] is not "/": - g_config["proxy_endpoint"] = "/" + g_config["proxy_endpoint"] -if g_config["proxy_endpoint"][-1] is "/": - g_config["proxy_endpoint"] = g_config["proxy_endpoint"][:-1] -# Sanitize PDP "policy" endpoint config value, VERY IMPORTANT to ensure proper function of the endpoint -if g_config["pdp_policy_endpoint"][0] is not "/": - g_config["pdp_policy_endpoint"] = "/" + g_config["pdp_policy_endpoint"] -if g_config["pdp_policy_endpoint"][-1] is not "/": - g_config["pdp_policy_endpoint"] = g_config["pdp_policy_endpoint"] + "/" - -# Global handlers -g_wkh = WellKnownHandler(g_config["auth_server_url"], secure=False) - -# Global setting to validate RPTs received at endpoints -api_rpt_uma_validation = g_config["api_rpt_uma_validation"] -if api_rpt_uma_validation: print("UMA RPT validation is ON.") -else: print("UMA RPT validation is OFF.") - -# Generate client dynamically if one is not configured. -if "client_id" not in g_config or "client_secret" not in g_config: - print ("NOTICE: Client not found, generating one... ") - scim_client = EOEPCA_Scim(g_config["auth_server_url"]) - new_client = scim_client.registerClient("PEP Dynamic Client", - grantTypes = ["client_credentials", "password"], - redirectURIs = [""], - logoutURI = "", - responseTypes = ["code","token","id_token"], - scopes = ['openid', 'uma_protection', 'permission', 'profile', 'is_operator'], - token_endpoint_auth_method = ENDPOINT_AUTH_CLIENT_POST) - print("NEW CLIENT created with ID '"+new_client["client_id"]+"', since no client config was found on config.json or environment") - - g_config["client_id"] = new_client["client_id"] - g_config["client_secret"] = new_client["client_secret"] - if use_env_var is False: - save_config("config/config.json", g_config) - else: - os.environ["PEP_CLIENT_ID"] = new_client["client_id"] - os.environ["PEP_CLIENT_SECRET"] = new_client["client_secret"] - print("New client saved to config!") -else: - print("Client found in config, using: "+g_config["client_id"]) - -save_config("config/config.json", g_config) +### INITIAL SETUP +g_config, g_wkh = get_config("config/config.json") oidc_client = OIDCHandler(g_wkh, client_id = g_config["client_id"], @@ -128,14 +42,6 @@ uma_handler = UMA_Handler(g_wkh, oidc_client, g_config["check_ssl_certs"]) uma_handler.status() -# Demo: register a new resource if it doesn't exist -# try: -# pass -# #uma_handler.create("ADES", ["Authenticated"], description="", ownership_id= '55b8f51f-4634-4bb0-a1dd-070ec5869d70', icon_uri="/pep/ADES") -# except Exception as e: -# if "already exists" in str(e): -# print("Resource already existed, moving on") -# else: raise e #PDP Policy Handler pdp_policy_handler = policy_handler(pdp_url=g_config["pdp_url"], pdp_port=g_config["pdp_port"], pdp_policy_endpoint=g_config["pdp_policy_endpoint"]) From 1383a9108a74a3154b2829ca67b8f973e9af1746 Mon Sep 17 00:00:00 2001 From: Tiago Fernandes Date: Thu, 26 Nov 2020 17:39:49 +0000 Subject: [PATCH 62/80] EOEPCA-203 threading + 401 no tkn fix --- src/blueprints/resources.py | 5 ++++ src/handlers/oidc_handler.py | 4 ++-- src/main.py | 46 ++++++++++++++++++++++++++---------- 3 files changed, 41 insertions(+), 14 deletions(-) diff --git a/src/blueprints/resources.py b/src/blueprints/resources.py index c1cad63..659a0bd 100644 --- a/src/blueprints/resources.py +++ b/src/blueprints/resources.py @@ -26,6 +26,11 @@ def get_resource_list(): head_protected = str(request.headers) headers_protected = head_protected.split() uid = oidc_client.verify_uid_headers(headers_protected, "sub") + if "NO TOKEN FOUND" in uid: + print("Error: no token passed!") + response.status_code = 401 + response.headers["Error"] = 'no token passed!' + return response except Exception as e: print("Error While passing the token: "+str(uid)) response.status_code = 500 diff --git a/src/handlers/oidc_handler.py b/src/handlers/oidc_handler.py index d353edf..b7bee68 100644 --- a/src/handlers/oidc_handler.py +++ b/src/handlers/oidc_handler.py @@ -118,12 +118,12 @@ def verify_OAuth_token(self, token, key): def verify_uid_headers(self, headers_protected, key): value = None + token_protected = None #Retrieve the token from the headers for i in headers_protected: if 'Bearer' in str(i): aux_protected=headers_protected.index('Bearer') - inputToken_protected = headers_protected[aux_protected+1] - token_protected = inputToken_protected + token_protected = headers_protected[aux_protected+1] if token_protected: #Compares between JWT id_token and OAuth access token to retrieve the requested key-value if len(str(token_protected))>40: diff --git a/src/main.py b/src/main.py index 081697a..5079fa9 100644 --- a/src/main.py +++ b/src/main.py @@ -22,6 +22,7 @@ import os import sys import traceback +import threading from jwkest.jws import JWS from jwkest.jwk import RSAKey, import_rsa_key_from_file, load_jwks_from_url, import_rsa_key @@ -59,17 +60,38 @@ def generateRSAKeyPair(): private_key = generateRSAKeyPair() -app = Flask(__name__) -app.secret_key = ''.join(choice(ascii_lowercase) for i in range(30)) # Random key +proxy_app = Flask(__name__) +proxy_app.secret_key = ''.join(choice(ascii_lowercase) for i in range(30)) # Random key + +resources_app = Flask(__name__) +resources_app.secret_key = ''.join(choice(ascii_lowercase) for i in range(30)) # Random key # Register api blueprints (module endpoints) -app.register_blueprint(resources.construct_blueprint(oidc_client, uma_handler, pdp_policy_handler, g_config)) -app.register_blueprint(proxy.construct_blueprint(oidc_client, uma_handler, g_config, private_key)) - -# Start reverse proxy for x endpoint -app.run( - debug=g_config["debug_mode"], - threaded=g_config["use_threads"], - port=int(g_config["service_port"]), - host=g_config["service_host"] -) +resources_app.register_blueprint(resources.construct_blueprint(oidc_client, uma_handler, pdp_policy_handler, g_config)) +proxy_app.register_blueprint(proxy.construct_blueprint(oidc_client, uma_handler, g_config, private_key)) + +# Define run methods for both Flask instances +# Start reverse proxy for proxy endpoint +def run_proxy_app(): + proxy_app.run( + debug=False, + threaded=True, + port=int(g_config["service_port"]), + host=g_config["service_host"] + ) + +# Start reverse proxy for resources endpoint +def run_resources_app(): + resources_app.run( + debug=False, + threaded=True, + port=int(g_config["service_port"])+10, + host=g_config["service_host"] + ) + +if __name__ == '__main__': + # Executing the Threads seperatly. + proxy_thread = threading.Thread(target=run_proxy_app) + resource_thread = threading.Thread(target=run_resources_app) + proxy_thread.start() + resource_thread.start() \ No newline at end of file From 1a49475378f873ce5468a4c4a3bde6b6451cd97d Mon Sep 17 00:00:00 2001 From: Tiago Fernandes Date: Thu, 26 Nov 2020 17:48:44 +0000 Subject: [PATCH 63/80] EOEPCA-203 POST resource split --- src/blueprints/resources.py | 62 +++++++++++++++++++++++++++---------- 1 file changed, 46 insertions(+), 16 deletions(-) diff --git a/src/blueprints/resources.py b/src/blueprints/resources.py index 659a0bd..9a423f4 100644 --- a/src/blueprints/resources.py +++ b/src/blueprints/resources.py @@ -61,8 +61,48 @@ def get_resource_list(): response.headers["Error"] = "No user-owned resources found!" return response + @resources_bp.route("/resources", methods=["POST"]) + def resource_creation(): + print("Processing " + request.method + " resource request...") + response = Response() + uid = None + #Inspect JWT token (UMA) or query OIDC userinfo endpoint (OAuth) for user id + try: + head_protected = str(request.headers) + headers_protected = head_protected.split() + uid = oidc_client.verify_uid_headers(headers_protected, "sub") + if "NO TOKEN FOUND" in uid: + print("Error: no token passed!") + response.status_code = 401 + response.headers["Error"] = 'no token passed!' + return response + except Exception as e: + print("Error While passing the token: "+str(uid)) + response.status_code = 500 + response.headers["Error"] = str(e) + return response + + #If UUID does not exist + if not uid: + print("UID for the user not found") + response.status_code = 401 + response.headers["Error"] = 'Could not get the UID for the user' + return response - @resources_bp.route("/resources/", methods=["GET", "PUT", "POST", "DELETE"]) + resource_reply = create_resource(uid, request, uma_handler, response) + #If the reply is not of type Response, the creation was successful + #Here we register a default ownership policy to the new resource, with the PDP + if not isinstance(resource_reply, Response): + resource_id = resource_reply + policy_reply = pdp_policy_handler.create_policy(policy_body=get_default_ownership_policy_body(resource_id, uid), input_headers=request.headers) + if policy_reply.status_code == 200: + return resource_id + response.status_code = policy_reply.status_code + response.headers["Error"] = "Error when registering resource ownership policy!" + return response + return resource_reply + + @resources_bp.route("/resources/", methods=["GET", "PUT", "DELETE"]) def resource_operation(resource_id): print("Processing " + request.method + " resource request...") response = Response() @@ -73,6 +113,11 @@ def resource_operation(resource_id): head_protected = str(request.headers) headers_protected = head_protected.split() uid = oidc_client.verify_uid_headers(headers_protected, "sub") + if "NO TOKEN FOUND" in uid: + print("Error: no token passed!") + response.status_code = 401 + response.headers["Error"] = 'no token passed!' + return response except Exception as e: print("Error While passing the token: "+str(uid)) response.status_code = 500 @@ -86,21 +131,6 @@ def resource_operation(resource_id): response.headers["Error"] = 'Could not get the UID for the user' return response - #add resource is outside of any extra validations, so it is called now - if request.method == "POST": - resource_reply = create_resource(uid, request, uma_handler, response) - #If the reply is not of type Response, the creation was successful - #Here we register a default ownership policy to the new resource, with the PDP - if not isinstance(resource_reply, Response): - resource_id = resource_reply - policy_reply = pdp_policy_handler.create_policy(policy_body=get_default_ownership_policy_body(resource_id, uid), input_headers=request.headers) - if policy_reply.status_code == 200: - return resource_id - response.status_code = policy_reply.status_code - response.headers["Error"] = "Error when registering resource ownership policy!" - return response - return resource_reply - try: #otherwise continue with validations #Is this user the resource's owner? From f9b9a147fb764c190a179b4191fc392282b4b46d Mon Sep 17 00:00:00 2001 From: Tiago Fernandes Date: Fri, 27 Nov 2020 12:49:41 +0000 Subject: [PATCH 64/80] EOEPCA-203 rm prefix + port exposure --- Dockerfile | 1 + docker-compose.yml | 1 + docs/SDD/03.design/00.design.adoc | 4 ++-- src/blueprints/proxy.py | 9 +++------ src/blueprints/resources.py | 6 +----- src/config.py | 13 ++----------- src/config/config.json | 2 +- src/config/um-pep-engine-config-env-vars | 4 ++-- src/main.py | 4 ++-- 9 files changed, 15 insertions(+), 29 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4746c37..5e39039 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,6 +10,7 @@ COPY src/ / # Declare and expose service listening port EXPOSE 5566/tcp +EXPOSE 5576/tcp # Declare entrypoint of that exposed service ENTRYPOINT ["python3", "./main.py"] diff --git a/docker-compose.yml b/docker-compose.yml index 229f752..503dcbb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,6 +10,7 @@ services: - eoepca_network ports: - '5566:5566' + - '5576:5576' volumes: - ./src/config:/config diff --git a/docs/SDD/03.design/00.design.adoc b/docs/SDD/03.design/00.design.adoc index 954b1de..c560118 100644 --- a/docs/SDD/03.design/00.design.adoc +++ b/docs/SDD/03.design/00.design.adoc @@ -51,9 +51,9 @@ The parameters that are accepted, and their meaning, are as follows: - **realm**: 'realm' parameter answered for each UMA ticket. Default is "eoepca" - **auth_server_url**: complete url (with "https") of the Authorization server. -- **proxy_endpoint**: "/path"-formatted string to indicate where the reverse proxy should listen. The proxy will catch any request that starts with that path. Default is "/pep" - **service_host**: Host for the proxy to listen on. For example, "0.0.0.0" will listen on all interfaces -- **service_port**: Port for the proxy to listen on. By default, **5566**. Keep in mind you will have to edit the docker file and/or kubernetes yaml file in order for all the prot forwarding to work. +- **proxy_service_port**: Port for the proxy to listen on. By default, **5566**. Keep in mind you will have to edit the docker file and/or kubernetes yaml file in order for all the prot forwarding to work. +- **resources_service_port**: Port for the resources to listen on. By default, **5576**. Keep in mind you will have to edit the docker file and/or kubernetes yaml file in order for all the prot forwarding to work. - **s_margin_rpt_valid**: An integer representing how many seconds of "margin" do we want when checking RPT. For example, using **5** will make sure the provided RPT is valid now AND AT LEAST in the next 5 seconds. - **rpt_limit_uses**: Number of uses for each of the RPTs. - **check_ssl_certs**: Toggle on/off (bool) to check certificates in all requests. This should be forced to True in a production environment diff --git a/src/blueprints/proxy.py b/src/blueprints/proxy.py index e443960..d830be7 100644 --- a/src/blueprints/proxy.py +++ b/src/blueprints/proxy.py @@ -23,8 +23,7 @@ def construct_blueprint(oidc_client, uma_handler, g_config, private_key): proxy_bp = Blueprint('proxy_bp', __name__) - @proxy_bp.route(g_config["proxy_endpoint"], defaults={'path': ''}) - @proxy_bp.route(g_config["proxy_endpoint"]+"/", methods=["GET","POST","PUT","DELETE"]) + @proxy_bp.route("/", methods=["GET","POST","PUT","DELETE"]) def resource_request(path): # Check for token print("Processing path: '"+path+"'") @@ -84,8 +83,6 @@ def resource_request(path): print("No matched resource, passing through to resource server to handle") # In this case, the PEP doesn't have that resource handled, and just redirects to it. try: - #Takes the full path, which contains query parameters, and removes the proxy_endpoint at the start - endpoint_path = request.full_path.replace(g_config["proxy_endpoint"], '', 1) cont = get(g_config["resource_server_endpoint"]+endpoint_path, headers=request.headers).content return cont except Exception as e: @@ -95,7 +92,7 @@ def resource_request(path): def proxy_request(request, new_header): try: - endpoint_path = request.full_path.replace(g_config["proxy_endpoint"], '', 1) + endpoint_path = request.full_path if request.method == 'POST': res = post(g_config["resource_server_endpoint"]+endpoint_path, headers=new_header, data=request.data, stream=False) elif request.method == 'GET': @@ -113,7 +110,7 @@ def proxy_request(request, new_header): response = Response(res.content, res.status_code, headers) if "Location" in response.headers: response.autocorrect_location_header = False - response.headers["Location"] = g_config["proxy_endpoint"] + response.headers["Location"].replace(g_config["resource_server_endpoint"], '') + response.headers["Location"] = response.headers["Location"].replace(g_config["resource_server_endpoint"], '') return response except Exception as e: response = Response() diff --git a/src/blueprints/resources.py b/src/blueprints/resources.py index 9a423f4..9a7fb08 100644 --- a/src/blueprints/resources.py +++ b/src/blueprints/resources.py @@ -182,11 +182,7 @@ def create_resource(uid, request, uma_handler, response): if request.is_json: data = request.get_json() if data.get("name") and data.get("resource_scopes"): - #Re-issue v0.2.3: ensure registered path does NOT contain proxy endpoint prefix - icon_uri_path = data.get("icon_uri") - if icon_uri_path.startswith(g_config["proxy_endpoint"]): - icon_uri_path = icon_uri_path.replace(g_config["proxy_endpoint"], '', 1) - return uma_handler.create(data.get("name"), data.get("resource_scopes"), data.get("description"), uid, icon_uri_path) + return uma_handler.create(data.get("name"), data.get("resource_scopes"), data.get("description"), uid, data.get("icon_uri")) else: response.status_code = 500 response.headers["Error"] = "Invalid data passed on URL called for resource creation!" diff --git a/src/config.py b/src/config.py index 88d6a1f..817341d 100644 --- a/src/config.py +++ b/src/config.py @@ -35,9 +35,9 @@ def get_config(config_path: str): env_vars = [ "PEP_REALM", "PEP_AUTH_SERVER_URL", - "PEP_PROXY_ENDPOINT", "PEP_SERVICE_HOST", - "PEP_SERVICE_PORT", + "PEP_PROXY_SERVICE_PORT", + "PEP_RESOURCES_SERVICE_PORT", "PEP_S_MARGIN_RPT_VALID", "PEP_CHECK_SSL_CERTS", "PEP_USE_THREADS", @@ -71,15 +71,6 @@ def get_config(config_path: str): else: g_config[env_var_config.lower()] = os.environ[env_var].replace('"', '') - # Sanitize proxy endpoint config value, VERY IMPORTANT to ensure proper function of the endpoint - proxy_endpoint_reject_list = ["/", "/resources", "resources"] - if g_config["proxy_endpoint"] in proxy_endpoint_reject_list: - raise Exception("PROXY_ENDPOINT value contains one of invalid values: " + str(proxy_endpoint_reject_list)) - if g_config["proxy_endpoint"][0] is not "/": - g_config["proxy_endpoint"] = "/" + g_config["proxy_endpoint"] - if g_config["proxy_endpoint"][-1] is "/": - g_config["proxy_endpoint"] = g_config["proxy_endpoint"][:-1] - # Sanitize PDP "policy" endpoint config value, VERY IMPORTANT to ensure proper function of the endpoint if g_config["pdp_policy_endpoint"][0] is not "/": g_config["pdp_policy_endpoint"] = "/" + g_config["pdp_policy_endpoint"] diff --git a/src/config/config.json b/src/config/config.json index ea1c155..18a81ea 100644 --- a/src/config/config.json +++ b/src/config/config.json @@ -1 +1 @@ -{"realm": "eoepca", "auth_server_url": "https://test.eoepca.org", "proxy_endpoint": "/ades", "service_host": "0.0.0.0", "service_port": 5566, "s_margin_rpt_valid": 5, "check_ssl_certs": false, "use_threads": true, "debug_mode": true, "resource_server_endpoint": "http://eoepca-ades-core", "api_rpt_uma_validation": true, "rpt_limit_uses": 5, "pdp_url": "http://test.eoepca.org", "pdp_port": 5567, "pdp_policy_endpoint": "/policy/", "verify_signature": false} +{"realm": "eoepca", "auth_server_url": "https://test.eoepca.org", "service_host": "0.0.0.0", "proxy_service_port": 5566, "resources_service_port": 5576, "s_margin_rpt_valid": 5, "check_ssl_certs": false, "use_threads": true, "debug_mode": true, "resource_server_endpoint": "http://eoepca-ades-core", "api_rpt_uma_validation": true, "rpt_limit_uses": 5, "pdp_url": "http://test.eoepca.org", "pdp_port": 5567, "pdp_policy_endpoint": "/policy/", "verify_signature": false} diff --git a/src/config/um-pep-engine-config-env-vars b/src/config/um-pep-engine-config-env-vars index f1b2236..708a602 100644 --- a/src/config/um-pep-engine-config-env-vars +++ b/src/config/um-pep-engine-config-env-vars @@ -1,8 +1,8 @@ PEP_REALM="eoepca" PEP_AUTH_SERVER_URL="https://test.eoepca.org" -PEP_PROXY_ENDPOINT="/pep" PEP_SERVICE_HOST="0.0.0.0" -PEP_SERVICE_PORT=5566 +PEP_PROXY_SERVICE_PORT=5566 +PEP_RESOURCES_SERVICE_PORT=5576 PEP_S_MARGIN_RPT_VALID=5 PEP_CHECK_SSL_CERTS=false PEP_USE_THREADS=true diff --git a/src/main.py b/src/main.py index 5079fa9..99b74da 100644 --- a/src/main.py +++ b/src/main.py @@ -76,7 +76,7 @@ def run_proxy_app(): proxy_app.run( debug=False, threaded=True, - port=int(g_config["service_port"]), + port=int(g_config["proxy_service_port"]), host=g_config["service_host"] ) @@ -85,7 +85,7 @@ def run_resources_app(): resources_app.run( debug=False, threaded=True, - port=int(g_config["service_port"])+10, + port=int(g_config["resources_service_port"]), host=g_config["service_host"] ) From ea337662614c445e1b21d1c8216488a6dfc5abb8 Mon Sep 17 00:00:00 2001 From: Hector Rodriguez Date: Mon, 30 Nov 2020 11:51:20 +0000 Subject: [PATCH 65/80] EOEPCA-221 #comment Added CLI Tools for Resource Management --- src/handlers/mongo_handler.py | 7 +++++ src/tools/resource_manager.py | 55 +++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 src/tools/resource_manager.py diff --git a/src/handlers/mongo_handler.py b/src/handlers/mongo_handler.py index 87a39e2..9ad130d 100644 --- a/src/handlers/mongo_handler.py +++ b/src/handlers/mongo_handler.py @@ -127,6 +127,13 @@ def get_all_resources(self): ''' col = self.db['resources'] return col.find() + + def remove_resources(self, filter_key=None, filter_value=None): + col = self.db['resources'] + query = {} + if filter_key is not None and filter_value is not None: + query = { filter_key: filter_value } + col.delete_many(query) #Functions for rpt db def insert_rpt_in_mongo(self, rpt: str, rpt_limit_uses: int, timestamp: str): diff --git a/src/tools/resource_manager.py b/src/tools/resource_manager.py new file mode 100644 index 0000000..a0a1bd1 --- /dev/null +++ b/src/tools/resource_manager.py @@ -0,0 +1,55 @@ +#!/usr/bin/python3 + +import argparse +import sys +from handlers.mongo_handler import Mongo_Handler + +custom_mongo = Mongo_Handler("resource_db", "resources") + +def list_resources(user,resource): + if resource is not None: + return custom_mongo.get_from_mongo("resource_id", resource) + if user is not None: + resources=custom_mongo.get_all_resources() + return list(filter(lambda x: x["ownership_id"] == user,resources)) + return custom_mongo.get_all_resources() + +def remove_resources(user,resource,all): + if resource is not None: + return custom_mongo.delete_in_mongo("resource_id", resource) + if user is not None and all: + return custom_mongo.remove_resources("ownership_id",user) + if user is None and all: + return custom_mongo.remove_resources() + return "No action taken (missing --all flag?)" + + +parser = argparse.ArgumentParser(description='Operational management of resources.') +parser.add_argument('action', metavar='action', type=str, + help='Operation to perform: list/remove') +parser.add_argument('-u', + '--user', + help='Filter action by user ID') +parser.add_argument('-r', + '--resource', + help='Filter action by resource ID') + +parser.add_argument('-a', + '--all', + action='store_true', + help='Apply action to all resources.') + + +args = vars(parser.parse_args()) + +if args["action"] == "list": + result = list_resources(args['user'],args['resource']) +elif args["action"] == "remove": + if args["resource"] is not None: + args["all"] = False + result = remove_resources(args['user'],args['resource'],args['all']) +else: + print("Allowed actions are 'remove' or 'list'") + sys.exit(-1) + +print(result) \ No newline at end of file From e952a0f551517c066c06a1f357a17f1a986ef3bd Mon Sep 17 00:00:00 2001 From: AlvaroVillanueva Date: Mon, 30 Nov 2020 15:47:45 +0000 Subject: [PATCH 66/80] EOEPCA-210 Updated changes on parallel branch #comment There has been changes on branch EOEPCA-203 applied to develop that implies environtment variables that needed to be included in the chart definition. The Helm deployment is functional and documented in the Wiki --- charts/pep-engine/templates/pep-cm.yml | 5 +++-- charts/pep-engine/values.yaml | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/charts/pep-engine/templates/pep-cm.yml b/charts/pep-engine/templates/pep-cm.yml index d852fad..5480ea3 100755 --- a/charts/pep-engine/templates/pep-cm.yml +++ b/charts/pep-engine/templates/pep-cm.yml @@ -5,7 +5,6 @@ metadata: data: PEP_REALM: {{ .Values.global.realm | quote }} PEP_AUTH_SERVER_URL: {{ .Values.global.domain | quote }} - PEP_PROXY_ENDPOINT: {{ .Values.global.proxyEndpoint | quote }} PEP_SERVICE_HOST: {{ .Values.global.serviceHost | quote }} PEP_SERVICE_PORT: {{ .Values.global.servicePort | quote }} PEP_S_MARGIN_RPT_VALID: {{ .Values.global.margin | quote }} @@ -18,4 +17,6 @@ data: PEP_PDP_URL: {{ .Values.global.pdpUrl | quote }} PEP_PDP_PORT: {{ .Values.global.pdpPort | quote }} PEP_PDP_POLICY_ENDPOINT: {{ .Values.global.pdpPolicy | quote }} - PEP_VERIFY_SIGNATURE: {{ .Values.global.verifySignature | quote }} \ No newline at end of file + PEP_VERIFY_SIGNATURE: {{ .Values.global.verifySignature | quote }} + PEP_PROXY_SERVICE_PORT: {{ .Values.global.proxyServicePort | quote }} + PEP_RESOURCES_SERVICE_PORT: {{ .Values.global.resourcesServicePort | quote }} \ No newline at end of file diff --git a/charts/pep-engine/values.yaml b/charts/pep-engine/values.yaml index 338d90f..e9f89bc 100644 --- a/charts/pep-engine/values.yaml +++ b/charts/pep-engine/values.yaml @@ -7,7 +7,6 @@ global: domain: https://demoexample.gluu.org pep: pep-engine realm: eoepca - proxyEndpoint: /ades serviceHost: 0.0.0.0 servicePort: 5566 margin: 5 @@ -22,13 +21,15 @@ global: pdpPolicy: /policy/ verifySignature: "'false'" nginxIp: 10.0.2.15 + proxyServicePort: 5566 + resourcesServicePort: 5576 image: statefulSetReplicas: 1 imagePullPolicy: Always - image: eoepca/um-pep-engine:latest + image: eoepca/um-pep-engine:task203_1 persistence: accessModes: ReadWriteMany dbStorageSize: 5Gi From aed3ff8f0066444c59384fa22af75f2a5eb09323 Mon Sep 17 00:00:00 2001 From: AlvaroVillanueva Date: Thu, 3 Dec 2020 09:42:44 +0000 Subject: [PATCH 67/80] EOEPCA-214 Chart definition updated with volume mount path --- charts/pep-engine/templates/pep-deployment.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/charts/pep-engine/templates/pep-deployment.yml b/charts/pep-engine/templates/pep-deployment.yml index 7c9c0bb..e9625ae 100755 --- a/charts/pep-engine/templates/pep-deployment.yml +++ b/charts/pep-engine/templates/pep-deployment.yml @@ -28,10 +28,6 @@ spec: envFrom: - configMapRef: name: pep-cm - volumeMounts: - - mountPath: /data/db/resource - sub_path: pep-engine/db/resource - name: eoepca-pep-pv-host - name: mongo imagePullPolicy: {{ .Values.image.imagePullPolicy }} image: mongo @@ -43,7 +39,7 @@ spec: - configMapRef: name: pep-cm volumeMounts: - - mountPath: /data/db/resource + - mountPath: /data/db/ sub_path: pep-engine/db/resource name: eoepca-pep-pv-host hostAliases: From 99a8a78f2bed273b7ea77f89c847d509658c3b7c Mon Sep 17 00:00:00 2001 From: Hector Rodriguez Date: Thu, 3 Dec 2020 15:29:33 +0100 Subject: [PATCH 68/80] Update index.adoc --- docs/SDD/index.adoc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/SDD/index.adoc b/docs/SDD/index.adoc index 0c3bd5e..351ec12 100644 --- a/docs/SDD/index.adoc +++ b/docs/SDD/index.adoc @@ -51,6 +51,10 @@ include::02.overview/00.overview.adoc[] include::03.design/00.design.adoc[] +<<< + +include::04.traceability.adoc[] + ''' include::end-of-document.adoc[] From d1a784dc82b68c0ad848f5630e162983344a9b2b Mon Sep 17 00:00:00 2001 From: Hector Rodriguez Date: Thu, 3 Dec 2020 15:32:46 +0100 Subject: [PATCH 69/80] Create 04.traceability.adoc --- docs/SDD/04.traceability.adoc | 71 +++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 docs/SDD/04.traceability.adoc diff --git a/docs/SDD/04.traceability.adoc b/docs/SDD/04.traceability.adoc new file mode 100644 index 0000000..d607f6b --- /dev/null +++ b/docs/SDD/04.traceability.adoc @@ -0,0 +1,71 @@ +[[traceabilityMatrix]] += User Story Traceability + +.User Stories +|=== +|Code |Description + +|EOEPCA-12 +|Reusable UMA Client Implementation + +|EOEPCA-221 +|Administrative tooling for integration and operation + +|EOEPCA-214 +|Add Usage of Persistence Volumes + +|EOEPCA-35 +|Ownership management for Resources + +|EOEPCA-210 +|Implementation of Helm Charts + +|EOEPCA-25 +|Registration of Resource References + +|EOEPCA-121 +|Propagation of End-User claims to the Resource Server + +|EOEPCA-205 +|Create Swagger Endpoint for Resource Protection API + +|EOEPCA-203 +|Separation of Proxy and Resource Management concerns + +|EOEPCA-194 +|Usage of relative URLs without proxy prefix + +|EOEPCA-189 +|Security: Verification of RPT Signatures + +|EOEPCA-187 +|Allow both RPT and ID Token Forwarding + +|EOEPCA-178 +|Default protection of resources + +|EOEPCA-126 +|Policy to Resource Data Model Extension + +|EOEPCA-173 +|Implementation of strict RPT Validation Measures + +|EOEPCA-120 +|Path-based resolution of Resource IDs + +|EOEPCA-114 +|Local Registration of Resources + +|EOEPCA-99 +|Command Line Interface UMA Client + +|EOEPCA-98 +|Baseline Enforcement Functionality + +|EOEPCA-94 +|Reusable UMA Client Implementation - End-User Functionality + +|EOEPCA-144 +|Resource Ownership Enforcement + +|=== From b43b2984aa3a0807d05d1888e95c3c429754a921 Mon Sep 17 00:00:00 2001 From: Hector Rodriguez Date: Thu, 3 Dec 2020 15:02:37 +0000 Subject: [PATCH 70/80] EOEPCA-221 #comment Added management scripts to container PATH --- Dockerfile | 4 + ...esource_manager.py => management_tools.py} | 110 +++++++++--------- 2 files changed, 59 insertions(+), 55 deletions(-) rename src/{tools/resource_manager.py => management_tools.py} (92%) diff --git a/Dockerfile b/Dockerfile index 4746c37..15bcd02 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,6 +8,10 @@ RUN pip install -r requirements.txt # Add requirements, code COPY src/ / + +RUN ln -sf /management_tools.py /bin/management_tools +RUN chmod +x /management_tools.py + # Declare and expose service listening port EXPOSE 5566/tcp diff --git a/src/tools/resource_manager.py b/src/management_tools.py similarity index 92% rename from src/tools/resource_manager.py rename to src/management_tools.py index a0a1bd1..172924a 100644 --- a/src/tools/resource_manager.py +++ b/src/management_tools.py @@ -1,55 +1,55 @@ -#!/usr/bin/python3 - -import argparse -import sys -from handlers.mongo_handler import Mongo_Handler - -custom_mongo = Mongo_Handler("resource_db", "resources") - -def list_resources(user,resource): - if resource is not None: - return custom_mongo.get_from_mongo("resource_id", resource) - if user is not None: - resources=custom_mongo.get_all_resources() - return list(filter(lambda x: x["ownership_id"] == user,resources)) - return custom_mongo.get_all_resources() - -def remove_resources(user,resource,all): - if resource is not None: - return custom_mongo.delete_in_mongo("resource_id", resource) - if user is not None and all: - return custom_mongo.remove_resources("ownership_id",user) - if user is None and all: - return custom_mongo.remove_resources() - return "No action taken (missing --all flag?)" - - -parser = argparse.ArgumentParser(description='Operational management of resources.') -parser.add_argument('action', metavar='action', type=str, - help='Operation to perform: list/remove') -parser.add_argument('-u', - '--user', - help='Filter action by user ID') -parser.add_argument('-r', - '--resource', - help='Filter action by resource ID') - -parser.add_argument('-a', - '--all', - action='store_true', - help='Apply action to all resources.') - - -args = vars(parser.parse_args()) - -if args["action"] == "list": - result = list_resources(args['user'],args['resource']) -elif args["action"] == "remove": - if args["resource"] is not None: - args["all"] = False - result = remove_resources(args['user'],args['resource'],args['all']) -else: - print("Allowed actions are 'remove' or 'list'") - sys.exit(-1) - -print(result) \ No newline at end of file +#!/usr/local/bin/python3 +import argparse +import sys +from handlers.mongo_handler import Mongo_Handler +from bson.json_util import dumps + +custom_mongo = Mongo_Handler("resource_db", "resources") + +def list_resources(user,resource): + if resource is not None: + return custom_mongo.get_from_mongo("resource_id", resource) + if user is not None: + resources=custom_mongo.get_all_resources() + return list(filter(lambda x: x["ownership_id"] == user,resources)) + return custom_mongo.get_all_resources() + +def remove_resources(user,resource,all): + if resource is not None: + return custom_mongo.delete_in_mongo("resource_id", resource) + if user is not None and all: + return custom_mongo.remove_resources("ownership_id",user) + if user is None and all: + return custom_mongo.remove_resources() + return "No action taken (missing --all flag?)" + + +parser = argparse.ArgumentParser(description='Operational management of resources.') +parser.add_argument('action', metavar='action', type=str, + help='Operation to perform: list/remove') +parser.add_argument('-u', + '--user', + help='Filter action by user ID') +parser.add_argument('-r', + '--resource', + help='Filter action by resource ID') + +parser.add_argument('-a', + '--all', + action='store_true', + help='Apply action to all resources.') + + +args = vars(parser.parse_args()) + +if args["action"] == "list": + result = dumps(list_resources(args['user'],args['resource'])) +elif args["action"] == "remove": + if args["resource"] is not None: + args["all"] = False + result = remove_resources(args['user'],args['resource'],args['all']) +else: + print("Allowed actions are 'remove' or 'list'") + sys.exit(-1) + +print(result) From d7b35892f4cbacfe8f773b22c968d14ace8dc291 Mon Sep 17 00:00:00 2001 From: Tiago Fernandes Date: Thu, 3 Dec 2020 17:56:56 +0000 Subject: [PATCH 71/80] EOEPCA-205 swagger ui split --- src/main.py | 21 +++- src/static/swagger_pep_proxy_ui.json | 99 +++++++++++++++++++ ..._ui.json => swagger_pep_resources_ui.json} | 73 -------------- 3 files changed, 115 insertions(+), 78 deletions(-) create mode 100644 src/static/swagger_pep_proxy_ui.json rename src/static/{swagger_pep_ui.json => swagger_pep_resources_ui.json} (76%) diff --git a/src/main.py b/src/main.py index a183066..8f5758e 100644 --- a/src/main.py +++ b/src/main.py @@ -70,15 +70,25 @@ def generateRSAKeyPair(): # SWAGGER initiation SWAGGER_URL = '/swagger-ui' # URL for exposing Swagger UI (without trailing '/') API_URL = "" # Our local swagger resource for PEP. Not used here as 'spec' parameter is used in config -SWAGGER_SPEC = json.load(open("./static/swagger_pep_ui.json")) +SWAGGER_SPEC_PROXY = json.load(open("./static/swagger_pep_proxy_ui.json")) +SWAGGER_SPEC_RESOURCES = json.load(open("./static/swagger_pep_resources_ui.json")) SWAGGER_APP_NAME = "Policy Enforcement Point Interfaces" -swaggerui_blueprint = get_swaggerui_blueprint( +swaggerui_proxy_blueprint = get_swaggerui_blueprint( SWAGGER_URL, API_URL, config={ # Swagger UI config overrides 'app_name': SWAGGER_APP_NAME, - 'spec': SWAGGER_SPEC + 'spec': SWAGGER_SPEC_PROXY + }, +) + +swaggerui_resources_blueprint = get_swaggerui_blueprint( + SWAGGER_URL, + API_URL, + config={ # Swagger UI config overrides + 'app_name': SWAGGER_APP_NAME, + 'spec': SWAGGER_SPEC_RESOURCES }, ) @@ -86,8 +96,9 @@ def generateRSAKeyPair(): resources_app.register_blueprint(resources.construct_blueprint(oidc_client, uma_handler, pdp_policy_handler, g_config)) proxy_app.register_blueprint(proxy.construct_blueprint(oidc_client, uma_handler, g_config, private_key)) -#???? -app.register_blueprint(swaggerui_blueprint) +# SWAGGER UI respective bindings +resources_app.register_blueprint(swaggerui_resources_blueprint) +proxy_app.register_blueprint(swaggerui_proxy_blueprint) # Define run methods for both Flask instances # Start reverse proxy for proxy endpoint diff --git a/src/static/swagger_pep_proxy_ui.json b/src/static/swagger_pep_proxy_ui.json new file mode 100644 index 0000000..f29c3c3 --- /dev/null +++ b/src/static/swagger_pep_proxy_ui.json @@ -0,0 +1,99 @@ +{ + "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" : "Policy Enforcement", + "description" : "Proxying functionality to enforce authorization policies" + } ], + "paths" : { + "/proxy/{path}" : { + "parameters" : [ { + "in" : "path", + "name" : "path", + "description" : "Path to the Back-End Service", + "required" : true, + "schema" : { + "type" : "string" + } + }, { + "in" : "header", + "name" : "Authorization", + "description" : "RPT Token generated through UMA Flow", + "schema" : { + "type" : "string" + } + } ], + "get" : { + "tags" : [ "Policy Enforcement" ], + "summary" : "Request to Back-End Service", + "description" : "This operation propagates all headers and query parameters", + "responses" : { + "200" : { + "description" : "OK" + }, + "401" : { + "$ref" : "#/components/responses/UMAUnauthorized" + } + } + }, + "post" : { + "tags" : [ "Policy Enforcement" ], + "summary" : "Request to Back-End Service", + "description" : "This operation propagates all headers, query parameters and body", + "responses" : { + "200" : { + "description" : "OK" + }, + "401" : { + "$ref" : "#/components/responses/UMAUnauthorized" + } + } + }, + "put" : { + "tags" : [ "Policy Enforcement" ], + "summary" : "Request to Back-End Service", + "description" : "This operation propagates all headers, query parameters and body", + "responses" : { + "200" : { + "description" : "OK" + }, + "401" : { + "$ref" : "#/components/responses/UMAUnauthorized" + } + } + }, + "delete" : { + "tags" : [ "Policy Enforcement" ], + "summary" : "Request to Back-End Service", + "description" : "This operation propagates all headers", + "responses" : { + "200" : { + "description" : "OK" + }, + "401" : { + "$ref" : "#/components/responses/UMAUnauthorized" + } + } + } + } + }, + "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\"'" + } + } + } + } + } + } \ No newline at end of file diff --git a/src/static/swagger_pep_ui.json b/src/static/swagger_pep_resources_ui.json similarity index 76% rename from src/static/swagger_pep_ui.json rename to src/static/swagger_pep_resources_ui.json index 0f1b8dc..5f17b1a 100644 --- a/src/static/swagger_pep_ui.json +++ b/src/static/swagger_pep_resources_ui.json @@ -6,83 +6,10 @@ "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" : "Policy Enforcement", - "description" : "Proxying functionality to enforce authorization policies" - }, { "name" : "Resources", "description" : "Operations to create, modify or delete resources" } ], "paths" : { - "/proxy/{path}" : { - "parameters" : [ { - "in" : "path", - "name" : "path", - "description" : "Path to the Back-End Service", - "required" : true, - "schema" : { - "type" : "string" - } - }, { - "in" : "header", - "name" : "Authorization", - "description" : "RPT Token generated through UMA Flow", - "schema" : { - "type" : "string" - } - } ], - "get" : { - "tags" : [ "Policy Enforcement" ], - "summary" : "Request to Back-End Service", - "description" : "This operation propagates all headers and query parameters", - "responses" : { - "200" : { - "description" : "OK" - }, - "401" : { - "$ref" : "#/components/responses/UMAUnauthorized" - } - } - }, - "post" : { - "tags" : [ "Policy Enforcement" ], - "summary" : "Request to Back-End Service", - "description" : "This operation propagates all headers, query parameters and body", - "responses" : { - "200" : { - "description" : "OK" - }, - "401" : { - "$ref" : "#/components/responses/UMAUnauthorized" - } - } - }, - "put" : { - "tags" : [ "Policy Enforcement" ], - "summary" : "Request to Back-End Service", - "description" : "This operation propagates all headers, query parameters and body", - "responses" : { - "200" : { - "description" : "OK" - }, - "401" : { - "$ref" : "#/components/responses/UMAUnauthorized" - } - } - }, - "delete" : { - "tags" : [ "Policy Enforcement" ], - "summary" : "Request to Back-End Service", - "description" : "This operation propagates all headers", - "responses" : { - "200" : { - "description" : "OK" - }, - "401" : { - "$ref" : "#/components/responses/UMAUnauthorized" - } - } - } - }, "/resources" : { "parameters" : [ { "in" : "header", From 8bb8bd0c636207516ba29987dcf1fc9233e0deb0 Mon Sep 17 00:00:00 2001 From: mamuniz Date: Wed, 9 Dec 2020 12:04:49 +0000 Subject: [PATCH 72/80] Fixed some bugs in code --- src/blueprints/proxy.py | 1 + src/blueprints/resources.py | 8 ++++---- src/handlers/mongo_handler.py | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/blueprints/proxy.py b/src/blueprints/proxy.py index d830be7..688e546 100644 --- a/src/blueprints/proxy.py +++ b/src/blueprints/proxy.py @@ -38,6 +38,7 @@ def resource_request(path): uid = None #If UUID exists and resource requested has same UUID + api_rpt_uma_validation = g_config["api_rpt_uma_validation"] if rpt: print("Token found: "+rpt) diff --git a/src/blueprints/resources.py b/src/blueprints/resources.py index 9a7fb08..b6f3bd6 100644 --- a/src/blueprints/resources.py +++ b/src/blueprints/resources.py @@ -265,13 +265,13 @@ def user_not_authorized(response): response.headers["Error"] = 'User lacking sufficient access privileges' return response - def get_default_ownership_policy_cfg(resource_id, user_name): - return { "resource_id": resource_id, "rules": [{ "AND": [ {"EQUAL": {"user_name" : user_name } }] }] } + def get_default_ownership_policy_cfg(resource_id, uid): + return { "resource_id": resource_id, "action": "view", "rules": [{ "AND": [ {"EQUAL": {"uid" : uid } }] }] } - def get_default_ownership_policy_body(resource_id, user_name): + def get_default_ownership_policy_body(resource_id, uid): name = "Default Ownership Policy of " + str(resource_id) description = "This is the default ownership policy for created resources through PEP" - policy_cfg = get_default_ownership_policy_cfg(resource_id, user_name) + policy_cfg = get_default_ownership_policy_cfg(resource_id, uid) scopes = ["protected_access"] return {"name": name, "description": description, "config": policy_cfg, "scopes": scopes} diff --git a/src/handlers/mongo_handler.py b/src/handlers/mongo_handler.py index 87a39e2..dbe36f6 100644 --- a/src/handlers/mongo_handler.py +++ b/src/handlers/mongo_handler.py @@ -71,7 +71,7 @@ def insert_resource_in_mongo(self, resource_id: str, name: str, ownership_id: st # Check if the resource is alredy registered in the collection x=None if self.mongo_exists("resource_id", resource_id): - x= self.update_resource(myres) + x= self.update_in_mongo("resource_id", myres) # Add the resource since it doesn't exist on the database else: x = col.insert_one(myres) From 644c24fdd85db4e4830d08a6a166658fe751596f Mon Sep 17 00:00:00 2001 From: mamuniz Date: Wed, 9 Dec 2020 12:24:39 +0000 Subject: [PATCH 73/80] Fixed missing public key pem --- src/main.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main.py b/src/main.py index 8f5758e..38443d7 100644 --- a/src/main.py +++ b/src/main.py @@ -57,6 +57,10 @@ def generateRSAKeyPair(): file_out.write(private_key) file_out.close() + file_out = open("config/public.pem", "wb+") + file_out.write(public_key) + file_out.close() + return private_key private_key = generateRSAKeyPair() From 3342e66dd503d6988726564f8a4cae02b2263071 Mon Sep 17 00:00:00 2001 From: mamuniz Date: Thu, 10 Dec 2020 08:48:56 +0000 Subject: [PATCH 74/80] Added unit tests for the pep --- tests/testPEPResources.py | 88 ++++++++++++++++++++++++++------------- 1 file changed, 59 insertions(+), 29 deletions(-) diff --git a/tests/testPEPResources.py b/tests/testPEPResources.py index fe3ce4f..e97fae7 100644 --- a/tests/testPEPResources.py +++ b/tests/testPEPResources.py @@ -27,45 +27,46 @@ def setUpClass(cls): wkh = WellKnownHandler(cls.g_config["auth_server_url"], secure=False) cls.__TOKEN_ENDPOINT = wkh.get(TYPE_OIDC, KEY_OIDC_TOKEN_ENDPOINT) - #Generate ID Token - _rsakey = RSA.generate(2048) - _private_key = _rsakey.exportKey() - _public_key = _rsakey.publickey().exportKey() - - file_out = open("private.pem", "wb") - file_out.write(_private_key) - file_out.close() - - file_out = open("public.pem", "wb") - file_out.write(_public_key) - file_out.close() - - _rsajwk = RSAKey(kid='RSA1', key=import_rsa_key(_private_key)) + _rsajwk = RSAKey(kid="RSA1", key=import_rsa_key_from_file("../src/config/private.pem")) _payload = { "iss": cls.g_config["client_id"], "sub": cls.g_config["client_id"], "aud": cls.__TOKEN_ENDPOINT, + "user_name": "admin", "jti": datetime.datetime.today().strftime('%Y%m%d%s'), "exp": int(time.time())+3600, - "isOperator": True + "isOperator": False } _jws = JWS(_payload, alg="RS256") + + _payload_ownership = { + "iss": cls.g_config["client_id"], + "sub": "54d10251-6cb5-4aee-8e1f-f492f1105c94", + "aud": cls.__TOKEN_ENDPOINT, + "user_name": "admin", + "jti": datetime.datetime.today().strftime('%Y%m%d%s'), + "exp": int(time.time())+3600, + "isOperator": False + } + _jws_ownership = JWS(_payload_ownership, alg="RS256") + cls.jwt = _jws.sign_compact(keys=[_rsajwk]) - cls.scopes = 'public_access' + cls.jwt_rotest = _jws_ownership.sign_compact(keys=[_rsajwk]) + #cls.scopes = 'public_access' + cls.scopes = 'protected_access' cls.resourceName = "TestResourcePEP" cls.PEP_HOST = "http://localhost:5566" - - @classmethod - def tearDownClass(cls): - os.remove("private.pem") - os.remove("public.pem") - + cls.PEP_RES_HOST = "http://localhost:5576" + def getJWT(self): return self.jwt + def getJWT_RO(self): + return self.jwt_rotest + def getResourceList(self, id_token="filler"): headers = { 'content-type': "application/x-www-form-urlencoded", "cache-control": "no-cache", "Authorization": "Bearer "+str(id_token)} - res = requests.get(self.PEP_HOST+"/resources", headers=headers, verify=False) + res = requests.get(self.PEP_RES_HOST+"/resources", headers=headers, verify=False) if res.status_code == 401: return 401, res.headers["Error"] if res.status_code == 404: @@ -77,14 +78,14 @@ def getResourceList(self, id_token="filler"): def createTestResource(self, id_token="filler"): payload = { "resource_scopes":[ self.scopes ], "icon_uri":"/"+self.resourceName, "name": self.resourceName } headers = { 'content-type': "application/json", "cache-control": "no-cache", "Authorization": "Bearer "+str(id_token) } - res = requests.post(self.PEP_HOST+"/resources/"+self.resourceName, headers=headers, json=payload, verify=False) + res = requests.post(self.PEP_RES_HOST+"/resources", headers=headers, json=payload, verify=False) if res.status_code == 200: return 200, res.text return 500, None def getResource(self, id_token="filler"): headers = { 'content-type': "application/json", "cache-control": "no-cache", "Authorization": "Bearer "+id_token } - res = requests.get(self.PEP_HOST+"/resources/"+self.resourceID, headers=headers, verify=False) + res = requests.get(self.PEP_RES_HOST+"/resources/"+self.resourceID, headers=headers, verify=False) if res.status_code == 401: return 401, res.headers["Error"] if res.status_code == 200: @@ -95,7 +96,7 @@ def getResource(self, id_token="filler"): def deleteResource(self, id_token="filler"): headers = { 'content-type': "application/json", "cache-control": "no-cache", "Authorization": "Bearer "+id_token } - res = requests.delete(self.PEP_HOST+"/resources/"+self.resourceID, headers=headers, verify=False) + res = requests.delete(self.PEP_RES_HOST+"/resources/"+self.resourceID, headers=headers, verify=False) if res.status_code == 401: return 401, res.headers["Error"] if res.status_code == 204: @@ -105,18 +106,31 @@ def deleteResource(self, id_token="filler"): def updateResource(self, id_token="filler"): headers = { 'content-type': "application/json", "cache-control": "no-cache", "Authorization": "Bearer "+id_token } payload = { "resource_scopes":[ self.scopes], "icon_uri":"/"+self.resourceName, "name":self.resourceName+"Mod" } - res = requests.put(self.PEP_HOST+"/resources/"+self.resourceID, headers=headers, json=payload, verify=False) + res = requests.put(self.PEP_RES_HOST+"/resources/"+self.resourceID, headers=headers, json=payload, verify=False) if res.status_code == 401: return 401, res.headers["Error"] if res.status_code == 200: return 200, None return 500, None + def updateResourceRO(self, id_token="filler"): + headers = { 'content-type': "application/json", "cache-control": "no-cache", "Authorization": "Bearer "+id_token } + payload = {"resource_scopes":[ self.scopes], "icon_uri":"/"+self.resourceName, "name":self.resourceName+"Mod", "ownership_id": "54d10251-6cb5-4aee-8e1f-f492f1105c94"} + res = requests.put(self.PEP_RES_HOST+"/resources/"+self.resourceID, headers=headers, json=payload, verify=False) + if res.status_code == 401: + return 401, res.headers["Error"] + if res.status_code == 403: + return 403, res.headers["Error"] + if res.status_code == 200: + return 200, None + return 500, None + #Monolithic test to avoid jumping through hoops to implement ordered tests #This test case assumes v0.3 of the PEP engine def test_resource(self): #Use a JWT token as id_token id_token = self.getJWT() + id_token_ro = self.getJWT_RO() #Create resource status, self.resourceID = self.createTestResource(id_token) @@ -167,8 +181,24 @@ def test_resource(self): print("=======================") print("") - #Delete created resource - status, reply = self.deleteResource(id_token) + # Change ownership with user ROTEST but using admin jwt - should fail + status, _ = self.updateResourceRO(id_token_ro) + self.assertEqual(status, 403) + del status + print("Invalid Ownership Change request successfully denied") + print("=======================") + print("") + + # Test ownership with user ROTEST with ROTEST jwt - should succeed + status, _ = self.updateResourceRO(id_token) + self.assertEqual(status, 200) + del status + print("Valid Ownership Change request successfull") + print("=======================") + print("") + + # Delete created resource + status, reply = self.deleteResource(id_token_ro) self.assertEqual(status, 204) print("Delete resource: Resource deleted.") del status, reply From 42d288c289f732159899d42b51526e4f78f199be Mon Sep 17 00:00:00 2001 From: Tiago Fernandes Date: Fri, 11 Dec 2020 16:03:20 +0000 Subject: [PATCH 75/80] EOEPCA-221 tool shell script --- Dockerfile | 3 ++- src/management_tools_script.sh | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 src/management_tools_script.sh diff --git a/Dockerfile b/Dockerfile index e243d8f..d7d2525 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,8 +9,9 @@ RUN pip install -r requirements.txt COPY src/ / -RUN ln -sf /management_tools.py /bin/management_tools +RUN ln -sf /management_tools_script.sh /bin/management_tools RUN chmod +x /management_tools.py +RUN chmod +x /management_tools_script.sh # Declare and expose service listening port EXPOSE 5566/tcp diff --git a/src/management_tools_script.sh b/src/management_tools_script.sh new file mode 100644 index 0000000..281a927 --- /dev/null +++ b/src/management_tools_script.sh @@ -0,0 +1,2 @@ +#!/bin/bash +python3 /management_tools.py "$@" \ No newline at end of file From 84a8d0d7f52bd1952b6fabcac9def56f70e71495 Mon Sep 17 00:00:00 2001 From: Tiago Fernandes Date: Thu, 17 Dec 2020 19:43:35 +0000 Subject: [PATCH 76/80] fix for policy mismatch --- src/blueprints/resources.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blueprints/resources.py b/src/blueprints/resources.py index b6f3bd6..00375d8 100644 --- a/src/blueprints/resources.py +++ b/src/blueprints/resources.py @@ -266,7 +266,7 @@ def user_not_authorized(response): return response def get_default_ownership_policy_cfg(resource_id, uid): - return { "resource_id": resource_id, "action": "view", "rules": [{ "AND": [ {"EQUAL": {"uid" : uid } }] }] } + return { "resource_id": resource_id, "action": "view", "rules": [{ "AND": [ {"EQUAL": {"id" : uid } }] }] } def get_default_ownership_policy_body(resource_id, uid): name = "Default Ownership Policy of " + str(resource_id) From 9bdbb5ac8c7dcd4f6b87901079e10a89a2f81345 Mon Sep 17 00:00:00 2001 From: Tiago Fernandes Date: Fri, 8 Jan 2021 10:47:40 +0000 Subject: [PATCH 77/80] v0.3 updated docs --- docs/ICD/03.interfaces/00.interfaces.adoc | 132 +++++++++++++++++++--- 1 file changed, 114 insertions(+), 18 deletions(-) diff --git a/docs/ICD/03.interfaces/00.interfaces.adoc b/docs/ICD/03.interfaces/00.interfaces.adoc index f5c1b96..dbe1ee4 100644 --- a/docs/ICD/03.interfaces/00.interfaces.adoc +++ b/docs/ICD/03.interfaces/00.interfaces.adoc @@ -901,6 +901,96 @@ ifdef::internal-generation[] endif::internal-generation[] +[.API] +=== API + +[.SwaggerUI] +==== Swagger UI + +`/swagger-ui` + +===== Description + +This operation accesses the API for the Policy Decision Point + + +// markup not found, no include::{specDir}swagger-ui/spec.adoc[opts=optional] + + + +===== Parameters + +====== Path Parameters + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| - +| - +| - + + +|=== + + + +====== Header Parameters + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| - +| - +| - + + +|=== + + + +===== Return Type + + + +- + + +===== Responses + +.http response codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| OK +| <<>> + +|=== + +===== Samples + + +// markup not found, no include::{snippetDir}swagger-ui/http-request.adoc[opts=optional] + + +// markup not found, no include::{snippetDir}swagger-ui/http-response.adoc[opts=optional] + + + +// file not found, no * wiremock data link :swagger-ui/swagger-ui.json[] + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}swagger-ui/implementation.adoc[opts=optional] + + +endif::internal-generation[] [#models] == Models @@ -917,22 +1007,28 @@ endif::internal-generation[] | Field Name| Required| Type| Description| Format | name -| +| Y | String | Human readable name for the resource -| +| - + +| description +| Y +| String +| Human readable description of the resource +| - | icon_uri -| +| Y | String | Protected uri of the resource. -| +| - -| scopes -| +| resource_scopes +| Y | List of <> | List of scopes associated with the resource -| +| - |=== @@ -948,34 +1044,34 @@ endif::internal-generation[] | Field Name| Required| Type| Description| Format | ownership_id -| +| Y | UUID | UUID of the Owner End-User | uuid -| id -| +| description +| Y | UUID -| UUID of the resource +| Human readable description of the resource | uuid | name -| +| Y | String | Human readable name for the resource -| +| - | icon_uri -| +| Y | String | Protected uri of the resource. -| +| - -| scopes -| +| resource_scopes +| Y | List of <> | List of scopes associated with the resource -| +| - |=== From 61057c6669588c5aa16138d600c9b7bd79d494bf Mon Sep 17 00:00:00 2001 From: Tiago Fernandes Date: Fri, 8 Jan 2021 18:36:07 +0000 Subject: [PATCH 78/80] documentation update --- docs/SDD/02.overview/00.overview.adoc | 5 +++++ docs/SDD/03.design/00.design.adoc | 22 ++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/docs/SDD/02.overview/00.overview.adoc b/docs/SDD/02.overview/00.overview.adoc index ab5a6f8..889d4ec 100644 --- a/docs/SDD/02.overview/00.overview.adoc +++ b/docs/SDD/02.overview/00.overview.adoc @@ -105,6 +105,11 @@ In case it is disabled, the signature will not be verified but the other steps a The UUID of the End-User will be included as attribute of the Resource description document (extending the data model) upon resource creation (with an “ownership_id” field). Subsequent requests to the specific Resource ID will perform a JWT or OAuth2.0 check, cross-checking against the “ownership_id” before performing actions and answering back with a 401 Unauthorized if there is no match. +==== Policy API (to Policy Decision Point) +When registering a new resource, the PEP will consume the Policy API to register a default policy with the PDP. The endpoint for this is: + +* /policy + == Required resources [NOTE] diff --git a/docs/SDD/03.design/00.design.adoc b/docs/SDD/03.design/00.design.adoc index c560118..970e2cd 100644 --- a/docs/SDD/03.design/00.design.adoc +++ b/docs/SDD/03.design/00.design.adoc @@ -117,3 +117,25 @@ image::../images/MongoFlow.png[top=5%, align="center", pdfwidth=6.5in] === Applicable Resources * MongoDB image from DockerHub - https://hub.docker.com/_/mongo + +== Resource default Protection Policy +=== Overview and Purpose +Together with the Resource Repository, the PEP will also contact the Policy Decision Point in order to register a default protection policy for the resource. + +This call to `/policy` will include a preset policy configuration, to be applied to the registering resource. It stands as follows: +``` +{"name": "Default Ownership Policy of ", + "description": "This is the default ownership policy for created resources through PEP", + "config": {"resource_id": resource_id, + "rules": [ { "AND": [ { "EQUAL": { "user_name" : user_name }}]}] + }, + "scopes": ["protected_access"]} +``` + +=== Data flow + +This subroutine is triggered by the successful registration of the resource. + +=== Applicable Resources + +* EOEPCA's Policy Decision Point - https://github.com/EOEPCA/um-pdp-engine \ No newline at end of file From 942487e5826076854539d8f105e359cf252ca781 Mon Sep 17 00:00:00 2001 From: Tiago Fernandes Date: Sat, 9 Jan 2021 19:02:49 +0000 Subject: [PATCH 79/80] misc doc updates for v0.3 --- docs/ICD/03.interfaces/00.interfaces.adoc | 2 +- docs/SDD/02.overview/00.overview.adoc | 3 ++- docs/SDD/03.design/00.design.adoc | 4 ++-- docs/SDD/images/init_flow3.png | Bin 0 -> 31227 bytes 4 files changed, 5 insertions(+), 4 deletions(-) create mode 100644 docs/SDD/images/init_flow3.png diff --git a/docs/ICD/03.interfaces/00.interfaces.adoc b/docs/ICD/03.interfaces/00.interfaces.adoc index dbe1ee4..7de97bf 100644 --- a/docs/ICD/03.interfaces/00.interfaces.adoc +++ b/docs/ICD/03.interfaces/00.interfaces.adoc @@ -911,7 +911,7 @@ endif::internal-generation[] ===== Description -This operation accesses the API for the Policy Decision Point +This operation accesses the API for the Policy Enforcement Point // markup not found, no include::{specDir}swagger-ui/spec.adoc[opts=optional] diff --git a/docs/SDD/02.overview/00.overview.adoc b/docs/SDD/02.overview/00.overview.adoc index 889d4ec..c8b864e 100644 --- a/docs/SDD/02.overview/00.overview.adoc +++ b/docs/SDD/02.overview/00.overview.adoc @@ -42,7 +42,7 @@ To further clarify the flow the PEP uses, you can also take a look at the Data F === Initialization flow -image::../images/init_flow.png[top=5%, align=left, pdfwidth=6.5in] +image::../images/init_flow3.png[top=5%, align=left, pdfwidth=6.5in] == External Interfaces @@ -129,6 +129,7 @@ The following Open-Source Software is required to support the deployment and int * EOEPCA's SCIM Client - https://github.com/EOEPCA/um-common-scim-client * EOEPCA's UMA Client - https://github.com/EOEPCA/um-common-uma-client * EOEPCA's Well Known Handler - https://github.com/EOEPCA/well-known-handler +* EOEPCA's Policy Decision Point - https://github.com/EOEPCA/um-pdp-engine * Flask - https://github.com/pallets/flask * MongoDB for Python - https://pymongo.readthedocs.io/en/stable/index.html diff --git a/docs/SDD/03.design/00.design.adoc b/docs/SDD/03.design/00.design.adoc index 970e2cd..2fdbd72 100644 --- a/docs/SDD/03.design/00.design.adoc +++ b/docs/SDD/03.design/00.design.adoc @@ -100,9 +100,9 @@ Included with the PEP there is a script at the source path that performs queries It is developed to generate a database called 'resource_db' in case it does not exist. The collection used for the storage of the documents is called 'resources'. The script defines methods to: -* **Insert resource data**: Generates a document with the resource data received as input and if it already exists, it gets updated. The main parameters of the resource would be an auto-generated id provided by mongo which identify each document in the database, the resource ID provided by the login-service, and the match url which will define the endpoint of the resource. This would be mandatory parameters in order to perform other kind of queries. +* **Insert resource data**: Generates a document with the resource data received as input and if it already exists, it gets updated. The main parameters of the resource would be an auto-generated id provided by mongo which identify each document in the database, the resource ID provided by the login-service, and the match url which will define the endpoint of the resource. This would be mandatory parameters in order to perform other kind of queries. For updated operations, it is also capable of querying the OIDC endpoint of the Authorization Server to query if the request was performed by a valid resource operator. * **Get the ID from a URI**: Returns the id for the best candidate of the match by a given URI. -* **Delete resources**: Receives a resource id and will find and delete the matched document +* **Delete resources**: Receives a resource id and will find and delete the matched document, if the requesting user is a valid resource operator. This script is manipulated by the API which would intercept the request in order to perform PUT,POST and DELETE methods. The GET method would be called by the reverse proxy since it will be in charge of filtering the resource with the given URI. diff --git a/docs/SDD/images/init_flow3.png b/docs/SDD/images/init_flow3.png new file mode 100644 index 0000000000000000000000000000000000000000..0b88968632ad42bcad1c86fdaff8b9d3087ba1e3 GIT binary patch literal 31227 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%0D3AOT;IPn9A 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 Ml%izTEd$U00Gai1=l}o! literal 0 HcmV?d00001 From 8d08cbc760da18189be56ac70f0f658caa1c30de Mon Sep 17 00:00:00 2001 From: mamuniz Date: Tue, 12 Jan 2021 09:03:15 +0000 Subject: [PATCH 80/80] Added test for the swagger endpoint --- tests/testPEPResources.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/testPEPResources.py b/tests/testPEPResources.py index e97fae7..95a64ff 100644 --- a/tests/testPEPResources.py +++ b/tests/testPEPResources.py @@ -125,6 +125,18 @@ def updateResourceRO(self, id_token="filler"): return 200, None return 500, None + def swaggerUI(self): + reply = requests.get(self.PEP_HOST+"/swagger-ui") + self.assertEqual(200, reply.status_code) + print("=================") + print("Get Web Page: 200 OK!") + print("=================") + page = reply.text + page_title = page[page.find("")+7: page.find('')] + print("Get Page Title found: " + page_title) + self.assertEqual("Policy Enforcement Point Interfaces", page_title) + print("Get Page: OK!") + #Monolithic test to avoid jumping through hoops to implement ordered tests #This test case assumes v0.3 of the PEP engine def test_resource(self): @@ -221,5 +233,11 @@ def test_resource(self): print("=======================") print("") + #Swagger UI Endpoint + print("Swagger UI Endpoint ") + self.swaggerUI() + print("=======================") + print("") + if __name__ == '__main__': unittest.main() \ No newline at end of file