From 8938d18b6cca716a318ae2837c2c1d584d675ad4 Mon Sep 17 00:00:00 2001 From: Andrea Dell'Amico Date: Wed, 20 Apr 2022 19:23:26 +0200 Subject: [PATCH] Added the sergencovid 19 example. --- sergencovid-19/Readme.md | 14 + sergencovid-19/config.js.j2 | 25 ++ sergencovid-19/nginx_pep.conf.j2 | 3 + sergencovid-19/nginx_virtualhost.conf.j2 | 164 +++++++++++ sergencovid-19/pep.js.j2 | 360 +++++++++++++++++++++++ 5 files changed, 566 insertions(+) create mode 100644 sergencovid-19/Readme.md create mode 100644 sergencovid-19/config.js.j2 create mode 100644 sergencovid-19/nginx_pep.conf.j2 create mode 100644 sergencovid-19/nginx_virtualhost.conf.j2 create mode 100644 sergencovid-19/pep.js.j2 diff --git a/sergencovid-19/Readme.md b/sergencovid-19/Readme.md new file mode 100644 index 0000000..b40eb16 --- /dev/null +++ b/sergencovid-19/Readme.md @@ -0,0 +1,14 @@ +# PEP installation + +The nginx ansible role is + +## Installation paths of the nginx and PEP components + +### Virtualhost + +The virtualhost is installed under `/etc/nginx/sites-available/.conf`, and then symlinked under `/etc/nginx/sites-enabled/.conf`. + +### PEP files + +The PEP files are installed under `/etc/nginx/conf.d` + diff --git a/sergencovid-19/config.js.j2 b/sergencovid-19/config.js.j2 new file mode 100644 index 0000000..b504d0d --- /dev/null +++ b/sergencovid-19/config.js.j2 @@ -0,0 +1,25 @@ +export default { config, exportBackendHeaders }; +function exportBackendHeaders(context){ + exportVariable("remote_user", context.authn.verified_token.preferred_username) + return context +} +var config = { + "pep-credentials" : "{{ keycloak_auth_credentials_prod }}", + "hosts" : [ + { + "host": "{{ php_app_servername }}", + "allow-basic-auth" : true, + "audience" : '{{ servencovid_app_authz_audience_name }}', + "paths" : [ + { + "name" : "operatore", + "path" : "^/operatore/?.*$", + }, + { + "name" : "ricercatore", + "path" : "^/ricercatore/?.*$", + } + ] + } + ] +} diff --git a/sergencovid-19/nginx_pep.conf.j2 b/sergencovid-19/nginx_pep.conf.j2 new file mode 100644 index 0000000..abab250 --- /dev/null +++ b/sergencovid-19/nginx_pep.conf.j2 @@ -0,0 +1,3 @@ +js_import /etc/nginx/conf.d/pep.js; +# added to bind enforce function +js_set $authorization pep.enforce; diff --git a/sergencovid-19/nginx_virtualhost.conf.j2 b/sergencovid-19/nginx_virtualhost.conf.j2 new file mode 100644 index 0000000..260ea35 --- /dev/null +++ b/sergencovid-19/nginx_virtualhost.conf.j2 @@ -0,0 +1,164 @@ +proxy_cache_path /var/cache/nginx/pep keys_zone=token_responses:1m max_size=2m; +js_var $auth_token; +js_var $pep_credentials; +subrequest_output_buffer_size 200k; +underscores_in_headers on; +map $http_authorization $source_auth { + default ""; +} + +server { + listen 80; + server_name {{ php_app_servername }} ; + location ~ /\.(?!well-known).* { + deny all; + access_log off; + log_not_found off; + return 404; + } + + include /etc/nginx/snippets/letsencrypt-proxy.conf; + access_log /var/log/nginx/{{ php_app_servername }}_access.log; + error_log /var/log/nginx/{{ php_app_servername }}_error.log; + server_tokens off; + location / { + return 301 https://{{ php_app_servername }}$request_uri; + } +} + +server { + listen 443 ssl http2; + server_name {{ php_app_servername }} ; + access_log /var/log/nginx/{{ php_app_servername }}_ssl_access.log; + error_log /var/log/nginx/{{ php_app_servername }}_ssl_error.log; + root /var/www/html; + index index.php; + + {% if haproxy_ips is defined %} + # We are behind haproxy + {% for ip in haproxy_ips %} + set_real_ip_from {{ ip }}; + {% endfor %} + real_ip_header X-Forwarded-For; + {% endif %} + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /var/www/html/errors; + } + error_page 401 /401.html; + location = /401.html { + root /var/www/html/errors; + } + error_page 403 /403.html; + location = /403.html { + root /var/www/html/errors; + } + error_page 404 /404.html; + location = /404.html { + root /var/www/html/errors; + } + location = /favicon.ico { + log_not_found off; + access_log off; + } + location = /robots.txt { + allow all; + log_not_found off; + access_log off; + } + location ~ /\.(?!well-known).* { + deny all; + access_log off; + log_not_found off; + return 404; + } + location /sql { + deny all; + access_log off; + log_not_found off; + return 404; + } + + client_max_body_size 100M; + client_body_timeout 240s; + + include /etc/nginx/snippets/nginx-server-ssl.conf; + + server_tokens off; + + location /utente/ { + if (!-e $request_filename){ + rewrite ^(.+)$ /utente/index.php last; + } + } + + location /operatore/res/ { + root /var/www/html; + } + + location /operatore/ { + js_content pep.enforce; + if (!-e $request_filename){ + rewrite ^(.+)$ /operatore/index.php last; + } + } + + location @backend { + proxy_set_header Authorization "Bearer $auth_token"; + proxy_set_header remote-user "$remote_user"; + } + + location ~ \.php$ { + try_files $uri =404; + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass unix:/run/php/php_app.sock; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param REMOTE_ADDR $http_x_forwarded_for; + fastcgi_param HTTP_Authorization "Bearer $auth_token"; + fastcgi_param HTTP_remote-user $remote_user; + include fastcgi_params; + } + location /gcube_user_info { + internal; + gunzip on; + proxy_method GET; + proxy_http_version 1.1; + resolver 146.48.122.10; + proxy_pass https://api.d4science.org/rest/2/people/profile?gcube-token=$auth_token; + } + + location /jwt_verify_request { + internal; + gunzip on; + proxy_method POST; + proxy_http_version 1.1; + proxy_set_header Authorization $pep_credentials; + proxy_set_header Content-Type "application/x-www-form-urlencoded"; + proxy_pass https://{{ keycloak_auth_server }}/auth/realms/d4science/protocol/openid-connect/token/introspect; + proxy_cache token_responses; # Enable caching + proxy_cache_key $source_auth; # Cache for each source authentication + proxy_cache_lock on; # Duplicate tokens must wait + proxy_cache_valid 200 10s; # How long to use each response + proxy_ignore_headers Cache-Control Expires Set-Cookie; + } + location /jwt_request { + internal; + gunzip on; + proxy_method POST; + proxy_http_version 1.1; + proxy_set_header Authorization $pep_credentials; + proxy_set_header Content-Type "application/x-www-form-urlencoded"; + proxy_pass https://{{ keycloak_auth_server }}/auth/realms/d4science/protocol/openid-connect/token; + } + location /permission_request { + internal; + gunzip on; + proxy_method POST; + proxy_http_version 1.1; + proxy_set_header Content-Type "application/x-www-form-urlencoded"; + proxy_set_header Authorization "Bearer $auth_token"; + proxy_pass https://{{ keycloak_auth_server }}/auth/realms/d4science/protocol/openid-connect/token; + } +} diff --git a/sergencovid-19/pep.js.j2 b/sergencovid-19/pep.js.j2 new file mode 100644 index 0000000..392d6c5 --- /dev/null +++ b/sergencovid-19/pep.js.j2 @@ -0,0 +1,360 @@ +export default { enforce }; + +import defaultExport from '/etc/nginx/conf.d/config.js'; + +function log(c, s){ + c.request.error(s) +} + +function enforce(r) { + + var context = { + request: r , + config : defaultExport["config"], + backend : (defaultExport.backend ? defaultExport.backend : "@backend"), + export_backend_headers : (defaultExport.backendHeaders ? defaultExport.backendHeaders : wkf.export_backend_headers) + } + +{% if nginx_pep_debug_enabled %} +log(context, "Inside NJS enforce for " + r.method + " @ " + r.headersIn.host + "/" + r.uri) +{% endif %} + + if(context.request.args.token){ + var token = context.request.args.token +{% if nginx_pep_debug_enabled %} + log(context, "token is " + token) +{% endif %} + exportVariable(context, "auth_token", token) + context.request.subrequest("/gcube_user_info") + .then(reply=>{ + if (reply.status === 200) { + var response = JSON.parse(reply.responseBody); +{% if nginx_pep_debug_enabled %} + log(context, "got response " + reply.responseBody) +{% endif %} + return response + } else { + log(context, reply.status + " got response " + reply.responseBody) + throw new Error("Unauthorized") + } + }).then(userinfo => { + exportVariable(context, "remote_user", userinfo.result.username) +{% if nginx_pep_debug_enabled %} + log(context, "username is " + userinfo.result.username) +{% endif %} + context.request.internalRedirect(context.backend) +{% if nginx_pep_debug_enabled %} + log(context, "context after setting the username:" + njs.dump(context)) +{% endif %} + return context + }).catch(e => { context.request.error("error .... " + njs.dump(e)); context.request.return(401)} ) + return + } + + context = computeProtection(context) + + wkf.run(wkf.build(context), context) +} + +// ######## WORKFLOW FUNCTIONS ############### +var wkf = { + + build : (context)=>{ + var actions = [ + "export_pep_credentials", + "parse_authentication", + "check_authentication", + "export_authn_token", + "pip", + "pdp", + "export_backend_headers", + "pass" + ] + return actions + }, + + run : (actions, context) => { + context.request.error("Starting workflow with " + njs.dump(actions)) + var w = actions.reduce( + (acc, f) => acc.then(typeof(f) === "function" ? f : wkf[f]), + Promise.resolve().then(()=>context) + ) + w.catch(e => { context.request.error(njs.dump(e)); context.request.return(401)} ) + }, + + export_pep_credentials : exportPepCredentials, + export_authn_token : exportAuthToken, + export_backend_headers : c=>c, + parse_authentication : parseAuthentication, + check_authentication : checkAuthentication, + verify_token : verifyToken, + request_token : requestToken, + pip : pipExecutor, + pdp : pdpExecutor, + pass : pass, + + //PIP utilities + "get-path-component" : (c, i) => c.request.uri.split("/")[i], + "get-token-field" : getTokenField, + "get-contexts" : (c) => { + var ra = c.authn.verified_token["resource_access"] + if(ra){ + var out = []; + for(var k in ra){ + if(ra[k].roles && ra[k].roles.length !== 0) out.push(k) + } + } + return out; + } +} + +function getTokenField(context, f){ + return context.authn.verified_token[f] +} + +function exportVariable(context, name, value){ + context.request.variables[name] = value +{% if nginx_pep_debug_enabled %} + log(context, "Exported variables:" + njs.dump(context.request.variables)) +{% endif %} + return context +} + +function exportPepCredentials(context){ + if(!context.config["pep-credentials"]){ + throw new Error("Need PEP credentials") + } + return exportVariable(context, "pep_credentials", "Basic " + context.config["pep-credentials"]) +} + +function exportAuthToken(context){ + return exportVariable(context, "auth_token", context.authn.token) +} + +function checkAuthentication(context){ + return context.authn.type === "bearer" ? wkf.verify_token(context) : wkf.request_token(context) +} + +function parseAuthentication(context){ +{% if nginx_pep_debug_enabled %} + context.request.log("Inside parseAuthentication") +{% endif %} + var incomingauth = context.request.headersIn["Authorization"] + + if(!incomingauth) throw new Error("Authentication required"); + + var arr = incomingauth.trim().replace(/\s\s+/g, " ").split(" ") + if(arr.length != 2) throw new Error("Unknown authentication scheme"); + + var type = arr[0].toLowerCase() + if(type === "basic" && context.authz.host && context.authz.host["allow-basic-auth"]){ + var unamepass = Buffer.from(arr[1], 'base64').toString().split(":") + if(unamepass.length != 2) return null; + context.authn = { type : type, raw : arr[1], user : unamepass[0], password : unamepass[1]} + return context + }else if(type === "bearer"){ + context.authn = { type : type, raw : arr[1], token : arr[1]} + return context + } + throw new Error("Unknown authentication scheme"); +} + +function verifyToken(context){ +{% if nginx_pep_debug_enabled %} + log(context, "Inside verifyToken") +{% endif %} + var options = { + "body" : "token=" + context.authn.token + "&token_type_hint=access_token" + } + return context.request.subrequest("/jwt_verify_request", options) + .then(reply=>{ + if (reply.status === 200) { +{% if nginx_pep_debug_enabled %} + log(context, reply.responseBody) +{% endif %} + var response = JSON.parse(reply.responseBody); + if (response.active === true) { + return response + } else { + throw new Error("Unauthorized") + } + } else { + throw new Error("Unauthorized") + } + }).then(verified_token => { + context.authn.verified_token = + JSON.parse(Buffer.from(context.authn.token.split('.')[1], 'base64url').toString()) + return context + }) +} + +function requestToken(context){ +{% if nginx_pep_debug_enabled %} + log(context, "Inside requestToken") +{% endif %} + var options = { + "body" : "grant_type=password&username="+context.authn.user+"&password="+context.authn.password + } + return context.request.subrequest("/jwt_request", options) + .then(reply=>{ + if (reply.status === 200) { + var response = JSON.parse(reply.responseBody); + context.authn.token = response.access_token + context.authn.verified_token = + JSON.parse(Buffer.from(context.authn.token.split('.')[1], 'base64url').toString()) + return context + } else { + throw new Error("Unauthorized") + } + }) +} + +function pipExecutor(context){ +{% if nginx_pep_debug_enabled %} + log(context, "Inside extra claims PIP") +{% endif %} + context.authz.pip.forEach(extra =>{ + //call extra claim pip function + try{ + var operator = extra.operator + var result = wkf[operator](context, extra.args) + //ensure array and add to extra_claims + if(!(result instanceof Array)) result = [result] + if(!context.extra_claims) context.extra_claims = {}; + context.extra_claims[extra.claim] = result + } catch (error){ +{% if nginx_pep_debug_enabled %} + log(context, "Skipping invalid extra claim " + njs.dump(error)) +{% endif %} + } + }) +{% if nginx_pep_debug_enabled %} + log(context, "Extra claims are " + njs.dump(context.extra_claims)) +{% endif %} + return context +} + +function pdpExecutor(context){ +{% if nginx_pep_debug_enabled %} + log(context, "Inside PDP") +{% endif %} + return context.authz.pdp(context) +} + +function umaCall(context){ +{% if nginx_pep_debug_enabled %} + log(context, "Inside UMA call") +{% endif %} + var options = { "body" : computePermissionRequestBody(context) }; + return context.request.subrequest("/permission_request", options) + .then(reply =>{ + if(reply.status === 200){ + return context + }else{ + throw new Error("Response for authorization request is not ok " + reply.status + " " + njs.dump(reply.responseBody)) + } + }) +} + +function pass(context){ +{% if nginx_pep_debug_enabled %} + log(context, "Inside pass"); +{% endif %} + if(typeof(context.backend) === "string") context.request.internalRedirect(context.backend); + else if (typeof(context.backend) === "function") context.request.internalRedirect(context.backend(context)) + return context; +} + +// ######## AUTHORIZATION PART ############### +function computePermissionRequestBody(context){ + + if(!context.authz.host || !context.authz.path ){ + throw new Error("Enforcemnt mode is always enforcing. Host or path not found...") + } + + var audience = computeAudience(context) + var grant = "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket" + var mode = "response_mode=decision" + var permissions = computePermissions(context) + var extra = "" + if(context.extra_claims){ + extra = + "claim_token_format=urn:ietf:params:oauth:token-type:jwt&claim_token=" + + JSON.stringify(context.extra_claims).toString("base64url") + } + var body = audience + "&" + grant + "&" + permissions + "&" + mode + "&" + extra + context.request.error("Computed permission request body is " + body) + return body +} + +function computeAudience(context){ + var aud = context.request.headersIn.host + if(context.authz.host){ + aud = context.authz.host.audience||context.authz.host.host + } + return "audience=" + aud +} + +function computePermissions(context){ + var resource = context.request.uri + if(context.authz.path){ + resource = context.authz.path.name||context.authz.path.path + } + var scopes = [] + if(context.authz.method && context.authz.method.scopes){ + scopes = context.authz.method.scopes + } + if(scopes.length > 0){ + return scopes.map(s=>"permission=" + resource + "#" + s).join("&") + } + return "permission=" + resource +} + +function getPath(hostconfig, incomingpath, incomingmethod){ + var paths = hostconfig.paths || [] + var matchingpaths = paths + .filter(p => {return incomingpath.match(p.path) != null}) + .reduce((acc, p) => { + if (!p.methods || p.methods.length === 0) acc.weak.push({ path: p}); + else{ + var matchingmethods = p.methods.filter(m=>m.method.toUpperCase() === incomingmethod) + if(matchingmethods.length > 0) acc.strong.push({ method : matchingmethods[0], path: p}); + } + return acc; + }, { strong: [], weak: []}) + return matchingpaths.strong.concat(matchingpaths.weak)[0] +} + +function getHost(config, host){ + var matching = config.hosts.filter(h=>{ + return h.host === host + }) + return matching.length > 0 ? matching[0] : null +} + +function computeProtection(context){ +{% if nginx_pep_debug_enabled %} + log(context, "Getting by host " + context.request.headersIn.host) +{% endif %} + context.authz = {} + context.authz.host = getHost(context.config, context.request.headersIn.host) + if(context.authz.host !== null){ + context.authz.pip = context.authz.host.pip ? context.authz.host.pip : []; + context.authz.pdp = context.authz.host.pdp ? context.authz.host.pdp : umaCall; + var pathandmethod = getPath(context.authz.host, context.request.uri, context.request.method); + if(pathandmethod){ + context.authz.path = pathandmethod.path; + context.authz.pip = context.authz.path.pip ? context.authz.pip.concat(context.authz.path.pip) : context.authz.pip; + context.authz.pdp = context.authz.path.pdp ? context.authz.path.pdp : context.authz.pdp; + context.authz.method = pathandmethod.method; + if(context.authz.method){ + context.authz.pip = context.authz.method.pip ? context.authz.pip.concat(context.authz.method.pip) : context.authz.pip; + context.authz.pdp = context.authz.method.pdp ? context.authz.method.pdp : context.authz.pdp; + } + } + } +{% if nginx_pep_debug_enabled %} + log(context, "Leaving protection computation: ") +{% endif %} + return context +}