Merge "Support identity groups in DC"
This commit is contained in:
@@ -13,7 +13,7 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
# Copyright (c) 2019 Wind River Systems, Inc.
|
||||
# Copyright (c) 2019-2021 Wind River Systems, Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
@@ -131,3 +131,99 @@ class UsersController(object):
|
||||
except Exception as e:
|
||||
LOG.exception(e)
|
||||
pecan.abort(500, _('Unable to update user'))
|
||||
|
||||
|
||||
class GroupsController(object):
|
||||
VERSION_ALIASES = {
|
||||
'Stein': '1.0',
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
super(GroupsController, self).__init__()
|
||||
|
||||
# to do the version compatibility for future purpose
|
||||
def _determine_version_cap(self, target):
|
||||
version_cap = 1.0
|
||||
return version_cap
|
||||
|
||||
@expose(generic=True, template='json')
|
||||
def index(self):
|
||||
# Route the request to specific methods with parameters
|
||||
pass
|
||||
|
||||
@index.when(method='GET', template='json')
|
||||
def get(self, group_ref=None):
|
||||
"""Get a list of groups."""
|
||||
context = restcomm.extract_context_from_environ()
|
||||
try:
|
||||
if group_ref is None:
|
||||
return db_api.group_get_all(context)
|
||||
|
||||
else:
|
||||
group = db_api.group_get(context, group_ref)
|
||||
return group
|
||||
|
||||
except exceptions.GroupNotFound as e:
|
||||
pecan.abort(404, _("Group not found: %s") % e)
|
||||
|
||||
except Exception as e:
|
||||
LOG.exception(e)
|
||||
pecan.abort(500, _('Unable to get group'))
|
||||
|
||||
@index.when(method='POST', template='json')
|
||||
def post(self):
|
||||
"""Create a new group."""
|
||||
|
||||
context = restcomm.extract_context_from_environ()
|
||||
|
||||
# Convert JSON string in request to Python dict
|
||||
try:
|
||||
payload = json.loads(request.body)
|
||||
except ValueError:
|
||||
pecan.abort(400, _('Request body decoding error'))
|
||||
|
||||
if not payload:
|
||||
pecan.abort(400, _('Body required'))
|
||||
group_name = payload.get('group').get('name')
|
||||
|
||||
if not group_name:
|
||||
pecan.abort(400, _('Group name required'))
|
||||
|
||||
try:
|
||||
# Insert the group into DB tables
|
||||
group_ref = db_api.group_create(context, payload)
|
||||
response.status = 201
|
||||
return (group_ref)
|
||||
|
||||
except Exception as e:
|
||||
LOG.exception(e)
|
||||
pecan.abort(500, _('Unable to create group'))
|
||||
|
||||
@index.when(method='PUT', template='json')
|
||||
def put(self, group_ref=None):
|
||||
"""Update a existing group."""
|
||||
|
||||
context = restcomm.extract_context_from_environ()
|
||||
|
||||
if group_ref is None:
|
||||
pecan.abort(400, _('Group ID required'))
|
||||
|
||||
# Convert JSON string in request to Python dict
|
||||
try:
|
||||
payload = json.loads(request.body)
|
||||
except ValueError:
|
||||
pecan.abort(400, _('Request body decoding error'))
|
||||
|
||||
if not payload:
|
||||
pecan.abort(400, _('Body required'))
|
||||
|
||||
try:
|
||||
# Update the group in DB tables
|
||||
return db_api.group_update(context, group_ref, payload)
|
||||
|
||||
except exceptions.GroupNotFound as e:
|
||||
pecan.abort(404, _("Group not found: %s") % e)
|
||||
|
||||
except Exception as e:
|
||||
LOG.exception(e)
|
||||
pecan.abort(500, _('Unable to update group'))
|
||||
|
@@ -13,7 +13,7 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
# Copyright (c) 2019 Wind River Systems, Inc.
|
||||
# Copyright (c) 2019-2021 Wind River Systems, Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
@@ -42,6 +42,7 @@ class IdentityController(object):
|
||||
|
||||
res_controllers = dict()
|
||||
res_controllers["users"] = identity.UsersController
|
||||
res_controllers["groups"] = identity.GroupsController
|
||||
res_controllers["projects"] = project.ProjectsController
|
||||
res_controllers["roles"] = role.RolesController
|
||||
res_controllers["token-revocation-events"] = \
|
||||
|
@@ -14,7 +14,7 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
# Copyright (c) 2019 Wind River Systems, Inc.
|
||||
# Copyright (c) 2019-2021 Wind River Systems, Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
@@ -79,6 +79,10 @@ class UserNotFound(NotFound):
|
||||
message = _("User with id %(user_id)s doesn't exist.")
|
||||
|
||||
|
||||
class GroupNotFound(NotFound):
|
||||
message = _("Group with id %(group_id)s doesn't exist.")
|
||||
|
||||
|
||||
class ProjectNotFound(NotFound):
|
||||
message = _("Project with id %(project_id)s doesn't exist.")
|
||||
|
||||
|
@@ -13,7 +13,7 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
# Copyright (c) 2019 Wind River Systems, Inc.
|
||||
# Copyright (c) 2019-2021 Wind River Systems, Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
@@ -69,6 +69,32 @@ def user_update(context, user_ref, payload):
|
||||
return IMPL.user_update(context, user_ref, payload)
|
||||
|
||||
|
||||
###################
|
||||
|
||||
# group db methods
|
||||
|
||||
###################
|
||||
|
||||
def group_get_all(context):
|
||||
"""Retrieve all groups."""
|
||||
return IMPL.group_get_all(context)
|
||||
|
||||
|
||||
def group_get(context, group_id):
|
||||
"""Retrieve details of a group."""
|
||||
return IMPL.group_get(context, group_id)
|
||||
|
||||
|
||||
def group_create(context, payload):
|
||||
"""Create a group."""
|
||||
return IMPL.group_create(context, payload)
|
||||
|
||||
|
||||
def group_update(context, group_ref, payload):
|
||||
"""Update a group"""
|
||||
return IMPL.group_update(context, group_ref, payload)
|
||||
|
||||
|
||||
###################
|
||||
|
||||
# project db methods
|
||||
|
@@ -13,7 +13,7 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
# Copyright (c) 2019-2020 Wind River Systems, Inc.
|
||||
# Copyright (c) 2019-2021 Wind River Systems, Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
@@ -230,8 +230,9 @@ def user_get_all(context):
|
||||
user_passwords = {'password': [password for password in passwords
|
||||
if password['local_user_id'] ==
|
||||
local_user['id']]}
|
||||
user_consolidated = dict({'local_user': local_user}.items() +
|
||||
user.items() + user_passwords.items())
|
||||
user_consolidated = dict(list({'local_user': local_user}.items()) +
|
||||
list(user.items()) +
|
||||
list(user_passwords.items()))
|
||||
result.append(user_consolidated)
|
||||
|
||||
return result
|
||||
@@ -328,15 +329,131 @@ def user_update(context, user_id, payload):
|
||||
updated_local_users[0]['id']
|
||||
insert(conn, table, password)
|
||||
# Need to update the actor_id in assignment and system_assignment
|
||||
# tables if the user id is updated
|
||||
# along with the user_id in user_group_membership tables if the
|
||||
# user id is updated
|
||||
if user_id != new_user_id:
|
||||
assignment = {'actor_id': new_user_id}
|
||||
user_group_membership = {'user_id': new_user_id}
|
||||
update(conn, 'assignment', 'actor_id', user_id, assignment)
|
||||
update(conn, 'system_assignment', 'actor_id', user_id, assignment)
|
||||
update(conn, 'user_group_membership', 'user_id', user_id, user_group_membership)
|
||||
|
||||
return user_get(context, new_user_id)
|
||||
|
||||
|
||||
###################
|
||||
|
||||
# identity groups
|
||||
|
||||
###################
|
||||
|
||||
@require_context
|
||||
def group_get_all(context):
|
||||
result = []
|
||||
|
||||
with get_read_connection() as conn:
|
||||
# groups table
|
||||
groups = query(conn, 'group')
|
||||
# user_group_membership table
|
||||
user_group_memberships = query(conn, 'user_group_membership')
|
||||
|
||||
for group in groups:
|
||||
local_user_id_list = [membership['user_id'] for membership
|
||||
in user_group_memberships if
|
||||
membership['group_id'] == group['id']]
|
||||
local_user_id_list.sort()
|
||||
local_user_ids = {'local_user_ids': local_user_id_list}
|
||||
group_consolidated = dict(list({'group': group}.items()) +
|
||||
list(local_user_ids.items()))
|
||||
result.append(group_consolidated)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@require_context
|
||||
def group_get(context, group_id):
|
||||
result = {}
|
||||
|
||||
with get_read_connection() as conn:
|
||||
local_user_id_list = []
|
||||
|
||||
# group table
|
||||
group = query(conn, 'group', 'id', group_id)
|
||||
if not group:
|
||||
raise exception.GroupNotFound(group_id=group_id)
|
||||
result['group'] = group[0]
|
||||
|
||||
# user_group_membership table
|
||||
user_group_memberships = query(conn, 'user_group_membership', 'group_id', group_id)
|
||||
|
||||
for user_group_membership in user_group_memberships:
|
||||
local_user = query(conn, 'local_user', 'user_id', user_group_membership.get('user_id'))
|
||||
if not local_user:
|
||||
raise exception.UserNotFound(user_id=user_group_membership.get('user_id'))
|
||||
local_user_id_list.append(local_user[0]['user_id'])
|
||||
|
||||
result['local_user_ids'] = local_user_id_list
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@require_admin_context
|
||||
def group_create(context, payload):
|
||||
group = payload['group']
|
||||
local_user_ids = payload['local_user_ids']
|
||||
with get_write_connection() as conn:
|
||||
|
||||
insert(conn, 'group', group)
|
||||
|
||||
for local_user_id in local_user_ids:
|
||||
user_group_membership = {'user_id': local_user_id, 'group_id': group['id']}
|
||||
insert(conn, 'user_group_membership', user_group_membership)
|
||||
|
||||
return group_get(context, payload['group']['id'])
|
||||
|
||||
|
||||
@require_admin_context
|
||||
def group_update(context, group_id, payload):
|
||||
with get_write_connection() as conn:
|
||||
new_group_id = group_id
|
||||
if 'group' in payload and 'local_user_ids' in payload:
|
||||
group = payload['group']
|
||||
new_group_id = group.get('id')
|
||||
# local_user_id_list is a sorted list of user IDs that
|
||||
# belong to this group
|
||||
local_user_id_list = payload['local_user_ids']
|
||||
user_group_memberships = query(conn, 'user_group_membership',
|
||||
'group_id', group_id)
|
||||
existing_user_list = [user_group_membership['user_id'] for user_group_membership
|
||||
in user_group_memberships]
|
||||
existing_user_list.sort()
|
||||
deleted = False
|
||||
if (group_id != new_group_id) or (local_user_id_list != existing_user_list):
|
||||
# Foreign key constraint exists on 'group_id' of user_group_membership table
|
||||
# and 'id' of group table. So delete user group membership records before
|
||||
# updating group if groups IDs are different
|
||||
# Alternatively, if there is a discrepency in the user group memberships,
|
||||
# delete and re-create them
|
||||
delete(conn, 'user_group_membership', 'group_id', group_id)
|
||||
deleted = True
|
||||
# Update group table
|
||||
update(conn, 'group', 'id', group_id, group)
|
||||
|
||||
if deleted:
|
||||
for local_user_id in local_user_id_list:
|
||||
item = {'user_id': local_user_id, 'group_id': new_group_id}
|
||||
insert(conn, 'user_group_membership', item)
|
||||
|
||||
# Need to update the actor_id in assignment and system_assignment
|
||||
# tables if the group id is updated
|
||||
if group_id != new_group_id:
|
||||
assignment = {'actor_id': new_group_id}
|
||||
update(conn, 'assignment', 'actor_id', group_id, assignment)
|
||||
update(conn, 'system_assignment', 'actor_id', group_id, assignment)
|
||||
|
||||
return group_get(context, new_group_id)
|
||||
|
||||
|
||||
###################
|
||||
|
||||
# identity projects
|
||||
|
@@ -14,7 +14,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# Copyright (c) 2019 Wind River Systems, Inc.
|
||||
# Copyright (c) 2019-2021 Wind River Systems, Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
@@ -23,7 +23,8 @@ import keystoneauth1.identity.generic as auth_plugin
|
||||
from keystoneauth1 import session as ks_session
|
||||
|
||||
from dcdbsync.dbsyncclient import httpclient
|
||||
from dcdbsync.dbsyncclient.v1.identity import identity_manager as im
|
||||
from dcdbsync.dbsyncclient.v1.identity import identity_group_manager as igm
|
||||
from dcdbsync.dbsyncclient.v1.identity import identity_user_manager as ium
|
||||
from dcdbsync.dbsyncclient.v1.identity import project_manager as pm
|
||||
from dcdbsync.dbsyncclient.v1.identity import role_manager as rm
|
||||
from dcdbsync.dbsyncclient.v1.identity \
|
||||
@@ -97,7 +98,8 @@ class Client(object):
|
||||
)
|
||||
|
||||
# Create all managers
|
||||
self.identity_manager = im.identity_manager(self.http_client)
|
||||
self.identity_user_manager = ium.identity_user_manager(self.http_client)
|
||||
self.identity_group_manager = igm.identity_group_manager(self.http_client)
|
||||
self.project_manager = pm.project_manager(self.http_client)
|
||||
self.role_manager = rm.role_manager(self.http_client)
|
||||
self.revoke_event_manager = trem.revoke_event_manager(self.http_client)
|
||||
|
@@ -0,0 +1,133 @@
|
||||
# Copyright (c) 2017 Ericsson AB.
|
||||
# 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.
|
||||
#
|
||||
# Copyright (c) 2019-2021 Wind River Systems, Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
|
||||
from dcdbsync.dbsyncclient import base
|
||||
from dcdbsync.dbsyncclient.base import get_json
|
||||
from dcdbsync.dbsyncclient import exceptions
|
||||
|
||||
|
||||
class Group(base.Resource):
|
||||
resource_name = 'group'
|
||||
|
||||
def __init__(self, manager, id, domain_id, name,
|
||||
description, local_user_ids, extra={}):
|
||||
self.manager = manager
|
||||
self.id = id
|
||||
self.domain_id = domain_id
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.local_user_ids = local_user_ids
|
||||
self.extra = extra
|
||||
|
||||
def info(self):
|
||||
resource_info = dict()
|
||||
resource_info.update({self.resource_name:
|
||||
{'name': self.name,
|
||||
'id': self.id,
|
||||
'domain_id': self.domain_id}})
|
||||
|
||||
return resource_info
|
||||
|
||||
|
||||
class identity_group_manager(base.ResourceManager):
|
||||
resource_class = Group
|
||||
|
||||
def group_create(self, url, data):
|
||||
resp = self.http_client.post(url, data)
|
||||
|
||||
# Unauthorized request
|
||||
if resp.status_code == 401:
|
||||
raise exceptions.Unauthorized('Unauthorized request.')
|
||||
if resp.status_code != 201:
|
||||
self._raise_api_exception(resp)
|
||||
|
||||
# Converted into python dict
|
||||
json_object = get_json(resp)
|
||||
return json_object
|
||||
|
||||
def group_list(self, url):
|
||||
resp = self.http_client.get(url)
|
||||
|
||||
# Unauthorized
|
||||
if resp.status_code == 401:
|
||||
raise exceptions.Unauthorized('Unauthorized request')
|
||||
if resp.status_code != 200:
|
||||
self._raise_api_exception(resp)
|
||||
|
||||
# Converted into python dict
|
||||
json_objects = get_json(resp)
|
||||
|
||||
groups = []
|
||||
for json_object in json_objects:
|
||||
group = Group(
|
||||
self,
|
||||
id=json_object['group']['id'],
|
||||
domain_id=json_object['group']['domain_id'],
|
||||
name=json_object['group']['name'],
|
||||
extra=json_object['group']['extra'],
|
||||
description=json_object['group']['description'],
|
||||
local_user_ids=json_object['local_user_ids'])
|
||||
|
||||
groups.append(group)
|
||||
|
||||
return groups
|
||||
|
||||
def _group_detail(self, url):
|
||||
resp = self.http_client.get(url)
|
||||
|
||||
# Unauthorized request
|
||||
if resp.status_code == 401:
|
||||
raise exceptions.Unauthorized('Unauthorized request.')
|
||||
if resp.status_code != 200:
|
||||
self._raise_api_exception(resp)
|
||||
|
||||
# Return group details in original json format,
|
||||
# ie, without convert it into python dict
|
||||
return resp.content
|
||||
|
||||
def _group_update(self, url, data):
|
||||
resp = self.http_client.put(url, data)
|
||||
|
||||
# Unauthorized request
|
||||
if resp.status_code == 401:
|
||||
raise exceptions.Unauthorized('Unauthorized request.')
|
||||
if resp.status_code != 200:
|
||||
self._raise_api_exception(resp)
|
||||
|
||||
# Converted into python dict
|
||||
json_object = get_json(resp)
|
||||
return json_object
|
||||
|
||||
def add_group(self, data):
|
||||
url = '/identity/groups/'
|
||||
return self.group_create(url, data)
|
||||
|
||||
def list_groups(self):
|
||||
url = '/identity/groups/'
|
||||
return self.group_list(url)
|
||||
|
||||
def group_detail(self, group_ref):
|
||||
url = '/identity/groups/%s' % group_ref
|
||||
return self._group_detail(url)
|
||||
|
||||
def update_group(self, group_ref, data):
|
||||
url = '/identity/groups/%s' % group_ref
|
||||
return self._group_update(url, data)
|
@@ -84,7 +84,7 @@ class User(base.Resource):
|
||||
return resource_info
|
||||
|
||||
|
||||
class identity_manager(base.ResourceManager):
|
||||
class identity_user_manager(base.ResourceManager):
|
||||
resource_class = User
|
||||
|
||||
def user_create(self, url, data):
|
@@ -789,16 +789,20 @@ class IdentityAPIController(APIController):
|
||||
def _generate_assignment_rid(self, url, environ):
|
||||
resource_id = None
|
||||
# for role assignment or revocation, the URL is of format:
|
||||
# /v3/projects/{project_id}/users/{user_id}/roles/{role_id}
|
||||
# /v3/projects/{project_id}/users/{user_id}/roles/{role_id} or
|
||||
# /v3/projects/{project_id}/groups/{group_id}/roles/{role_id}
|
||||
# We need to extract all ID parameters from the URL
|
||||
role_id = proxy_utils.get_routing_match_value(environ, 'role_id')
|
||||
proj_id = proxy_utils.get_routing_match_value(environ, 'project_id')
|
||||
user_id = proxy_utils.get_routing_match_value(environ, 'user_id')
|
||||
if 'user_id' in proxy_utils.get_routing_match_arguments(environ):
|
||||
actor_id = proxy_utils.get_routing_match_value(environ, 'user_id')
|
||||
else:
|
||||
actor_id = proxy_utils.get_routing_match_value(environ, 'group_id')
|
||||
|
||||
if (not role_id or not proj_id or not user_id):
|
||||
if (not role_id or not proj_id or not actor_id):
|
||||
LOG.error("Malformed Role Assignment or Revocation URL: %s", url)
|
||||
else:
|
||||
resource_id = "{}_{}_{}".format(proj_id, user_id, role_id)
|
||||
resource_id = "{}_{}_{}".format(proj_id, actor_id, role_id)
|
||||
return resource_id
|
||||
|
||||
def _retrieve_token_revoke_event_rid(self, url, environ):
|
||||
@@ -826,7 +830,7 @@ class IdentityAPIController(APIController):
|
||||
resource_type = self._get_resource_type_from_environ(environ)
|
||||
|
||||
# if this is a Role Assignment or Revocation request then
|
||||
# we need to extract Project ID, User ID and Role ID from the
|
||||
# we need to extract Project ID, User ID/Group ID and Role ID from the
|
||||
# URL, and not just the Role ID
|
||||
if (resource_type ==
|
||||
consts.RESOURCE_TYPE_IDENTITY_PROJECT_ROLE_ASSIGNMENTS):
|
||||
@@ -852,6 +856,20 @@ class IdentityAPIController(APIController):
|
||||
if operation_type == consts.OPERATION_TYPE_POST:
|
||||
operation_type = consts.OPERATION_TYPE_PATCH
|
||||
resource_type = consts.RESOURCE_TYPE_IDENTITY_USERS
|
||||
elif (resource_type == consts.RESOURCE_TYPE_IDENTITY_GROUPS
|
||||
and operation_type != consts.OPERATION_TYPE_POST):
|
||||
if("users" in request_header):
|
||||
# Requests for adding a user (PUT) and removing a user (DELETE)
|
||||
# should be converted to a PUT request
|
||||
# The url in this case looks like /groups/{group_id}/users/{user_id}
|
||||
# We need to extract the group_id and assign that to resource_id
|
||||
index = request_header.find("/users")
|
||||
resource_id = self.get_resource_id_from_link(request_header[0:index])
|
||||
resource_info = {'group':
|
||||
{'id': resource_id}}
|
||||
operation_type = consts.OPERATION_TYPE_PUT
|
||||
else:
|
||||
resource_id = self.get_resource_id_from_link(request_header)
|
||||
else:
|
||||
if operation_type == consts.OPERATION_TYPE_POST:
|
||||
# Retrieve the ID from the response
|
||||
|
@@ -287,6 +287,7 @@ IDENTITY_PROJECTS_PATH = [
|
||||
|
||||
IDENTITY_PROJECTS_ROLE_PATH = [
|
||||
'/v3/projects/{project_id}/users/{user_id}/roles/{role_id}',
|
||||
'/v3/projects/{project_id}/groups/{group_id}/roles/{role_id}',
|
||||
]
|
||||
|
||||
IDENTITY_TOKEN_REVOKE_EVENTS_PATH = [
|
||||
@@ -296,6 +297,7 @@ IDENTITY_TOKEN_REVOKE_EVENTS_PATH = [
|
||||
IDENTITY_PATH_MAP = {
|
||||
consts.RESOURCE_TYPE_IDENTITY_USERS: IDENTITY_USERS_PATH,
|
||||
consts.RESOURCE_TYPE_IDENTITY_USERS_PASSWORD: IDENTITY_USERS_PW_PATH,
|
||||
consts.RESOURCE_TYPE_IDENTITY_GROUPS: IDENTITY_USER_GROUPS_PATH,
|
||||
consts.RESOURCE_TYPE_IDENTITY_ROLES: IDENTITY_ROLES_PATH,
|
||||
consts.RESOURCE_TYPE_IDENTITY_PROJECTS: IDENTITY_PROJECTS_PATH,
|
||||
consts.RESOURCE_TYPE_IDENTITY_PROJECT_ROLE_ASSIGNMENTS:
|
||||
@@ -346,6 +348,8 @@ ROUTE_METHOD_MAP = {
|
||||
consts.ENDPOINT_TYPE_IDENTITY: {
|
||||
consts.RESOURCE_TYPE_IDENTITY_USERS:
|
||||
['POST', 'PATCH', 'DELETE'],
|
||||
consts.RESOURCE_TYPE_IDENTITY_GROUPS:
|
||||
['POST', 'PUT', 'PATCH', 'DELETE'],
|
||||
consts.RESOURCE_TYPE_IDENTITY_USERS_PASSWORD:
|
||||
['POST'],
|
||||
consts.RESOURCE_TYPE_IDENTITY_ROLES:
|
||||
|
@@ -106,6 +106,7 @@ RESOURCE_TYPE_QOS_POLICY = "qos"
|
||||
|
||||
# Identity Resources
|
||||
RESOURCE_TYPE_IDENTITY_USERS = "users"
|
||||
RESOURCE_TYPE_IDENTITY_GROUPS = "groups"
|
||||
RESOURCE_TYPE_IDENTITY_USERS_PASSWORD = "users_password"
|
||||
RESOURCE_TYPE_IDENTITY_ROLES = "roles"
|
||||
RESOURCE_TYPE_IDENTITY_PROJECTS = "projects"
|
||||
|
@@ -1,4 +1,4 @@
|
||||
# Copyright 2018-2020 Wind River
|
||||
# Copyright 2018-2021 Wind River
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -46,6 +46,8 @@ class IdentitySyncThread(SyncThread):
|
||||
self.sync_handler_map = {
|
||||
consts.RESOURCE_TYPE_IDENTITY_USERS:
|
||||
self.sync_identity_resource,
|
||||
consts.RESOURCE_TYPE_IDENTITY_GROUPS:
|
||||
self.sync_identity_resource,
|
||||
consts.RESOURCE_TYPE_IDENTITY_USERS_PASSWORD:
|
||||
self.sync_identity_resource,
|
||||
consts.RESOURCE_TYPE_IDENTITY_ROLES:
|
||||
@@ -63,6 +65,7 @@ class IdentitySyncThread(SyncThread):
|
||||
# that users are replicated prior to assignment data (roles/projects)
|
||||
self.audit_resources = [
|
||||
consts.RESOURCE_TYPE_IDENTITY_USERS,
|
||||
consts.RESOURCE_TYPE_IDENTITY_GROUPS,
|
||||
consts.RESOURCE_TYPE_IDENTITY_PROJECTS,
|
||||
consts.RESOURCE_TYPE_IDENTITY_ROLES,
|
||||
consts.RESOURCE_TYPE_IDENTITY_PROJECT_ROLE_ASSIGNMENTS,
|
||||
@@ -79,6 +82,8 @@ class IdentitySyncThread(SyncThread):
|
||||
consts.RESOURCE_TYPE_IDENTITY_ROLES:
|
||||
['heat_stack_owner', 'heat_stack_user', 'ResellerAdmin'],
|
||||
consts.RESOURCE_TYPE_IDENTITY_PROJECTS:
|
||||
[],
|
||||
consts.RESOURCE_TYPE_IDENTITY_GROUPS:
|
||||
[]
|
||||
}
|
||||
|
||||
@@ -109,8 +114,8 @@ class IdentitySyncThread(SyncThread):
|
||||
# sysinv, and dcmanager users are special cases as the id's will match
|
||||
# (as this is forced during the subcloud deploy) but the details will
|
||||
# not so we still need to sync them here.
|
||||
m_client = self.get_dbs_client(self.master_region_name).identity_manager
|
||||
sc_client = self.get_dbs_client(self.region_name).identity_manager
|
||||
m_client = self.get_dbs_client(self.master_region_name).identity_user_manager
|
||||
sc_client = self.get_dbs_client(self.region_name).identity_user_manager
|
||||
|
||||
for m_user in m_users:
|
||||
for sc_user in sc_users:
|
||||
@@ -141,7 +146,7 @@ class IdentitySyncThread(SyncThread):
|
||||
sdk.OpenStackDriver.delete_region_clients(self.region_name)
|
||||
# Retry with a new token
|
||||
sc_client = self.get_dbs_client(
|
||||
self.region_name).identity_manager
|
||||
self.region_name).identity_user_manager
|
||||
user_ref = sc_client.update_user(sc_user.id,
|
||||
user_records)
|
||||
if not user_ref:
|
||||
@@ -149,6 +154,45 @@ class IdentitySyncThread(SyncThread):
|
||||
" in subcloud.".format(sc_user.id))
|
||||
raise exceptions.SyncRequestFailed
|
||||
|
||||
def _initial_sync_groups(self, m_groups, sc_groups):
|
||||
# Particularly sync groups with same name but different ID.
|
||||
m_client = self.get_dbs_client(self.master_region_name).identity_group_manager
|
||||
sc_client = self.get_dbs_client(self.region_name).identity_group_manager
|
||||
|
||||
for m_group in m_groups:
|
||||
for sc_group in sc_groups:
|
||||
if (m_group.name == sc_group.name and
|
||||
m_group.domain_id == sc_group.domain_id and
|
||||
m_group.id != sc_group.id):
|
||||
group_records = m_client.group_detail(m_group.id)
|
||||
if not group_records:
|
||||
LOG.error("No data retrieved from master cloud for"
|
||||
" group {} to update its equivalent in"
|
||||
" subcloud.".format(m_group.id))
|
||||
raise exceptions.SyncRequestFailed
|
||||
# update the group by pushing down the DB records to
|
||||
# subcloud
|
||||
try:
|
||||
group_ref = sc_client.update_group(sc_group.id,
|
||||
group_records)
|
||||
# Retry once if unauthorized
|
||||
except dbsync_exceptions.Unauthorized as e:
|
||||
LOG.info("Update group {} request failed for {}: {}."
|
||||
.format(sc_group.id,
|
||||
self.region_name, str(e)))
|
||||
# Clear the cache so that the old token will not be validated
|
||||
sdk.OpenStackDriver.delete_region_clients(self.region_name)
|
||||
sc_client = self.get_dbs_client(
|
||||
self.region_name).identity_group_manager
|
||||
group_ref = sc_client.update_group(sc_group.id,
|
||||
group_records)
|
||||
|
||||
if not group_ref:
|
||||
LOG.error("No group data returned when updating"
|
||||
" group {} in subcloud.".
|
||||
format(sc_group.id))
|
||||
raise exceptions.SyncRequestFailed
|
||||
|
||||
def _initial_sync_projects(self, m_projects, sc_projects):
|
||||
# Particularly sync projects with same name but different ID.
|
||||
m_client = self.get_dbs_client(self.master_region_name).project_manager
|
||||
@@ -232,10 +276,10 @@ class IdentitySyncThread(SyncThread):
|
||||
# before dcorch starts to audit resources. Later on when dcorch audits
|
||||
# and sync them over(including their IDs) to the subcloud, running
|
||||
# services at the subcloud with tokens issued before their ID are
|
||||
# changed will get user/project not found error since their IDs are
|
||||
# changed will get user/group/project not found error since their IDs are
|
||||
# changed. This will continue until their tokens expire in up to
|
||||
# 1 hour. Before that these services basically stop working.
|
||||
# By an initial synchronization on existing users/projects,
|
||||
# By an initial synchronization on existing users/groups/projects,
|
||||
# synchronously followed by a fernet keys synchronization, existing
|
||||
# tokens at subcloud are revoked and services are forced to
|
||||
# re-authenticate to get new tokens. This significantly decreases
|
||||
@@ -261,6 +305,25 @@ class IdentitySyncThread(SyncThread):
|
||||
|
||||
self._initial_sync_users(m_users, sc_users)
|
||||
|
||||
# get groups from master cloud
|
||||
m_groups = self.get_master_resources(
|
||||
consts.RESOURCE_TYPE_IDENTITY_GROUPS)
|
||||
|
||||
if not m_groups:
|
||||
LOG.info("No groups returned from {}".
|
||||
format(dccommon_consts.VIRTUAL_MASTER_CLOUD))
|
||||
|
||||
# get groups from the subcloud
|
||||
sc_groups = self.get_subcloud_resources(
|
||||
consts.RESOURCE_TYPE_IDENTITY_GROUPS)
|
||||
|
||||
if not sc_groups:
|
||||
LOG.info("No groups returned from subcloud {}".
|
||||
format(self.region_name))
|
||||
|
||||
if m_groups and sc_groups:
|
||||
self._initial_sync_groups(m_groups, sc_groups)
|
||||
|
||||
# get projects from master cloud
|
||||
m_projects = self.get_master_resources(
|
||||
consts.RESOURCE_TYPE_IDENTITY_PROJECTS)
|
||||
@@ -368,7 +431,7 @@ class IdentitySyncThread(SyncThread):
|
||||
# format
|
||||
try:
|
||||
user_records = self.get_dbs_client(self.master_region_name).\
|
||||
identity_manager.user_detail(user_id)
|
||||
identity_user_manager.user_detail(user_id)
|
||||
except dbsync_exceptions.Unauthorized:
|
||||
raise dbsync_exceptions.UnauthorizedMaster
|
||||
if not user_records:
|
||||
@@ -379,7 +442,7 @@ class IdentitySyncThread(SyncThread):
|
||||
|
||||
# Create the user on subcloud by pushing the DB records to subcloud
|
||||
user_ref = self.get_dbs_client(
|
||||
self.region_name).identity_manager.add_user(
|
||||
self.region_name).identity_user_manager.add_user(
|
||||
user_records)
|
||||
if not user_ref:
|
||||
LOG.error("No user data returned when creating user {} in"
|
||||
@@ -421,7 +484,7 @@ class IdentitySyncThread(SyncThread):
|
||||
# format
|
||||
try:
|
||||
user_records = self.get_dbs_client(self.master_region_name).\
|
||||
identity_manager.user_detail(user_id)
|
||||
identity_user_manager.user_detail(user_id)
|
||||
except dbsync_exceptions.Unauthorized:
|
||||
raise dbsync_exceptions.UnauthorizedMaster
|
||||
if not user_records:
|
||||
@@ -432,7 +495,7 @@ class IdentitySyncThread(SyncThread):
|
||||
|
||||
# Update the corresponding user on subcloud by pushing the DB records
|
||||
# to subcloud
|
||||
user_ref = self.get_dbs_client(self.region_name).identity_manager.\
|
||||
user_ref = self.get_dbs_client(self.region_name).identity_user_manager.\
|
||||
update_user(sc_user_id, user_records)
|
||||
if not user_ref:
|
||||
LOG.error("No user data returned when updating user {} in"
|
||||
@@ -531,6 +594,180 @@ class IdentitySyncThread(SyncThread):
|
||||
extra=self.log_extra)
|
||||
user_subcloud_rsrc.delete()
|
||||
|
||||
def post_groups(self, request, rsrc):
|
||||
# Create this group on this subcloud
|
||||
# The DB level resource creation process is, retrieve the resource
|
||||
# records from master cloud by its ID, send the records in its original
|
||||
# JSON format by REST call to the DB synchronization service on this
|
||||
# subcloud, which then inserts the resource records into DB tables.
|
||||
group_id = request.orch_job.source_resource_id
|
||||
if not group_id:
|
||||
LOG.error("Received group create request without required "
|
||||
"'source_resource_id' field", extra=self.log_extra)
|
||||
raise exceptions.SyncRequestFailed
|
||||
|
||||
# Retrieve DB records of the group just created. The records is in JSON
|
||||
# format
|
||||
try:
|
||||
group_records = self.get_dbs_client(self.master_region_name).\
|
||||
identity_group_manager.group_detail(group_id)
|
||||
except dbsync_exceptions.Unauthorized:
|
||||
raise dbsync_exceptions.UnauthorizedMaster
|
||||
if not group_records:
|
||||
LOG.error("No data retrieved from master cloud for group {} to"
|
||||
" create its equivalent in subcloud.".format(group_id),
|
||||
extra=self.log_extra)
|
||||
raise exceptions.SyncRequestFailed
|
||||
|
||||
# Create the group on subcloud by pushing the DB records to subcloud
|
||||
group_ref = self.get_dbs_client(
|
||||
self.region_name).identity_group_manager.add_group(
|
||||
group_records)
|
||||
if not group_ref:
|
||||
LOG.error("No group data returned when creating group {} in"
|
||||
" subcloud.".format(group_id), extra=self.log_extra)
|
||||
raise exceptions.SyncRequestFailed
|
||||
|
||||
# Persist the subcloud resource.
|
||||
group_ref_id = group_ref.get('group').get('id')
|
||||
subcloud_rsrc_id = self.persist_db_subcloud_resource(rsrc.id,
|
||||
group_ref_id)
|
||||
groupname = group_ref.get('group').get('name')
|
||||
LOG.info("Created Keystone group {}:{} [{}]"
|
||||
.format(rsrc.id, subcloud_rsrc_id, groupname),
|
||||
extra=self.log_extra)
|
||||
|
||||
def put_groups(self, request, rsrc):
|
||||
# Update this group on this subcloud
|
||||
# The DB level resource update process is, retrieve the resource
|
||||
# records from master cloud by its ID, send the records in its original
|
||||
# JSON format by REST call to the DB synchronization service on this
|
||||
# subcloud, which then updates the resource records in its DB tables.
|
||||
group_id = request.orch_job.source_resource_id
|
||||
if not group_id:
|
||||
LOG.error("Received group update request without required "
|
||||
"source resource id", extra=self.log_extra)
|
||||
raise exceptions.SyncRequestFailed
|
||||
|
||||
group_dict = jsonutils.loads(request.orch_job.resource_info)
|
||||
if 'group' in group_dict:
|
||||
group_dict = group_dict['group']
|
||||
|
||||
sc_group_id = group_dict.pop('id', None)
|
||||
if not sc_group_id:
|
||||
LOG.error("Received group update request without required "
|
||||
"subcloud resource id", extra=self.log_extra)
|
||||
raise exceptions.SyncRequestFailed
|
||||
|
||||
# Retrieve DB records of the group. The records is in JSON
|
||||
# format
|
||||
try:
|
||||
group_records = self.get_dbs_client(self.master_region_name).\
|
||||
identity_group_manager.group_detail(group_id)
|
||||
except dbsync_exceptions.Unauthorized:
|
||||
raise dbsync_exceptions.UnauthorizedMaster
|
||||
if not group_records:
|
||||
LOG.error("No data retrieved from master cloud for group {} to"
|
||||
" update its equivalent in subcloud.".format(group_id),
|
||||
extra=self.log_extra)
|
||||
raise exceptions.SyncRequestFailed
|
||||
|
||||
# Update the corresponding group on subcloud by pushing the DB records
|
||||
# to subcloud
|
||||
group_ref = self.get_dbs_client(self.region_name).identity_group_manager.\
|
||||
update_group(sc_group_id, group_records)
|
||||
if not group_ref:
|
||||
LOG.error("No group data returned when updating group {} in"
|
||||
" subcloud.".format(sc_group_id), extra=self.log_extra)
|
||||
raise exceptions.SyncRequestFailed
|
||||
|
||||
# Persist the subcloud resource.
|
||||
group_ref_id = group_ref.get('group').get('id')
|
||||
subcloud_rsrc_id = self.persist_db_subcloud_resource(rsrc.id,
|
||||
group_ref_id)
|
||||
groupname = group_ref.get('group').get('name')
|
||||
LOG.info("Updated Keystone group {}:{} [{}]"
|
||||
.format(rsrc.id, subcloud_rsrc_id, groupname),
|
||||
extra=self.log_extra)
|
||||
|
||||
def patch_groups(self, request, rsrc):
|
||||
# Update group reference on this subcloud
|
||||
group_update_dict = jsonutils.loads(request.orch_job.resource_info)
|
||||
if not group_update_dict.keys():
|
||||
LOG.error("Received group update request "
|
||||
"without any update fields", extra=self.log_extra)
|
||||
raise exceptions.SyncRequestFailed
|
||||
|
||||
group_update_dict = group_update_dict['group']
|
||||
group_subcloud_rsrc = self.get_db_subcloud_resource(rsrc.id)
|
||||
if not group_subcloud_rsrc:
|
||||
LOG.error("Unable to update group reference {}:{}, "
|
||||
"cannot find equivalent Keystone group in subcloud."
|
||||
.format(rsrc, group_update_dict),
|
||||
extra=self.log_extra)
|
||||
return
|
||||
|
||||
# instead of stowing the entire group reference or
|
||||
# retrieving it, we build an opaque wrapper for the
|
||||
# v3 Group Manager, containing the ID field which is
|
||||
# needed to update this group reference
|
||||
GroupReferenceWrapper = namedtuple('GroupReferenceWrapper',
|
||||
'id')
|
||||
group_id = group_subcloud_rsrc.subcloud_resource_id
|
||||
original_group_ref = GroupReferenceWrapper(id=group_id)
|
||||
|
||||
sc_ks_client = self.get_ks_client(self.region_name)
|
||||
# Update the group in the subcloud
|
||||
group_ref = sc_ks_client.groups.update(
|
||||
original_group_ref,
|
||||
name=group_update_dict.pop('name', None),
|
||||
domain=group_update_dict.pop('domain', None),
|
||||
description=group_update_dict.pop('description', None))
|
||||
|
||||
if group_ref.id == group_id:
|
||||
LOG.info("Updated Keystone group: {}:{}"
|
||||
.format(rsrc.id, group_ref.id), extra=self.log_extra)
|
||||
else:
|
||||
LOG.error("Unable to update Keystone group {}:{} for subcloud"
|
||||
.format(rsrc.id, group_id), extra=self.log_extra)
|
||||
|
||||
def delete_groups(self, request, rsrc):
|
||||
# Delete group reference on this subcloud
|
||||
group_subcloud_rsrc = self.get_db_subcloud_resource(rsrc.id)
|
||||
if not group_subcloud_rsrc:
|
||||
LOG.error("Unable to delete group reference {}, "
|
||||
"cannot find equivalent Keystone group in subcloud."
|
||||
.format(rsrc), extra=self.log_extra)
|
||||
return
|
||||
|
||||
# instead of stowing the entire group reference or
|
||||
# retrieving it, we build an opaque wrapper for the
|
||||
# v3 User Manager, containing the ID field which is
|
||||
# needed to delete this group reference
|
||||
GroupReferenceWrapper = namedtuple('GroupReferenceWrapper',
|
||||
'id')
|
||||
group_id = group_subcloud_rsrc.subcloud_resource_id
|
||||
original_group_ref = GroupReferenceWrapper(id=group_id)
|
||||
|
||||
# Delete the group in the subcloud
|
||||
try:
|
||||
sc_ks_client = self.get_ks_client(self.region_name)
|
||||
sc_ks_client.groups.delete(original_group_ref)
|
||||
except keystone_exceptions.NotFound:
|
||||
LOG.info("Delete group: group {} not found in {}, "
|
||||
"considered as deleted.".
|
||||
format(original_group_ref.id,
|
||||
self.region_name),
|
||||
extra=self.log_extra)
|
||||
|
||||
# Master Resource can be deleted only when all subcloud resources
|
||||
# are deleted along with corresponding orch_job and orch_requests.
|
||||
LOG.info("Keystone group {}:{} [{}] deleted"
|
||||
.format(rsrc.id, group_subcloud_rsrc.id,
|
||||
group_subcloud_rsrc.subcloud_resource_id),
|
||||
extra=self.log_extra)
|
||||
group_subcloud_rsrc.delete()
|
||||
|
||||
def post_projects(self, request, rsrc):
|
||||
# Create this project on this subcloud
|
||||
# The DB level resource creation process is, retrieve the resource
|
||||
@@ -881,7 +1118,7 @@ class IdentitySyncThread(SyncThread):
|
||||
role_subcloud_rsrc.delete()
|
||||
|
||||
def post_project_role_assignments(self, request, rsrc):
|
||||
# Assign this role to user on project on this subcloud
|
||||
# Assign this role to user/group on project on this subcloud
|
||||
# Project role assignments creation is still using keystone APIs since
|
||||
# the APIs can be used to sync them.
|
||||
resource_tags = rsrc.master_id.split('_')
|
||||
@@ -892,7 +1129,8 @@ class IdentitySyncThread(SyncThread):
|
||||
raise exceptions.SyncRequestFailed
|
||||
|
||||
project_id = resource_tags[0]
|
||||
user_id = resource_tags[1]
|
||||
# actor_id can be either user_id or group_id
|
||||
actor_id = resource_tags[1]
|
||||
role_id = resource_tags[2]
|
||||
|
||||
# Ensure that we have already synced the project, user and role
|
||||
@@ -928,31 +1166,50 @@ class IdentitySyncThread(SyncThread):
|
||||
sc_user = None
|
||||
sc_user_list = self._get_all_users(sc_ks_client)
|
||||
for user in sc_user_list:
|
||||
if user.id == user_id:
|
||||
if user.id == actor_id:
|
||||
sc_user = user
|
||||
break
|
||||
if not sc_user:
|
||||
LOG.error("Unable to assign role to user on project reference {}:"
|
||||
"{}, cannot find equivalent Keystone User in subcloud."
|
||||
.format(rsrc, user_id),
|
||||
sc_group = None
|
||||
sc_group_list = self._get_all_groups(sc_ks_client)
|
||||
for group in sc_group_list:
|
||||
if group.id == actor_id:
|
||||
sc_group = group
|
||||
break
|
||||
if not sc_user and not sc_group:
|
||||
LOG.error("Unable to assign role to user/group on project reference {}:"
|
||||
"{}, cannot find equivalent Keystone User/Group in subcloud."
|
||||
.format(rsrc, actor_id),
|
||||
extra=self.log_extra)
|
||||
raise exceptions.SyncRequestFailed
|
||||
|
||||
# Create role assignment
|
||||
sc_ks_client.roles.grant(
|
||||
sc_role,
|
||||
user=sc_user,
|
||||
project=sc_proj)
|
||||
role_ref = sc_ks_client.role_assignments.list(
|
||||
user=sc_user,
|
||||
project=sc_proj,
|
||||
role=sc_role)
|
||||
if sc_user:
|
||||
sc_ks_client.roles.grant(
|
||||
sc_role,
|
||||
user=sc_user,
|
||||
project=sc_proj)
|
||||
role_ref = sc_ks_client.role_assignments.list(
|
||||
user=sc_user,
|
||||
project=sc_proj,
|
||||
role=sc_role)
|
||||
elif sc_group:
|
||||
sc_ks_client.roles.grant(
|
||||
sc_role,
|
||||
group=sc_group,
|
||||
project=sc_proj)
|
||||
role_ref = sc_ks_client.role_assignments.list(
|
||||
group=sc_group,
|
||||
project=sc_proj,
|
||||
role=sc_role)
|
||||
|
||||
if role_ref:
|
||||
LOG.info("Added Keystone role assignment: {}:{}"
|
||||
.format(rsrc.id, role_ref), extra=self.log_extra)
|
||||
# Persist the subcloud resource.
|
||||
sc_rid = sc_proj.id + '_' + sc_user.id + '_' + sc_role.id
|
||||
if sc_user:
|
||||
sc_rid = sc_proj.id + '_' + sc_user.id + '_' + sc_role.id
|
||||
elif sc_group:
|
||||
sc_rid = sc_proj.id + '_' + sc_group.id + '_' + sc_role.id
|
||||
subcloud_rsrc_id = self.persist_db_subcloud_resource(rsrc.id,
|
||||
sc_rid)
|
||||
LOG.info("Created Keystone role assignment {}:{} [{}]"
|
||||
@@ -986,33 +1243,55 @@ class IdentitySyncThread(SyncThread):
|
||||
resource_tags = subcloud_rid.split('_')
|
||||
if len(resource_tags) < 3:
|
||||
LOG.error("Malformed subcloud resource tag {}, expected to be in "
|
||||
"format: ProjectID_UserID_RoleID."
|
||||
"format: ProjectID_UserID_RoleID or ProjectID_GroupID_RoleID."
|
||||
.format(assignment_subcloud_rsrc), extra=self.log_extra)
|
||||
assignment_subcloud_rsrc.delete()
|
||||
return
|
||||
|
||||
project_id = resource_tags[0]
|
||||
user_id = resource_tags[1]
|
||||
actor_id = resource_tags[1]
|
||||
role_id = resource_tags[2]
|
||||
|
||||
# Revoke role assignment
|
||||
actor = None
|
||||
try:
|
||||
sc_ks_client = self.get_ks_client(self.region_name)
|
||||
sc_ks_client.roles.revoke(
|
||||
role_id,
|
||||
user=user_id,
|
||||
user=actor_id,
|
||||
project=project_id)
|
||||
actor = 'user'
|
||||
except keystone_exceptions.NotFound:
|
||||
LOG.info("Revoke role assignment: (role {}, user {}, project {})"
|
||||
" not found in {}, considered as deleted.".
|
||||
format(role_id, user_id, project_id,
|
||||
format(role_id, actor_id, project_id,
|
||||
self.region_name),
|
||||
extra=self.log_extra)
|
||||
try:
|
||||
sc_ks_client = self.get_ks_client(self.region_name)
|
||||
sc_ks_client.roles.revoke(
|
||||
role_id,
|
||||
group=actor_id,
|
||||
project=project_id)
|
||||
actor = 'group'
|
||||
except keystone_exceptions.NotFound:
|
||||
LOG.info("Revoke role assignment: (role {}, group {}, project {})"
|
||||
" not found in {}, considered as deleted.".
|
||||
format(role_id, actor_id, project_id,
|
||||
self.region_name),
|
||||
extra=self.log_extra)
|
||||
|
||||
role_ref = sc_ks_client.role_assignments.list(
|
||||
user=user_id,
|
||||
project=project_id,
|
||||
role=role_id)
|
||||
role_ref = None
|
||||
if actor == 'user':
|
||||
role_ref = sc_ks_client.role_assignments.list(
|
||||
user=actor_id,
|
||||
project=project_id,
|
||||
role=role_id)
|
||||
elif actor == 'group':
|
||||
role_ref = sc_ks_client.role_assignments.list(
|
||||
group=actor_id,
|
||||
project=project_id,
|
||||
role=role_id)
|
||||
|
||||
if not role_ref:
|
||||
LOG.info("Deleted Keystone role assignment: {}:{}"
|
||||
@@ -1182,7 +1461,9 @@ class IdentitySyncThread(SyncThread):
|
||||
# ---- Override common audit functions ----
|
||||
def _get_resource_audit_handler(self, resource_type, client):
|
||||
if resource_type == consts.RESOURCE_TYPE_IDENTITY_USERS:
|
||||
return self._get_users_resource(client.identity_manager)
|
||||
return self._get_users_resource(client.identity_user_manager)
|
||||
if resource_type == consts.RESOURCE_TYPE_IDENTITY_GROUPS:
|
||||
return self._get_groups_resource(client.identity_group_manager)
|
||||
elif resource_type == consts.RESOURCE_TYPE_IDENTITY_ROLES:
|
||||
return self._get_roles_resource(client.role_manager)
|
||||
elif resource_type == consts.RESOURCE_TYPE_IDENTITY_PROJECTS:
|
||||
@@ -1211,6 +1492,14 @@ class IdentitySyncThread(SyncThread):
|
||||
users = users + domain_users
|
||||
return users
|
||||
|
||||
def _get_all_groups(self, client):
|
||||
domains = client.domains.list()
|
||||
groups = []
|
||||
for domain in domains:
|
||||
domain_groups = client.groups.list(domain=domain)
|
||||
groups = groups + domain_groups
|
||||
return groups
|
||||
|
||||
def _get_users_resource(self, client):
|
||||
try:
|
||||
services = []
|
||||
@@ -1251,6 +1540,33 @@ class IdentitySyncThread(SyncThread):
|
||||
# None will force skip of audit
|
||||
return None
|
||||
|
||||
def _get_groups_resource(self, client):
|
||||
try:
|
||||
# get groups from DB API
|
||||
if hasattr(client, 'list_groups'):
|
||||
groups = client.list_groups()
|
||||
# get groups from keystone API
|
||||
else:
|
||||
groups = client.groups.list()
|
||||
|
||||
# Filter out admin or services projects
|
||||
filtered_list = self.filtered_audit_resources[
|
||||
consts.RESOURCE_TYPE_IDENTITY_GROUPS]
|
||||
|
||||
filtered_groups = [group for group in groups if
|
||||
all(group.name != filtered for
|
||||
filtered in filtered_list)]
|
||||
return filtered_groups
|
||||
except (keystone_exceptions.connection.ConnectTimeout,
|
||||
keystone_exceptions.ConnectFailure,
|
||||
dbsync_exceptions.ConnectTimeout,
|
||||
dbsync_exceptions.ConnectFailure) as e:
|
||||
LOG.info("Group Audit: subcloud {} is not reachable [{}]"
|
||||
.format(self.region_name,
|
||||
str(e)), extra=self.log_extra)
|
||||
# None will force skip of audit
|
||||
return None
|
||||
|
||||
def _get_roles_resource(self, client):
|
||||
try:
|
||||
# get roles from DB API
|
||||
@@ -1316,22 +1632,28 @@ class IdentitySyncThread(SyncThread):
|
||||
roles = self._get_roles_resource(client)
|
||||
projects = self._get_projects_resource(client)
|
||||
users = self._get_users_resource(client)
|
||||
groups = self._get_groups_resource(client)
|
||||
for assignment in assignments:
|
||||
if 'project' not in assignment.scope:
|
||||
# this is a domain scoped role, we don't care
|
||||
# about syncing or auditing them for now
|
||||
continue
|
||||
role_id = assignment.role['id']
|
||||
user_id = assignment.user['id']
|
||||
actor_id = assignment.user['id'] if hasattr(assignment, 'user') else assignment.group['id']
|
||||
project_id = assignment.scope['project']['id']
|
||||
assignment_dict = {}
|
||||
|
||||
for user in users:
|
||||
if user.id == user_id:
|
||||
assignment_dict['user'] = user
|
||||
if user.id == actor_id:
|
||||
assignment_dict['actor'] = user
|
||||
break
|
||||
else:
|
||||
continue
|
||||
for group in groups:
|
||||
if group.id == actor_id:
|
||||
assignment_dict['actor'] = group
|
||||
break
|
||||
else:
|
||||
continue
|
||||
|
||||
for role in roles:
|
||||
if role.id == role_id:
|
||||
@@ -1350,7 +1672,7 @@ class IdentitySyncThread(SyncThread):
|
||||
# The id of a Role Assigment is:
|
||||
# projectID_userID_roleID
|
||||
assignment_dict['id'] = "{}_{}_{}".format(
|
||||
project_id, user_id, role_id)
|
||||
project_id, actor_id, role_id)
|
||||
|
||||
# Build an opaque object wrapper for this RoleAssignment
|
||||
refactored_assignment = namedtuple(
|
||||
@@ -1411,15 +1733,15 @@ class IdentitySyncThread(SyncThread):
|
||||
# None will force skip of audit
|
||||
return None
|
||||
|
||||
def _same_identity_resource(self, m, sc):
|
||||
def _same_identity_user_resource(self, m, sc):
|
||||
LOG.debug("master={}, subcloud={}".format(m, sc),
|
||||
extra=self.log_extra)
|
||||
# For user the comparison is DB records by DB records.
|
||||
# The user DB records are from multiple tables, including user,
|
||||
# local_user, and password tables. If any of them are not matched,
|
||||
# it is considered not a same identity resource.
|
||||
# Note that the user id is compared, since user id is to be synced
|
||||
# to subcloud too.
|
||||
# it is considered as a different identity resource.
|
||||
# Note that the user id is compared, since user id has to be synced
|
||||
# to the subcloud too.
|
||||
same_user = (m.id == sc.id and
|
||||
m.domain_id == sc.domain_id and
|
||||
m.default_project_id == sc.default_project_id and
|
||||
@@ -1451,7 +1773,31 @@ class IdentitySyncThread(SyncThread):
|
||||
result = True
|
||||
return result
|
||||
|
||||
def _has_same_identity_ids(self, m, sc):
|
||||
def _same_identity_group_resource(self, m, sc):
|
||||
LOG.debug("master={}, subcloud={}".format(m, sc),
|
||||
extra=self.log_extra)
|
||||
# For group the comparison is DB records by DB records.
|
||||
# The group DB records are from two tables - group and
|
||||
# user_group_membership tables. If any of them are not matched,
|
||||
# it is considered as different identity resource.
|
||||
# Note that the group id is compared, since group id has to be synced
|
||||
# to the subcloud too.
|
||||
same_group = (m.id == sc.id and
|
||||
m.domain_id == sc.domain_id and
|
||||
m.description == sc.description and
|
||||
m.name == sc.name and
|
||||
m.extra == sc.extra)
|
||||
if not same_group:
|
||||
return False
|
||||
|
||||
same_local_user_ids = (m.local_user_ids ==
|
||||
sc.local_user_ids)
|
||||
if not same_local_user_ids:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _has_same_identity_user_ids(self, m, sc):
|
||||
# If (user name + domain name) or use id is the same,
|
||||
# the resources are considered to be the same resource.
|
||||
# Any difference in other attributes will trigger an update (PUT)
|
||||
@@ -1459,6 +1805,14 @@ class IdentitySyncThread(SyncThread):
|
||||
return ((m.local_user.name == sc.local_user.name and
|
||||
m.domain_id == sc.domain_id) or m.id == sc.id)
|
||||
|
||||
def _has_same_identity_group_ids(self, m, sc):
|
||||
# If (group name + domain name) or group id is the same,
|
||||
# then the resources are considered to be the same.
|
||||
# Any difference in other attributes will trigger an update (PUT)
|
||||
# to that resource in subcloud.
|
||||
return ((m.name == sc.name and m.domain_id == sc.domain_id)
|
||||
or m.id == sc.id)
|
||||
|
||||
def _same_project_resource(self, m, sc):
|
||||
LOG.debug("master={}, subcloud={}".format(m, sc),
|
||||
extra=self.log_extra)
|
||||
@@ -1510,7 +1864,7 @@ class IdentitySyncThread(SyncThread):
|
||||
LOG.debug("same_assignment master={}, subcloud={}".format(m, sc),
|
||||
extra=self.log_extra)
|
||||
# For an assignment to be the same, all 3 of its role, project and
|
||||
# user information must match up.
|
||||
# actor (user/group) information must match up.
|
||||
# Compare by names here is fine, since this comparison gets called
|
||||
# only if the mapped subcloud assignment is found by id in subcloud
|
||||
# resources just retrieved. In another word, the ids are guaranteed
|
||||
@@ -1518,8 +1872,8 @@ class IdentitySyncThread(SyncThread):
|
||||
# audit_find_missing(). same_resource() in audit_find_missing() is
|
||||
# actually redundant for assignment but it's the generic algorithm
|
||||
# for all types of resources.
|
||||
return((m.user.name == sc.user.name and
|
||||
m.user.domain_id == sc.user.domain_id) and
|
||||
return((m.actor.name == sc.actor.name and
|
||||
m.actor.domain_id == sc.actor.domain_id) and
|
||||
(m.role.name == sc.role.name and
|
||||
m.role.domain_id == sc.role.domain_id) and
|
||||
(m.project.name == sc.project.name and
|
||||
@@ -1558,7 +1912,7 @@ class IdentitySyncThread(SyncThread):
|
||||
|
||||
def get_master_resources(self, resource_type):
|
||||
# Retrieve master resources from DB or through Keystone.
|
||||
# users, projects, roles, and token revocation events use
|
||||
# users, groups, projects, roles, and token revocation events use
|
||||
# dbsync client, other resources use keystone client.
|
||||
if self.is_resource_handled_by_dbs_client(resource_type):
|
||||
try:
|
||||
@@ -1642,7 +1996,10 @@ class IdentitySyncThread(SyncThread):
|
||||
def same_resource(self, resource_type, m_resource, sc_resource):
|
||||
if (resource_type ==
|
||||
consts.RESOURCE_TYPE_IDENTITY_USERS):
|
||||
return self._same_identity_resource(m_resource, sc_resource)
|
||||
return self._same_identity_user_resource(m_resource, sc_resource)
|
||||
elif (resource_type ==
|
||||
consts.RESOURCE_TYPE_IDENTITY_GROUPS):
|
||||
return self._same_identity_group_resource(m_resource, sc_resource)
|
||||
elif (resource_type ==
|
||||
consts.RESOURCE_TYPE_IDENTITY_PROJECTS):
|
||||
return self._same_project_resource(m_resource, sc_resource)
|
||||
@@ -1662,7 +2019,10 @@ class IdentitySyncThread(SyncThread):
|
||||
def has_same_ids(self, resource_type, m_resource, sc_resource):
|
||||
if (resource_type ==
|
||||
consts.RESOURCE_TYPE_IDENTITY_USERS):
|
||||
return self._has_same_identity_ids(m_resource, sc_resource)
|
||||
return self._has_same_identity_user_ids(m_resource, sc_resource)
|
||||
elif (resource_type ==
|
||||
consts.RESOURCE_TYPE_IDENTITY_GROUPS):
|
||||
return self._has_same_identity_group_ids(m_resource, sc_resource)
|
||||
elif (resource_type ==
|
||||
consts.RESOURCE_TYPE_IDENTITY_PROJECTS):
|
||||
return self._has_same_project_ids(m_resource, sc_resource)
|
||||
@@ -1790,6 +2150,7 @@ class IdentitySyncThread(SyncThread):
|
||||
def is_resource_handled_by_dbs_client(resource_type):
|
||||
if resource_type in [
|
||||
consts.RESOURCE_TYPE_IDENTITY_USERS,
|
||||
consts.RESOURCE_TYPE_IDENTITY_GROUPS,
|
||||
consts.RESOURCE_TYPE_IDENTITY_PROJECTS,
|
||||
consts.RESOURCE_TYPE_IDENTITY_ROLES,
|
||||
consts.RESOURCE_TYPE_IDENTITY_TOKEN_REVOKE_EVENTS,
|
||||
|
Reference in New Issue
Block a user