From ec2af808dbfb989c130044fb26c1ae28f3b73a39 Mon Sep 17 00:00:00 2001 From: Jiachen Fan Date: Thu, 16 Jul 2026 16:33:38 +0800 Subject: [PATCH] feat(DM01-6092): add endpoint to get project secrets with decrypted values Add GET /api/v1/projects//secrets/values returning each secret's name and decrypted plaintext value for a project. - New SecretValues resource: validates project uuid (400), returns 404 when the project does not exist, decrypts each secret value. - New OPA rule allowing GET on the .../secrets/values path, reusing the existing project-administrator gate (user token + role >= 20). WARNING: this endpoint exposes plaintext secret values; access is restricted to project administrators by the OPA policy. --- src/api/handlers/projects/secrets.py | 41 ++++++++++++++++++- .../policies/projects_secrets.rego | 8 ++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/api/handlers/projects/secrets.py b/src/api/handlers/projects/secrets.py index dd9f1c914..ac6bc9237 100644 --- a/src/api/handlers/projects/secrets.py +++ b/src/api/handlers/projects/secrets.py @@ -7,7 +7,7 @@ from pyinfrabox.utils import validate_uuid from pyinfraboxutils.ibflask import OK from pyinfraboxutils.ibrestplus import api, response_model -from pyinfraboxutils.secrets import encrypt_secret +from pyinfraboxutils.secrets import encrypt_secret, decrypt_secret ns = api.namespace('Secrets', path='/api/v1/projects//secrets', @@ -110,3 +110,42 @@ def delete(self, project_id, secret_id): g.db.commit() return OK('Successfully deleted secret.') + + +secret_value_model = api.model('SecretValue', { + 'name': fields.String(required=True), + 'value': fields.String(required=True), +}) + + +@ns.route('/values') +@api.doc(responses={403: 'Not Authorized', 404: 'Project not found'}) +class SecretValues(Resource): + + @api.marshal_list_with(secret_value_model) + def get(self, project_id): + ''' + Returns project's secrets with decrypted values. + + WARNING: this endpoint exposes plaintext secret values and is + restricted to project administrators by the OPA policy. + ''' + if not validate_uuid(project_id): + abort(400, 'Invalid project uuid.') + + project = g.db.execute_one_dict(''' + SELECT id FROM project WHERE id = %s + ''', [project_id]) + + if not project: + abort(404, 'Project not found.') + + secrets = g.db.execute_many_dict(''' + SELECT name, value FROM secret + WHERE project_id = %s + ''', [project_id]) + + for secret in secrets: + secret['value'] = decrypt_secret(secret['value']) + + return secrets diff --git a/src/openpolicyagent/policies/projects_secrets.rego b/src/openpolicyagent/policies/projects_secrets.rego index abecd4fee..dbd40e56e 100644 --- a/src/openpolicyagent/policies/projects_secrets.rego +++ b/src/openpolicyagent/policies/projects_secrets.rego @@ -20,6 +20,14 @@ allow { projects_secrets_administrator([api.token.user.id, project_id]) } +# Allow GET access to /api/v1/projects//secrets/values for project administrators +allow { + api.method = "GET" + api.path = ["api", "v1", "projects", project_id, "secrets", "values"] + api.token.type = "user" + projects_secrets_administrator([api.token.user.id, project_id]) +} + # Allow POST access to /api/v1/projects//secrets for project administrators allow { api.method = "POST"