From fe2fa8135fb8da5910412791f771fbd631de4073 Mon Sep 17 00:00:00 2001 From: amantri Date: Wed, 9 Apr 2025 11:24:30 -0400 Subject: [PATCH] Implement configurator role RBAC for sysinv API A RBAC for configurator role is implemented using oslo policy(see https://docs.openstack.org/neutron/pike/admin/config-rbac.html https://docs.openstack.org/oslo.policy/latest/user/usage.html# when-using-oslo-policy) The new configurator role will have the same privileges as admin role but blocked access to the following commands system ca-certificate-install system ca-certificate-uninstall system os-certificate-install the base rules are changed to accommodate the configurator role, enforce policy implemented for the certificate controller, also fixed a bug to show forbidden access text on cli when a configurator role user executes "system ca-certificate-install", "system os-certificate-install" commands. PASS: verify execution of following commands is denied for configurator role user system ca-certificate-install system ca-certificate-uninstall system os-certificate-install PASS: execute the following commands as configurator,operator,admin role user and expect it to work system host-lock / host-unlock system host-swact system host-power-off / host-power-on system host-reboot / host-reset system host-update "id" "path=value" system host-reinstall "id" system host-ptp-instance-assign/host-ptp-instance-remove system registry-garbage-collect PASS: execute the following commands as reader role user and expect access denied system host-lock / host-unlock system host-swact system host-power-off / host-power-on system host-reboot / host-reset system host-update "id" "path=value" system host-reinstall "id" system host-ptp-instance-assign/host-ptp-instance-remove system registry-garbage-collect PASS: execute the following as configurator,admin,reader,operator role user and expect it to work system host-list system registry-image-list system service-parameter-list PASS: verify that only admin,configurator role user is allowed to execute the following system registry-image-delete system host-delete PASS: verify that only admin,configurator role user is allowed to execute the following system service-parameter-add system service-parameter-delete system service-parameter-apply PASS: verify access to openstack commands are blocked for configurator role user Story: 2011348 Task: 51956 Change-Id: I779aa473c70abfb5fbd80f0138e9e340cf953d19 Signed-off-by: amantri --- .../cgtsclient/v1/certificate_shell.py | 3 + sysinv/sysinv/sysinv/etc/sysinv/policy.yaml | 95 +++++++++++++++---- .../sysinv/api/controllers/v1/certificate.py | 14 +++ sysinv/sysinv/sysinv/sysinv/api/hooks.py | 6 +- .../sysinv/sysinv/api/policies/__init__.py | 4 +- .../sysinv/sysinv/sysinv/api/policies/base.py | 36 ++++--- .../sysinv/sysinv/api/policies/certificate.py | 77 +++++++++++++++ .../sysinv/sysinv/api/policies/ihosts.py | 8 +- .../sysinv/api/policies/registry_image.py | 6 +- .../sysinv/api/policies/service_parameter.py | 10 +- 10 files changed, 211 insertions(+), 48 deletions(-) create mode 100644 sysinv/sysinv/sysinv/sysinv/api/policies/certificate.py diff --git a/sysinv/cgts-client/cgts-client/cgtsclient/v1/certificate_shell.py b/sysinv/cgts-client/cgts-client/cgtsclient/v1/certificate_shell.py index c79fc709bb..f7bd145158 100644 --- a/sysinv/cgts-client/cgts-client/cgtsclient/v1/certificate_shell.py +++ b/sysinv/cgts-client/cgts-client/cgtsclient/v1/certificate_shell.py @@ -57,6 +57,9 @@ def _install_cert(cc, certificate_file, data): try: response = cc.certificate.certificate_install(sec_file, data=data) + err_msg = response.get('error_message') + if err_msg: + raise exc.HTTPForbidden(err_msg) except exc.HTTPNotFound: raise exc.CommandError('Certificate not installed %s. No response.' % certificate_file) diff --git a/sysinv/sysinv/sysinv/etc/sysinv/policy.yaml b/sysinv/sysinv/sysinv/etc/sysinv/policy.yaml index 532489acad..62e45be6b1 100644 --- a/sysinv/sysinv/sysinv/etc/sysinv/policy.yaml +++ b/sysinv/sysinv/sysinv/etc/sysinv/policy.yaml @@ -1,23 +1,80 @@ ---- -# The commented lines below contains the default values for presented rules. +# admin role of admin,services projects +# "admin": "role:admin and (project_name:admin or project_name:services)" -# admin_in_system_projects: role:admin and (project_name:admin or project_name:services) -# reader_or_operator_in_system_projects: (role:reader or role:operator) and -# (project_name:admin or project_name:services) -# admin_or_operator_in_system_projects: (role:admin or role:operator) and -# (project_name:admin or project_name:services) +# admin,configurator roles of admin,services projects +# "admin_or_configurator": "(role:admin or role:configurator) and +# (project_name:admin or project_name:services)" -# config_api:service_parameter:add: rule:admin_in_system_projects -# config_api:service_parameter:apply: rule:admin_in_system_projects -# config_api:service_parameter:delete: rule:admin_in_system_projects -# config_api:service_parameter:get: rule:reader_or_operator_in_system_projects -# config_api:service_parameter:modify: rule:admin_or_operator_in_system_projects +# admin,operator,configurator roles of admin,services projects +# "admin_or_operator_or_configurator": "(role:admin or role:operator or +# role:configurator) and (project_name:admin or project_name:services)" -# config_api:ihosts:add: rule:admin_in_system_projects -# config_api:ihosts:delete: rule:admin_in_system_projects -# config_api:ihosts:get: rule:reader_or_operator_in_system_projects -# config_api:ihosts:modify: rule:admin_or_operator_in_system_projects +# reader,operator,configurator roles of admin,services projects +# "reader_or_operator_or_configurator": "(role:reader or role:operator or +# role:configurator) and (project_name:admin or project_name:services)" -# config_api:registry_image:add: rule:admin_or_operator_in_system_projects -# config_api:registry_image:delete: rule:admin_in_system_projects -# config_api:registry_image:get: "rule:reader_or_operator_in_system_projects +# Add a Service Parameter. +# POST /v1/service_parameter +# "config_api:service_parameter:add": "rule:admin_or_configurator" + +# Apply Service Parameters. +# POST /v1/service_parameter/apply +# "config_api:service_parameter:apply": "rule:admin_or_configurator" + +# Delete a Service Parameter. +# DELETE /v1/service_parameter/{parameter_id} +# "config_api:service_parameter:delete": "rule:admin_or_configurator" + +# Get Service Parameters. +# GET /v1/service_parameter +# GET /v1/service_parameter/{parameter_id} +# "config_api:service_parameter:get": "rule:reader_or_operator_or_configurator" + +# Modify Service Parameter value. +# PATCH /v1/service_parameter/{parameter_id} +# "config_api:service_parameter:modify": "rule:admin_or_configurator" + +# Add a host Parameter. +# POST /v1/ihosts +# "config_api:ihosts:post": "rule:admin_or_configurator" + +# Delete a host Parameter. +# DELETE /v1/ihosts/{parameter_id} +# "config_api:ihosts:delete": "rule:admin_or_configurator" + +# Get host Parameters. +# GET /v1/ihosts +# GET /v1/ihosts/{parameter_id} +# "config_api:ihosts:get": "rule:reader_or_operator_or_configurator" + +# Modify host value. +# PATCH /v1/ihosts/{parameter_id} +# "config_api:ihosts:patch": "rule:admin_or_operator_or_configurator" + +# Run registry garbage collect. +# POST /v1/registry_image +# "config_api:registry_image:add": "rule:admin_or_operator_or_configurator" + +# Delete a registry image. +# DELETE /v1/registry_image +# "config_api:registry_image:delete": "rule:admin_or_configurator" + +# Get registry images list. +# GET /v1/registry_image +# "config_api:registry_image:get": "rule:reader_or_operator_or_configurator" + +# Get certificate +# GET /v1/certificate +# GET /v1/certificate/{parameter_id} +# GET /v1/certificate/get_all_certs +# GET /v1/certificate/get_all_k8s_certs +# "config_api:certificate:get": "rule:reader_or_operator_or_configurator" + +# Delete certificate +# DELETE /v1/certificate/{parameter_id} +# "config_api:certificate:delete": "rule:admin" + +# install/renew certificate +# POST /v1/certificate/certificate_install +# POST /v1/certificate/certificate_renew +# "config_api:certificate:post": "rule:admin" diff --git a/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/certificate.py b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/certificate.py index 11d49d5a1f..11e3c6faed 100644 --- a/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/certificate.py +++ b/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/certificate.py @@ -39,9 +39,11 @@ from sysinv.api.controllers.v1 import collection from sysinv.api.controllers.v1 import link from sysinv.api.controllers.v1 import types from sysinv.api.controllers.v1 import utils +from sysinv.api.policies import certificate as cert_policy from sysinv.common import constants from sysinv.common import exception from sysinv.common import kubernetes as sys_kube +from sysinv.common import policy from sysinv.common import utils as cutils from sysinv.openstack.common.rpc.common import RemoteError from wsme import types as wtypes @@ -647,6 +649,18 @@ class CertificateController(rest.RestController): return soon_to_expiry_certs return cert_data + def enforce_policy(self, method_name, request): + """Check policy rules for each action of this controller.""" + context_dict = request.context.to_dict() + if method_name in ["get_all", "get_one", "get_all_certs", "get_all_k8s_certs"]: + policy.authorize(cert_policy.POLICY_ROOT % "get", {}, context_dict) + elif method_name in ["certificate_install", "certificate_renew"]: + policy.authorize(cert_policy.POLICY_ROOT % "post", {}, context_dict) + elif method_name in ["delete"]: + policy.authorize(cert_policy.POLICY_ROOT % method_name, {}, context_dict) + else: + raise exception.PolicyNotFound() + def _check_endpoint_domain_exists(): # Check that public endpoint FQDN is configured diff --git a/sysinv/sysinv/sysinv/sysinv/api/hooks.py b/sysinv/sysinv/sysinv/sysinv/api/hooks.py index 11dcf383a7..f062ce4b6f 100644 --- a/sysinv/sysinv/sysinv/sysinv/api/hooks.py +++ b/sysinv/sysinv/sysinv/sysinv/api/hooks.py @@ -164,7 +164,7 @@ class ContextHook(hooks.PecanHook): 'project_name': project_name, 'roles': roles } - is_admin = policy.authorize(base_policy.ADMIN_IN_SYSTEM_PROJECTS, {}, + is_admin = policy.authorize(base_policy.ADMIN_OR_CONFIGURATOR, {}, credentials, do_raise=False) utils.safe_rstrip(state.request.path, '/') @@ -209,12 +209,12 @@ class AccessPolicyHook(hooks.PecanHook): if method == 'GET': role = "reader or operator" has_api_access = policy.authorize( - base_policy.READER_OR_OPERATOR_IN_SYSTEM_PROJECTS, {}, + base_policy.READER_OR_OPERATOR_OR_CONFIGURATOR, {}, context.to_dict(), do_raise=False) else: role = "admin" has_api_access = policy.authorize( - base_policy.ADMIN_IN_SYSTEM_PROJECTS, {}, + base_policy.ADMIN_OR_CONFIGURATOR, {}, context.to_dict(), do_raise=False) if not has_api_access: raise exc.HTTPForbidden("Not allowed/Role " + role + " is needed") diff --git a/sysinv/sysinv/sysinv/sysinv/api/policies/__init__.py b/sysinv/sysinv/sysinv/sysinv/api/policies/__init__.py index 8edda13a16..4a52875052 100644 --- a/sysinv/sysinv/sysinv/sysinv/api/policies/__init__.py +++ b/sysinv/sysinv/sysinv/sysinv/api/policies/__init__.py @@ -20,6 +20,7 @@ from sysinv.api.policies import base from sysinv.api.policies import ihosts from sysinv.api.policies import registry_image from sysinv.api.policies import service_parameter +from sysinv.api.policies import certificate def list_rules(): @@ -27,5 +28,6 @@ def list_rules(): base.list_rules(), service_parameter.list_rules(), ihosts.list_rules(), - registry_image.list_rules() + registry_image.list_rules(), + certificate.list_rules(), ) diff --git a/sysinv/sysinv/sysinv/sysinv/api/policies/base.py b/sysinv/sysinv/sysinv/sysinv/api/policies/base.py index 6157a88c41..0eb0968c25 100644 --- a/sysinv/sysinv/sysinv/sysinv/api/policies/base.py +++ b/sysinv/sysinv/sysinv/sysinv/api/policies/base.py @@ -16,28 +16,38 @@ from oslo_policy import policy -ADMIN_IN_SYSTEM_PROJECTS = 'admin_in_system_projects' -READER_OR_OPERATOR_IN_SYSTEM_PROJECTS = 'reader_or_operator_in_system_projects' -ADMIN_OR_OPERATOR_IN_SYSTEM_PROJECTS = 'admin_or_operator_in_system_projects' +ADMIN = 'admin' +ADMIN_OR_CONFIGURATOR = 'admin_or_configurator' +READER_OR_OPERATOR_OR_CONFIGURATOR = 'reader_or_operator_or_configurator' +ADMIN_OR_OPERATOR_OR_CONFIGURATOR = 'admin_or_operator_or_configurator' base_rules = [ policy.RuleDefault( - name=ADMIN_IN_SYSTEM_PROJECTS, + name=ADMIN, check_str='role:admin and (project_name:admin or ' + 'project_name:services)', - description="Base rule.", + description='admin role of admin,services projects', ), policy.RuleDefault( - name=ADMIN_OR_OPERATOR_IN_SYSTEM_PROJECTS, - check_str='(role:admin or role:operator) and (project_name:admin or ' + - 'project_name:services)', - description="Base rule." + name=ADMIN_OR_CONFIGURATOR, + check_str='(role:admin or role:configurator) and ' + + '(project_name:admin or project_name:services)', + description='admin,configurator roles of admin,services ' + + 'projects', ), policy.RuleDefault( - name=READER_OR_OPERATOR_IN_SYSTEM_PROJECTS, - check_str='(role:reader or role:operator) and (project_name:admin or ' + - 'project_name:services)', - description="Base rule." + name=ADMIN_OR_OPERATOR_OR_CONFIGURATOR, + check_str='(role:admin or role:operator or role:configurator) and ' + + '(project_name:admin or project_name:services)', + description='admin,operator,configurator roles of admin,services ' + + 'projects', + ), + policy.RuleDefault( + name=READER_OR_OPERATOR_OR_CONFIGURATOR, + check_str='(role:reader or role:operator or role:configurator) and ' + + '(project_name:admin or project_name:services)', + description='reader,operator,configurator roles of admin,services ' + + 'projects', ) ] diff --git a/sysinv/sysinv/sysinv/sysinv/api/policies/certificate.py b/sysinv/sysinv/sysinv/sysinv/api/policies/certificate.py new file mode 100644 index 0000000000..07bca6837b --- /dev/null +++ b/sysinv/sysinv/sysinv/sysinv/api/policies/certificate.py @@ -0,0 +1,77 @@ +# Copyright (c) 2025 Wind River Systems, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +from oslo_policy import policy +from sysinv.api.policies import base + +POLICY_ROOT = 'config_api:certificate:%s' + + +certificate_rules = [ + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'get', + check_str='rule:' + base.READER_OR_OPERATOR_OR_CONFIGURATOR, + description="Get certificate", + operations=[ + { + 'method': 'GET', + 'path': '/v1/certificate' + }, + { + 'method': 'GET', + 'path': '/v1/certificate/{parameter_id}' + }, + { + 'method': 'GET', + 'path': '/v1/certificate/get_all_certs' + }, + { + 'method': 'GET', + 'path': '/v1/certificate/get_all_k8s_certs' + } + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'delete', + check_str='rule:' + base.ADMIN, + description="Delete certificate", + operations=[ + { + 'method': 'DELETE', + 'path': '/v1/certificate/{parameter_id}' + } + ] + ), + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'post', + check_str='rule:' + base.ADMIN, + description="install/renew certificate", + operations=[ + { + 'method': 'POST', + 'path': '/v1/certificate/certificate_install' + }, + { + 'method': 'POST', + 'path': '/v1/certificate/certificate_renew' + }, + ] + ), +] + + +def list_rules(): + return certificate_rules diff --git a/sysinv/sysinv/sysinv/sysinv/api/policies/ihosts.py b/sysinv/sysinv/sysinv/sysinv/api/policies/ihosts.py index 66f79c931f..2a83e627ce 100644 --- a/sysinv/sysinv/sysinv/sysinv/api/policies/ihosts.py +++ b/sysinv/sysinv/sysinv/sysinv/api/policies/ihosts.py @@ -23,7 +23,7 @@ POLICY_ROOT = 'config_api:ihosts:%s' ihosts_rules = [ policy.DocumentedRuleDefault( name=POLICY_ROOT % 'post', - check_str='rule:' + base.ADMIN_IN_SYSTEM_PROJECTS, + check_str='rule:' + base.ADMIN_OR_CONFIGURATOR, description="Add a host Parameter.", operations=[ { @@ -34,7 +34,7 @@ ihosts_rules = [ ), policy.DocumentedRuleDefault( name=POLICY_ROOT % 'delete', - check_str='rule:' + base.ADMIN_IN_SYSTEM_PROJECTS, + check_str='rule:' + base.ADMIN_OR_CONFIGURATOR, description="Delete a host Parameter.", operations=[ { @@ -45,7 +45,7 @@ ihosts_rules = [ ), policy.DocumentedRuleDefault( name=POLICY_ROOT % 'get', - check_str='rule:' + base.READER_OR_OPERATOR_IN_SYSTEM_PROJECTS, + check_str='rule:' + base.READER_OR_OPERATOR_OR_CONFIGURATOR, description="Get host Parameters.", operations=[ { @@ -60,7 +60,7 @@ ihosts_rules = [ ), policy.DocumentedRuleDefault( name=POLICY_ROOT % 'patch', - check_str='rule:' + base.ADMIN_OR_OPERATOR_IN_SYSTEM_PROJECTS, + check_str='rule:' + base.ADMIN_OR_OPERATOR_OR_CONFIGURATOR, description="Modify host value.", operations=[ { diff --git a/sysinv/sysinv/sysinv/sysinv/api/policies/registry_image.py b/sysinv/sysinv/sysinv/sysinv/api/policies/registry_image.py index 857d3336f1..e9360f1a93 100644 --- a/sysinv/sysinv/sysinv/sysinv/api/policies/registry_image.py +++ b/sysinv/sysinv/sysinv/sysinv/api/policies/registry_image.py @@ -23,7 +23,7 @@ POLICY_ROOT = 'config_api:registry_image:%s' registry_image_rules = [ policy.DocumentedRuleDefault( name=POLICY_ROOT % 'add', - check_str='rule:' + base.ADMIN_OR_OPERATOR_IN_SYSTEM_PROJECTS, + check_str='rule:' + base.ADMIN_OR_OPERATOR_OR_CONFIGURATOR, description="Run registry garbage collect.", operations=[ { @@ -34,7 +34,7 @@ registry_image_rules = [ ), policy.DocumentedRuleDefault( name=POLICY_ROOT % 'delete', - check_str='rule:' + base.ADMIN_IN_SYSTEM_PROJECTS, + check_str='rule:' + base.ADMIN_OR_CONFIGURATOR, description="Delete a registry image.", operations=[ { @@ -45,7 +45,7 @@ registry_image_rules = [ ), policy.DocumentedRuleDefault( name=POLICY_ROOT % 'get', - check_str='rule:' + base.READER_OR_OPERATOR_IN_SYSTEM_PROJECTS, + check_str='rule:' + base.READER_OR_OPERATOR_OR_CONFIGURATOR, description="Get registry images list.", operations=[ { diff --git a/sysinv/sysinv/sysinv/sysinv/api/policies/service_parameter.py b/sysinv/sysinv/sysinv/sysinv/api/policies/service_parameter.py index 1128f2a544..5618b2f4be 100644 --- a/sysinv/sysinv/sysinv/sysinv/api/policies/service_parameter.py +++ b/sysinv/sysinv/sysinv/sysinv/api/policies/service_parameter.py @@ -23,7 +23,7 @@ POLICY_ROOT = 'config_api:service_parameter:%s' service_parameter_rules = [ policy.DocumentedRuleDefault( name=POLICY_ROOT % 'add', - check_str='rule:' + base.ADMIN_IN_SYSTEM_PROJECTS, + check_str='rule:' + base.ADMIN_OR_CONFIGURATOR, description="Add a Service Parameter.", operations=[ { @@ -34,7 +34,7 @@ service_parameter_rules = [ ), policy.DocumentedRuleDefault( name=POLICY_ROOT % 'apply', - check_str='rule:' + base.ADMIN_IN_SYSTEM_PROJECTS, + check_str='rule:' + base.ADMIN_OR_CONFIGURATOR, description="Apply Service Parameters.", operations=[ { @@ -45,7 +45,7 @@ service_parameter_rules = [ ), policy.DocumentedRuleDefault( name=POLICY_ROOT % 'delete', - check_str='rule:' + base.ADMIN_IN_SYSTEM_PROJECTS, + check_str='rule:' + base.ADMIN_OR_CONFIGURATOR, description="Delete a Service Parameter.", operations=[ { @@ -56,7 +56,7 @@ service_parameter_rules = [ ), policy.DocumentedRuleDefault( name=POLICY_ROOT % 'get', - check_str='rule:' + base.READER_OR_OPERATOR_IN_SYSTEM_PROJECTS, + check_str='rule:' + base.READER_OR_OPERATOR_OR_CONFIGURATOR, description="Get Service Parameters.", operations=[ { @@ -71,7 +71,7 @@ service_parameter_rules = [ ), policy.DocumentedRuleDefault( name=POLICY_ROOT % 'modify', - check_str='rule:' + base.ADMIN_IN_SYSTEM_PROJECTS, + check_str='rule:' + base.ADMIN_OR_CONFIGURATOR, description="Modify Service Parameter value.", operations=[ {