diff --git a/shinyproxy/ansible_vars/nginx.yml b/shinyproxy/ansible_vars/nginx.yml index a90b160..25de34a 100644 --- a/shinyproxy/ansible_vars/nginx.yml +++ b/shinyproxy/ansible_vars/nginx.yml @@ -1,45 +1,71 @@ --- -nginx_set_frame_origin: True +nginx_use_nginx_org_repo: true +nginx_org_modules: + - pkg_name: nginx-module-njs + mod_name: ngx_http_js_module.so + enabled: true +nginx_set_frame_origin: true +nginx_set_content_security_options: true +nginx_disable_content_security_options: true nginx_x_frame_options: '' +nginx_pep_debug_enabled: true +nginx_conf_remote_snippets: + - url: 'https://code-repo.d4science.org/gCubeSystem/d4s-nginx-pep-examples/raw/branch/master/shinyproxy/pep-gcube.js.j2' + name: pep-gcube.js + - url: 'https://code-repo.d4science.org/gCubeSystem/d4s-nginx-pep-examples/raw/branch/master/shinyproxy/nginx_pep_gcube.conf.j2' + name: nginx_pep_gcube.conf nginx_virthosts: - - virthost_name: 'shinyproxy-test.d4science.org' - server_name: 'shinyproxy-test.d4science.org' + - virthost_name: 'shinyproxy.garr.d4science.org' + server_name: 'shinyproxy.garr.d4science.org' + serveraliases: 'shinyproxy-sdg.d4science.org' + plain_http_enabled: false upstream_backends: - - name: shinyproxy_test + - name: shinyproxy_sdg servers: - '{{ docker_garr_swarm1_ip }}:8080' - '{{ docker_garr_swarm2_ip }}:8080' - '{{ docker_garr_swarm3_ip }}:8080' - ssl_enabled: True - ssl_only: True + ssl_enabled: true + ssl_only: true ssl_letsencrypt_certs: '{{ nginx_letsencrypt_managed }}' root: '{{ nginx_webroot }}' server_tokens: 'off' - proxy_standard_setup: True - include_global_proxy_conf: True - websockets: True + additional_options: + - 'include /etc/nginx/snippets/nginx_pep_gcube.conf;' + proxy_standard_setup: true + include_global_proxy_conf: true + websockets: true locations: - - location: / + - location: '~ /app/' other_opts: + - 'subrequest_output_buffer_size 128k' - 'js_content pep.enforce' - - location: /jwt_verify_request - target: 'https://{{ keycloak_auth_server }}/auth/realms/d4science/protocol/openid-connect/token/introspect' + - location: '~ /[^_].+' + target: 'http://shinyproxy_sdg$request_uri' + other_opts: + - 'subrequest_output_buffer_size 128k' + - location: /_jwt_verify_request + target: '{{ keycloak_auth_server }}/auth/realms/d4science/protocol/openid-connect/token/introspect' other_opts: - 'internal' - 'proxy_method POST' - 'gunzip on' - - 'proxy_set_header Authorization {{ keycloak_auth_credentials_prod }}' + - 'proxy_set_header Authorization "{{ keycloak_auth_credentials_prod }}"' - 'proxy_set_header Content-Type "application/x-www-form-urlencoded"' - - 'proxy_cache' + - 'proxy_cache social_cache' - 'proxy_cache_key $source_auth' - 'proxy_cache_lock on' - 'proxy_cache_valid 200 10s' - 'proxy_ignore_headers Cache-Control Expires Set-Cookie' - location: /_backend + other_opts: - 'internal' + - 'subrequest_output_buffer_size 128k' - 'resolver 146.48.122.10' + - 'proxy_set_header Host $host' + - 'proxy_set_header X-Forwarded-Proto "https"' - 'proxy_set_header Authorization "$auth_token"' - target: 'http://shinyproxy_test$request_uri' + target: 'http://shinyproxy_sdg$request_uri' - location: /_accounting target: 'https://accounting-service.d4science.org/accounting-service/record' other_opts: @@ -47,3 +73,19 @@ nginx_virthosts: - 'proxy_method POST' - 'proxy_set_header Authorization "$auth_token"' - 'proxy_set_header Content-Type "application/json"' + - location: /_homeserv + target: 'https://192.168.100.54/' + other_opts: + - 'internal' + - 'proxy_method POST' + - 'proxy_set_header Content-Type "application/x-www-form-urlencoded"' + - 'proxy_ssl_verify off' + - location: /_gcube_user_info + target: 'https://api.d4science.org/rest/2/people/profile' + other_opts: + - 'internal' + - 'proxy_method GET' + - 'gunzip on' + - 'proxy_set_header gcube-token "$auth_token"' + - 'proxy_cache social_cache' + - 'proxy_cache_key $auth_token' diff --git a/shinyproxy/nginx_pep.conf.j2 b/shinyproxy/nginx_pep.conf.j2 index 3fec03a..91e008b 100644 --- a/shinyproxy/nginx_pep.conf.j2 +++ b/shinyproxy/nginx_pep.conf.j2 @@ -9,6 +9,6 @@ map $http_authorization $source_auth { default ""; } -js_import /etc/nginx/conf.d/pep.js; +js_import /etc/nginx/snippets/pep.js; # added to bind enforce function js_set $authorization pep.enforce; diff --git a/shinyproxy/nginx_pep_gcube.conf.j2 b/shinyproxy/nginx_pep_gcube.conf.j2 new file mode 100644 index 0000000..5995ba4 --- /dev/null +++ b/shinyproxy/nginx_pep_gcube.conf.j2 @@ -0,0 +1,14 @@ +# variables computed by njs and which may possibly be passed among locations +js_var $auth_token; +js_var $account_record; + +proxy_cache_path /tmp levels=1:2 keys_zone=social_cache:10m max_size=10g inactive=60m use_temp_path=off; + +underscores_in_headers on; +map $http_authorization $source_auth { + default ""; +} + +js_import /etc/nginx/snippets/pep-gcube.js; +# added to bind enforce function +js_set $authorization pep.enforce; diff --git a/shinyproxy/pep-gcube.js.j2 b/shinyproxy/pep-gcube.js.j2 new file mode 100644 index 0000000..6c56815 --- /dev/null +++ b/shinyproxy/pep-gcube.js.j2 @@ -0,0 +1,165 @@ +export default { enforce }; + +function log(c, s){ + c.request.error(s) +} + +function debug(c, s){ + return {{ nginx_pep_debug_enabled }} ? c.request.error(s) : null +} + +function enforce(r) { + + var context = { + request: r + } + + log(context, "Inside NJS enforce for " + r.method + " @ " + r.headersIn.host + "/" + r.uri) + context.authn = {} + context.authn.token = getGCubeToken(context) + if(context.authn.token != null){ + debug(context, "[PEP] token is " + context.authn.token) + exportVariable(context, "auth_token", context.authn.token) + verifyGCubeToken(context) + .then(ctx=>{ + debug(context, "[PEP] Token is valid:" + njs.dump(context.userinfo)) + if(!checkAudience(context, context.userinfo.context)){ + throw new Error("Unauthorized context") + } + return Promise.resolve(context) + }).then(ctx => { + debug(context, "[HOMESERV] creating home for username " + context.userinfo.username) + return context.request.subrequest("/_homeserv", {"body" : "user=" + context.userinfo.username }) + }).then(reply => { + if(reply.status !== 200){ + throw new Error("Unauthorized: Unable to create home for user") + } + context.record = buildAccountingRecord(context) + return context.request.subrequest("/_backend", { method : context.request.method, args : context.request.args, headers : context.request.headersIn}) + }).then(reply=>{ + debug(context, "[SHINYPROXY] response status: " + reply.status) + copyHeaders(context, reply.headersOut, r.headersOut) + closeAccountingRecord(context.record, (reply.status === 200 || reply.status === 201 || reply.status === 204 || reply.status === 302)) + context.request.subrequest("/_accounting", { detached : true, body : JSON.stringify(context.record) }) + r.return(reply.status, reply.responseBody) + }).catch(e => { log(context, "Error .... " + njs.dump(e)); context.request.return(e.message === "Unauthorized" ? 403 : 500)} ) + + return + } + + r.return(401, "Authorization required") +} + +function copyHeaders(context, hin, hout){ + for (var h in hin) { + hout[h] = hin[h]; + } +} + +function getBearerToken(context){ + if (context.request.headersIn['Authorization']){ + return context.request.headersIn['Authorization'].split(/Bearer\s+/)[1]; + } + return null; +} + +function getGCubeToken(context){ + if(context.request.args["gcube-token"]){ + return context.request.args["gcube-token"]; + }else if (context.request.headersIn['gcube-token']){ + return context.request.headersIn['gcube-token']; + } + return null; +} + +function checkAudience(context, aud){ + debug(context, "Audience to verify is " + aud) + return true +} + +function buildAccountingRecord(context){ + const t = (new Date()).getTime() + return { + "recordType": "ServiceUsageRecord", + "operationCount": 1, + "creationTime": t, + "callerHost": context.request.remoteAddress, + "serviceClass": "ShinyApp", + "callerQualifier": "TOKEN", + "consumerId": context.userinfo.username, + "aggregated": true, + "serviceName": context.request.uri.split("app/")[1], + "duration": 0, + "maxInvocationTime": 0, + "scope": context.userinfo.context, + "host": context.request.host, + "startTime": t, + "id": uuid(), + "calledMethod": context.request.method + " " + context.request.uri, + "endTime": 0, + "minInvocationTime": 0, + "operationResult": null + } +} + +function closeAccountingRecord(record, success){ + const t = (new Date()).getTime() + record.duration = t - record.startTime + record.endTime = t + record.minInvocationTime = record.duration + record.operationResult = success ? "SUCCESS" : "FAILED"; +} + +function uuid() { + return 'xxxxxxxx-xxxx-4xxx-8xxx-xxxxxxxxxxxx'.replace(/[x]/g, function (c) { + const r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); +} + +function exportVariable(context, name, value){ + context.request.variables[name] = value + return context +} + +function verifyiBearerToken(context){ + debug(context, "Inside verifyToken") + 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) { + debug(context, reply.responseBody) + 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 verifyGCubeToken(context){ + return context.request.subrequest("/_gcube_user_info") + .then(reply=>{ + if (reply.status === 200) { + debug(context, "[Social Service] got response " + reply.responseBody) + var response = JSON.parse(reply.responseBody); + return response + } else { + debug(context, "[Social Service] failed " + reply.status + ":" + reply.responseBody) + throw new Error("Unauthorized") + } + }).then(response => { + context.userinfo = response.result + return context + }) +} diff --git a/shinyproxy/pep.js.j2 b/shinyproxy/pep.js.j2 index a71c8db..f595651 100644 --- a/shinyproxy/pep.js.j2 +++ b/shinyproxy/pep.js.j2 @@ -114,7 +114,7 @@ function verifyToken(context){ var options = { "body" : "token=" + context.authn.token + "&token_type_hint=access_token" } - return context.request.subrequest("/jwt_verify_request", options) + return context.request.subrequest("/_jwt_verify_request", options) .then(reply=>{ if (reply.status === 200) { debug(context, reply.responseBody)