
This commit implements access control for DC API. The reference doc can be found at "https://docs.starlingx.io/api-ref/distcloud/api-ref-dcmanager-v1.html". Unit tests and YAML file support will be done in other tasks. The access control implementation for GET requests requires the user to have "reader" role and to be present in either "admin" or "services" project. For other requests, it requires the user to have "admin" role and to be present in either "admin" or "services" project. Requests using public API URLs require no credentials. As all default system users of StarlingX have "admin" role and are present in either project "admin" or "services", there should be no regression with the change introduced here. The implementation done here is a little bit different from the one done for sysinv and FM APIs, because the routing of requests is not done when "before()" method of Pecan hooks are called, so the controller is not defined at this point. To test the access control of DC API, the following commands are used (long list of parameters is replaced by "<params>"): dcmanager subcloud add <params> dcmanager subcloud manage subcloud2 dcmanager subcloud list dcmanager subcloud delete subcloud2 dcmanager subcloud-deploy upload <params> dcmanager subcloud-deploy show dcmanager alarm summary dcmanager patch-strategy create dcmanager patch-strategy show dcmanager patch-strategy apply dcmanager patch-strategy abort dcmanager patch-strategy delete dcmanager strategy-config update <params> subcloud1 dcmanager strategy-config list dcmanager strategy-config delete subcloud1 dcmanager subcloud-group add --name group01 dcmanager subcloud-group update --description test group01 dcmanager subcloud-group list dcmanager subcloud-group delete group01 dcmanager subcloud-backup create --subcloud subcloud1 On test plan, these commands are reffered as "test commands". The access control is not implemented for "dcdbsync" and "dcorch" servers. Also, it is also not implemented for action POST "/v1.0/notifications" in dcmanager API server, as it it is only called indirectly by sysinv controllers. Test Plan: PASS: Successfully deploy a Distributed Cloud (with 1 subcloud) using a CentOS image with this commit present. Successfully create, through openstack CLI, the users: 'testreader' with role 'reader' in project 'admin', 'adminsvc' with role 'admin' in project 'services' and 'otheradmin' with role 'admin' in project 'notadminproject'. Create openrc files for all new users. Note: the other user used is the already existing 'admin' with role 'admin' in project 'admin'. PASS: In the deployed DC, check the behavior of test commands through different users: for "admin" and "adminsvc" users, all commands are successful; for "testreader" user, only the test commands ending with "list" or "summary" (GET requests) are successful; for "otheradmin" user, all commands fail. PASS: In the deployed DC, to assert that public API works without authentication, execute the command "curl -v http://<MGMT_IP>:8119/" and verify that it is accepted and that the HTTP response is 200, and execute the command "curl -v http://<MGMT_IP>:8119/v1.0/subclouds" and verify that it is rejected and that the HTTP response is 401. PASS: In the deployed DC, check through Horizon interface that DC management works correctly with default admin user. Story: 2010149 Task: 46287 Signed-off-by: Joao Victor Portal <Joao.VictorPortal@windriver.com> Change-Id: Icfe24fd62096c7bf0bbb1f97e819dee5aac675e4
295 lines
12 KiB
Python
295 lines
12 KiB
Python
# Copyright (c) 2017 Ericsson AB.
|
|
# Copyright (c) 2020-2022 Wind River Systems, Inc.
|
|
# All Rights Reserved.
|
|
#
|
|
# 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.
|
|
#
|
|
|
|
from oslo_config import cfg
|
|
from oslo_db import exception as db_exc
|
|
from oslo_log import log as logging
|
|
from oslo_messaging import RemoteError
|
|
|
|
import http.client as httpclient
|
|
import pecan
|
|
from pecan import expose
|
|
from pecan import request
|
|
|
|
from dcmanager.api.controllers import restcomm
|
|
from dcmanager.api.policies import subcloud_group as subcloud_group_policy
|
|
from dcmanager.api import policy
|
|
from dcmanager.common import consts
|
|
from dcmanager.common.i18n import _
|
|
from dcmanager.common import utils
|
|
from dcmanager.db import api as db_api
|
|
from dcmanager.rpc import client as rpc_client
|
|
|
|
CONF = cfg.CONF
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
SUPPORTED_GROUP_APPLY_TYPES = [
|
|
consts.SUBCLOUD_APPLY_TYPE_PARALLEL,
|
|
consts.SUBCLOUD_APPLY_TYPE_SERIAL
|
|
]
|
|
|
|
# validation constants for Subcloud Group
|
|
MAX_SUBCLOUD_GROUP_NAME_LEN = 255
|
|
MAX_SUBCLOUD_GROUP_DESCRIPTION_LEN = 255
|
|
MIN_SUBCLOUD_GROUP_MAX_PARALLEL_SUBCLOUDS = 1
|
|
MAX_SUBCLOUD_GROUP_MAX_PARALLEL_SUBCLOUDS = 500
|
|
|
|
|
|
class SubcloudGroupsController(object):
|
|
|
|
def __init__(self):
|
|
super(SubcloudGroupsController, self).__init__()
|
|
self.rpc_client = rpc_client.ManagerClient()
|
|
|
|
@expose(generic=True, template='json')
|
|
def index(self):
|
|
# Route the request to specific methods with parameters
|
|
pass
|
|
|
|
def _get_subcloud_list_for_group(self, context, group_id):
|
|
subclouds = db_api.subcloud_get_for_group(context, group_id)
|
|
return utils.subcloud_db_list_to_dict(subclouds)
|
|
|
|
def _get_subcloud_group_list(self, context):
|
|
groups = db_api.subcloud_group_get_all(context)
|
|
subcloud_group_list = []
|
|
|
|
for group in groups:
|
|
group_dict = db_api.subcloud_group_db_model_to_dict(group)
|
|
subcloud_group_list.append(group_dict)
|
|
|
|
result = dict()
|
|
result['subcloud_groups'] = subcloud_group_list
|
|
return result
|
|
|
|
@index.when(method='GET', template='json')
|
|
def get(self, group_ref=None, subclouds=False):
|
|
"""Get details about subcloud group.
|
|
|
|
:param group_ref: ID or name of subcloud group
|
|
"""
|
|
policy.authorize(subcloud_group_policy.POLICY_ROOT % "get", {},
|
|
restcomm.extract_credentials_for_policy())
|
|
context = restcomm.extract_context_from_environ()
|
|
|
|
if group_ref is None:
|
|
# List of subcloud groups requested
|
|
return self._get_subcloud_group_list(context)
|
|
|
|
group = utils.subcloud_group_get_by_ref(context, group_ref)
|
|
if group is None:
|
|
pecan.abort(httpclient.NOT_FOUND, _('Subcloud Group not found'))
|
|
if subclouds:
|
|
# Return only the subclouds for this subcloud group
|
|
return self._get_subcloud_list_for_group(context, group.id)
|
|
subcloud_group_dict = db_api.subcloud_group_db_model_to_dict(group)
|
|
return subcloud_group_dict
|
|
|
|
def _validate_name(self, name):
|
|
# Reject post and update operations for name that:
|
|
# - attempt to set to None
|
|
# - attempt to set to a number
|
|
# - attempt to set to the Default subcloud group
|
|
# - exceed the max length
|
|
if not name:
|
|
return False
|
|
if name.isdigit():
|
|
return False
|
|
if name == consts.DEFAULT_SUBCLOUD_GROUP_NAME:
|
|
return False
|
|
if len(name) >= MAX_SUBCLOUD_GROUP_NAME_LEN:
|
|
return False
|
|
return True
|
|
|
|
def _validate_description(self, description):
|
|
if not description:
|
|
return False
|
|
if len(description) >= MAX_SUBCLOUD_GROUP_DESCRIPTION_LEN:
|
|
return False
|
|
return True
|
|
|
|
def _validate_update_apply_type(self, update_apply_type):
|
|
if not update_apply_type:
|
|
return False
|
|
if update_apply_type not in SUPPORTED_GROUP_APPLY_TYPES:
|
|
return False
|
|
return True
|
|
|
|
def _validate_max_parallel_subclouds(self, max_parallel_str):
|
|
if not max_parallel_str:
|
|
return False
|
|
try:
|
|
# Check the value is an integer
|
|
val = int(max_parallel_str)
|
|
except ValueError:
|
|
return False
|
|
|
|
# We do not support less than min or greater than max
|
|
if val < MIN_SUBCLOUD_GROUP_MAX_PARALLEL_SUBCLOUDS:
|
|
return False
|
|
if val > MAX_SUBCLOUD_GROUP_MAX_PARALLEL_SUBCLOUDS:
|
|
return False
|
|
return True
|
|
|
|
@index.when(method='POST', template='json')
|
|
def post(self):
|
|
"""Create a new subcloud group."""
|
|
policy.authorize(subcloud_group_policy.POLICY_ROOT % "create", {},
|
|
restcomm.extract_credentials_for_policy())
|
|
context = restcomm.extract_context_from_environ()
|
|
|
|
payload = eval(request.body)
|
|
if not payload:
|
|
pecan.abort(httpclient.BAD_REQUEST, _('Body required'))
|
|
|
|
name = payload.get('name')
|
|
description = payload.get('description')
|
|
update_apply_type = payload.get('update_apply_type')
|
|
max_parallel_subclouds = payload.get('max_parallel_subclouds')
|
|
|
|
# Validate payload
|
|
if not self._validate_name(name):
|
|
pecan.abort(httpclient.BAD_REQUEST, _('Invalid group name'))
|
|
if not self._validate_description(description):
|
|
pecan.abort(httpclient.BAD_REQUEST, _('Invalid group description'))
|
|
if not self._validate_update_apply_type(update_apply_type):
|
|
pecan.abort(httpclient.BAD_REQUEST,
|
|
_('Invalid group update_apply_type'))
|
|
if not self._validate_max_parallel_subclouds(max_parallel_subclouds):
|
|
pecan.abort(httpclient.BAD_REQUEST,
|
|
_('Invalid group max_parallel_subclouds'))
|
|
try:
|
|
group_ref = db_api.subcloud_group_create(context,
|
|
name,
|
|
description,
|
|
update_apply_type,
|
|
max_parallel_subclouds)
|
|
return db_api.subcloud_group_db_model_to_dict(group_ref)
|
|
except db_exc.DBDuplicateEntry:
|
|
LOG.info("Group create failed. Group %s already exists" % name)
|
|
pecan.abort(httpclient.BAD_REQUEST,
|
|
_('A subcloud group with this name already exists'))
|
|
except RemoteError as e:
|
|
pecan.abort(httpclient.UNPROCESSABLE_ENTITY, e.value)
|
|
except Exception as e:
|
|
# TODO(abailey) add support for GROUP already exists (409)
|
|
LOG.exception(e)
|
|
pecan.abort(httpclient.INTERNAL_SERVER_ERROR,
|
|
_('Unable to create subcloud group'))
|
|
|
|
@index.when(method='PATCH', template='json')
|
|
def patch(self, group_ref):
|
|
"""Update a subcloud group.
|
|
|
|
:param group_ref: ID or name of subcloud group to update
|
|
"""
|
|
|
|
policy.authorize(subcloud_group_policy.POLICY_ROOT % "modify", {},
|
|
restcomm.extract_credentials_for_policy())
|
|
context = restcomm.extract_context_from_environ()
|
|
if group_ref is None:
|
|
pecan.abort(httpclient.BAD_REQUEST,
|
|
_('Subcloud Group Name or ID required'))
|
|
|
|
payload = eval(request.body)
|
|
if not payload:
|
|
pecan.abort(httpclient.BAD_REQUEST, _('Body required'))
|
|
|
|
group = utils.subcloud_group_get_by_ref(context, group_ref)
|
|
if group is None:
|
|
pecan.abort(httpclient.NOT_FOUND, _('Subcloud Group not found'))
|
|
|
|
name = payload.get('name')
|
|
description = payload.get('description')
|
|
update_apply_type = payload.get('update_apply_type')
|
|
max_parallel_str = payload.get('max_parallel_subclouds')
|
|
|
|
if not (name or description or update_apply_type or max_parallel_str):
|
|
pecan.abort(httpclient.BAD_REQUEST, _('nothing to update'))
|
|
|
|
# Check value is not None or empty before calling validate
|
|
if name:
|
|
if not self._validate_name(name):
|
|
pecan.abort(httpclient.BAD_REQUEST,
|
|
_('Invalid group name'))
|
|
# Special case. Default group name cannot be changed
|
|
if group.id == consts.DEFAULT_SUBCLOUD_GROUP_ID:
|
|
pecan.abort(httpclient.BAD_REQUEST,
|
|
_('Default group name cannot be changed'))
|
|
|
|
if description:
|
|
if not self._validate_description(description):
|
|
pecan.abort(httpclient.BAD_REQUEST,
|
|
_('Invalid group description'))
|
|
if update_apply_type:
|
|
if not self._validate_update_apply_type(update_apply_type):
|
|
pecan.abort(httpclient.BAD_REQUEST,
|
|
_('Invalid group update_apply_type'))
|
|
if max_parallel_str:
|
|
if not self._validate_max_parallel_subclouds(max_parallel_str):
|
|
pecan.abort(httpclient.BAD_REQUEST,
|
|
_('Invalid group max_parallel_subclouds'))
|
|
|
|
try:
|
|
updated_group = db_api.subcloud_group_update(
|
|
context,
|
|
group.id,
|
|
name=name,
|
|
description=description,
|
|
update_apply_type=update_apply_type,
|
|
max_parallel_subclouds=max_parallel_str)
|
|
return db_api.subcloud_group_db_model_to_dict(updated_group)
|
|
except RemoteError as e:
|
|
pecan.abort(httpclient.UNPROCESSABLE_ENTITY, e.value)
|
|
except Exception as e:
|
|
# additional exceptions.
|
|
LOG.exception(e)
|
|
pecan.abort(httpclient.INTERNAL_SERVER_ERROR,
|
|
_('Unable to update subcloud group'))
|
|
|
|
@index.when(method='delete', template='json')
|
|
def delete(self, group_ref):
|
|
"""Delete the subcloud group."""
|
|
policy.authorize(subcloud_group_policy.POLICY_ROOT % "delete", {},
|
|
restcomm.extract_credentials_for_policy())
|
|
context = restcomm.extract_context_from_environ()
|
|
|
|
if group_ref is None:
|
|
pecan.abort(httpclient.BAD_REQUEST,
|
|
_('Subcloud Group Name or ID required'))
|
|
group = utils.subcloud_group_get_by_ref(context, group_ref)
|
|
if group is None:
|
|
pecan.abort(httpclient.NOT_FOUND, _('Subcloud Group not found'))
|
|
if group.name == consts.DEFAULT_SUBCLOUD_GROUP_NAME:
|
|
pecan.abort(httpclient.BAD_REQUEST,
|
|
_('Default Subcloud Group may not be deleted'))
|
|
try:
|
|
# a subcloud group may not be deleted if it is use by any subclouds
|
|
subclouds = db_api.subcloud_get_for_group(context, group.id)
|
|
if len(subclouds) > 0:
|
|
pecan.abort(httpclient.BAD_REQUEST,
|
|
_('Subcloud Group not empty'))
|
|
db_api.subcloud_group_destroy(context, group.id)
|
|
except RemoteError as e:
|
|
pecan.abort(httpclient.UNPROCESSABLE_ENTITY, e.value)
|
|
except Exception as e:
|
|
LOG.exception(e)
|
|
pecan.abort(httpclient.INTERNAL_SERVER_ERROR,
|
|
_('Unable to delete subcloud group'))
|
|
# This should return nothing
|
|
return None
|