From 78cf73eb7549b05ec0b7881e21dabab5a61ca4f8 Mon Sep 17 00:00:00 2001 From: asm-hystax Date: Tue, 10 Mar 2026 16:21:58 +0400 Subject: [PATCH 1/5] OSN-1368. Increased last_error text length for more get more error info while resource discover (#1843) ## Description ## Related issue number ## Special notes ## Checklist * [ ] The pull request title is a good summary of the changes * [ ] Unit tests for the changes exist * [ ] New and existing unit tests pass locally --- docker_images/resource_discovery/worker.py | 2 +- .../handlers/v2/discovery_infos.py | 2 +- .../unittests/test_discovery_infos_api.py | 22 +++++++++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/docker_images/resource_discovery/worker.py b/docker_images/resource_discovery/worker.py index b4bced622..711c980ae 100644 --- a/docker_images/resource_discovery/worker.py +++ b/docker_images/resource_discovery/worker.py @@ -401,7 +401,7 @@ def discover_resources(self, task): self._update_discovery_info( cloud_acc_id, resource_type, last_error_at=utcnow_timestamp(), - last_error=str(ex)[:255]) + last_error=str(ex)[:1024]) raise def _update_discovery_info(self, cloud_acc_id, resource_type, **kwargs): diff --git a/rest_api/rest_api_server/handlers/v2/discovery_infos.py b/rest_api/rest_api_server/handlers/v2/discovery_infos.py index ba4180ce4..d7d0609bb 100644 --- a/rest_api/rest_api_server/handlers/v2/discovery_infos.py +++ b/rest_api/rest_api_server/handlers/v2/discovery_infos.py @@ -118,7 +118,7 @@ def _validate_params(self, item, **kwargs): val = kwargs.get(param) if val is not None: if param in string_param: - check_string_attribute(param, val) + check_string_attribute(param, val, max_length=1024) else: check_int_attribute(param, val) except WrongArgumentsException as ex: diff --git a/rest_api/rest_api_server/tests/unittests/test_discovery_infos_api.py b/rest_api/rest_api_server/tests/unittests/test_discovery_infos_api.py index 56e0a3c6d..6d1052d89 100644 --- a/rest_api/rest_api_server/tests/unittests/test_discovery_infos_api.py +++ b/rest_api/rest_api_server/tests/unittests/test_discovery_infos_api.py @@ -1,4 +1,6 @@ import uuid +import string +import random from datetime import datetime from unittest.mock import patch from tools.optscale_time import utcnow_timestamp @@ -116,6 +118,26 @@ def test_update_discovery_info(self): self.assertEqual(res['observe_time'], 1625086700) self.assertEqual(res['last_error'], 'Test Error Text') + def test_update_discovery_info_with_long_error_test(self): + _, res = self.client.discovery_info_list(self.cloud_acc_id) + some_time = utcnow_timestamp() + random_long_error_text = ''.join(random.choices( + string.ascii_letters, k=1024)) + for di_info in res['discovery_info']: + code, res = self.client.discovery_info_update( + di_info['id'], { + 'last_discovery_at': some_time, + 'last_error_at': 1625086800, + 'last_error': random_long_error_text, + 'observe_time': 1625086700 + } + ) + self.assertEqual(code, 200) + self.assertEqual(res['last_discovery_at'], some_time) + self.assertEqual(res['last_error_at'], 1625086800) + self.assertEqual(res['observe_time'], 1625086700) + self.assertEqual(res['last_error'], random_long_error_text) + def test_update_discovery_info_wrong_arg(self): _, res = self.client.discovery_info_list(self.cloud_acc_id) di_info_id = res['discovery_info'][0]['id'] From 1a03a2d2e0965754283dc59684f39b147daeb747 Mon Sep 17 00:00:00 2001 From: asm-hystax Date: Wed, 11 Mar 2026 09:58:50 +0400 Subject: [PATCH 2/5] OSN-1369. Added new user options APIs, new user_options table, unittests, updated auth-client (#1844) ## Description Added new user options APIs for setting specific user information and store it. ## Related issue number ## Special notes ## Checklist * [ ] The pull request title is a good summary of the changes * [ ] Unit tests for the changes exist * [ ] New and existing unit tests pass locally --- .../versions/88f7bebcdcb9_add_user_options.py | 40 +++ auth/auth_server/constants.py | 5 +- auth/auth_server/controllers/user_option.py | 91 ++++++ auth/auth_server/handlers/v2/__init__.py | 1 + auth/auth_server/handlers/v2/base.py | 6 +- auth/auth_server/handlers/v2/user_options.py | 308 ++++++++++++++++++ auth/auth_server/models/models.py | 17 + auth/auth_server/server.py | 6 + .../tests/unittests/test_api_user.py | 13 + .../tests/unittests/test_api_user_options.py | 213 ++++++++++++ auth/auth_server/utils.py | 7 + optscale_client/auth_client/client_v2.py | 27 ++ 12 files changed, 730 insertions(+), 4 deletions(-) create mode 100644 auth/auth_server/alembic/versions/88f7bebcdcb9_add_user_options.py create mode 100644 auth/auth_server/controllers/user_option.py create mode 100644 auth/auth_server/handlers/v2/user_options.py create mode 100644 auth/auth_server/tests/unittests/test_api_user_options.py diff --git a/auth/auth_server/alembic/versions/88f7bebcdcb9_add_user_options.py b/auth/auth_server/alembic/versions/88f7bebcdcb9_add_user_options.py new file mode 100644 index 000000000..48f047114 --- /dev/null +++ b/auth/auth_server/alembic/versions/88f7bebcdcb9_add_user_options.py @@ -0,0 +1,40 @@ +# pylint: disable=C0103 +"""Add user option table + +Revision ID: 88f7bebcdcb9 +Revises: 998f27cb8c46 +Create Date: 2026-03-10 16:27:40.340018 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '88f7bebcdcb9' +down_revision = '998f27cb8c46' +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + 'user_option', + sa.Column('id', sa.String(36), nullable=False), + sa.Column('created_at', sa.Integer(), nullable=False), + sa.Column('deleted_at', sa.Integer(), nullable=False), + sa.Column('user_id', sa.String(36), nullable=False), + sa.Column('name', sa.String(256), nullable=False), + sa.Column('value', sa.TEXT(), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.ForeignKeyConstraint(['user_id'], ['user.id']), + sa.UniqueConstraint('user_id', 'name', 'deleted_at', + name='uc_user_id_name_deleted_at') + ) + op.create_index(op.f('ix_user_option_user_name'), + 'user_option', ['user_id', 'name'], + unique=False) + + +def downgrade(): + op.drop_table('user_option') diff --git a/auth/auth_server/constants.py b/auth/auth_server/constants.py index 7e4e940c8..d16fa03c6 100644 --- a/auth/auth_server/constants.py +++ b/auth/auth_server/constants.py @@ -28,7 +28,10 @@ class Urls: r"%s/users/(?P[^/]+)/action_resources", 'bulk_action_resources': r"%s/bulk_action_resources", 'signin': r"%s/signin", - 'verification_codes': r"%s/verification_codes" + 'verification_codes': r"%s/verification_codes", + 'user_options_collection': r"%s/users/(?P[^/]+)/options", + 'user_options': + r"%s/users/(?P[^/]+)/options/(?P[^/]+)" } def __init__(self): diff --git a/auth/auth_server/controllers/user_option.py b/auth/auth_server/controllers/user_option.py new file mode 100644 index 000000000..9762a1f52 --- /dev/null +++ b/auth/auth_server/controllers/user_option.py @@ -0,0 +1,91 @@ +import logging + +from auth.auth_server.controllers.base import BaseController +from auth.auth_server.controllers.base_async import BaseAsyncControllerWrapper +from auth.auth_server.exceptions import Err +from auth.auth_server.models.models import UserOption, User +from auth.auth_server.utils import strtobool, check_string_attribute +from tools.optscale_exceptions.common_exc import ( + WrongArgumentsException, ForbiddenException, NotFoundException) + +LOG = logging.getLogger(__name__) + + +class UserOptionsController(BaseController): + def _get_model_type(self): + return UserOption + + def check_user_access(self, user_id, token): + req_user = self.get_user_by_id(user_id) + if not req_user: + raise NotFoundException(Err.OA0003, [User.__name__, user_id]) + if token: + token_user = self.get_user(token) + if token_user.id != user_id: + raise ForbiddenException(Err.OA0012, []) + return req_user + + def get_by_name(self, user_id, option_name, **kwargs): + token = kwargs.get('token', None) + self.check_user_access(user_id, token) + user_options = super().list(user_id=user_id, name=option_name) + if len(user_options) > 1: + raise WrongArgumentsException(Err.OA0029, []) + elif len(user_options) == 0: + return '{}' + else: + return user_options[0].value + + def list(self, user_id, **kwargs): + token = kwargs.get('token', None) + self.check_user_access(user_id, token) + with_values = kwargs.get('with_values', False) + try: + if not isinstance(with_values, bool): + with_values = strtobool(with_values) + except ValueError: + raise WrongArgumentsException(Err.OA0063, ['with_values']) + base_list = super().list(user_id=user_id) + result = [ + { + 'name': obj.name, 'value': obj.value + } if with_values else obj.name for obj in base_list + ] + return result + + def patch(self, user_id, option_name, value_data, **kwargs): + token = kwargs.get('token', None) + self.check_user_access(user_id, token) + + options = super().list(user_id=user_id, name=option_name) + if len(options) > 1: + raise WrongArgumentsException(Err.OA0029, []) + elif len(options) == 0: + check_string_attribute('option_name', option_name, + max_length=256) + res = super().create(user_id=user_id, name=option_name, + deleted_at=0, value=value_data) + return res.value + else: + option = options[0] + res = super().edit(option.id, value=value_data) + return res.value + + def delete(self, user_id, option_name, **kwargs): + token = kwargs.get('token', None) + self.check_user_access(user_id, token) + + options = super().list(user_id=user_id, name=option_name) + if len(options) > 1: + raise WrongArgumentsException(Err.OA0029, []) + elif len(options) == 0: + raise NotFoundException( + Err.OA0003, [UserOption.__name__, option_name]) + else: + option = options[0] + super().delete(option.id) + + +class UserOptionsAsyncController(BaseAsyncControllerWrapper): + def _get_controller_class(self): + return UserOptionsController diff --git a/auth/auth_server/handlers/v2/__init__.py b/auth/auth_server/handlers/v2/__init__.py index f1ce1ff99..8ec6c5b2c 100644 --- a/auth/auth_server/handlers/v2/__init__.py +++ b/auth/auth_server/handlers/v2/__init__.py @@ -12,3 +12,4 @@ import auth.auth_server.handlers.v2.types import auth.auth_server.handlers.v2.signin import auth.auth_server.handlers.v2.verification_codes +import auth.auth_server.handlers.v2.user_options diff --git a/auth/auth_server/handlers/v2/base.py b/auth/auth_server/handlers/v2/base.py index 28a9af369..49d6da032 100644 --- a/auth/auth_server/handlers/v2/base.py +++ b/auth/auth_server/handlers/v2/base.py @@ -1,6 +1,6 @@ import json -from tools.optscale_exceptions.common_exc import WrongArgumentsException +from tools.optscale_exceptions.http_exc import OptHTTPError from auth.auth_server.exceptions import Err from auth.auth_server.handlers.v1.base import BaseHandler as BaseHandler_v1 @@ -17,13 +17,13 @@ def get_arg(self, name, type_, default=None, repeated=False): if type_ == bool and isinstance(arg, str): lowered = arg.lower() if lowered not in ['true', 'false']: - raise WrongArgumentsException(Err.OA0063, [name]) + raise OptHTTPError(400, Err.OA0063, [name]) return lowered == 'true' return type_(arg) else: return arg except ValueError: - raise WrongArgumentsException(Err.OA0060, [name]) + raise OptHTTPError(400, Err.OA0060, [name]) def parse_url_params_into_payload(self, payload_map_params): data = {} diff --git a/auth/auth_server/handlers/v2/user_options.py b/auth/auth_server/handlers/v2/user_options.py new file mode 100644 index 000000000..f983a5fce --- /dev/null +++ b/auth/auth_server/handlers/v2/user_options.py @@ -0,0 +1,308 @@ +import json + +from auth.auth_server.handlers.v1.base import ( + BaseAsyncAuthCollectionHandler, BaseAsyncAuthItemHandler, + BaseSecretHandler) +from auth.auth_server.handlers.v2.base import BaseHandler as BaseHandler_v2 +from auth.auth_server.exceptions import Err +from auth.auth_server.controllers.user_option import UserOptionsAsyncController +from auth.auth_server.utils import ModelEncoder, run_task +from tools.optscale_exceptions.http_exc import OptHTTPError + + +class UserOptionsAsyncCollectionHandler(BaseAsyncAuthCollectionHandler, + BaseSecretHandler, BaseHandler_v2): + def _get_controller_class(self): + return UserOptionsAsyncController + + def prepare(self): + pass + + async def post(self, user_id, **url_params): + self.raise405() + + async def get(self, user_id): + """ + --- + description: | + Returns a list of options specified for user. + Required permission: TOKEN or CLUSTER_SECRET + tags: [user_options] + summary: List of options specified for user + parameters: + - name: user_id + in: path + description: User ID + required: true + type: string + - name: with_values + in: query + description: Options with values + required: false + type: boolean + responses: + 200: + description: User options list + schema: + type: object + properties: + options: + type: array + items: + type: object + properties: + name: + type: string + description: option name + value: + type: string + description: option value + 400: + description: | + Wrong arguments: + - OA0060: Invalid argument + - OA0063: Parameter should be true or false + 401: + description: | + Unauthorized: + - OA0010: Token not found + - OA0023: Unauthorized + - OA0062: This resource requires an authorization token + 403: + description: | + Forbidden: + - OA0006: Bad secret + - OA0012: Forbidden! + 404: + description: | + Not found: + - OA0003: User not found + - OA0024: User was not found + security: + - token: [] + - secret: [] + """ + args = {} + if self.secret: + self.check_cluster_secret() + else: + await self.check_token() + args.update(self.token) + with_values = self.get_arg('with_values', bool, False) + args.update({'with_values': with_values}) + result = await run_task( + self.controller.list, user_id, **args) + option_dict = {'options': result} + self.write(json.dumps(option_dict, cls=ModelEncoder)) + + +class UserOptionsAsyncItemHandler(BaseAsyncAuthItemHandler, + BaseSecretHandler): + def _get_controller_class(self): + return UserOptionsAsyncController + + def prepare(self): + pass + + async def put(self, **url_params): + self.raise405() + + async def get(self, user_id, option_name): + """ + --- + description: | + Returns the option value for the specified user. + Required permission: TOKEN or CLUSTER_SECRET + tags: [user_options] + summary: Option value for the specified user + parameters: + - name: user_id + in: path + description: User ID + required: true + type: string + - name: option_name + in: path + description: option name + required: true + type: string + responses: + 200: + description: Option value + schema: + type: object + properties: + value: + type: string + description: Option value + 400: + description: | + Wrong arguments: + - OA0029: Non unique parameters in get request + 401: + description: | + Unauthorized: + - OA0010: Token not found + - OA0023: Unauthorized + - OA0062: This resource requires an authorization token + 403: + description: | + Forbidden: + - OA0006: Bad secret + - OA0012: Forbidden! + 404: + description: | + Not found: + - OA0003: User not found + - OA0024: User was not found + security: + - token: [] + - secret: [] + """ + args = {} + if self.secret: + self.check_cluster_secret() + else: + await self.check_token() + args.update(self.token) + result = await run_task( + self.controller.get_by_name, user_id, option_name, **args) + value_dict = {'value': result} + self.write(json.dumps(value_dict, cls=ModelEncoder)) + + async def patch(self, user_id, option_name): + """ + --- + description: | + Modifies or creates an option for a user + Required permission: TOKEN or CLUSTER_SECRET + tags: [user_options] + summary: Modify/create option + parameters: + - name: user_id + in: path + description: User ID + required: true + type: string + - name: option_name + in: path + description: Option name + required: true + type: string + - name: body + in: body + description: Option value + required: true + schema: + type: object + properties: + value: + type: string + description: Option value + + responses: + 200: + description: Success (returns created/modified value) + schema: + type: object + properties: + value: + type: string + description: Option value + 400: + description: | + Wrong arguments: + - OA0029: Non unique parameters in get request + - OA0032: Parameter is not provided + - OA0033: Parameter should be a string + - OA0048: Parameter should contain 1-256 characters + - OA0065: Parameter should not contain only whitespaces + 401: + description: | + Unauthorized: + - OA0010: Token not found + - OA0023: Unauthorized + - OA0062: This resource requires an authorization token + 403: + description: | + Forbidden: + - OA0006: Bad secret + - OA0012: Forbidden! + 404: + description: | + Not found: + - OA0003: User not found + - OA0024: User was not found + security: + - token: [] + - secret: [] + """ + args = {} + if self.secret: + self.check_cluster_secret() + else: + await self.check_token() + args.update(self.token) + value_data = self._request_body().get('value') + if not value_data: + raise OptHTTPError(400, Err.OA0032, ['value']) + result = await run_task( + self.controller.patch, user_id, option_name, value_data, **args) + value_dict = {'value': result} + self.write(json.dumps(value_dict, cls=ModelEncoder)) + + async def delete(self, user_id, option_name): + """ + --- + description: | + Deletes the specified option for the user + Required permission: TOKEN or CLUSTER_SECRET + tags: [user_options] + summary: Delete option + parameters: + - name: user_id + in: path + description: User ID + required: true + type: string + - name: option_name + in: path + description: Option name + required: true + type: string + responses: + 204: + description: Success + 400: + description: | + Wrong arguments: + - OA0029: Non unique parameters in get request + 401: + description: | + Unauthorized: + - OA0010: Token not found + - OA0023: Unauthorized + - OA0062: This resource requires an authorization token + 403: + description: | + Forbidden: + - OA0006: Bad secret + - OA0012: Forbidden! + 404: + description: | + Not found: + - OA0003: User not found + - OA0024: User was not found + security: + - token: [] + - secret: [] + """ + args = {} + if self.secret: + self.check_cluster_secret() + else: + await self.check_token() + args.update(self.token) + await run_task( + self.controller.delete, user_id, option_name, **args) + self.set_status(204) diff --git a/auth/auth_server/models/models.py b/auth/auth_server/models/models.py index 83a5f1b73..2ee473346 100644 --- a/auth/auth_server/models/models.py +++ b/auth/auth_server/models/models.py @@ -413,3 +413,20 @@ class VerificationCode(Base, BaseMixin): valid_until = Column(TIMESTAMP, nullable=False) code = Column(String(32), nullable=False, info=ColumnPermissions.create_only) + + +class UserOption(Base, BaseMixin): + __tablename__ = 'user_option' + __table_args__ = ( + Index('ix_user_option_user_name', "user_id", "name", unique=True), + UniqueConstraint("user_id", "name", "deleted_at", + name="uc_user_id_name_deleted_at") + ) + + user_id = Column(String(36), ForeignKey('user.id'), nullable=False, + info=ColumnPermissions.create_only) + user = relationship("User", foreign_keys=[user_id]) + name = Column(String(256), nullable=False, + info=ColumnPermissions.create_only) + value = Column(TEXT, nullable=False, default='{}', + info=ColumnPermissions.full) diff --git a/auth/auth_server/server.py b/auth/auth_server/server.py index cfe63bdc3..083f27583 100644 --- a/auth/auth_server/server.py +++ b/auth/auth_server/server.py @@ -88,6 +88,12 @@ def get_handlers(handler_kwargs): h_v2, "signin").SignInAsyncHandler, handler_kwargs), (urls_v2.verification_codes, get_handler_version( h_v2, "verification_codes").VerificationCodeAsyncHandler, + handler_kwargs), + (urls_v2.user_options_collection, get_handler_version( + h_v2, "user_options").UserOptionsAsyncCollectionHandler, + handler_kwargs), + (urls_v2.user_options, get_handler_version( + h_v2, "user_options").UserOptionsAsyncItemHandler, handler_kwargs) ] diff --git a/auth/auth_server/tests/unittests/test_api_user.py b/auth/auth_server/tests/unittests/test_api_user.py index 7c3d4f177..167ddd43c 100644 --- a/auth/auth_server/tests/unittests/test_api_user.py +++ b/auth/auth_server/tests/unittests/test_api_user.py @@ -722,6 +722,19 @@ def test_create_string_type_id(self): self.assertEqual(response['error']['reason'], 'Parameter "type_id" is immutable') + def test_check_invalid_user_info(self): + code, response = self.client.user_exists( + 'test@email.com', user_info=True) + self.assertEqual(code, 200) + self.assertEqual(response['exists'], False) + self.assertIsNone(response.get('user_info')) + + for invalid_value in [1234, 'invalid']: + code, response = self.client.user_exists( + 'test@email.com', user_info=invalid_value) + self.assertEqual(code, 400) + self.assertEqual(response['error']['error_code'], 'OA0063') + def test_check_existence(self): code, response = self.client.user_exists('test@email.com') self.assertEqual(code, 200) diff --git a/auth/auth_server/tests/unittests/test_api_user_options.py b/auth/auth_server/tests/unittests/test_api_user_options.py new file mode 100644 index 000000000..a102d9384 --- /dev/null +++ b/auth/auth_server/tests/unittests/test_api_user_options.py @@ -0,0 +1,213 @@ +import json + +from auth.auth_server.models.models import ( + Type, User, Action, Role, Assignment, ActionGroup, UserOption) +from auth.auth_server.models.models import gen_salt +from auth.auth_server.tests.unittests.test_api_base import TestAuthBase +from auth.auth_server.utils import hash_password + + +class TestUserOptionsApi(TestAuthBase): + def setUp(self, version="v2"): + super().setUp(version) + self.partner_scope_id = 'a5cb80ad-891d-4ec2-99de-ba4f20ba2c5d' + self.customer1_scope_id = '19a00828-fbff-4318-8291-4b6c14a8066d' + self.customer2_scope_id = '6cfea3e7-a037-4529-9a14-dd9c5151b1f5' + self.group11_scope_id = 'be7b4d5e-33b6-40aa-bc6a-00c7d822606f' + self.hierarchy = ( + {'root': {'null': {'partner': { + 'a5cb80ad-891d-4ec2-99de-ba4f20ba2c5d': + {'customer': { + '19a00828-fbff-4318-8291-4b6c14a8066d': + {'group': ['be7b4d5e-33b6-40aa-bc6a-00c7d822606f'] + }, + '6cfea3e7-a037-4529-9a14-dd9c5151b1f5': + {'group': ['e8b8b4e9-a92d-40b5-a5db-b38bf5314ef9', + '42667dde-0427-49be-9541-8e99362ee96e'] + }, + }}, + '843f42c4-76b5-467f-b5e3-f7370b1235d6': {'customer': {}}}}}}) + admin_user = self.create_root_user() + session = self.db_session + type_partner = Type(id_=10, name='partner', parent=admin_user.type) + type_customer = Type(id_=20, name='customer', parent=type_partner) + type_group = Type(id_=30, name='group', parent=type_customer) + self.user_type_id = int(type_group.id) + salt = gen_salt() + self.user_partner_email = 'partner@domain.com' + self.user_partner_password = 'passwd!!!111' + user_partner = User( + self.user_partner_email, type_=type_partner, + password=hash_password( + self.user_partner_password, salt), + display_name='Partner user', scope_id=self.partner_scope_id, + salt=salt, type_id=type_partner.id) + self.user_customer_password = 'p@sswRD!' + user_customer = User( + 'customer@domain.com', type_=type_customer, + salt=salt, + display_name='Customer user', + password=hash_password( + self.user_customer_password, salt), + scope_id=self.customer1_scope_id, type_id=type_customer.id) + customer2_salt = gen_salt() + self.user_customer2_password = 'p4$$w0rddd' + self.user_customer2 = User( + 'customer2@domain.com', type_=type_customer, + salt=customer2_salt, + display_name='user customer2', + password=hash_password(self.user_customer2_password, + customer2_salt), + scope_id=self.customer2_scope_id, type_id=type_customer.id) + + user_action_group = ActionGroup(name='Manage users and assignments') + # admin action has type=root + action_list_users = Action(name='LIST_USERS', type_=type_customer, + action_group=user_action_group) + action_edit_user_info = Action(name='EDIT_USER_INFO', + type_=type_customer, + action_group=user_action_group) + admin_role = Role(name='ADMIN', type_=type_customer, + lvl=type_customer, scope_id=self.customer1_scope_id, + description='Admin') + partner1_nodelete_role = Role(name='P1 No delete', type_=type_partner, + lvl=type_customer, + scope_id=self.partner_scope_id) + partner1_delete_role = Role(name='P1 User Deleter', type_=type_partner, + lvl=type_customer) + session.add(type_partner) + session.add(type_customer) + session.add(type_group) + session.add(user_partner) + session.add(user_customer) + session.add(self.user_customer2) + session.add(action_list_users) + session.add(action_edit_user_info) + session.add(admin_role) + session.add(partner1_nodelete_role) + session.add(partner1_delete_role) + admin_role.assign_action(action_list_users) + admin_role.assign_action(action_edit_user_info) + partner1_nodelete_role.assign_action(action_list_users) + assignment = Assignment(user_customer, admin_role, + type_customer, self.customer1_scope_id) + assignment_p_c1 = Assignment(user_partner, partner1_nodelete_role, + type_customer, self.customer1_scope_id) + assignment_p_c1_del = Assignment(user_partner, partner1_delete_role, + type_partner, self.partner_scope_id) + + session.add(assignment) + session.add(assignment_p_c1) + session.add(assignment_p_c1_del) + session.commit() + self.client.token = self.get_token(user_customer.email, + self.user_customer_password) + + self.user_id1 = user_customer.id + self.user_id2 = self.user_customer2.id + self.user_value1 = json.dumps({'key1': 'value1'}) + self.value1 = {'value': self.user_value1} + self.name1 = 'default_option' + self.user_value2 = json.dumps({'key2': 'value2'}) + self.value2 = {'value': self.user_value2} + self.name2 = 'new_option' + self.user_option_1 = UserOption(user_id=self.user_id1, name=self.name1, + value=self.user_value1) + self.user_option_2 = UserOption(user_id=self.user_id2, name=self.name2, + value=self.user_value2) + session.add(self.user_option_1) + session.add(self.user_option_2) + session.commit() + + def test_create_user_option(self): + code, resp = self.client.user_options_create( + self.user_id1, self.name2, self.value1) + self.assertEqual(code, 200) + self.assertEqual(resp, self.value1) + # trying to create an option with the same keys should result + # in the record being updated + code, resp = self.client.user_options_create( + self.user_id1, self.name2, self.value2) + self.assertEqual(code, 200) + self.assertEqual(resp, self.value2) + code, _ = self.client.user_options_create( + 'abcd', self.name1, self.value1) + self.assertEqual(code, 404) + + def test_update_user_option(self): + code, resp = self.client.user_options_update( + self.user_id1, self.name1, self.value2) + self.assertEqual(code, 200) + self.assertEqual(resp, self.value2) + # trying to update an option with nonexistent keys should result + # in a record being created + code, resp = self.client.user_options_update( + self.user_id2, self.name1, self.value2) + self.assertEqual(code, 200) + self.assertEqual(resp, self.value2) + code, _ = self.client.user_options_update( + 'abcd', self.name1, self.value1) + self.assertEqual(code, 404) + + def test_get_user_option(self): + code, resp = self.client.user_options_get( + self.user_id1, self.name1) + self.assertEqual(code, 200) + self.assertEqual(resp, self.value1) + code, resp = self.client.user_options_get( + self.user_id1, self.name2) + self.assertEqual(code, 200) + self.assertEqual(resp.get('value'), '{}') + code, _ = self.client.user_options_get('abcd', self.name1) + self.assertEqual(code, 404) + + def test_list_user_option(self): + code, resp = self.client.user_options_list(self.user_id1) + self.assertEqual(code, 200) + self.assertEqual(len(resp.get('options')), 1) + _, _ = self.client.user_options_create( + self.user_id1, self.name2, self.value2) + + code, resp = self.client.user_options_list(self.user_id1) + self.assertEqual(code, 200) + self.assertEqual(len(resp.get('options')), 2) + self.assertEqual(resp['options'], ['default_option', 'new_option']) + + for invalid_value in [1234, 'invalid']: + code, resp = self.client.user_options_list( + self.user_id1, with_values=invalid_value) + self.assertEqual(code, 400) + self.assertEqual(resp['error']['error_code'], 'OA0063') + + code, resp = self.client.user_options_list( + self.user_id1, with_values=True) + self.assertEqual(code, 200) + self.assertEqual(len(resp.get('options')), 2) + self.assertEqual(resp['options'], [{ + 'name': 'default_option', + 'value': '{"key1": "value1"}' + }, { + 'name': 'new_option', + 'value': '{"key2": "value2"}' + }]) + + _, _ = self.client.user_options_delete(self.user_id2, self.name2) + code, resp = self.client.user_options_list(self.user_id2) + self.assertEqual(code, 200) + self.assertEqual(len(resp.get('options')), 0) + code, _ = self.client.user_options_list('abcd') + self.assertEqual(code, 404) + + def test_delete_user_option(self): + code, _ = self.client.user_options_delete( + self.user_id1, self.name1) + self.assertEqual(code, 204) + code, resp = self.client.user_options_list(self.user_id1) + self.assertEqual(code, 200) + self.assertEqual(len(resp.get('options')), 0) + code, _ = self.client.user_options_delete( + self.user_id1, self.name1) + self.assertEqual(code, 404) + code, _ = self.client.user_options_delete( + 'abcd', self.name1) + self.assertEqual(code, 404) diff --git a/auth/auth_server/utils.py b/auth/auth_server/utils.py index f14d15f14..7d3ce8b9d 100644 --- a/auth/auth_server/utils.py +++ b/auth/auth_server/utils.py @@ -149,6 +149,13 @@ def check_list_attribute(name, value, required=True): raise WrongArgumentsException(Err.OA0055, [name]) +def strtobool(val): + val = val.lower() + if val not in ['true', 'false']: + raise ValueError('Should be false or true') + return val == 'true' + + class ModelEncoder(json.JSONEncoder): # pylint: disable=E0202 def default(self, obj): diff --git a/optscale_client/auth_client/client_v2.py b/optscale_client/auth_client/client_v2.py index d760858aa..e119c9ba8 100644 --- a/optscale_client/auth_client/client_v2.py +++ b/optscale_client/auth_client/client_v2.py @@ -49,6 +49,13 @@ def verification_codes_url(): url = 'verification_codes' return url + @staticmethod + def user_options_url(user_id, option_name=None): + url = '%s/options' % Client.user_url(user_id) + if option_name is not None: + url = '%s/%s' % (url, option_name) + return url + @staticmethod def query_url(**query): query = { @@ -178,3 +185,23 @@ def verification_code_create(self, email, code): "code": code, } return self.post(self.verification_codes_url(), body) + + def user_options_list(self, user_id, with_values=False): + url = self.user_options_url(user_id) + self.query_url( + with_values=with_values) + return self.get(url) + + def user_options_get(self, user_id, option_name): + url = self.user_options_url(user_id, option_name) + return self.get(url) + + def user_options_update(self, user_id, option_name, value): + url = self.user_options_url(user_id, option_name) + return self.patch(url, value) + + def user_options_create(self, user_id, option_name, value): + return self.user_options_update(user_id, option_name, value) + + def user_options_delete(self, user_id, option_name): + url = self.user_options_url(user_id, option_name) + return self.delete(url) From f4112d65af3d3cd4a8c2ce50406818f6c47e4578 Mon Sep 17 00:00:00 2001 From: asm-hystax Date: Thu, 12 Mar 2026 13:13:54 +0400 Subject: [PATCH 3/5] OSN-1372. Updated swagger errors, added option value json validation (#1845) ## Description Updated swagger errors: Remove next error codes for APIs: GET /options: OA0060, OA0003 GET /options/{option_name}: OA0029, OA003 DELETE /options/{option_name}: OA0029, OA003 PATCH/options/{option_name}: OA0029, OA003, OA0065 Added OA0046 (invalid json error) to PATCH Added json validation for user option value. ## Related issue number https://datatrendstech.atlassian.net/browse/OSN-1372 --- auth/auth_server/controllers/user_option.py | 4 ++- auth/auth_server/exceptions.py | 2 +- auth/auth_server/handlers/v2/user_options.py | 16 +----------- .../tests/unittests/test_api_user_options.py | 14 +++++++++- auth/auth_server/utils.py | 26 ++++++++++++++----- 5 files changed, 38 insertions(+), 24 deletions(-) diff --git a/auth/auth_server/controllers/user_option.py b/auth/auth_server/controllers/user_option.py index 9762a1f52..cc9e4e6e9 100644 --- a/auth/auth_server/controllers/user_option.py +++ b/auth/auth_server/controllers/user_option.py @@ -4,7 +4,8 @@ from auth.auth_server.controllers.base_async import BaseAsyncControllerWrapper from auth.auth_server.exceptions import Err from auth.auth_server.models.models import UserOption, User -from auth.auth_server.utils import strtobool, check_string_attribute +from auth.auth_server.utils import (strtobool, check_string_attribute, + check_valid_json) from tools.optscale_exceptions.common_exc import ( WrongArgumentsException, ForbiddenException, NotFoundException) @@ -58,6 +59,7 @@ def patch(self, user_id, option_name, value_data, **kwargs): self.check_user_access(user_id, token) options = super().list(user_id=user_id, name=option_name) + check_valid_json(value_data, 'value') if len(options) > 1: raise WrongArgumentsException(Err.OA0029, []) elif len(options) == 0: diff --git a/auth/auth_server/exceptions.py b/auth/auth_server/exceptions.py index 04f3b8fa0..014035aad 100644 --- a/auth/auth_server/exceptions.py +++ b/auth/auth_server/exceptions.py @@ -130,7 +130,7 @@ class Err(enum.Enum): "Invalid model type: %s", ] OA0046 = [ - "Payload is not a valid json", + "%s is not a valid json", ] OA0047 = [ "Payload is malformed", diff --git a/auth/auth_server/handlers/v2/user_options.py b/auth/auth_server/handlers/v2/user_options.py index f983a5fce..20001c0ae 100644 --- a/auth/auth_server/handlers/v2/user_options.py +++ b/auth/auth_server/handlers/v2/user_options.py @@ -60,7 +60,6 @@ async def get(self, user_id): 400: description: | Wrong arguments: - - OA0060: Invalid argument - OA0063: Parameter should be true or false 401: description: | @@ -76,7 +75,6 @@ async def get(self, user_id): 404: description: | Not found: - - OA0003: User not found - OA0024: User was not found security: - token: [] @@ -135,10 +133,6 @@ async def get(self, user_id, option_name): value: type: string description: Option value - 400: - description: | - Wrong arguments: - - OA0029: Non unique parameters in get request 401: description: | Unauthorized: @@ -153,7 +147,6 @@ async def get(self, user_id, option_name): 404: description: | Not found: - - OA0003: User not found - OA0024: User was not found security: - token: [] @@ -212,11 +205,10 @@ async def patch(self, user_id, option_name): 400: description: | Wrong arguments: - - OA0029: Non unique parameters in get request - OA0032: Parameter is not provided - OA0033: Parameter should be a string + - OA0046: Value is not a valid json - OA0048: Parameter should contain 1-256 characters - - OA0065: Parameter should not contain only whitespaces 401: description: | Unauthorized: @@ -231,7 +223,6 @@ async def patch(self, user_id, option_name): 404: description: | Not found: - - OA0003: User not found - OA0024: User was not found security: - token: [] @@ -273,10 +264,6 @@ async def delete(self, user_id, option_name): responses: 204: description: Success - 400: - description: | - Wrong arguments: - - OA0029: Non unique parameters in get request 401: description: | Unauthorized: @@ -291,7 +278,6 @@ async def delete(self, user_id, option_name): 404: description: | Not found: - - OA0003: User not found - OA0024: User was not found security: - token: [] diff --git a/auth/auth_server/tests/unittests/test_api_user_options.py b/auth/auth_server/tests/unittests/test_api_user_options.py index a102d9384..c6b21c5a9 100644 --- a/auth/auth_server/tests/unittests/test_api_user_options.py +++ b/auth/auth_server/tests/unittests/test_api_user_options.py @@ -130,6 +130,12 @@ def test_create_user_option(self): self.user_id1, self.name2, self.value2) self.assertEqual(code, 200) self.assertEqual(resp, self.value2) + + code, resp = self.client.user_options_create( + self.user_id1, self.name1, {'value': 'some_str_not_json'}) + self.assertEqual(code, 400) + self.assertEqual(resp['error']['error_code'], 'OA0046') + code, _ = self.client.user_options_create( 'abcd', self.name1, self.value1) self.assertEqual(code, 404) @@ -145,7 +151,13 @@ def test_update_user_option(self): self.user_id2, self.name1, self.value2) self.assertEqual(code, 200) self.assertEqual(resp, self.value2) - code, _ = self.client.user_options_update( + + code, resp = self.client.user_options_update( + self.user_id2, self.name1, {'value': 'some_str_not_json'}) + self.assertEqual(code, 400) + self.assertEqual(resp['error']['error_code'], 'OA0046') + + code, resp = self.client.user_options_update( 'abcd', self.name1, self.value1) self.assertEqual(code, 404) diff --git a/auth/auth_server/utils.py b/auth/auth_server/utils.py index 7d3ce8b9d..97a6c7494 100644 --- a/auth/auth_server/utils.py +++ b/auth/auth_server/utils.py @@ -205,14 +205,28 @@ def unique_list(list_to_filter): return list(set(list_to_filter)) -def load_payload(payload): +def _get_valid_json(json_str_value, json_value_key): try: - payload_dict = json.loads(payload) - if not isinstance(payload_dict, dict): - raise WrongArgumentsException(Err.OA0047, []) + json_dict = json.loads(json_str_value) + if not isinstance(json_dict, dict): + return None except ValueError: - raise WrongArgumentsException(Err.OA0046, []) - return payload_dict + raise WrongArgumentsException(Err.OA0046, [json_value_key]) + return json_dict + + +def check_valid_json(json_str_value, json_value_key): + json_dict = _get_valid_json(json_str_value, json_value_key) + if json_dict is None: + raise WrongArgumentsException(Err.OA0046, [json_value_key]) + return json_dict + + +def load_payload(json_payload, payload_key='Payload'): + json_dict = _get_valid_json(json_payload, payload_key) + if json_dict is None: + raise WrongArgumentsException(Err.OA0047, []) + return json_dict def popkey(obj, key): From e1ce3005da2faf362eadba17cc5a6ef2d02c3030 Mon Sep 17 00:00:00 2001 From: asm-hystax Date: Thu, 12 Mar 2026 16:06:25 +0400 Subject: [PATCH 4/5] OSN-1372-fix. Added additional fixes to swagger, except all json convert error types --- auth/auth_server/handlers/v2/user_options.py | 2 +- .../tests/unittests/test_api_user_options.py | 15 +++++++++++++++ auth/auth_server/utils.py | 2 +- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/auth/auth_server/handlers/v2/user_options.py b/auth/auth_server/handlers/v2/user_options.py index 20001c0ae..79db91235 100644 --- a/auth/auth_server/handlers/v2/user_options.py +++ b/auth/auth_server/handlers/v2/user_options.py @@ -206,7 +206,6 @@ async def patch(self, user_id, option_name): description: | Wrong arguments: - OA0032: Parameter is not provided - - OA0033: Parameter should be a string - OA0046: Value is not a valid json - OA0048: Parameter should contain 1-256 characters 401: @@ -278,6 +277,7 @@ async def delete(self, user_id, option_name): 404: description: | Not found: + - OA0003: UserOption not found - OA0024: User was not found security: - token: [] diff --git a/auth/auth_server/tests/unittests/test_api_user_options.py b/auth/auth_server/tests/unittests/test_api_user_options.py index c6b21c5a9..0108f75ae 100644 --- a/auth/auth_server/tests/unittests/test_api_user_options.py +++ b/auth/auth_server/tests/unittests/test_api_user_options.py @@ -136,6 +136,21 @@ def test_create_user_option(self): self.assertEqual(code, 400) self.assertEqual(resp['error']['error_code'], 'OA0046') + code, resp = self.client.user_options_create( + self.user_id1, self.name1, {'value': 123}) + self.assertEqual(code, 400) + self.assertEqual(resp['error']['error_code'], 'OA0046') + + code, resp = self.client.user_options_create( + self.user_id1, self.name1, {'value': True}) + self.assertEqual(code, 400) + self.assertEqual(resp['error']['error_code'], 'OA0046') + + code, resp = self.client.user_options_create( + self.user_id1, self.name1, {'value': 1.23}) + self.assertEqual(code, 400) + self.assertEqual(resp['error']['error_code'], 'OA0046') + code, _ = self.client.user_options_create( 'abcd', self.name1, self.value1) self.assertEqual(code, 404) diff --git a/auth/auth_server/utils.py b/auth/auth_server/utils.py index 97a6c7494..e086840ce 100644 --- a/auth/auth_server/utils.py +++ b/auth/auth_server/utils.py @@ -210,7 +210,7 @@ def _get_valid_json(json_str_value, json_value_key): json_dict = json.loads(json_str_value) if not isinstance(json_dict, dict): return None - except ValueError: + except (TypeError, ValueError): raise WrongArgumentsException(Err.OA0046, [json_value_key]) return json_dict From 59dac5ec6da5999660d4f6e2b10c17d5b6b37878 Mon Sep 17 00:00:00 2001 From: sd-hystax <110374605+sd-hystax@users.noreply.github.com> Date: Fri, 20 Mar 2026 09:56:24 +0400 Subject: [PATCH 5/5] OSN-1379. Reworked downward hierarchy ## Description Request downward hierarchy based to user assignments ## Related issue number OSN-1379 ## Special notes ## Checklist * [ ] The pull request title is a good summary of the changes * [ ] Unit tests for the changes exist * [ ] New and existing unit tests pass locally --- auth/auth_server/controllers/base.py | 63 +++++++++---------- auth/auth_server/controllers/scope.py | 12 ++-- auth/auth_server/controllers/user.py | 12 ++-- .../unittests/test_api_action_resource.py | 31 +++++---- .../tests/unittests/test_api_assignment.py | 4 +- .../unittests/test_api_authorize_userlist.py | 4 +- .../tests/unittests/test_api_role.py | 4 +- .../tests/unittests/test_api_role_root.py | 3 +- .../tests/unittests/test_api_scope.py | 12 ++-- .../tests/unittests/test_api_signin.py | 4 +- .../tests/unittests/test_api_user.py | 6 +- .../tests/unittests/test_api_user_options.py | 4 +- .../controllers/auth_hierarchy.py | 13 ++-- .../unittests/test_auth_hierarchy_api.py | 26 ++------ 14 files changed, 92 insertions(+), 106 deletions(-) diff --git a/auth/auth_server/controllers/base.py b/auth/auth_server/controllers/base.py index bcddae146..4cdfa19de 100644 --- a/auth/auth_server/controllers/base.py +++ b/auth/auth_server/controllers/base.py @@ -193,43 +193,36 @@ def get_types(self): return self.session.query(Type).filter( Type.deleted.is_(False)).all() - def render(self, action_resources, action_set, hierarchy): - types = self.get_types() - ordered_types = [x.name for x in sorted(types, key=lambda x: x.id)] - # map of any hierarchy id <> related hierarchy part - id_item_hierarchy_map = {} - for i, type_ in enumerate(ordered_types): - # todo: create common approach without hardcode - # in root case id will be "null" - if i == 0: - id_item_hierarchy_map[None] = hierarchy[type_]['null'] - elif i == len(ordered_types) - 1: - # last type doesn't have children - continue - else: - for _id in id_item_hierarchy_map.copy(): - if type_ in id_item_hierarchy_map[_id]: - id_item_hierarchy_map.update( - id_item_hierarchy_map[_id][type_]) - - def render_item(res_id, res_type, action): - if res_type == ordered_types[-1]: - return - down_hierarchy = id_item_hierarchy_map.get(res_id, {}) - next_type = ordered_types[ordered_types.index(res_type) + 1] - for child_id in down_hierarchy.get(next_type, []): - action_set.add((child_id, next_type, action)) - render_item(child_id, next_type, action) - - for res_id, res_type, action in action_resources: - render_item(res_id, res_type, action) + def render(self, action_resources, aset, hierarchy): + def render_item(node, action, collect=False): + if isinstance(node, dict): + for entity_type, entities in node.items(): + if isinstance(entities, dict): + for entity_id, children in entities.items(): + start_collect = collect or entity_id == target_id + if start_collect: + aset.add((entity_id, entity_type, action)) + render_item(children, action, start_collect) + elif isinstance(entities, list): + for entity_id in entities: + aset.add((entity_id, entity_type, action)) + + for target_id, target_type, action in action_resources: + render_item(hierarchy, action) def format_user_action_resources(self, action_resources, action_list): - response = self.get_downward_hierarchy('root', None) - aset = OrderedSet(action_resources) - - self.render(action_resources, aset, response) - + resource_set = set() + aset = OrderedSet() + for id_, res_type, _ in action_resources: + resource_set.add((id_, res_type)) + + ordered_set = sorted(resource_set, key=self.get_type_sorter()) + for id_, res_type in ordered_set: + if id_ in [x[0] for x in aset]: + # downward hierarchy was derived from a higher entity + continue + response = self.get_downward_hierarchy(res_type, id_) + self.render(action_resources, aset, response) result = dict(map(lambda k: (k, list()), action_list)) for i in aset: res_id, res_type, action = i diff --git a/auth/auth_server/controllers/scope.py b/auth/auth_server/controllers/scope.py index da3886eb5..d3c513ab4 100644 --- a/auth/auth_server/controllers/scope.py +++ b/auth/auth_server/controllers/scope.py @@ -44,12 +44,16 @@ def _action_resourses(self, action, filter_assignable=False, **kwargs): user, [action]) if not action_resources: return [] - downward_hierarchy = base_controller.get_downward_hierarchy( - 'root', None) sorted_action_res = sorted(action_resources, key=base_controller.get_type_sorter()) - aset = OrderedSet(sorted_action_res) - base_controller.render(sorted_action_res, aset, downward_hierarchy) + aset = OrderedSet() + for id_, res_type, action in sorted_action_res: + if (id_, res_type, action) in aset: + # downward hierarchy was derived from a higher entity + continue + downward_hierarchy = base_controller.get_downward_hierarchy( + res_type, id_) + base_controller.render(sorted_action_res, aset, downward_hierarchy) payload = list(map(lambda x: (x[1], x[0]), aset)) resources_info = base_controller.get_resources_info(payload) res_type_dict = self._list_types() diff --git a/auth/auth_server/controllers/user.py b/auth/auth_server/controllers/user.py index bb3dd8628..b637b12e7 100644 --- a/auth/auth_server/controllers/user.py +++ b/auth/auth_server/controllers/user.py @@ -211,12 +211,12 @@ def get(self, item_id, **kwargs): if 'token' in kwargs: token = kwargs.pop('token') user = self.get_user(token) - action_resources = self.get_action_resources(token, - ['LIST_USERS']) - if not (check_action(action_resources, 'LIST_USERS', - item.type.name, item.scope_id) or - self._is_self_edit(user, item_id)): - raise ForbiddenException(Err.OA0012, []) + if not self._is_self_edit(user, item_id): + action_resources = self.get_action_resources( + token, ['LIST_USERS']) + if not check_action(action_resources, 'LIST_USERS', + item.type.name, item.scope_id): + raise ForbiddenException(Err.OA0012, []) payload = ((item.type.name, item.scope_id),) scope_info = self.get_resources_info(payload).get(item.scope_id, {}) return item, scope_info diff --git a/auth/auth_server/tests/unittests/test_api_action_resource.py b/auth/auth_server/tests/unittests/test_api_action_resource.py index 90ea0169d..cc20c5a02 100644 --- a/auth/auth_server/tests/unittests/test_api_action_resource.py +++ b/auth/auth_server/tests/unittests/test_api_action_resource.py @@ -20,7 +20,7 @@ def setUp(self, version="v2"): self.customer3_scope_id = 'c39e4f59-e2a0-4199-80b2-fa688b53598a' self.group3_scope_id = '4c6a6953-1c31-402d-b864-99790badd361' self.hierarchy = ( - {'root': {'null': {'partner': { + {'partner': { 'a5cb80ad-891d-4ec2-99de-ba4f20ba2c5d': {'customer': { '19a00828-fbff-4318-8291-4b6c14a8066d': @@ -36,7 +36,7 @@ def setUp(self, version="v2"): 'c39e4f59-e2a0-4199-80b2-fa688b53598a': {'group': ['4c6a6953-1c31-402d-b864-99790badd361'] } - }}}}}}) + }}}}) self.admin_user_password = 'password!' self.admin_user = self.create_root_user( password=self.admin_user_password) @@ -156,21 +156,23 @@ def test_action_root(self, p_hierarchy): code, response = self.client.action_resources_get( ['CREATE_USER']) self.assertEqual(code, 200) - self.assertEqual(len(list(filter(lambda x: x[1] in [ - self.partner_scope_id, self.partner2_scope_id, - self.customer1_scope_id, self.customer2_scope_id, - self.customer3_scope_id], response['CREATE_USER']))), 5) + self.assertEqual(response['CREATE_USER'], []) @patch(HIERARCHY_URL) def test_action_user_id(self, p_hierarchy): p_hierarchy.return_value = self.hierarchy code, response = self.client.action_resources_get( - ['CREATE_USER'], user_id=self.admin_user.id) + ['CREATE_USER'], user_id=self.user_partner.id) self.assertEqual(code, 200) self.assertEqual(len(list(filter(lambda x: x[1] in [ - self.partner_scope_id, self.partner2_scope_id, - self.customer1_scope_id, self.customer2_scope_id, - self.customer3_scope_id], response['CREATE_USER']))), 5) + self.partner_scope_id, self.customer1_scope_id, + self.customer2_scope_id], response['CREATE_USER']))), 3) + + code, response = self.client.action_resources_get( + ['CREATE_USER'], user_id=self.user_customer.id) + self.assertEqual(code, 200) + self.assertEqual(len(list(filter(lambda x: x[1] in [ + self.customer3_scope_id], response['CREATE_USER']))), 1) @patch(HIERARCHY_URL) def test_bulk_action_resources(self, p_hierarchy): @@ -186,11 +188,14 @@ def test_bulk_action_resources(self, p_hierarchy): self.assertCountEqual(response[user_id].keys(), ['CREATE_USER']) admin_response = response[self.admin_user.id] - self.assertCountEqual(admin_response['CREATE_USER'], [ - ['root', None], + self.assertEqual(admin_response['CREATE_USER'], []) + partner_response = response[self.user_partner.id] + self.assertCountEqual(partner_response['CREATE_USER'], [ ['partner', self.partner_scope_id], - ['partner', self.partner2_scope_id], ['customer', self.customer1_scope_id], ['customer', self.customer2_scope_id], + ]) + customer_response = response[self.user_customer.id] + self.assertCountEqual(customer_response['CREATE_USER'], [ ['customer', self.customer3_scope_id], ]) diff --git a/auth/auth_server/tests/unittests/test_api_assignment.py b/auth/auth_server/tests/unittests/test_api_assignment.py index 2611d95d0..c10d63dbd 100644 --- a/auth/auth_server/tests/unittests/test_api_assignment.py +++ b/auth/auth_server/tests/unittests/test_api_assignment.py @@ -22,7 +22,7 @@ def setUp(self, version="v2"): self.partner2_scope_id = '843f42c4-76b5-467f-b5e3-f7370b1235d6' self.customer2_1_scope_id = '70e552f4-032f-4989-bdbb-50a9d66920a2' self.hierarchy = ( - {'root': {'null': {'partner': { + {'partner': { 'a5cb80ad-891d-4ec2-99de-ba4f20ba2c5d': {'customer': { '19a00828-fbff-4318-8291-4b6c14a8066d': @@ -37,7 +37,7 @@ def setUp(self, version="v2"): {'customer': { '70e552f4-032f-4989-bdbb-50a9d66920a2': {'group': ['18559e53-fde8-4d73-81ef-57146bb0dde9']} - }}}}}}) + }}}}) admin_user = self.create_root_user() session = self.db_session self.type_partner = Type(id_=10, name='partner', diff --git a/auth/auth_server/tests/unittests/test_api_authorize_userlist.py b/auth/auth_server/tests/unittests/test_api_authorize_userlist.py index db0287c18..5977bde65 100644 --- a/auth/auth_server/tests/unittests/test_api_authorize_userlist.py +++ b/auth/auth_server/tests/unittests/test_api_authorize_userlist.py @@ -20,7 +20,7 @@ def setUp(self, version="v2"): self.customer3_scope_id = 'c39e4f59-e2a0-4199-80b2-fa688b53598a' self.group3_scope_id = '4c6a6953-1c31-402d-b864-99790badd361' self.hierarchy = ( - {'root': {'null': {'partner': { + {'partner': { self.partner_scope_id: {'customer': { self.customer1_scope_id: @@ -36,7 +36,7 @@ def setUp(self, version="v2"): self.customer3_scope_id: {'group': [self.group3_scope_id] } - }}}}}}) + }}}}) self.admin_user_password = 'password!' self.admin_user = self.create_root_user( password=self.admin_user_password) diff --git a/auth/auth_server/tests/unittests/test_api_role.py b/auth/auth_server/tests/unittests/test_api_role.py index 6c0abd5b3..5223d9e7d 100644 --- a/auth/auth_server/tests/unittests/test_api_role.py +++ b/auth/auth_server/tests/unittests/test_api_role.py @@ -32,7 +32,7 @@ def setUp(self, version="v2"): "group": self.group_scope_id } self.hierarchy = ( - {'root': {'null': {'partner': { + {'partner': { self.partner_1_scope_id: {'customer': { self.customer_1_scope_id: @@ -46,7 +46,7 @@ def setUp(self, version="v2"): {'group': [self.group_scope_id]} }}, } - }}}) + }) admin_user = self.create_root_user() session = self.db_session self.type_partner = Type(id_=10, name='partner', diff --git a/auth/auth_server/tests/unittests/test_api_role_root.py b/auth/auth_server/tests/unittests/test_api_role_root.py index d194c359a..569a330c8 100644 --- a/auth/auth_server/tests/unittests/test_api_role_root.py +++ b/auth/auth_server/tests/unittests/test_api_role_root.py @@ -86,4 +86,5 @@ def test_list_roles_assignable(self, p_get_hierarchy, p_get_context, assignable_to_user_id=self.admin_user.id) self.assertEqual(code, 200) # will return admin role - self.assertEqual(len(response), 1) + # denied in OSN-1379 + self.assertEqual(len(response), 0) diff --git a/auth/auth_server/tests/unittests/test_api_scope.py b/auth/auth_server/tests/unittests/test_api_scope.py index b61dfe4df..bdd6ff9ec 100644 --- a/auth/auth_server/tests/unittests/test_api_scope.py +++ b/auth/auth_server/tests/unittests/test_api_scope.py @@ -24,7 +24,7 @@ def setUp(self, version="v2"): self.group11_scope_id = 'be7b4d5e-33b6-40aa-bc6a-00c7d822606f' self.customer3_scope_id = 'c39e4f59-e2a0-4199-80b2-fa688b53598a' self.hierarchy = ( - {'root': {'null': {'partner': { + {'partner': { 'a5cb80ad-891d-4ec2-99de-ba4f20ba2c5d': {'customer': { '19a00828-fbff-4318-8291-4b6c14a8066d': @@ -40,7 +40,7 @@ def setUp(self, version="v2"): 'c39e4f59-e2a0-4199-80b2-fa688b53598a': {'group': ['4c6a6953-1c31-402d-b864-99790badd361'] } - }}}}}}) + }}}}) self.admin_user_password = 'password!' self.admin_user = self.create_root_user( password=self.admin_user_password) @@ -259,8 +259,8 @@ def test_assign_user_scope_bad_request(self): @patch(HIERARCHY_URL) def test_scope_assign_partner_role(self, p_hierarchy, p_res_info, p_context): - self.client.token = self.get_token(self.admin_user.email, - self.admin_user_password) + self.client.token = self.get_token(self.user_partner.email, + self.user_partner_password) partner_hierarchy = ( {'partner': { 'a5cb80ad-891d-4ec2-99de-ba4f20ba2c5d': @@ -294,8 +294,8 @@ def test_scope_assign_partner_role(self, p_hierarchy, p_res_info, @patch(CONTEXT_URL) def test_scope_assign_customer_role(self, p_context, p_hierarchy, p_res_info): - self.client.token = self.get_token(self.admin_user.email, - self.admin_user_password) + self.client.token = self.get_token(self.user_partner.email, + self.user_partner_password) partner_hierarchy = ( {'partner': { 'a5cb80ad-891d-4ec2-99de-ba4f20ba2c5d': diff --git a/auth/auth_server/tests/unittests/test_api_signin.py b/auth/auth_server/tests/unittests/test_api_signin.py index 836a7598b..fba3dcfda 100644 --- a/auth/auth_server/tests/unittests/test_api_signin.py +++ b/auth/auth_server/tests/unittests/test_api_signin.py @@ -18,14 +18,14 @@ def setUp(self, version="v2"): self.customer1_scope_id = '19a00828-fbff-4318-8291-4b6c14a8066d' self.group11_scope_id = 'be7b4d5e-33b6-40aa-bc6a-00c7d822606f' self.hierarchy = ( - {'root': {'null': {'partner': { + {'partner': { 'a5cb80ad-891d-4ec2-99de-ba4f20ba2c5d': {'customer': { '19a00828-fbff-4318-8291-4b6c14a8066d': {'group': ['be7b4d5e-33b6-40aa-bc6a-00c7d822606f'] }, }}, - }}}} + }} ) admin_user = self.create_root_user() session = self.db_session diff --git a/auth/auth_server/tests/unittests/test_api_user.py b/auth/auth_server/tests/unittests/test_api_user.py index 167ddd43c..86e819202 100644 --- a/auth/auth_server/tests/unittests/test_api_user.py +++ b/auth/auth_server/tests/unittests/test_api_user.py @@ -25,7 +25,7 @@ def setUp(self, version="v2"): self.customer2_scope_id = '6cfea3e7-a037-4529-9a14-dd9c5151b1f5' self.group11_scope_id = 'be7b4d5e-33b6-40aa-bc6a-00c7d822606f' self.hierarchy = ( - {'root': {'null': {'partner': { + {'partner': { 'a5cb80ad-891d-4ec2-99de-ba4f20ba2c5d': {'customer': { '19a00828-fbff-4318-8291-4b6c14a8066d': @@ -36,7 +36,7 @@ def setUp(self, version="v2"): '42667dde-0427-49be-9541-8e99362ee96e'] }, }}, - '843f42c4-76b5-467f-b5e3-f7370b1235d6': {'customer': {}}}}}}) + '843f42c4-76b5-467f-b5e3-f7370b1235d6': {'customer': {}}}}) admin_user = self.create_root_user() session = self.db_session type_partner = Type(id_=10, name='partner', parent=admin_user.type) @@ -287,7 +287,7 @@ def test_get_user(self, p_hierarchy, p_res_info): @patch(HIERARCHY_URL) def test_list_user_assigned_resource_deleted(self, p_hierarchy, p_res_info): - del self.hierarchy['root']['null']['partner'][ + del self.hierarchy['partner'][ self.partner_scope_id]['customer'][self.customer1_scope_id] p_hierarchy.return_value = self.hierarchy p_res_info.return_value = {} diff --git a/auth/auth_server/tests/unittests/test_api_user_options.py b/auth/auth_server/tests/unittests/test_api_user_options.py index 0108f75ae..26e2bd9c5 100644 --- a/auth/auth_server/tests/unittests/test_api_user_options.py +++ b/auth/auth_server/tests/unittests/test_api_user_options.py @@ -15,7 +15,7 @@ def setUp(self, version="v2"): self.customer2_scope_id = '6cfea3e7-a037-4529-9a14-dd9c5151b1f5' self.group11_scope_id = 'be7b4d5e-33b6-40aa-bc6a-00c7d822606f' self.hierarchy = ( - {'root': {'null': {'partner': { + {'partner': { 'a5cb80ad-891d-4ec2-99de-ba4f20ba2c5d': {'customer': { '19a00828-fbff-4318-8291-4b6c14a8066d': @@ -26,7 +26,7 @@ def setUp(self, version="v2"): '42667dde-0427-49be-9541-8e99362ee96e'] }, }}, - '843f42c4-76b5-467f-b5e3-f7370b1235d6': {'customer': {}}}}}}) + '843f42c4-76b5-467f-b5e3-f7370b1235d6': {'customer': {}}}}) admin_user = self.create_root_user() session = self.db_session type_partner = Type(id_=10, name='partner', parent=admin_user.type) diff --git a/rest_api/rest_api_server/controllers/auth_hierarchy.py b/rest_api/rest_api_server/controllers/auth_hierarchy.py index 8b58d09c6..809580973 100644 --- a/rest_api/rest_api_server/controllers/auth_hierarchy.py +++ b/rest_api/rest_api_server/controllers/auth_hierarchy.py @@ -46,19 +46,19 @@ def _check_resource(self, type, scope_id): def auth_hierarchy(self, type=None, scope_id=None): if not type: raise WrongArgumentsException(Err.OE0216, ['type']) - if not scope_id and type != 'root': + if not scope_id: raise WrongArgumentsException(Err.OE0216, ['scope_id']) - if type != 'root': - self._check_resource(type, scope_id) + self._check_resource(type, scope_id) result = dict() sql = self.session.query(Organization.id, Pool.id).outerjoin( Pool, and_( Pool.organization_id == Organization.id, Pool.deleted.is_(False)) - ).filter(Organization.deleted.is_(False)) - if type != 'root': - sql = sql.filter(self.model_map.get(type).id == scope_id) + ).filter( + Organization.deleted.is_(False), + self.model_map.get(type).id == scope_id + ) queryset = sql.order_by(Organization.id, Pool.id).all() if result.get('organization') is None: result['organization'] = dict() @@ -71,7 +71,6 @@ def auth_hierarchy(self, type=None, scope_id=None): if pool_id is not None: result['organization'][organization_id]['pool'].append(pool_id) result_scope = { - 'root': lambda t: dict(root=dict(null=result)), 'organization': lambda t: dict(organization=copy.deepcopy(result[t])), 'pool': lambda t: dict( pool=copy.deepcopy(result['organization'][queryset[0][0]][t])) diff --git a/rest_api/rest_api_server/tests/unittests/test_auth_hierarchy_api.py b/rest_api/rest_api_server/tests/unittests/test_auth_hierarchy_api.py index 8d8e1f0ce..f9432d237 100644 --- a/rest_api/rest_api_server/tests/unittests/test_auth_hierarchy_api.py +++ b/rest_api/rest_api_server/tests/unittests/test_auth_hierarchy_api.py @@ -16,30 +16,14 @@ def setUp(self, version='v2'): _, self.o2 = self.client.organization_create({'name': 'Organization2'}) def test_root_hierarchy(self): - code, hierarchy = self.client.auth_hierarchy_get('root', None) - self.assertEqual(code, 200) - self.assertEqual(len(hierarchy['root']['null']['organization']), 2) - self.assertEqual(len( - list(filter(lambda x: x in [self.o1['id'], self.o2['id']], - hierarchy['root']['null']['organization'].keys()))), 2) - self.assertEqual( - hierarchy['root']['null']['organization'][self.o2['id']]['pool'], - [self.o2['pool_id']]) - self.assertEqual(len( - hierarchy['root']['null']['organization'][self.o1['id']]['pool'] - ), 3) - self.assertEqual(len(list(filter( - lambda x: x in [self.o1['pool_id'], self.b11['id'], self.b12['id']], - hierarchy['root']['null']['organization'][self.o1['id']]['pool']))), 3) + code, hierarchy = self.client.auth_hierarchy_get('root', self.o1['id']) + self.assertEqual(code, 400) def test_root_hierarchy_deleted_organization(self): self.delete_organization(self.o1['id']) - code, hierarchy = self.client.auth_hierarchy_get('root', None) - self.assertEqual(code, 200) - self.assertEqual(len(hierarchy['root']['null']['organization']), 1) - self.assertEqual( - hierarchy['root']['null']['organization'][self.o2['id']]['pool'], - [self.o2['pool_id']]) + code, hierarchy = self.client.auth_hierarchy_get( + 'organization', self.o1['id']) + self.assertEqual(code, 404) def test_organization_hierarchy(self): code, hierarchy = self.client.auth_hierarchy_get(