Skip to content
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
40 changes: 40 additions & 0 deletions auth/auth_server/alembic/versions/88f7bebcdcb9_add_user_options.py
Original file line number Diff line number Diff line change
@@ -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')
5 changes: 4 additions & 1 deletion auth/auth_server/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ class Urls:
r"%s/users/(?P<user_id>[^/]+)/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<user_id>[^/]+)/options",
'user_options':
r"%s/users/(?P<user_id>[^/]+)/options/(?P<option_name>[^/]+)"
}

def __init__(self):
Expand Down
63 changes: 28 additions & 35 deletions auth/auth_server/controllers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 8 additions & 4 deletions auth/auth_server/controllers/scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
12 changes: 6 additions & 6 deletions auth/auth_server/controllers/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
93 changes: 93 additions & 0 deletions auth/auth_server/controllers/user_option.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
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,
check_valid_json)
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)
check_valid_json(value_data, 'value')
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
2 changes: 1 addition & 1 deletion auth/auth_server/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions auth/auth_server/handlers/v2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 3 additions & 3 deletions auth/auth_server/handlers/v2/base.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 = {}
Expand Down
Loading
Loading