From b2b3d8f62e275942eee36d10ca122f71196a6ef4 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Mon, 30 Dec 2024 17:05:44 -0800 Subject: [PATCH 1/3] update rego to v1 --- Dockerfile | 2 +- permissions_engine/authz.rego | 44 ++++----- permissions_engine/calculate.rego | 141 ++++++++++++++-------------- permissions_engine/idp.rego | 67 ++++++------- permissions_engine/mask.rego | 2 +- permissions_engine/permissions.rego | 105 +++++++++++---------- permissions_engine/service.rego | 9 +- permissions_engine/vault.rego | 31 +++--- 8 files changed, 206 insertions(+), 195 deletions(-) diff --git a/Dockerfile b/Dockerfile index 90a1ba9..4d3b91d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,7 +28,7 @@ USER candig WORKDIR /app/ -RUN curl -L -o opa https://openpolicyagent.org/downloads/v0.63.0/opa_linux_amd64_static +RUN curl -L -o opa https://openpolicyagent.org/downloads/v1.0.0/opa_linux_amd64_static RUN chmod 755 ./opa diff --git a/permissions_engine/authz.rego b/permissions_engine/authz.rego index 216be92..613874e 100644 --- a/permissions_engine/authz.rego +++ b/permissions_engine/authz.rego @@ -1,46 +1,48 @@ package system.authz +import rego.v1 + # this defines authentication to have access to opa at all # from: https://www.openpolicyagent.org/docs/v0.22.0/security/#token-based-authentication-example # Reject requests by default -default allow = false +default allow := false # Site admin should be able to see anything -allow { - data.permissions.site_admin == true +allow if { + data.permissions.site_admin == true } # Any service should be able to verify that a service is who it says it is: -allow { - input.path == ["v1", "data", "service", "verified"] - input.method == "POST" +allow if { + input.path == ["v1", "data", "service", "verified"] + input.method == "POST" } # Opa should be able to store its vault token -allow { - input.path == ["v1", "data", "store_token"] - input.method == "PUT" - input.headers["X-Opa"][_] == data.opa_secret +allow if { + input.path == ["v1", "data", "store_token"] + input.method == "PUT" + input.headers["X-Opa"][_] == data.opa_secret } # Service-info path for healthcheck -allow { - input.path == ["v1", "data", "service", "service-info"] - input.method == "GET" +allow if { + input.path == ["v1", "data", "service", "service-info"] + input.method == "GET" } # The authx library uses these paths: -authx_paths = { - "permissions": ["v1", "data", "permissions"], - "user_id": ["v1", "data", "idp", "user_key"] +authx_paths := { + "permissions": ["v1", "data", "permissions"], + "user_id": ["v1", "data", "idp", "user_key"], } # An authorized user has a valid token (and passes in that same token for both bearer and body) # Authz users can access the authx paths -allow { - input.path == authx_paths[_] - input.method == "POST" - data.permissions.valid_token == true - input.body.input.token == input.identity +allow if { + input.path == authx_paths[_] + input.method == "POST" + data.permissions.valid_token == true + input.body.input.token == input.identity } diff --git a/permissions_engine/calculate.rego b/permissions_engine/calculate.rego index a87b109..59fb90b 100644 --- a/permissions_engine/calculate.rego +++ b/permissions_engine/calculate.rego @@ -1,4 +1,5 @@ package calculate + # # This is the set of policy definitions for the permissions engine. # @@ -13,18 +14,20 @@ package calculate # } # import data.idp.user_key as user_key -import future.keywords.in + +import rego.v1 # # This user is a site admin if they have the site_admin role # import data.vault.site_roles as site_roles -site_admin = true { - user_key in site_roles.admin + +site_admin if { + user_key in site_roles.admin } -site_curator = true { - user_key in site_roles.curator +site_curator if { + user_key in site_roles.curator } # @@ -36,113 +39,107 @@ import data.vault.program_auths as program_auths import data.vault.user_programs as user_programs # compile list of programs specifically authorized for the user by DACs and within the authorized time period -user_readable_programs[p["program_id"]] := output { - some p in user_programs - time.parse_ns("2006-01-02", p["start_date"]) <= time.now_ns() - time.parse_ns("2006-01-02", p["end_date"]) >= time.now_ns() - output := p +user_readable_programs[p.program_id] := output if { + some p in user_programs + time.parse_ns("2006-01-02", p.start_date) <= time.now_ns() + time.parse_ns("2006-01-02", p.end_date) >= time.now_ns() + output := p } # compile list of programs that list the user as a team member -team_readable_programs[p] := output { - some p in all_programs - user_key in program_auths[p].team_members - output := program_auths[p].team_members +team_readable_programs[p] := output if { + some p in all_programs + user_key in program_auths[p].team_members + output := program_auths[p].team_members } # user can read programs that are either team-readable or user-readable readable_programs := object.keys(object.union(team_readable_programs, user_readable_programs)) # user can curate programs that list the user as a program curator -curateable_programs[p] { - some p in all_programs - user_key in program_auths[p].program_curators +curateable_programs[p] if { + some p in all_programs + user_key in program_auths[p].program_curators } import data.vault.paths as paths # debugging -readable_get[p] := output { - some p in paths.read.get - output := regex.match(p, input.body.path) +readable_get[p] := output if { + some p in paths.read.get + output := regex.match(p, input.body.path) } -readable_post[p] := output { - some p in paths.read.post - output := regex.match(p, input.body.path) + +readable_post[p] := output if { + some p in paths.read.post + output := regex.match(p, input.body.path) } -curateable_get[p] := output { - some p in paths.curate.get - output := regex.match(p, input.body.path) + +curateable_get[p] := output if { + some p in paths.curate.get + output := regex.match(p, input.body.path) } -curateable_post[p] := output { - some p in paths.curate.post - output := regex.match(p, input.body.path) + +curateable_post[p] := output if { + some p in paths.curate.post + output := regex.match(p, input.body.path) } -curateable_delete[p] := output { - some p in paths.curate.delete - output := regex.match(p, input.body.path) + +curateable_delete[p] := output if { + some p in paths.curate.delete + output := regex.match(p, input.body.path) } # which datasets can this user see for this method, path -default datasets = [] +default datasets := [] # site admins can see all programs -datasets := all_programs -{ - site_admin +datasets := all_programs if { + site_admin } # if user is a team_member, they can access programs that allow read access for this method, path -else := readable_programs -{ - input.body.method = "GET" - regex.match(paths.read.get[_], input.body.path) == true +else := readable_programs if { + input.body.method = "GET" + regex.match(paths.read.get[_], input.body.path) == true } -else := readable_programs -{ - input.body.method = "POST" - regex.match(paths.read.post[_], input.body.path) == true +else := readable_programs if { + input.body.method = "POST" + regex.match(paths.read.post[_], input.body.path) == true } # if user is a site curator, they can access all programs that allow curate access for this method, path -else := all_programs -{ - user_key in site_roles.curator - input.body.method = "GET" - regex.match(paths.curate.get[_], input.body.path) == true +else := all_programs if { + user_key in site_roles.curator + input.body.method = "GET" + regex.match(paths.curate.get[_], input.body.path) == true } -else := all_programs -{ - user_key in site_roles.curator - input.body.method = "POST" - regex.match(paths.curate.post[_], input.body.path) == true +else := all_programs if { + user_key in site_roles.curator + input.body.method = "POST" + regex.match(paths.curate.post[_], input.body.path) == true } -else := all_programs -{ - user_key in site_roles.curator - input.body.method = "DELETE" - regex.match(paths.curate.delete[_], input.body.path) == true +else := all_programs if { + user_key in site_roles.curator + input.body.method = "DELETE" + regex.match(paths.curate.delete[_], input.body.path) == true } # if user is a program_curator, they can access programs that allow curate access for them for this method, path -else := curateable_programs -{ - input.body.method = "GET" - regex.match(paths.curate.get[_], input.body.path) == true +else := curateable_programs if { + input.body.method = "GET" + regex.match(paths.curate.get[_], input.body.path) == true } -else := curateable_programs -{ - input.body.method = "POST" - regex.match(paths.curate.post[_], input.body.path) == true +else := curateable_programs if { + input.body.method = "POST" + regex.match(paths.curate.post[_], input.body.path) == true } -else := curateable_programs -{ - input.body.method = "DELETE" - regex.match(paths.curate.delete[_], input.body.path) == true +else := curateable_programs if { + input.body.method = "DELETE" + regex.match(paths.curate.delete[_], input.body.path) == true } - diff --git a/permissions_engine/idp.rego b/permissions_engine/idp.rego index 020fe01..b9ea0df 100644 --- a/permissions_engine/idp.rego +++ b/permissions_engine/idp.rego @@ -1,4 +1,5 @@ package idp + # for interacting with the IdP # @@ -6,32 +7,33 @@ package idp # import data.vault.keys as keys -import future.keywords.in + +import rego.v1 # # Function to decode and verify if a token is valid against a key # -decode_verify_token(key, token) := output { - issuer := key.iss - cert := key.cert - aud := key.aud[_] - output := io.jwt.decode_verify( # Decode and verify in one-step - token, - { # With the supplied constraints: - "cert": cert, - "iss": issuer, - "aud": aud - } - ) +decode_verify_token(key, token) := output if { + issuer := key.iss + cert := key.cert + aud := key.aud[_] + output := io.jwt.decode_verify( + token, # Decode and verify in one-step + { + "cert": cert, # With the supplied constraints: + "iss": issuer, + "aud": aud, + }, + ) } -decode_token(token) := output { - output := io.jwt.decode(token) +decode_token(token) := output if { + output := io.jwt.decode(token) } -decoded_output := output { - possible_tokens := ["identity", "token"] - output := decode_token(input[possible_tokens[_]]) +decoded_output := output if { + possible_tokens := ["identity", "token"] + output := decode_token(input[possible_tokens[_]]) } user_info := decoded_output[1] @@ -41,42 +43,41 @@ user_info := decoded_output[1] # user_key := user_info.CANDIG_USER_KEY - # # If either input.identity or input.token are valid against an issuer, decode and verify # -decode_verify_token_output[issuer] := output { - possible_tokens := ["identity", "token"] - some i - issuer := keys[i].iss - output := decode_verify_token(keys[i], input[possible_tokens[_]]) +decode_verify_token_output[issuer] := output if { + possible_tokens := ["identity", "token"] + some i + issuer := keys[i].iss + output := decode_verify_token(keys[i], input[possible_tokens[_]]) } # # The issuer of this token # -token_issuer := i { - some i in object.keys(decode_verify_token_output) - decode_verify_token_output[i][0] == true +token_issuer := i if { + some i in object.keys(decode_verify_token_output) + decode_verify_token_output[i][0] == true } # # Check if token is valid by checking whether decoded_verify output exists or not # -valid_token = true { - decode_verify_token_output[_][0] +valid_token if { + decode_verify_token_output[_][0] } # # Check trusted_researcher in the token payload # -trusted_researcher = true { - decode_verify_token_output[_][2].trusted_researcher == "true" +trusted_researcher if { + decode_verify_token_output[_][2].trusted_researcher == "true" } # # If the token_issuer is the same as the first listed in keys, this is a local token # -is_local_token = true { - keys[0].iss == token_issuer +is_local_token if { + keys[0].iss == token_issuer } diff --git a/permissions_engine/mask.rego b/permissions_engine/mask.rego index 9f3b16a..8c96672 100644 --- a/permissions_engine/mask.rego +++ b/permissions_engine/mask.rego @@ -3,4 +3,4 @@ package system.log import rego.v1 # To mask certain fields unconditionally, omit the rule body. -mask contains "/input/token" \ No newline at end of file +mask contains "/input/token" diff --git a/permissions_engine/permissions.rego b/permissions_engine/permissions.rego index d17578b..a860910 100644 --- a/permissions_engine/permissions.rego +++ b/permissions_engine/permissions.rego @@ -1,82 +1,86 @@ package permissions -import future.keywords.in + +import rego.v1 # # Values that are used by authx # -valid_token := true { - data.idp.valid_token +valid_token if { + data.idp.valid_token } + else := false -site_admin := data.calculate.site_admin { - valid_token +site_admin := data.calculate.site_admin if { + valid_token } -site_curator := data.calculate.site_curator { - valid_token +site_curator := data.calculate.site_curator if { + valid_token } -datasets := data.calculate.datasets { - valid_token +datasets := data.calculate.datasets if { + valid_token } -else := [] +else := [] # true if the path and method in the input match a readable combo in paths.json -readable_method_path := true { - input.body.method = "GET" - data.calculate.readable_get[_] +readable_method_path if { + input.body.method = "GET" + data.calculate.readable_get[_] } -else := true { - input.body.method = "POST" - data.calculate.readable_post[_] + +else if { + input.body.method = "POST" + data.calculate.readable_post[_] } -else := true { - input.body.method = "DELETE" - data.calculate.curateable_delete[_] + +else if { + input.body.method = "DELETE" + data.calculate.curateable_delete[_] } -else := false +else := false # true if the path and method in the input match a curateable combo in paths.json -curateable_method_path := true { - input.body.method = "GET" - data.calculate.curateable_get[_] +curateable_method_path if { + input.body.method = "GET" + data.calculate.curateable_get[_] } -else := true { - input.body.method = "POST" - data.calculate.curateable_post[_] + +else if { + input.body.method = "POST" + data.calculate.curateable_post[_] } -else := true { - input.body.method = "DELETE" - data.calculate.curateable_delete[_] + +else if { + input.body.method = "DELETE" + data.calculate.curateable_delete[_] } -else := false +else := false # if a specific program is in the body, allowed = true if that program is in datasets # or if the user is a site admin # or if the user is a site curator and wants to curate something -allowed := true -{ - input.body.program in datasets -} -else := true -{ - site_admin +allowed if { + datasets[input.body.program] == true } -else := true -{ - site_curator - curateable_method_path + +else if { + site_admin } -else := true -{ - site_curator - readable_method_path + +else if { + site_curator + curateable_method_path } +else if { + site_curator + readable_method_path +} # # User information, for decision log @@ -84,20 +88,23 @@ else := true # information from the jwt user_key := data.idp.user_key + issuer := data.idp.user_info.iss # # Debugging information for decision log # -user_is_site_admin := true { - user_key in data.vault.site_roles.admin +user_is_site_admin if { + user_key in data.vault.site_roles.admin } + else := false -user_is_site_curator := true { - user_key in data.vault.site_roles.curator +user_is_site_curator if { + user_key in data.vault.site_roles.curator } + else := false # programs the user is listed as a team member for diff --git a/permissions_engine/service.rego b/permissions_engine/service.rego index 62eb3b3..89f2094 100644 --- a/permissions_engine/service.rego +++ b/permissions_engine/service.rego @@ -1,12 +1,13 @@ package service + # # Verifies that a service is who it says it is # - import data.vault.service_token as service_token +import rego.v1 -verified { - service_token == input.token +verified if { + service_token == input.token } -service-info := "opa service is running" \ No newline at end of file +minus(service, info) := "opa service is running" diff --git a/permissions_engine/vault.rego b/permissions_engine/vault.rego index 0df7562..da0e7be 100644 --- a/permissions_engine/vault.rego +++ b/permissions_engine/vault.rego @@ -1,34 +1,37 @@ package vault + # # Obtain secrets from Opa's service secret store in Vault # +import rego.v1 -import future.keywords.in -import data.store_token.token as vault_token import data.idp.user_key +import data.store_token.token as vault_token # keys are the IDP keys for verifying JWTs, used by idp.rego and authz.rego -keys = http.send({"method": "get", "url": "VAULT_URL/v1/opa/data", "headers": {"X-Vault-Token": vault_token}}).body.data.keys +keys := http.send({"method": "get", "url": "VAULT_URL/v1/opa/data", "headers": {"X-Vault-Token": vault_token}}).body.data.keys # paths are the paths authorized for methods, used by permissions.rego -paths = http.send({"method": "get", "url": "VAULT_URL/v1/opa/paths", "headers": {"X-Vault-Token": vault_token}}).body.data.paths +paths := http.send({"method": "get", "url": "VAULT_URL/v1/opa/paths", "headers": {"X-Vault-Token": vault_token}}).body.data.paths # service_token gets the token saved for a service, used by service.rego -service_token = http.send({"method": "get", "url": concat("/", ["VAULT_URL/v1", input.service, "token", input.token]), "headers": {"X-Vault-Token": vault_token}}).body.data.token +service_token := http.send({"method": "get", "url": concat("/", ["VAULT_URL/v1", input.service, "token", input.token]), "headers": {"X-Vault-Token": vault_token}}).body.data.token # site_roles are site-wide authorizations, used by permissions.rego and authz.rego -site_roles = http.send({"method": "get", "url": "VAULT_URL/v1/opa/site_roles", "headers": {"X-Vault-Token": vault_token}}).body.data.site_roles +site_roles := http.send({"method": "get", "url": "VAULT_URL/v1/opa/site_roles", "headers": {"X-Vault-Token": vault_token}}).body.data.site_roles -all_programs = http.send({"method": "get", "url": "VAULT_URL/v1/opa/programs", "headers": {"X-Vault-Token": vault_token}}).body.data.programs -program_auths[p] := program { - some p in all_programs - program := http.send({"method": "get", "url": concat("/", ["VAULT_URL/v1/opa/programs", p]) , "headers": {"X-Vault-Token": vault_token}}).body.data[p] +all_programs := http.send({"method": "get", "url": "VAULT_URL/v1/opa/programs", "headers": {"X-Vault-Token": vault_token}}).body.data.programs + +program_auths[p] := program if { + some p in all_programs + program := http.send({"method": "get", "url": concat("/", ["VAULT_URL/v1/opa/programs", p]), "headers": {"X-Vault-Token": vault_token}}).body.data[p] } # check to see if the user is authorized for any other programs via DACs -user_auth = http.send({"method": "get", "url": concat("/", ["VAULT_URL/v1/opa/users", urlquery.encode(user_key)]), "headers": {"X-Vault-Token": vault_token}, "raise_error": false}) +user_auth := http.send({"method": "get", "url": concat("/", ["VAULT_URL/v1/opa/users", urlquery.encode(user_key)]), "headers": {"X-Vault-Token": vault_token}, "raise_error": false}) + +default user_programs := [] -default user_programs = [] -user_programs = user_auth.body.data.programs { - user_auth.status_code = 200 +user_programs := user_auth.body.data.programs if { + user_auth.status_code = 200 } From a752482167f8bef3831cd67898c068f77fa75639 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Mon, 30 Dec 2024 17:05:54 -0800 Subject: [PATCH 2/3] default python is 3.12 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 4d3b91d..68f4dd5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -ARG venv_python +ARG venv_python=3.12 FROM python:${venv_python} LABEL Maintainer="CanDIG Project" From 2e3b1779fd1a19f275ee2b2ed308636c393b4a01 Mon Sep 17 00:00:00 2001 From: Daisie Huang Date: Tue, 31 Dec 2024 13:21:02 -0800 Subject: [PATCH 3/3] possible other form --- permissions_engine/permissions.rego | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/permissions_engine/permissions.rego b/permissions_engine/permissions.rego index a860910..0df8827 100644 --- a/permissions_engine/permissions.rego +++ b/permissions_engine/permissions.rego @@ -68,6 +68,10 @@ allowed if { datasets[input.body.program] == true } +else if { + input.body.program in datasets +} + else if { site_admin }