Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FOGL-8760 permissions optional support added in configuration Manager along with restrictions on configuration update API #1411

Merged
merged 13 commits into from
Jul 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 101 additions & 12 deletions python/fledge/common/configuration_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import collections
import ast

import aiohttp.web_request
from fledge.common.storage_client.payload_builder import PayloadBuilder
from fledge.common.storage_client.storage_client import StorageClientAsync
from fledge.common.storage_client.exceptions import StorageServerError
Expand All @@ -37,7 +38,7 @@
'JSON', 'URL', 'enumeration', 'script', 'code', 'northTask', 'ACL', 'bucket',
'list', 'kvlist'])
_optional_items = sorted(['readonly', 'order', 'length', 'maximum', 'minimum', 'rule', 'deprecated', 'displayName',
'validity', 'mandatory', 'group', 'listSize', 'listName'])
'validity', 'mandatory', 'group', 'listSize', 'listName', 'permissions'])
RESERVED_CATG = ['South', 'North', 'General', 'Advanced', 'Utilities', 'rest_api', 'Security', 'service', 'SCHEDULER',
'SMNTR', 'PURGE_READ', 'Notifications']

Expand Down Expand Up @@ -266,7 +267,7 @@ async def _validate_category_val(self, category_name, category_val, set_value_va

optional_item_entries = {'readonly': 0, 'order': 0, 'length': 0, 'maximum': 0, 'minimum': 0,
'deprecated': 0, 'displayName': 0, 'rule': 0, 'validity': 0, 'mandatory': 0,
'group': 0, 'listSize': 0, 'listName': 0}
'group': 0, 'listSize': 0, 'listName': 0, 'permissions': 0}
expected_item_entries = {'description': 0, 'default': 0, 'type': 0}

if require_entry_value:
Expand Down Expand Up @@ -301,6 +302,19 @@ def get_entry_val(k):
else:
d = {entry_name: entry_val}
expected_item_entries.update(d)
elif entry_name == "permissions":
if not isinstance(entry_val, list):
raise ValueError(
'For {} category, {} entry value must be a list of string for item name {}; got {}.'
''.format(category_name, entry_name, item_name, type(entry_val)))
if not entry_val:
raise ValueError(
'For {} category, {} entry value must not be empty for item name '
'{}.'.format(category_name, entry_name, item_name))
else:
if not all(isinstance(ev, str) and ev != '' for ev in entry_val):
raise ValueError('For {} category, {} entry values must be a string and non-empty '
'for item name {}.'.format(category_name, entry_name, item_name))
else:
if type(entry_val) is not str:
raise TypeError('For {} category, entry value must be a string for item name {} and '
Expand Down Expand Up @@ -331,7 +345,7 @@ def get_entry_val(k):
type(entry_val)))
# Validate list type and mandatory items
elif 'type' in item_val and get_entry_val("type") in ('list', 'kvlist'):
if entry_name not in ('properties', 'options') and not isinstance(entry_val, str):
if entry_name not in ('properties', 'options', 'permissions') and not isinstance(entry_val, str):
raise TypeError('For {} category, entry value must be a string for item name {} and '
'entry name {}; got {}'.format(category_name, item_name, entry_name,
type(entry_val)))
Expand All @@ -348,6 +362,20 @@ def get_entry_val(k):
raise ValueError('For {} category, listName cannot be empty for item name '
'{}'.format(category_name, item_name))
item_val['listName'] = list_name
elif "permissions" in item_val:
permissions = item_val['permissions']
if not isinstance(permissions, list):
raise ValueError(
'For {} category, permissions entry value must be a list of string for item name {}; '
'got {}.'.format(category_name, item_name, type(permissions)))
if not permissions:
raise ValueError(
'For {} category, permissions entry value must not be empty for item name {}.'.format(
category_name, item_name))
else:
if not all(isinstance(ev, str) and ev != '' for ev in permissions):
raise ValueError('For {} category, permissions entry values must be a string and '
'non-empty for item name {}.'.format(category_name, item_name))
if entry_name == 'items':
if entry_val not in ("string", "float", "integer", "object", "enumeration"):
raise ValueError("For {} category, items value should either be in string, float, "
Expand Down Expand Up @@ -487,6 +515,19 @@ def get_entry_val(k):
if entry_name in ('properties', 'options'):
d = {entry_name: entry_val}
expected_item_entries.update(d)
elif entry_name == "permissions":
if not isinstance(entry_val, list):
raise ValueError(
'For {} category, {} entry value must be a list of string for item name {}; got {}.'
''.format(category_name, entry_name, item_name, type(entry_val)))
if not entry_val:
raise ValueError(
'For {} category, {} entry value must not be empty for item name '
'{}.'.format(category_name, entry_name, item_name))
else:
if not all(isinstance(ev, str) and ev != '' for ev in entry_val):
raise ValueError('For {} category, {} entry values must be a string and non-empty '
'for item name {}.'.format(category_name, entry_name, item_name))
else:
if type(entry_val) is not str:
raise TypeError('For {} category, entry value must be a string for item name {} and '
Expand All @@ -510,6 +551,19 @@ def get_entry_val(k):
self._validate_type_value('float', entry_val)) is False:
raise ValueError('For {} category, entry value must be an integer or float for item name '
'{}; got {}'.format(category_name, entry_name, type(entry_val)))
elif entry_name == "permissions":
if not isinstance(entry_val, list):
raise ValueError(
'For {} category, {} entry value must be a list of string for item name {}; got {}.'
''.format(category_name, entry_name, item_name, type(entry_val)))
if not entry_val:
raise ValueError(
'For {} category, {} entry value must not be empty for item name '
'{}.'.format(category_name, entry_name, item_name))
ashish-jabble marked this conversation as resolved.
Show resolved Hide resolved
else:
if not all(isinstance(ev, str) and ev != '' for ev in entry_val):
raise ValueError('For {} category, {} entry values must be a string and non-empty '
'for item name {}.'.format(category_name, entry_name, item_name))
elif entry_name in ('displayName', 'group', 'rule', 'validity', 'listName'):
if not isinstance(entry_val, str):
raise ValueError('For {} category, entry value must be string for item name {}; got {}'
Expand Down Expand Up @@ -751,26 +805,30 @@ async def _update_value_val(self, category_name, item_name, new_value_val):
err_response = ex.error
raise ValueError(err_response)

async def update_configuration_item_bulk(self, category_name, config_item_list):
async def update_configuration_item_bulk(self, category_name, config_item_list, request=None):
""" Bulk update config items

Args:
category_name: category name
config_item_list: dict containing config item values
request: request details to identify user info

Returns:
None
"""

try:
payload = {"updates": []}
audit_details = {'category': category_name, 'items': {}}
cat_info = await self.get_category_all_items(category_name)
if cat_info is None:
raise NameError("No such Category found for {}".format(category_name))
""" Note: Update reject to the properties with permissions property when the logged in user type is not
given in the list of permissions. """
user_role_name = await self._check_updates_by_role(request)
for item_name, new_val in config_item_list.items():
if item_name not in cat_info:
raise KeyError('{} config item not found'.format(item_name))
self._check_permissions(request, cat_info[item_name], user_role_name)
# Evaluate new_val as per rule if defined
if 'rule' in cat_info[item_name]:
rule = cat_info[item_name]['rule'].replace("value", new_val)
Expand Down Expand Up @@ -874,7 +932,8 @@ async def update_configuration_item_bulk(self, category_name, config_item_list):
await audit.information('CONCH', audit_details)

except Exception as ex:
_logger.exception(ex, 'Unable to bulk update config items')
if 'Forbidden' not in str(ex):
_logger.exception(ex, 'Unable to bulk update config items')
raise

try:
Expand Down Expand Up @@ -1035,14 +1094,16 @@ async def get_category_item_value_entry(self, category_name, item_name):
item_name)
raise

async def set_category_item_value_entry(self, category_name, item_name, new_value_entry, script_file_path=""):
async def set_category_item_value_entry(self, category_name, item_name, new_value_entry, script_file_path="",
request=None):
"""Set the "value" entry of a given item within a given category.

Keyword Arguments:
category_name -- name of the category (required)
item_name -- name of item within the category whose "value" entry needs to be changed (required)
new_value_entry -- new value entry to replace old value entry
script_file_path -- Script file path for the config item whose type is script
request -- request details to identify user info

Side Effects:
An update to storage will not be issued if a new_value_entry is the same as the new_value_entry from storage.
Expand Down Expand Up @@ -1075,7 +1136,11 @@ async def set_category_item_value_entry(self, category_name, item_name, new_valu
.format(category_name, item_name))
if storage_value_entry == new_value_entry:
return

""" Note: Update reject to the properties with permissions property when the logged in user type is not
given in the list of permissions. """
user_role_name = await self._check_updates_by_role(request)
if user_role_name:
self._check_permissions(request, storage_value_entry, user_role_name)
# Special case for enumeration field type handling
if storage_value_entry['type'] == 'enumeration':
if new_value_entry == '':
Expand Down Expand Up @@ -1122,10 +1187,11 @@ async def set_category_item_value_entry(self, category_name, item_name, new_valu
self._cacheManager.cache[category_name]['value'][item_name]["file"] = script_file_path
else:
self._cacheManager.cache[category_name]['value'].update({item_name: cat_item['value']})
except:
_logger.exception(
'Unable to set item value entry based on category_name %s and item_name %s and value_item_entry %s',
category_name, item_name, new_value_entry)
except Exception as ex:
if 'Forbidden' not in str(ex):
_logger.exception(
'Unable to set item value entry based on category_name %s and item_name %s and value_item_entry %s',
category_name, item_name, new_value_entry)
raise
try:
await self._run_callbacks(category_name)
Expand Down Expand Up @@ -2001,3 +2067,26 @@ def _handle_config_items(self, cat_name: str, cat_value: dict) -> None:
if 'cacheSize' in cat_value:
self._cacheManager.max_cache_size = int(cat_value['cacheSize']['value'])

async def _check_updates_by_role(self, request: aiohttp.web_request.Request) -> str:
async def get_role_name():
from fledge.services.core.user_model import User
name = await User.Objects.get_role_name_by_id(request.user['role_id'])
if name is None:
raise ValueError("Requesting user's role is not matched with any existing roles.")
return name

role_name = ""
if request is not None:
if hasattr(request, "user_is_admin"):
if not request.user_is_admin:
role_name = await get_role_name()
return role_name

def _check_permissions(self, request: aiohttp.web_request.Request, cat_info: str, role_name: str) -> None:
if request is not None:
if hasattr(request, "user_is_admin"):
if not request.user_is_admin:
if 'permissions' in cat_info:
if not (role_name in cat_info['permissions']):
raise Exception('Forbidden')

54 changes: 20 additions & 34 deletions python/fledge/services/core/api/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,23 +245,8 @@ async def set_configuration_item(request):
"""
category_name = request.match_info.get('category_name', None)
config_item = request.match_info.get('config_item', None)

category_name = urllib.parse.unquote(category_name) if category_name is not None else None
config_item = urllib.parse.unquote(config_item) if config_item is not None else None
"""
FIXME: FOGL-6774 We should handle in better way, there may be more cases.
GIVEN: config item 'authentication' And category 'rest_api'
WHEN: if non-admin user is trying to update
THEN: 403 Forbidden case
"""
if hasattr(request, "user"):
if request.user and (category_name == 'rest_api' and config_item == 'authentication'):
if not request.user_is_admin:
msg = "Admin role permissions required to change the {} value for category {}.".format(
config_item, category_name)
_logger.warning(msg)
raise web.HTTPForbidden(reason=msg, body=json.dumps({"message": msg}))

data = await request.json()
cf_mgr = ConfigurationManager(connect.get_storage_async())
found_optional = {}
Expand Down Expand Up @@ -293,14 +278,21 @@ async def set_configuration_item(request):
if 'readonly' in storage_value_entry:
if storage_value_entry['readonly'] == 'true':
raise TypeError("Update not allowed for {} item_name as it has readonly attribute set".format(config_item))
await cf_mgr.set_category_item_value_entry(category_name, config_item, value)
request_details = request if hasattr(request, "user") else None
await cf_mgr.set_category_item_value_entry(category_name, config_item, value, request=request_details)
else:
await cf_mgr.set_optional_value_entry(category_name, config_item, list(found_optional.keys())[0], list(found_optional.values())[0])
except ValueError as ex:
raise web.HTTPNotFound(reason=ex) if not found_optional else web.HTTPBadRequest(reason=ex)
except (TypeError, KeyError) as ex:
raise web.HTTPBadRequest(reason=ex)

except Exception as ex:
msg = str(ex)
if 'Forbidden' in msg:
msg = "Insufficient access privileges to change the value for '{}' category and '{}' config item.".format(
category_name, config_item)
_logger.warning(msg)
raise web.HTTPForbidden(reason=msg, body=json.dumps({"message": msg}))
category_item = await cf_mgr.get_category_item(category_name, config_item)
if category_item is None:
raise web.HTTPNotFound(reason="No detail found for the category_name: {} and config_item: {}".format(category_name, config_item))
Expand All @@ -325,20 +317,7 @@ async def update_configuration_item_bulk(request):
data = await request.json()
if not data:
return web.HTTPBadRequest(reason='Nothing to update')
"""
FIXME: FOGL-6774 We should handle in better way, there may be more cases.
GIVEN: config item 'authentication' And category 'rest_api'
WHEN: if non-admin user is trying to update
THEN: 403 Forbidden case
"""
if hasattr(request, "user"):
config_items = [k for k, v in data.items() if k == 'authentication']
if request.user and (category_name == 'rest_api' and config_items):
if not request.user_is_admin:
msg = "Admin role permissions required to change the authentication value for category {}.".format(
category_name)
_logger.warning(msg)
return web.HTTPForbidden(reason=msg, body=json.dumps({"message": msg}))
request_details = request if hasattr(request, "user") else None
cf_mgr = ConfigurationManager(connect.get_storage_async())
try:
is_core_mgt = request.is_core_mgt
Expand All @@ -352,15 +331,22 @@ async def update_configuration_item_bulk(request):
if storage_value_entry['readonly'] == 'true':
raise TypeError(
"Bulk update not allowed for {} item_name as it has readonly attribute set".format(item_name))
await cf_mgr.update_configuration_item_bulk(category_name, data)
await cf_mgr.update_configuration_item_bulk(category_name, data, request_details)
except (NameError, KeyError) as ex:
raise web.HTTPNotFound(reason=ex)
except (ValueError, TypeError) as ex:
raise web.HTTPBadRequest(reason=ex)
except Exception as ex:
msg = str(ex)
_logger.error(ex, "Failed to bulk update {} category.".format(category_name))
raise web.HTTPInternalServerError(reason=msg, body=json.dumps({"message": msg}))
if 'Forbidden' in msg:
if 'Forbidden' in msg:
msg = "Insufficient access privileges to change the value for given data for '{}' category.".format(
category_name)
_logger.warning(msg)
raise web.HTTPForbidden(reason=msg, body=json.dumps({"message": msg}))
else:
_logger.error(ex, "Failed to bulk update {} category.".format(category_name))
raise web.HTTPInternalServerError(reason=msg, body=json.dumps({"message": msg}))
else:
cat = await cf_mgr.get_category_all_items(category_name)
try:
Expand Down
Loading