diff --git a/charts/pep-engine/scripts/default-resources.json b/charts/pep-engine/scripts/default-resources.json index 0e2152f..2a77f06 100644 --- a/charts/pep-engine/scripts/default-resources.json +++ b/charts/pep-engine/scripts/default-resources.json @@ -1,6 +1,6 @@ { "default_resources": [ - {"name": "Base Path", "description": "Base path for Open Access to PEP", "resource_uri": "/", "scopes": ["public_access"], "default_owner": "0000000000000"} + {"name": "Base Path", "description": "Base path for Open Access to PEP", "resource_uri": "/", "scopes": ["protected_access"], "default_owner": "0000000000000"} ] } diff --git a/charts/pep-engine/templates/pep-cm.yml b/charts/pep-engine/templates/pep-cm.yml index 4d922dc..8a39c0e 100755 --- a/charts/pep-engine/templates/pep-cm.yml +++ b/charts/pep-engine/templates/pep-cm.yml @@ -21,6 +21,7 @@ data: PEP_PROXY_SERVICE_PORT: {{ .Values.global.proxyServicePort | quote }} PEP_RESOURCES_SERVICE_PORT: {{ .Values.global.resourcesServicePort | quote }} PEP_DEFAULT_RESOURCE_PATH: {{ .Values.configMap.defaultResourcePath | quote }} + PEP_WORKING_MODE: {{ .Values.configMap.workingMode | quote }} --- diff --git a/charts/pep-engine/values.yaml b/charts/pep-engine/values.yaml index c515729..380bdb0 100644 --- a/charts/pep-engine/values.yaml +++ b/charts/pep-engine/values.yaml @@ -32,6 +32,7 @@ configMap: pdpPolicy: /pdp/policy/ verifySignature: "'false'" defaultResourcePath: /data/default-resources.json + workingMode: "FULL" readinessProbe: initialDelaySeconds: 1 diff --git a/docs/ICD/03.interfaces/00.interfaces.adoc b/docs/ICD/03.interfaces/00.interfaces.adoc index 09e4ee5..2da5752 100644 --- a/docs/ICD/03.interfaces/00.interfaces.adoc +++ b/docs/ICD/03.interfaces/00.interfaces.adoc @@ -588,6 +588,581 @@ ifdef::internal-generation[] endif::internal-generation[] +[.PolicyAuthorize] +=== PolicyAuthorize + + +[.authorizePathDelete] +==== Authorize DELETE + +`DELETE /authorize` + +Request to Back-End Service + +===== Description + +This operation propagates all headers + + +// markup not found, no include::{specDir}authorize/}/DELETE/spec.adoc[opts=optional] + + + +===== Parameters + +====== Path Parameters + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| path +| Path to the Back-End Service +| X + + +|=== + + + +====== Header Parameters + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| Authorization +| RPT Token generated through UMA Flow +| - + + +|=== + + + +===== Return Type + + + +- + + +===== Responses + +.http response codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| OK +| <<>> + + +| 401 +| Unauthorized access request. +| <<>> + +|=== + +===== Samples + + +// markup not found, no include::{snippetDir}authorize/}/DELETE/http-request.adoc[opts=optional] + + +// markup not found, no include::{snippetDir}authorize/}/DELETE/http-response.adoc[opts=optional] + + + +// file not found, no * wiremock data link :authorize/DELETE/DELETE.json[] + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}authorize/}/DELETE/implementation.adoc[opts=optional] + + +endif::internal-generation[] + + +[.authorizePathGet] +==== Authorize GET + +`GET /authorize` + +Request to Back-End Service + +===== Description + +This operation propagates all headers and query parameters + + +// markup not found, no include::{specDir}authorize/}/GET/spec.adoc[opts=optional] + + + +===== Parameters + +====== Path Parameters + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| path +| Path to the Back-End Service +| X + + +|=== + + + +====== Header Parameters + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| Authorization +| RPT Token generated through UMA Flow +| - + + +|=== + + + +===== Return Type + + + +- + + +===== Responses + +.http response codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| OK +| <<>> + + +| 401 +| Unauthorized access request. +| <<>> + +|=== + +===== 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[] + +[.authorizePathHead] +==== Authorize HEAD + +`HEAD /authorize` + +Request to Back-End Service + +===== Description + +This operation propagates all headers and query parameters + + +// markup not found, no include::{specDir}authorize/}/HEAD/spec.adoc[opts=optional] + + + +===== Parameters + +====== Path Parameters + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| path +| Path to the Back-End Service +| X + + +|=== + + + +====== Header Parameters + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| Authorization +| RPT Token generated through UMA Flow +| - + + +|=== + + + +===== Return Type + + + +- + + +===== Responses + +.http response codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| OK +| <<>> + + +| 401 +| Unauthorized access request. +| <<>> + +|=== + +===== Samples + + +// markup not found, no include::{snippetDir}authorize/}/HEAD/http-request.adoc[opts=optional] + + +// markup not found, no include::{snippetDir}authorize/}/HEAD/http-response.adoc[opts=optional] + + + +// file not found, no * wiremock data link :authorize/HEAD/HEAD.json[] + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}authorize/}/HEAD/implementation.adoc[opts=optional] + + +endif::internal-generation[] + +[.authorizePathPost] +==== Authorize POST + +`POST /authorize` + +Request to Back-End Service + +===== Description + +This operation propagates all headers, query parameters and body + + +// markup not found, no include::{specDir}authorize/}/POST/spec.adoc[opts=optional] + + + +===== Parameters + +====== Path Parameters + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| path +| Path to the Back-End Service +| X + + +|=== + + + +====== Header Parameters + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| Authorization +| RPT Token generated through UMA Flow +| - + + +|=== + + + +===== Return Type + + + +- + + +===== Responses + +.http response codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| OK +| <<>> + + +| 401 +| Unauthorized access request. +| <<>> + +|=== + +===== 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[] + + +[.authorizePathPut] +==== Authorize PUT + +`PUT /authorize` + +Request to Back-End Service + +===== Description + +This operation propagates all headers, query parameters and body + + +// markup not found, no include::{specDir}authorize/}/PUT/spec.adoc[opts=optional] + + + +===== Parameters + +====== Path Parameters + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| path +| Path to the Back-End Service +| X + + +|=== + + + +====== Header Parameters + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| Authorization +| RPT Token generated through UMA Flow +| - + + +|=== + + + +===== Return Type + + + +- + + +===== Responses + +.http response codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| OK +| <<>> + + +| 401 +| Unauthorized access request. +| <<>> + +|=== + +===== Samples + + +// markup not found, no include::{snippetDir}authorize/}/PUT/http-request.adoc[opts=optional] + + +// markup not found, no include::{snippetDir}authorize/}/PUT/http-response.adoc[opts=optional] + + + +// file not found, no * wiremock data link :authorize/PUT/PUT.json[] + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}authorize/}/PUT/implementation.adoc[opts=optional] + + +endif::internal-generation[] + +[.authorizePathPatch] +==== Authorize PATCH + +`PATCH /authorize` + +Request to Back-End Service + +===== Description + +This operation propagates all headers, query parameters and body + + +// markup not found, no include::{specDir}authorize/}/PATCH/spec.adoc[opts=optional] + + + +===== Parameters + +====== Path Parameters + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| path +| Path to the Back-End Service +| X + + +|=== + + + +====== Header Parameters + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| Authorization +| RPT Token generated through UMA Flow +| - + + +|=== + + + +===== Return Type + + + +- + + +===== Responses + +.http response codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| OK +| <<>> + + +| 401 +| Unauthorized access request. +| <<>> + +|=== + +===== Samples + + +// markup not found, no include::{snippetDir}authorize/}/PATCH/http-request.adoc[opts=optional] + + +// markup not found, no include::{snippetDir}authorize/}/PATCH/http-response.adoc[opts=optional] + + + +// file not found, no * wiremock data link :authorize/PATCH/PATCH.json[] + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}authorize/}/PATCH/implementation.adoc[opts=optional] + + +endif::internal-generation[] [.Resources] === Resources @@ -673,6 +1248,96 @@ ifdef::internal-generation[] endif::internal-generation[] +===== Filter by path + +`GET /resources?path=` + +Get the resource with the path specified + +===== Description + +This operation returns the resource filtered by ownership ID and the path passed as argument in the URL. Ownership ID is extracted from the OpenID Connect Token + + +// markup not found, no include::{specDir}resources/GET/spec.adoc[opts=optional] + + + +===== Parameters + + + + +====== Header Parameters + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| Authorization +| JWT or Bearer Token +| - + + +|=== + +====== URL Parameters + +[cols="2,3,1"] +|=== +|Name| Description| Required + +| path +| reverse_match_url of resource +| - + + +|=== + + +===== Return Type + +JSON + + +===== Content Type + +* application/json + +===== Responses + +.http response codes +[cols="2,3,1"] +|=== +| Code | Message | Datatype + + +| 200 +| OK +| JSON + +|=== + +===== Samples + + +// markup not found, no include::{snippetDir}resources/GET/http-request.adoc[opts=optional] + + +// markup not found, no include::{snippetDir}resources/GET/http-response.adoc[opts=optional] + + + +// file not found, no * wiremock data link :resources/GET/GET.json[] + + +ifdef::internal-generation[] +===== Implementation + +// markup not found, no include::{specDir}resources/GET/implementation.adoc[opts=optional] + + +endif::internal-generation[] [.resourcesPost] ==== Resources POST diff --git a/docs/SDD/02.overview/00.overview.adoc b/docs/SDD/02.overview/00.overview.adoc index b7f3e49..4be9cc9 100644 --- a/docs/SDD/02.overview/00.overview.adoc +++ b/docs/SDD/02.overview/00.overview.adoc @@ -50,14 +50,22 @@ image::../images/init_flow3.png[top=5%, align=left, pdfwidth=6.5in] ==== HTTP(S) (Reverse Proxy Listener) An HTTP listener, which can be configured through the config file. -This is the only input interface to interact directly with the PEP from outside, and is managed by the reverse proxy. +This is the only input interface to interact directly with the PEP from outside when in FULL mode, and is managed by the reverse proxy. The default listener for this interface is `/pep/`. This interface will parse the path and the headers in order to assert authentication and authorization of the client requesting the resource. +==== HTTP(S) (Nginx Proxy Listener) +An HTTP listener, which can be configured through the config file. +This is the only input interface to interact directly with the PEP from outside when in PARTIAL mode, and is managed by the authorize api. + +The default listener for this interface is `/pep/authorize`. + +This interface will parse the path and the headers in order to assert authentication and authorization of the client requesting the resource, reporting back the results. + ==== HTTP(S) (to Resource Server) -The PEP will contact via HTTP with the configured Resource Server whenever a valid request with a valid RPT is done, or whenever RPT is not needed to access the resource. +The PEP (when in FULL mode) will contact via HTTP with the configured Resource Server whenever a valid request with a valid RPT is done, or whenever RPT is not needed to access the resource. The PEP will make a request to the RS, and will return the answer verbatim to the client that requested it, effectively acting like a transparent proxy from the client's point of view. This allows the mentioned desired behaviour of being able to protect anything just placing the PEP "in front of" the resource to protect. @@ -210,3 +218,7 @@ A new dynamically (via CRUD operations) registered resource will have a default === PEP-UC-007: Policy Enforcement Point API The current implemented functionalities can be consulted through a specific OpenAPI webpage, available at the PEP level. + +=== PEP-UC-008: Policy Enforcement Point PARTIAL mode + +The PEP is also capable of functioning in a PARTIAL mode, where the reverse proxy is disabled and is delegated to an external nginx instance. In this mode, the PEP provides an authorization endpoint that can be called for RPT validation and ticket generation, informing the nginx instance of the replies. \ No newline at end of file diff --git a/docs/SDD/03.design/00.design.adoc b/docs/SDD/03.design/00.design.adoc index 9c3e38c..62611ca 100644 --- a/docs/SDD/03.design/00.design.adoc +++ b/docs/SDD/03.design/00.design.adoc @@ -22,7 +22,9 @@ When a breakdown is necessary, a general overview of the building block can be g == Reverse Proxy Service === Overview and Purpose -The Flask-based reverse proxy serves as the interface for input queries. This reverse proxy is in charge of recieving the queries and returning the appropiate HTTP response. +The Flask-based reverse proxy serves as the interface for input queries. This reverse proxy is in charge of receiving the queries and returning the appropiate HTTP response. + +This is the default behaviour of the PEP when working in FULL mode. There is an alternate mode, PARTIAL, where the reverse proxy functionality is delegated to an external nginx instance, and the PEP itself works in an authorization api fashion, validating RPTs and issuing access tickets for requests, informing the caller of the result of said actions. === Software Reuse and Dependencies @@ -61,6 +63,7 @@ The parameters that are accepted, and their meaning, are as follows. For the `co - **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. +- **working_mode**: PEP working mode, FULL or PARTIAL. - **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. @@ -140,7 +143,7 @@ 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. 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. +* **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. As an operator all resources are available for register and update, but in case the one registering a resource is a user, it will need to ask for an operator to first register a resource in its name. After that all resources derived from the resource asigned will be allowed to register by taht user. * **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, if the requesting user is a valid resource operator. diff --git a/src/blueprints/authorize.py b/src/blueprints/authorize.py new file mode 100644 index 0000000..9d900e4 --- /dev/null +++ b/src/blueprints/authorize.py @@ -0,0 +1,155 @@ +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 handlers.log_handler import LogHandler +from werkzeug.datastructures import Headers +from random import choice +from string import ascii_lowercase +from requests import get, post, put, delete, head, patch +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 + +def construct_blueprint(oidc_client, uma_handler, g_config, private_key): + authorize_bp = Blueprint('authorize_bp', __name__) + logger = logging.getLogger("PEP_ENGINE") + log_handler = LogHandler.get_instance() + + @authorize_bp.route("/authorize", methods=["GET","POST","PUT","DELETE","HEAD","PATCH"]) + def resource_request(): + # Check for token + logger.debug("Processing authorization request...") + custom_mongo = Mongo_Handler("resource_db", "resources") + rpt = request.headers.get('Authorization') + if "X-Original-Uri" in request.headers: + path = request.headers.get('X-Original-Uri') + else: + path = "" + if "X-Original-Method" in request.headers: + http_method = request.headers.get('X-Original-Method') + else: + #Defaults to GET method if X-Original-Method is not sent + http_method = "GET" + # Get resource + resource_id = custom_mongo.get_id_from_uri(path) + scopes= None + if resource_id: + scopes = [] + if http_method == 'GET': + scopes.append('protected_get') + elif http_method == 'POST': + scopes.append('protected_post') + elif http_method == 'PUT': + scopes.append('protected_put') + elif http_method == 'DELETE': + scopes.append('protected_delete') + elif http_method == 'HEAD': + scopes.append('protected_head') + elif http_method == 'PATCH': + scopes.append('protected_patch') + + uid = None + + #If UUID exists and resource requested has same UUID + api_rpt_uma_validation = g_config["api_rpt_uma_validation"] + + response = Response() + if rpt: + logger.debug("Token found: "+rpt) + rpt = rpt.replace("Bearer ","").strip() + + # Validate for a specific resource for any other HTTP method call + if (uma_handler.validate_rpt(rpt, [{"resource_id": resource_id, "resource_scopes": ["public_access"] }], int(g_config["s_margin_rpt_valid"]), int(g_config["rpt_limit_uses"]), g_config["verify_signature"]) or + uma_handler.validate_rpt(rpt, [{"resource_id": resource_id, "resource_scopes": ["Authenticated"] }], int(g_config["s_margin_rpt_valid"]), int(g_config["rpt_limit_uses"]), g_config["verify_signature"]) or + 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): + logger.debug("RPT valid, accessing ") + + # RPT validated, allow nginx to redirect request to Resource Server + activity = {"User":uid,"Resource":resource_id,"Description":"Token validated"} + logger.info(log_handler.format_message(subcomponent="AUTHORIZE",action_id="HTTP",action_type=http_method,log_code=2103,activity=activity)) + response.status_code = 200 + return response + logger.debug("Invalid RPT!, sending ticket") + # In any other case, we have an invalid RPT, so send a ticket. + # Fallthrough intentional + logger.debug("No auth token, or auth token is invalid") + if resource_id is not None: + try: + logger.debug("Matched resource: "+str(resource_id)) + # Generate ticket if token is not present + ticket = "" + try: + #Ticket for default protected_XXX scopes + ticket = uma_handler.request_access_ticket([{"resource_id": resource_id, "resource_scopes": scopes }]) + 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. + activity = {"Ticket":ticket,"Description":"Invalid token, generating ticket for resource:"+resource_id} + logger.info(log_handler.format_message(subcomponent="AUTHORIZE",action_id="HTTP",action_type=http_method,log_code=2104,activity=activity)) + return response + except Exception as e: + pass #Resource is not registered with default scopes + try: + #Try again, but with "Authenticated" scope + ticket = uma_handler.request_access_ticket([{"resource_id": resource_id, "resource_scopes": ["Authenticated"] }]) + 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. + activity = {"Ticket":ticket,"Description":"Invalid token, generating ticket for resource:"+resource_id} + logger.info(log_handler.format_message(subcomponent="AUTHORIZE",action_id="HTTP",action_type=http_method,log_code=2104,activity=activity)) + return response + except Exception as e: + pass #Resource is not registered with "Authenticated" scope + try: + #Try again, but with "public_access" scope + ticket = uma_handler.request_access_ticket([{"resource_id": resource_id, "resource_scopes": ["public_access"] }]) + 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. + activity = {"Ticket":ticket,"Description":"Invalid token, generating ticket for resource:"+resource_id} + logger.info(log_handler.format_message(subcomponent="AUTHORIZE",action_id="HTTP",action_type=http_method,log_code=2104,activity=activity)) + return response + except Exception as e: + #Resource is not registered with any known scope, throw generalized exception + raise Exception("An error occurred while requesting permission for a resource: 500: no valid scopes found for specified resource") + except Exception as e: + response.status_code = int(str(e).split(":")[1].strip()) + response.headers["Error"] = str(e) + activity = {"Ticket":None,"Error":str(e)} + logger.info(log_handler.format_message(subcomponent="AUTHORIZE",action_id="HTTP",action_type=http_method,log_code=2104,activity=activity)) + return response + else: + logger.debug("No matched resource, forward to Resource Server.") + # In this case, the PEP doesn't have that resource handled, so it replies a 200 so the request is forwarded to the Resource Server + response.status_code = 200 + activity = {"User":uid,"Description":"No resource found, forwarding to Resource Server."} + logger.info(log_handler.format_message(subcomponent="AUTHORIZE",action_id="HTTP",action_type=http_method,log_code=2105,activity=activity)) + 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 authorize_bp diff --git a/src/blueprints/proxy.py b/src/blueprints/proxy.py index a9a6079..b6c01a9 100644 --- a/src/blueprints/proxy.py +++ b/src/blueprints/proxy.py @@ -24,6 +24,7 @@ def construct_blueprint(oidc_client, uma_handler, g_config, private_key): logger = logging.getLogger("PEP_ENGINE") log_handler = LogHandler.get_instance() + @proxy_bp.route('/', defaults={'path': ''}, methods=["GET","POST","PUT","DELETE","HEAD","PATCH"]) @proxy_bp.route("/", methods=["GET","POST","PUT","DELETE","HEAD","PATCH"]) def resource_request(path): # Check for token @@ -47,9 +48,7 @@ def resource_request(path): scopes.append('protected_head') elif request.method == 'PATCH': scopes.append('protected_patch') - uid = None - #If UUID exists and resource requested has same UUID api_rpt_uma_validation = g_config["api_rpt_uma_validation"] @@ -93,14 +92,39 @@ def resource_request(path): if resource_id is not None: try: logger.debug("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. - activity = {"Ticket":ticket,"Description":"Invalid token, generating ticket for resource:"+resource_id} - logger.info(log_handler.format_message(subcomponent="PROXY",action_id="HTTP",action_type=request.method,log_code=2104,activity=activity)) - return response + # Generate ticket if token is not present + ticket = "" + try: + #Ticket for default protected_XXX scopes + ticket = uma_handler.request_access_ticket([{"resource_id": resource_id, "resource_scopes": scopes }]) + 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. + activity = {"Ticket":ticket,"Description":"Invalid token, generating ticket for resource:"+resource_id} + logger.info(log_handler.format_message(subcomponent="AUTHORIZE",action_id="HTTP",action_type=request.method,log_code=2104,activity=activity)) + return response + except Exception as e: + pass #Resource is not registered with default scopes + try: + #Try again, but with "Authenticated" scope + ticket = uma_handler.request_access_ticket([{"resource_id": resource_id, "resource_scopes": ["Authenticated"] }]) + 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. + activity = {"Ticket":ticket,"Description":"Invalid token, generating ticket for resource:"+resource_id} + logger.info(log_handler.format_message(subcomponent="AUTHORIZE",action_id="HTTP",action_type=request.method,log_code=2104,activity=activity)) + return response + except Exception as e: + pass #Resource is not registered with "Authenticated" scope + try: + #Try again, but with "public_access" scope + ticket = uma_handler.request_access_ticket([{"resource_id": resource_id, "resource_scopes": ["public_access"] }]) + 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. + activity = {"Ticket":ticket,"Description":"Invalid token, generating ticket for resource:"+resource_id} + logger.info(log_handler.format_message(subcomponent="AUTHORIZE",action_id="HTTP",action_type=request.method,log_code=2104,activity=activity)) + return response + except Exception as e: + #Resource is not registered with any known scope, throw generalized exception + raise Exception("An error occurred while requesting permission for a resource: 500: no valid scopes found for specified resource") except Exception as e: response.status_code = int(str(e).split(":")[1].strip()) response.headers["Error"] = str(e) @@ -150,7 +174,7 @@ def proxy_request(request, new_header): return response except Exception as e: response = Response() - logger.debug("Error while redirecting to resource: "+ traceback.format_exc(),file=sys.stderr) + logger.debug("Error while redirecting to resource: "+ str(e)) response.status_code = 500 response.content = "Error while redirecting to resource: "+str(e) return response diff --git a/src/blueprints/resources.py b/src/blueprints/resources.py index 70c4114..00b2df7 100644 --- a/src/blueprints/resources.py +++ b/src/blueprints/resources.py @@ -29,6 +29,10 @@ def get_resource_list(): try: head_protected = str(request.headers) headers_protected = head_protected.split() + 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 uid = oidc_client.verify_uid_headers(headers_protected, "sub") if "NO TOKEN FOUND" in uid: response.status_code = 401 @@ -44,7 +48,7 @@ def get_resource_list(): logger.info(log_handler.format_message(subcomponent="RESOURCES",action_id="HTTP",action_type=request.method,log_code=2001,activity=activity)) return response - if not uid: + if not uid and not is_operator: logger.debug("UID for the user not found") response.status_code = 401 response.headers["Error"] = 'Could not get the UID for the user' @@ -53,14 +57,31 @@ def get_resource_list(): 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: - #If UUID exists and owns the requested resource - if uid and custom_mongo.verify_uid(rsrc["resource_id"], uid): - logger.debug("Matching owned-resource found!") - #Add resource to return list - resourceListToReturn.append({'_id': rsrc["resource_id"], '_name': rsrc["name"]}) - found_uid = True + + path = request.args.get('path') + if path: + resource = custom_mongo.get_from_mongo("reverse_match_url", str(path)) + if resource: + activity = {"User":uid,"Description":"Returning matched resource by path: "+ str({'_id': resource["resource_id"], '_name': resource["name"], '_reverse_match_url': resource["reverse_match_url"]})} + logger.info(log_handler.format_message(subcomponent="RESOURCES",action_id="HTTP",action_type=request.method,log_code=2007,activity=activity)) + if request.method == "HEAD": + return + return {'_id': resource["resource_id"], '_name': resource["name"], '_reverse_match_url': resource["reverse_match_url"]} + else: + response.status_code = 404 + response.headers["Error"] = "No user-owned resources found!" + activity = {"User":uid,"Description":"No matching resources found for user!"} + logger.info(log_handler.format_message(subcomponent="RESOURCES",action_id="HTTP",action_type=request.method,log_code=2008,activity=activity)) + return response + else: + #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): + logger.debug("Matching owned-resource found!") + #Add resource to return list + resourceListToReturn.append({'_id': rsrc["resource_id"], '_name': rsrc["name"], '_reverse_match_url': rsrc["reverse_match_url"]}) + found_uid = True #If user-owned resources were found, return the list if found_uid: @@ -86,7 +107,14 @@ def resource_creation(): head_protected = str(request.headers) headers_protected = head_protected.split() logger.debug(head_protected) - uid = oidc_client.verify_uid_headers(headers_protected, "sub") + 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 + if is_operator and "uuid" in request.get_json(): + uid = request.get_json()["uuid"] + else: + uid = oidc_client.verify_uid_headers(headers_protected, "sub") logger.debug(uid) if "NO TOKEN FOUND" in uid: response.status_code = 401 @@ -111,7 +139,19 @@ def resource_creation(): logger.info(log_handler.format_message(subcomponent="RESOURCES",action_id="HTTP",action_type=request.method,log_code=2002,activity=activity)) return response - resource_reply = create_resource(uid, request, uma_handler, response) + #Above query returns a None in case of Exception, following condition asserts False for that case + if not is_operator: + is_operator = False + + data = request.get_json() + custom_mongo = Mongo_Handler("resource_db", "resources") + + if is_operator or custom_mongo.verify_previous_uri_ownership(uid,data.get("icon_uri")): + resource_reply = create_resource(uid, request, uma_handler, response) + else: + response.status_code = 401 + response.headers["Error"] = "Operator constraint, no authorization for given UID" + return response logger.debug("Creating resource!") logger.debug(resource_reply) #If the reply is not of type Response, the creation was successful @@ -224,7 +264,7 @@ def resource_operation(resource_id): if request.method == "PUT": reply = update_resource(request, resource_id, uid, response) if reply.status_code == 200: - activity = {"User":uid,"Description":"PUT operation called","Reply":reply.text} + activity = {"User":uid,"Description":"PUT operation called","Reply":"OK"} else: activity = {"User":uid,"Description":"PUT operation called","Reply":reply.headers["Error"]} logger.info(log_handler.format_message(subcomponent="RESOURCE",action_id="HTTP",action_type=request.method,log_code=2011,activity=activity)) @@ -235,7 +275,7 @@ def resource_operation(resource_id): # reply = patch_resource(request, custom_mongo, resource_id, uid, response) reply = update_resource(request, resource_id, uid, response) if reply.status_code == 200: - activity = {"User":uid,"Description":"PATCH operation called","Reply":reply.text} + activity = {"User":uid,"Description":"PATCH operation called","Reply":"OK"} else: activity = {"User":uid,"Description":"PATCH operation called","Reply":reply.headers["Error"]} logger.info(log_handler.format_message(subcomponent="RESOURCE",action_id="HTTP",action_type=request.method,log_code=2011,activity=activity)) @@ -403,7 +443,7 @@ def get_resource(custom_mongo, resource_id, response): 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"]} + resource = {"_id": resource["resource_id"], "_name": resource["name"], "_reverse_match_url": resource["reverse_match_url"]} return resource def get_resource_head(custom_mongo, resource_id, response): diff --git a/src/config.py b/src/config.py index 23ca873..4294534 100644 --- a/src/config.py +++ b/src/config.py @@ -63,7 +63,8 @@ def get_config(config_path: str): "PEP_PDP_PORT", "PEP_PDP_POLICY_ENDPOINT", "PEP_VERIFY_SIGNATURE", - "PEP_DEFAULT_RESOURCE_PATH"] + "PEP_DEFAULT_RESOURCE_PATH", + "PEP_WORKING_MODE"] #Sets logger logger = logging.getLogger("PEP_ENGINE") diff --git a/src/config/config.json b/src/config/config.json index 7b03acb..d55a9fa 100644 --- a/src/config/config.json +++ b/src/config/config.json @@ -1 +1 @@ -{"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": "/pdp/policy/", "verify_signature": false, "default_resource_path": "./config/default-resources.json"} \ No newline at end of file +{"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": "/pdp/policy/", "verify_signature": false, "default_resource_path": "./config/default-resources.json", "working_mode": "FULL"} \ No newline at end of file diff --git a/src/handlers/mongo_handler.py b/src/handlers/mongo_handler.py index 80e22ff..50cf95e 100644 --- a/src/handlers/mongo_handler.py +++ b/src/handlers/mongo_handler.py @@ -24,6 +24,30 @@ def mongo_exists(self, mongo_key, mongo_value): else: return False + def verify_previous_uri_ownership(self, uid, url): + ''' + Recursive check of a resource existance by matching the reverse_match_uri and the subpath within of a specific UID + Return boolean result + ''' + #In case the first url is '/' + if str(url) == '/': return False + col = self.db[self.db_obj] + found = col.find_one({ "reverse_match_url": url }) + if found: + if str(found['ownership_id']) == str(uid): + return True + else: return False + else: + next_url= '/'.join(str(url).split('/')[0:-1]) + #If is not '/' + if next_url: + return self.verify_previous_uri_ownership(uid, next_url) + #In case no resource was registered before for that user and path + else: return False + + + + def get_from_mongo(self, mongo_key, mongo_value): ''' Gets an existing object from the database, or None if not found @@ -156,7 +180,7 @@ def insert_rpt_in_mongo(self, rpt: str, rpt_limit_uses: int, timestamp: str): # Check if the resource is alredy registered in the collection x=None if self.mongo_exists("rpt", rpt): - x= self.update_rpt(myres) + x= self.update_in_mongo("rpt", myres) # Add the resource since it doesn't exist on the database else: x = col.insert_one(myres) diff --git a/src/handlers/uma_handler.py b/src/handlers/uma_handler.py index ebb38c1..ac5e1f6 100644 --- a/src/handlers/uma_handler.py +++ b/src/handlers/uma_handler.py @@ -26,7 +26,7 @@ def create(self, name: str, scopes: List[str], description: str, ownership_id: s Creates a new resource IF A RESOURCE WITH THAT ICON_URI DOESN'T EXIST YET. Will throw an exception if it exists """ - + self.logger.debug("Creating resource through UMA Handler") if self.resource_exists(icon_uri): raise Exception("Resource already exists for URI "+icon_uri) diff --git a/src/main.py b/src/main.py index cf979da..5b7b88c 100644 --- a/src/main.py +++ b/src/main.py @@ -20,6 +20,7 @@ from handlers.policy_handler import policy_handler import blueprints.resources as resources import blueprints.proxy as proxy +import blueprints.authorize as authorize import os import sys import traceback @@ -60,6 +61,9 @@ #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"]) +def is_partial_mode_enabled(): + return g_config["working_mode"] == "PARTIAL" + def generateRSAKeyPair(): _rsakey = RSA.generate(2048) private_key = _rsakey.exportKey() @@ -78,8 +82,8 @@ def generateRSAKeyPair(): private_key = generateRSAKeyPair() logger.info("==========Configuration loaded==========") -proxy_app = Flask(__name__) -proxy_app.secret_key = ''.join(choice(ascii_lowercase) for i in range(30)) # Random key +ext_interface_app = Flask(__name__) +ext_interface_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 @@ -87,19 +91,24 @@ 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_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" +#Partial mode check +if is_partial_mode_enabled(): + SWAGGER_SPEC_EXT_INTERFACE = json.load(open("./static/swagger_pep_authenticate_ui.json")) +#Full mode enabled +else: + SWAGGER_SPEC_EXT_INTERFACE = json.load(open("./static/swagger_pep_proxy_ui.json")) swaggerui_proxy_blueprint = get_swaggerui_blueprint( SWAGGER_URL, API_URL, config={ # Swagger UI config overrides 'app_name': SWAGGER_APP_NAME, - 'spec': SWAGGER_SPEC_PROXY + 'spec': SWAGGER_SPEC_EXT_INTERFACE }, ) +SWAGGER_SPEC_RESOURCES = json.load(open("./static/swagger_pep_resources_ui.json")) swaggerui_resources_blueprint = get_swaggerui_blueprint( SWAGGER_URL, API_URL, @@ -111,19 +120,23 @@ def generateRSAKeyPair(): # Register api blueprints (module endpoints) 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)) +# Mode load +if is_partial_mode_enabled(): + ext_interface_app.register_blueprint(authorize.construct_blueprint(oidc_client, uma_handler, g_config, private_key)) +else: + ext_interface_app.register_blueprint(proxy.construct_blueprint(oidc_client, uma_handler, g_config, private_key)) logger.info("==========Resources endpoint Loaded==========") # SWAGGER UI respective bindings resources_app.register_blueprint(swaggerui_resources_blueprint) -proxy_app.register_blueprint(swaggerui_proxy_blueprint) +ext_interface_app.register_blueprint(swaggerui_proxy_blueprint) logger.info("==========Proxy endpoint Loaded==========") logger.info("==========Startup complete. PEP Engine is available!==========") # Define run methods for both Flask instances # Start reverse proxy for proxy endpoint -def run_proxy_app(): - proxy_app.run( +def run_ext_interface_app(): + ext_interface_app.run( debug=False, threaded=True, port=int(g_config["proxy_service_port"]), @@ -185,8 +198,8 @@ def deploy_default_resources(): if __name__ == '__main__': # Executing the Threads seperatly. - proxy_thread = threading.Thread(target=run_proxy_app) + ext_interface_thread = threading.Thread(target=run_ext_interface_app) resource_thread = threading.Thread(target=run_resources_app) - proxy_thread.start() + ext_interface_thread.start() resource_thread.start() deploy_default_resources() diff --git a/src/static/swagger_pep_authenticate_ui.json b/src/static/swagger_pep_authenticate_ui.json new file mode 100644 index 0000000..a1d7dcc --- /dev/null +++ b/src/static/swagger_pep_authenticate_ui.json @@ -0,0 +1,133 @@ +{ + "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" : "Authenticate functionality to function as an authorization API when PEP is configured in PARTIAL mode, to work in tandem with an nginx instance" + } ], + "paths" : { + "/authorize" : { + "parameters" : [ { + "in" : "header", + "name" : "X-Original-Method", + "description" : "Original HTTP method performed on the calling nginx instance", + "required" : true, + "schema" : { + "type" : "string" + } + }, { + "in" : "header", + "name" : "X-Original-Uri", + "description" : "Resource path being performed on the calling nginx instance", + "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" + } + } + }, + "patch" : { + "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" + } + } + }, + "head" : { + "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/tests/concurrencyTest.py b/tests/concurrencyTest.py new file mode 100644 index 0000000..edb2352 --- /dev/null +++ b/tests/concurrencyTest.py @@ -0,0 +1,92 @@ +import requests +from multiprocessing import Process +import urllib3 +from time import sleep +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +n_threads = 60 +token_ep = "https://demoexample.gluu.org/oxauth/restv1/token" +pep = '10.104.167.58' +resources_ep = 'http://'+pep+':5576/resources' +proxy_ep = 'http://'+pep+':5566' +client_id = "99466173-3148-4278-821b-54b3f38b0c2e" +client_secret = "908183bc-c07c-4f9f-ab4e-0ed0a8f025b5" + +#Returns JWT Token for specific user and client +def get_jwt(client_id, client_secret): + headers = { 'cache-control': "no-cache" } + data = { + "scope": "openid user_name is_operator profile permission clientinfo", + "grant_type": "password", + "username": "admin", + "password": "admin_Abcd1234#", + "client_id": client_id, + "client_secret": client_secret + } + r = requests.post(token_ep, headers=headers, data=data, verify=False) + id_token = r.json()["id_token"] + return id_token + +def uma_flow_single_resource(): + + #Requesting ticket (Expected code: 401) + ticket_response= requests.get(proxy_ep + "/res", verify = False) + ticket = ticket_response.headers["WWW-Authenticate"].split("ticket=")[1] + assert ticket_response.status_code == 401 + #Requesting RPT (Expected code: 200) + data = "claim_token_format=http://openid.net/specs/openid-connect-core-1_0.html#IDToken&claim_token="+jwt+"&ticket="+ticket+"&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Auma-ticket&client_id="+client_id+"&client_secret="+client_secret+"&scope=openid" + headers= { 'Content-Type': 'application/x-www-form-urlencoded', 'cache-control': 'no-cache' } + rpts_response = requests.post(token_ep, data=data, headers=headers, verify = False) + rpt = rpts_response.json()["access_token"] + assert rpts_response.status_code == 200 + #Accessing resource (Expected code: 500) + headers= { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + str(rpt) } + resource_response = requests.get(proxy_ep + "/res", headers=headers, verify = False) + assert resource_response.status_code == 500 + + +def uma_flow(u): + #Inserting resource (Expected code: 200/422) + data= {"icon_uri":"/res"+str(u),"name":"resource"+str(u),"scopes":["protected_access"]} + headers= { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + str(jwt) } + insertion_response= requests.post(resources_ep, json=data, headers=headers, verify = False) + assert insertion_response.status_code in [200,422] + #Requesting ticket (Expected code: 401) + ticket_response= requests.get(proxy_ep + "/res"+str(u), verify = False) + ticket = ticket_response.headers["WWW-Authenticate"].split("ticket=")[1] + assert ticket_response.status_code == 401 + #Requesting RPT (Expected code: 200) + data = "claim_token_format=http://openid.net/specs/openid-connect-core-1_0.html#IDToken&claim_token="+jwt+"&ticket="+ticket+"&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Auma-ticket&client_id="+client_id+"&client_secret="+client_secret+"&scope=openid" + headers= { 'Content-Type': 'application/x-www-form-urlencoded', 'cache-control': 'no-cache' } + rpts_response = requests.post(token_ep, data=data, headers=headers, verify = False) + rpt = rpts_response.json()["access_token"] + assert rpts_response.status_code == 200 + #Accessing resource (Expected code: 500) + headers= { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + str(rpt) } + resource_response = requests.get(proxy_ep + "/res"+str(u), headers=headers, verify = False) + assert resource_response.status_code == 500 + +jwt= get_jwt(client_id, client_secret) +n = [] +single_res = [] +#Inserting resource (Expected code: 200/422) +data= {"icon_uri":"/res","name":"resource","scopes":["protected_access"]} +headers= { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + str(jwt) } +insertion_response= requests.post(resources_ep, json=data, headers=headers, verify = False) +assert insertion_response.status_code in [200,422] +print("Setting up "+str(n_threads)+" concurrent requests") +for i in range(n_threads): + # n.append(Process(target=uma_flow,args=(i,))) + single_res.append(Process(target=uma_flow_single_resource)) +# print("Starting all threads...") +# for i in n: +# i.start() +# for i in n: +# i.join() +print("All processes have been succesfully completed") +print("Starting "+str(n_threads)+' different rpts for same resource') +for i in single_res: + i.start() +for i in single_res: + i.join() +print("Finished") diff --git a/tests/testPEPAuthorize.py b/tests/testPEPAuthorize.py new file mode 100644 index 0000000..36ff228 --- /dev/null +++ b/tests/testPEPAuthorize.py @@ -0,0 +1,153 @@ +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 PEPResourceTest(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) + + _rsajwk = RSAKey(kid="RSA1", key=import_rsa_key_from_file("../src/config/private.pem")) + # sub is inum for a user, in this case admin + _payload = { + "iss": "25611146-fee4-4454-b84b-b4b5010f516b", + "sub": "25611146-fee4-4454-b84b-b4b5010f516b", + "aud": cls.__TOKEN_ENDPOINT, + "user_name": "admin", + "jti": datetime.datetime.today().strftime('%Y%m%d%s'), + "exp": int(time.time())+3600, + "isOperator": True + } + _jws = JWS(_payload, alg="RS256") + + cls.jwt = _jws.sign_compact(keys=[_rsajwk]) + cls.scopes = '' + cls.resourceName = "TestAuthorizePEP101" + cls.PEP_HOST = "http://localhost:5566" + cls.PEP_RES_HOST = "http://localhost:5576" + + def getJWT(self): + return self.jwt + + def getJWT_RO(self): + return self.jwt_rotest + + def createTestResource(self, id_token="filler"): + payload = { "resource_scopes":[ self.scopes ], "icon_uri":"/test/"+self.resourceName, "name": self.resourceName } + headers = { 'content-type': "application/json", "cache-control": "no-cache", "Authorization": "Bearer "+str(id_token)} + res = requests.post(self.PEP_RES_HOST+"/resources", headers=headers, json=payload, verify=False) + if res.status_code == 200: + return 200, res.json()["id"] + return 500, None + + def getResourceAuthorize(self, id_token="filler"): + headers = { 'content-type': "application/json", "cache-control": "no-cache", "Authorization": "Bearer "+id_token, "X-Original-Method": "GET", "X-Original-Uri": "/test/"+self.resourceName } + res = requests.get(self.PEP_HOST+"/authorize", headers=headers, verify=False) + if res.status_code == 401: + return 401, res.headers + if res.status_code == 200: + return 200, None + if res.status_code == 404: + return 404, res.headers["Error"] + return 500, None + + def getResource(self, token="filler"): + headers = { 'content-type': "application/json", "cache-control": "no-cache", "Authorization": "Bearer "+token } + 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: + return 200, res.json() + if res.status_code == 404: + return 404, res.headers["Error"] + return 500, None + + def deleteResourceAuthorize(self, token="filler"): + headers = { 'content-type': "application/json", "cache-control": "no-cache", "Authorization": "Bearer "+token, "X-Original-Method": "DELETE", "X-Original-Uri": "/test/"+self.resourceName } + res = requests.delete(self.PEP_HOST+"/authorize", headers=headers, verify=False) + if res.status_code == 401: + return 401, res.headers["Error"] + if res.status_code == 204: + return 204, None + return 500, None + + def deleteResource(self, token="filler"): + headers = { 'content-type': "application/json", "cache-control": "no-cache", "Authorization": "Bearer "+token } + 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: + return 204, None + return 500, None + + def getRPT(self, id_token, 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": id_token, "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": 'Authenticated'} + res = requests.post(self.__TOKEN_ENDPOINT, headers=headers, data=payload, verify=False) + return res.json()["access_token"] + + #Monolithic test to avoid jumping through hoops to implement ordered tests + #This test case assumes v0.9.1 of the PEP engine + def test_resource(self): + #Use a JWT token as id_token + id_token = self.getJWT() + + #Create resource + status, self.resourceID = self.createTestResource(id_token) + self.assertEqual(status, 200) + print("Create resource: Resource created with id: "+self.resourceID) + del status + print("=======================") + print("") + + #self.resourceID = "ec46567e-f0de-4216-9a19-066ec5eaf650" + + #Get created resource authorization + status, reply = self.getResourceAuthorize(id_token) + #First attempt should return a ticket, so we test for a 401 containing it + print("Reply status: " + str(status)) + self.assertTrue("WWW-Authenticate" in reply) + print("Ticket correctly detected!") + ticket = reply["WWW-Authenticate"].split("ticket=")[1] + #Get RPT from id_token and ticket + rpt = self.getRPT(id_token, ticket) + #Repeat request, but now with the rpt + status, reply = self.getResourceAuthorize(rpt) + + self.assertEqual(status, 200) + # This means we have authorization + print("Get resource authorization: Success.") + del status + print("=======================") + print("") + + # Delete created resource + status, reply = self.deleteResource(id_token) + 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 diff --git a/tests/testPEPResources.py b/tests/testPEPResources.py index d3a1ee6..ecbdd0d 100644 --- a/tests/testPEPResources.py +++ b/tests/testPEPResources.py @@ -35,10 +35,13 @@ def setUpClass(cls): "user_name": "admin", "jti": datetime.datetime.today().strftime('%Y%m%d%s'), "exp": int(time.time())+3600, - "isOperator": False + "isOperator": True } _jws = JWS(_payload, alg="RS256") + # Needs to generate test user for this. Insert user inum instead of user name + cls.delegated_user_id = "" + _payload_ownership = { "iss": cls.g_config["client_id"], "sub": "54d10251-6cb5-4aee-8e1f-f492f1105c94", @@ -54,7 +57,7 @@ def setUpClass(cls): cls.jwt_rotest = _jws_ownership.sign_compact(keys=[_rsajwk]) #cls.scopes = 'public_access' cls.scopes = 'protected_access' - cls.resourceName = "TestResourcePEP" + cls.resourceName = "TestResourcePEP3" cls.PEP_HOST = "http://localhost:5566" cls.PEP_RES_HOST = "http://localhost:5576" @@ -79,6 +82,14 @@ 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_RES_HOST+"/resources", headers=headers, json=payload, verify=False) + if res.status_code == 200: + return 200, res.json()["id"] + return 500, None + + def createDelegatedTestResource(self, id_token="filler"): + payload = { "resource_scopes":[ self.scopes ], "icon_uri":"/"+self.resourceName, "name": self.resourceName, "uuid": self.delegated_user_id} + headers = { 'content-type': "application/json", "cache-control": "no-cache", "Authorization": "Bearer "+str(id_token) } + 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 @@ -94,6 +105,17 @@ def getResource(self, id_token="filler"): return 404, res.headers["Error"] return 500, None + def getResourceByPath(self, id_token="filler"): + headers = { 'content-type': "application/json", "cache-control": "no-cache", "Authorization": "Bearer "+id_token } + res = requests.get(self.PEP_RES_HOST+"/resources?path=/", 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_RES_HOST+"/resources/"+self.resourceID, headers=headers, verify=False) @@ -267,7 +289,16 @@ def test_resource(self): print("Access Enforcement") self.access_enforcement(id_token) print("=======================") - del status, reply, id_token + del status, reply + print("") + + #Create delegated resource onto a second user + print("Create delegated resource: Delegating resource for user: "+self.delegated_user_id) + status, self.resourceID = self.createDelegatedTestResource(id_token) + self.assertEqual(status, 200) + print("Create delegated resource: Delegated resource created with id: "+self.resourceID) + del status + print("=======================") print("") if __name__ == '__main__':