diff --git a/nginx/auth.js b/nginx/auth.js index d91b591388a96b999faa9b82aa2b1e0be9b5aac4..905db872e655199d2be2a45411c66a56335e2424 100644 --- a/nginx/auth.js +++ b/nginx/auth.js @@ -1,86 +1,146 @@ -async function authenticate_and_authorize(r) { - try { - const required_params = { - path: r.variables["request_uri"], - method: r.variables["request_method"], - server_name: r.variables["ssl_server_name"], +const InvalidJSONError = JSON.stringify({ + status: 400, + title: "Invalid JSON", +}); + +function check_body_size(r) { + // Check if the body has been buffered to a temp file. + // If this is the case, warn the admin to configure the client_body_buffer_size + // to be equal to client_max_body_size + // https://nginx.org/en/docs/http/ngx_http_core_module.html#client_body_buffer_size + if (r.variables["request_body_file"]) { + // maybe return 500 + return { + error: + "Request Entity Too Large. Please set 'client_body_buffer_size' to be equal to 'client_max_body_size' in the NGINX configuration file.", + status: 413, + message: "Request Entity Too Large", }; + } else { + return {}; + } +} - let opa_input = required_params; - if(r.requestBuffer){ - opa_input = Object.assign( - JSON.parse(r.requestBuffer), - required_params - ); - } +async function token_is_valid(r, token) { + const response = await r.subrequest("/_opa/v1/data/authn", { + method: "POST", + body: JSON.stringify({ input: { access_token: token } }), + }); + const body = JSON.parse(response.responseText); + return body.result.jwt_is_valid; +} - const authz_header = r.headersIn["Authorization"]; - - let is_bearer_authn = false; - if (authz_header && authz_header.startsWith("Bearer")) { - const jwt = authz_header.substring(7); - opa_input["access_token"] = jwt; - is_bearer_authn = true; - } else if (r.variables["voms_fqans"]) { - opa_input["voms_fqan"] = r.variables["voms_fqans"].split(","); - } else if (r.variables["ssl_client_s_dn"]) { - opa_input["ssl_client_s_dn"] = r.variables["ssl_client_s_dn"]; +async function process_authn(r) { + const authz_header = r.headersIn["Authorization"]; + if (authz_header && authz_header.startsWith("Bearer")) { + const jwt = authz_header.substring(7); + const valid = await token_is_valid(r, jwt); + if (valid) { + return { access_token: jwt }; } else { - r.return(401, "no usable credentials presented"); - return; + return { error: "Invalid access token", status: 401 }; } + } else if (r.variables["voms_fqans"]) { + return { voms_fqan: r.variables["voms_fqans"].split(",") }; + } else if (r.variables["ssl_client_s_dn"]) { + return { ssl_client_s_dn: r.variables["ssl_client_s_dn"] }; + } + return { error: "No usable credentials presented", status: 401 }; +} - const opts = { - method: "POST", - body: JSON.stringify({ input: opa_input }), - }; +function is_body_valid(body) { + const paths_are_valid = () => { + const paths = body.paths; + if (!paths || !paths instanceof Array) { + return false; + } + return paths.every((path) => typeof path === "string"); + }; - if(is_bearer_authn){ - const opa_res = await r.subrequest("/_opa/v1/data/authn", opts); - r.log(`OPA Auth responded with status ${opa_res.status}`); - const body = JSON.parse(opa_res.responseText); - - const request_method = r.variables["request_method"] - switch(request_method){ - case "GET": - case "DELETE": - case "HEAD": - if(!body.result.jwt_is_valid){ - r.return(401); - return; - } - break; - default: - if(!body.result.jwt_is_valid_and_trusted){ - r.return(401); - return; - } - } + const files_are_valid = () => { + const files = body.files; + if (!files || !files instanceof Array) { + return false; } + return files.every((file) => typeof file.path === "string"); + }; - const opa_res = await r.subrequest("/_opa/v1/data/rules/allowed", opts); - r.log(`OPA Rules responded with status ${opa_res.status}`); - const body = JSON.parse(opa_res.responseText); - if (!body.result) { - r.return(403); + return paths_are_valid(body) || files_are_valid(body); +} + +async function process_authz(r, authn_response, request_body) { + const opa_input = { + path: r.variables["request_uri"], + method: r.variables["request_method"], + }; + Object.assign(opa_input, authn_response, request_body); + + const opa_res = await r.subrequest("/_opa/v1/data/rules", { + method: "POST", + body: JSON.stringify({ input: opa_input }), + }); + + r.log(`OPA AuthZ responded with status ${opa_res.status}`); + + const body = JSON.parse(opa_res.responseText); + if (body && body.result.allowed) { + return { status: 200 }; + } + + return { error: "Forbidden", status: 403 }; +} + +async function process_storm(r) { + const url = "/_storm-tape" + r.variables["request_uri"].replace(/\/$/, ""); + const method = r.variables["request_method"]; + const res = await r.subrequest(url, { method }); + r.log(`StoRM Tape responded with status ${res.status}`); + return res; +} + +async function process(r) { + try { + const resp = check_body_size(r); + if (resp.error) { + r.log(resp.error); + r.return(resp.status, resp.message); + } + // AuthN + const authn = await process_authn(r); + if (authn.error) { + r.log(authn.error); + r.return(authn.status); return; } - const storm_res = await r.subrequest( - "/_storm-tape" + r.variables["request_uri"].replace(/\/$/, ""), - { - method: r.variables["request_method"], - body: r.requestBuffer, - } - ); + // Validate body + const request_body = r.requestText ? JSON.parse(r.requestText) : null; + if (request_body && !is_body_valid(request_body)) { + r.headersOut["Content-Type"] = "application/json+problem"; + r.log("Invalid JSON"); + r.return(400, InvalidJSONError); + return; + } + + // AuthZ + const authz = await process_authz(r, authn, request_body); + if (authz.error) { + r.log(authz.error); + r.return(403); + return; + } - r.log(`StoRM Tape Responded with status ${storm_res.status}`); + // Sent request to StoRM Tape + const storm_res = await process_storm(r); r.headersOut["Location"] = storm_res.headersOut["Location"]; - r.return(storm_res.status, storm_res.responseText); + if (storm_res.status) { + r.return(storm_res.status, storm_res.responseText); + return; + } } catch (err) { r.log(err); - r.return(500); + r.return(403); // or 500? } } -export default { authenticate_and_authorize }; +export default { process }; diff --git a/nginx/storm.conf b/nginx/storm.conf index 2716e3c2ab19d8213cb3f8491c64199826f1f874..7051f694c3fa49425c5b2e62dd32e29cbe16806f 100644 --- a/nginx/storm.conf +++ b/nginx/storm.conf @@ -18,6 +18,7 @@ server { ssl_verify_depth 100; client_max_body_size 0; + client_body_buffer_size 1m; location /api/v1 { @@ -39,7 +40,8 @@ server { proxy_set_header X-Request-Id $request_id; proxy_pass_request_body on; - js_content auth.authenticate_and_authorize; + js_content auth.process; + } location /_storm-tape/ {