From 7a7365c7e015c3108c3f024fa5155e8dd51e9bdd Mon Sep 17 00:00:00 2001 From: Lance Bragstad Date: Wed, 28 Oct 2020 21:06:45 +0000 Subject: [PATCH] Implement secure RBAC for usage This commit updates the policies for the usage resource in placement to support read-only roles. This is part of a broader community effort to support read-only roles and implement secure, consistent default policies. This commit also allows project readers to see project-specific usages, resolve a long-standing policy TODO. Co-Authored-By: Stephen Finucane Change-Id: I80afaae965d3c5f862320ac11a7d05db2f6b6553 --- placement/handlers/usage.py | 13 +- placement/policies/base.py | 2 + placement/policies/usage.py | 44 +++-- .../functional/gabbits/usage-legacy-rbac.yaml | 54 ++++++ .../functional/gabbits/usage-secure-rbac.yaml | 154 ++++++++++++++++++ 5 files changed, 246 insertions(+), 21 deletions(-) create mode 100644 placement/tests/functional/gabbits/usage-legacy-rbac.yaml create mode 100644 placement/tests/functional/gabbits/usage-secure-rbac.yaml diff --git a/placement/handlers/usage.py b/placement/handlers/usage.py index 0bf322327..dff126acf 100644 --- a/placement/handlers/usage.py +++ b/placement/handlers/usage.py @@ -88,18 +88,17 @@ def get_total_usages(req): sum/total of usages. Return 404 Not Found if the wanted microversion does not match. """ + project_id = req.GET.get('project_id') + user_id = req.GET.get('user_id') + context = req.environ['placement.context'] - # TODO(mriedem): When we support non-admins to use GET /usages we - # should pass the project_id (and user_id?) from the query parameters - # into context.can() for the target. - context.can(policies.TOTAL_USAGES) + context.can( + policies.TOTAL_USAGES, + target={'project_id': project_id}) want_version = req.environ[microversion.MICROVERSION_ENVIRON] util.validate_query_params(req, schema.GET_USAGES_SCHEMA_1_9) - project_id = req.GET.get('project_id') - user_id = req.GET.get('user_id') - usages = usage_obj.get_all_by_project_user(context, project_id, user_id=user_id) diff --git a/placement/policies/base.py b/placement/policies/base.py index 4d2bf5f29..102f25a2b 100644 --- a/placement/policies/base.py +++ b/placement/policies/base.py @@ -19,6 +19,8 @@ RULE_ADMIN_API = 'rule:admin_api' # let's continue using generic check strings. SYSTEM_ADMIN = 'role:admin and system_scope:all' SYSTEM_READER = 'role:reader and system_scope:all' +PROJECT_READER = 'role:reader and project_id:%(project_id)s' +PROJECT_READER_OR_SYSTEM_READER = f'({SYSTEM_READER}) or ({PROJECT_READER})' rules = [ # "placement" is the default rule (action) used for all routes that do diff --git a/placement/policies/usage.py b/placement/policies/usage.py index 42ce354b4..9a69f2904 100644 --- a/placement/policies/usage.py +++ b/placement/policies/usage.py @@ -11,6 +11,7 @@ # under the License. +from oslo_log import versionutils from oslo_policy import policy from placement.policies import base @@ -19,34 +20,49 @@ from placement.policies import base PROVIDER_USAGES = 'placement:resource_providers:usages' TOTAL_USAGES = 'placement:usages' +DEPRECATED_REASON = """ +The usage API now supports a read-only role by default. +""" + +deprecated_list_rp_usages = policy.DeprecatedRule( + name=PROVIDER_USAGES, + check_str=base.RULE_ADMIN_API +) +deprecated_list_total_usages = policy.DeprecatedRule( + name=TOTAL_USAGES, + check_str=base.RULE_ADMIN_API +) + rules = [ policy.DocumentedRuleDefault( - PROVIDER_USAGES, - base.RULE_ADMIN_API, - "List resource provider usages.", - [ + name=PROVIDER_USAGES, + check_str=base.SYSTEM_READER, + description="List resource provider usages.", + operations=[ { 'method': 'GET', 'path': '/resource_providers/{uuid}/usages' } ], - scope_types=['system']), + scope_types=['system'], + deprecated_rule=deprecated_list_rp_usages, + deprecated_reason=DEPRECATED_REASON, + deprecated_since=versionutils.deprecated.WALLABY), policy.DocumentedRuleDefault( - # TODO(mriedem): At some point we might set scope_types=['project'] - # so that non-admin project-scoped token users can query usages for - # their project. The context.can() target will need to change as well - # in the actual policy enforcement check in the handler code. - TOTAL_USAGES, - base.RULE_ADMIN_API, - "List total resource usages for a given project.", - [ + name=TOTAL_USAGES, + check_str=base.PROJECT_READER_OR_SYSTEM_READER, + description="List total resource usages for a given project.", + operations=[ { 'method': 'GET', 'path': '/usages' } ], - scope_types=['system']) + scope_types=['system', 'project'], + deprecated_rule=deprecated_list_total_usages, + deprecated_reason=DEPRECATED_REASON, + deprecated_since=versionutils.deprecated.WALLABY) ] diff --git a/placement/tests/functional/gabbits/usage-legacy-rbac.yaml b/placement/tests/functional/gabbits/usage-legacy-rbac.yaml new file mode 100644 index 000000000..95942fe55 --- /dev/null +++ b/placement/tests/functional/gabbits/usage-legacy-rbac.yaml @@ -0,0 +1,54 @@ +--- +fixtures: + - LegacyRBACPolicyFixture + +vars: + - &project_id 9520f97991e94f30a8dd205ef3ce735a + - &project_admin_headers + x-auth-token: user + x-roles: admin,member,reader + x-project-id: *project_id + accept: application/json + content-type: application/json + openstack-api-version: placement latest + - &project_member_headers + x-auth-token: user + x-roles: member,reader + x-project-id: *project_id + accept: application/json + content-type: application/json + openstack-api-version: placement latest + +tests: + +- name: project admin can create resource provider + POST: /resource_providers + request_headers: *project_admin_headers + data: + name: $ENVIRON['RP_NAME'] + uuid: $ENVIRON['RP_UUID'] + status: 200 + +- name: project member cannot list provider usage + GET: /resource_providers/$ENVIRON['RP_UUID']/usages + request_headers: *project_member_headers + status: 403 + +- name: project admin can list provider usage + GET: /resource_providers/$ENVIRON['RP_UUID']/usages + request_headers: *project_admin_headers + status: 200 + response_json_paths: + usages: {} + +- name: project member cannot get total usage for project + GET: /usages?project_id=$ENVIRON['PROJECT_ID'] + request_headers: *project_member_headers + status: 403 + +- name: project admin can get total usage for project + GET: /usages?project_id=$ENVIRON['PROJECT_ID'] + request_headers: *project_admin_headers + status: 200 + response_json_paths: + usages: {} diff --git a/placement/tests/functional/gabbits/usage-secure-rbac.yaml b/placement/tests/functional/gabbits/usage-secure-rbac.yaml new file mode 100644 index 000000000..9ae40727a --- /dev/null +++ b/placement/tests/functional/gabbits/usage-secure-rbac.yaml @@ -0,0 +1,154 @@ +--- +fixtures: + - SecureRBACPolicyFixture + +vars: + - &project_id $ENVIRON['PROJECT_ID'] + - &project_id_alt $ENVIRON['PROJECT_ID_ALT'] + - &system_admin_headers + x-auth-token: user + x-roles: admin,member,reader + accept: application/json + content-type: application/json + openstack-api-version: placement latest + openstack-system-scope: all + - &system_reader_headers + x-auth-token: user + x-roles: reader + accept: application/json + content-type: application/json + openstack-api-version: placement latest + openstack-system-scope: all + - &project_admin_headers + x-auth-token: user + x-roles: admin,member,reader + x-project-id: *project_id + accept: application/json + content-type: application/json + openstack-api-version: placement latest + - &project_member_headers + x-auth-token: user + x-roles: member,reader + x-project-id: *project_id + accept: application/json + content-type: application/json + openstack-api-version: placement latest + - &project_reader_headers + x-auth-token: user + x-roles: reader + x-project-id: *project_id + accept: application/json + content-type: application/json + openstack-api-version: placement latest + - &alt_project_admin_headers + x-auth-token: user + x-roles: admin,member,reader + x-project-id: *project_id_alt + accept: application/json + content-type: application/json + openstack-api-version: placement latest + - &alt_project_member_headers + x-auth-token: user + x-roles: member,reader + x-project-id: *project_id_alt + accept: application/json + content-type: application/json + openstack-api-version: placement latest + - &alt_project_reader_headers + x-auth-token: user + x-roles: reader + x-project-id: *project_id_alt + accept: application/json + content-type: application/json + openstack-api-version: placement latest + +tests: + +- name: system admin can create resource provider + POST: /resource_providers + request_headers: *system_admin_headers + data: + name: $ENVIRON['RP_NAME'] + uuid: $ENVIRON['RP_UUID'] + status: 200 + +- name: project admin cannot list provider usage + GET: /resource_providers/$ENVIRON['RP_UUID']/usages + request_headers: *project_admin_headers + status: 403 + +- name: project member cannot list provider usage + GET: /resource_providers/$ENVIRON['RP_UUID']/usages + request_headers: *project_member_headers + status: 403 + +- name: project reader cannot list provider usage + GET: /resource_providers/$ENVIRON['RP_UUID']/usages + request_headers: *project_reader_headers + status: 403 + +- name: system reader can list provider usage + GET: /resource_providers/$ENVIRON['RP_UUID']/usages + request_headers: *system_reader_headers + status: 200 + response_json_paths: + usages: {} + +- name: system admin can list provider usage + GET: /resource_providers/$ENVIRON['RP_UUID']/usages + request_headers: *system_admin_headers + status: 200 + response_json_paths: + usages: {} + +- name: project admin can get total usage for project + GET: /usages?project_id=$ENVIRON['PROJECT_ID'] + request_headers: *project_admin_headers + status: 200 + response_json_paths: + usages: {} + +- name: project member can get total usage for project + GET: /usages?project_id=$ENVIRON['PROJECT_ID'] + request_headers: *project_member_headers + status: 200 + response_json_paths: + usages: {} + +- name: project reader can get total usage for project + GET: /usages?project_id=$ENVIRON['PROJECT_ID'] + request_headers: *project_reader_headers + status: 200 + response_json_paths: + usages: {} + +# Make sure users from other projects can't snoop around for usage on projects +# they have no business knowing about. +- name: project admin cannot get total usage for unauthorized project + GET: /usages?project_id=$ENVIRON['PROJECT_ID'] + request_headers: *alt_project_admin_headers + status: 403 + +- name: project member cannot get total usage for unauthorized project + GET: /usages?project_id=$ENVIRON['PROJECT_ID'] + request_headers: *alt_project_member_headers + status: 403 + +- name: project reader cannot get total usage for unauthorized project + GET: /usages?project_id=$ENVIRON['PROJECT_ID'] + request_headers: *alt_project_reader_headers + status: 403 + +- name: system reader can get total usage for project + GET: /usages?project_id=$ENVIRON['PROJECT_ID'] + request_headers: *system_reader_headers + status: 200 + response_json_paths: + usages: {} + +- name: system admin can get total usage for project + GET: /usages?project_id=$ENVIRON['PROJECT_ID'] + request_headers: *system_admin_headers + status: 200 + response_json_paths: + usages: {}