diff --git a/server/src/uds/REST/methods/accounts.py b/server/src/uds/REST/methods/accounts.py index 61bc1e044..f74fc0e2b 100644 --- a/server/src/uds/REST/methods/accounts.py +++ b/server/src/uds/REST/methods/accounts.py @@ -39,7 +39,7 @@ from django.utils.translation import gettext_lazy as _ from uds.REST.model import ModelHandler from uds.core import types import uds.core.types.permissions -from uds.core.util import permissions, ensure +from uds.core.util import permissions, ensure, ui as ui_utils from uds.models import Account from .accountsusage import AccountsUsage @@ -51,7 +51,7 @@ logger = logging.getLogger(__name__) # Enclosed methods under /item path -class AccountItem(types.rest.ItemDictType): +class AccountItem(types.rest.BaseRestItem): id: str name: str tags: typing.List[str] @@ -95,13 +95,11 @@ class Accounts(ModelHandler[AccountItem]): } def get_gui(self, for_type: str) -> list[types.ui.GuiElement]: - return self.compose_gui( - [ - types.rest.stock.StockField.NAME, - types.rest.stock.StockField.COMMENTS, - types.rest.stock.StockField.TAGS, - ], - ) + return ui_utils.GuiBuilder( + types.rest.stock.StockField.NAME, + types.rest.stock.StockField.COMMENTS, + types.rest.stock.StockField.TAGS, + ).build() def timemark(self, item: 'Model') -> typing.Any: """ @@ -128,5 +126,5 @@ class Accounts(ModelHandler[AccountItem]): Clears all usage associated with the account """ item = ensure.is_instance(item, Account) - self.ensure_has_access(item, uds.core.types.permissions.PermissionType.MANAGEMENT) + self.check_access(item, uds.core.types.permissions.PermissionType.MANAGEMENT) return item.usages.filter(user_service=None).delete() diff --git a/server/src/uds/REST/methods/accountsusage.py b/server/src/uds/REST/methods/accountsusage.py index 5d705a356..cd3ef0126 100644 --- a/server/src/uds/REST/methods/accountsusage.py +++ b/server/src/uds/REST/methods/accountsusage.py @@ -49,7 +49,7 @@ if typing.TYPE_CHECKING: logger = logging.getLogger(__name__) -class AccountItem(types.rest.ItemDictType): +class AccountItem(types.rest.BaseRestItem): uuid: str pool_uuid: str pool_name: str @@ -89,7 +89,7 @@ class AccountsUsage(DetailHandler[AccountItem]): # pylint: disable=too-many-pub 'permission': perm, } - def get_items(self, parent: 'Model', item: typing.Optional[str]) -> types.rest.GetItemsResult[AccountItem]: + def get_items(self, parent: 'Model', item: typing.Optional[str]) -> types.rest.ItemsResult[AccountItem]: parent = ensure.is_instance(parent, Account) # Check what kind of access do we have to parent provider perm = permissions.effective_permissions(self._user, parent) diff --git a/server/src/uds/REST/methods/actor_token.py b/server/src/uds/REST/methods/actor_token.py index c7eef7555..f45754ec2 100644 --- a/server/src/uds/REST/methods/actor_token.py +++ b/server/src/uds/REST/methods/actor_token.py @@ -51,7 +51,7 @@ logger = logging.getLogger(__name__) # Enclosed methods under /osm path -class ActorTokenItem(types.rest.ItemDictType): +class ActorTokenItem(types.rest.BaseRestItem): id: str name: str stamp: datetime.datetime @@ -119,7 +119,7 @@ class ActorTokens(ModelHandler[ActorTokenItem]): if len(self._args) != 1: raise RequestError('Delete need one and only one argument') - self.ensure_has_access( + self.check_access( self.model(), permissions.PermissionType.ALL, root=True ) # Must have write permissions to delete diff --git a/server/src/uds/REST/methods/authenticators.py b/server/src/uds/REST/methods/authenticators.py index 0a02b144c..1361e1d57 100644 --- a/server/src/uds/REST/methods/authenticators.py +++ b/server/src/uds/REST/methods/authenticators.py @@ -57,7 +57,7 @@ if typing.TYPE_CHECKING: logger = logging.getLogger(__name__) -class AuthenticatorItem(types.rest.ManagedObjectDictType): +class AuthenticatorItem(types.rest.ManagedObjectItem): numeric_id: int id: str name: str @@ -206,7 +206,7 @@ class Authenticators(ModelHandler[AuthenticatorItem]): Search for users or groups in this authenticator """ item = ensure.is_instance(item, Authenticator) - self.ensure_has_access(item, types.permissions.PermissionType.READ) + self.check_access(item, types.permissions.PermissionType.READ) try: type_ = self._params['type'] if type_ not in ('user', 'group'): diff --git a/server/src/uds/REST/methods/calendarrules.py b/server/src/uds/REST/methods/calendarrules.py index b3dc33938..b7877cecc 100644 --- a/server/src/uds/REST/methods/calendarrules.py +++ b/server/src/uds/REST/methods/calendarrules.py @@ -50,7 +50,7 @@ if typing.TYPE_CHECKING: logger = logging.getLogger(__name__) -class CalendarRuleItem(types.rest.ItemDictType): +class CalendarRuleItem(types.rest.BaseRestItem): id: str name: str comments: str @@ -87,7 +87,7 @@ class CalendarRules(DetailHandler[CalendarRuleItem]): # pylint: disable=too-man 'permission': perm, } - def get_items(self, parent: 'Model', item: typing.Optional[str]) -> types.rest.GetItemsResult[CalendarRuleItem]: + def get_items(self, parent: 'Model', item: typing.Optional[str]) -> types.rest.ItemsResult[CalendarRuleItem]: parent = ensure.is_instance(parent, Calendar) # Check what kind of access do we have to parent provider perm = permissions.effective_permissions(self._user, parent) diff --git a/server/src/uds/REST/methods/calendars.py b/server/src/uds/REST/methods/calendars.py index 79982625e..e2d1bab88 100644 --- a/server/src/uds/REST/methods/calendars.py +++ b/server/src/uds/REST/methods/calendars.py @@ -37,7 +37,7 @@ import typing from django.utils.translation import gettext_lazy as _ from uds.core import types from uds.models import Calendar -from uds.core.util import permissions, ensure +from uds.core.util import permissions, ensure, ui as ui_utils from uds.REST.model import ModelHandler from .calendarrules import CalendarRules @@ -49,7 +49,7 @@ if typing.TYPE_CHECKING: logger = logging.getLogger(__name__) -class CalendarItem(types.rest.ItemDictType): +class CalendarItem(types.rest.BaseRestItem): id: str name: str tags: list[str] @@ -104,10 +104,8 @@ class Calendars(ModelHandler[CalendarItem]): } def get_gui(self, for_type: str) -> list[typing.Any]: - return self.compose_gui( - [ - types.rest.stock.StockField.NAME, - types.rest.stock.StockField.COMMENTS, - types.rest.stock.StockField.TAGS, - ], - ) + return ui_utils.GuiBuilder( + types.rest.stock.StockField.NAME, + types.rest.stock.StockField.COMMENTS, + types.rest.stock.StockField.TAGS, + ).build() diff --git a/server/src/uds/REST/methods/images.py b/server/src/uds/REST/methods/images.py index d5e24458c..b3c6e4fd2 100644 --- a/server/src/uds/REST/methods/images.py +++ b/server/src/uds/REST/methods/images.py @@ -48,7 +48,7 @@ logger = logging.getLogger(__name__) # Enclosed methods under /item path -class ImageItem(types.rest.ItemDictType): +class ImageItem(types.rest.BaseRestItem): id: str name: str data: typing.NotRequired[str] diff --git a/server/src/uds/REST/methods/meta_pools.py b/server/src/uds/REST/methods/meta_pools.py index e9e451d70..c2429f1a4 100644 --- a/server/src/uds/REST/methods/meta_pools.py +++ b/server/src/uds/REST/methods/meta_pools.py @@ -55,7 +55,7 @@ if typing.TYPE_CHECKING: logger = logging.getLogger(__name__) -class MetaPoolItem(types.rest.ItemDictType): +class MetaPoolItem(types.rest.BaseRestItem): id: str name: str short_name: str @@ -289,7 +289,7 @@ class MetaPools(ModelHandler[MetaPoolItem]): API: Sets the fallback access for a metapool """ - self.ensure_has_access(item, types.permissions.PermissionType.MANAGEMENT) + self.check_access(item, types.permissions.PermissionType.MANAGEMENT) fallback = self._params.get('fallbackAccess', 'ALLOW') logger.debug('Setting fallback of %s to %s', item.name, fallback) diff --git a/server/src/uds/REST/methods/meta_service_pools.py b/server/src/uds/REST/methods/meta_service_pools.py index 331325225..bc4f16dfd 100644 --- a/server/src/uds/REST/methods/meta_service_pools.py +++ b/server/src/uds/REST/methods/meta_service_pools.py @@ -53,7 +53,7 @@ if typing.TYPE_CHECKING: logger = logging.getLogger(__name__) -class MetaItem(types.rest.ItemDictType): +class MetaItem(types.rest.BaseRestItem): """ Item type for a Meta Pool Member """ @@ -86,7 +86,7 @@ class MetaServicesPool(DetailHandler[MetaItem]): 'user_services_in_preparation': item.pool.userServices.filter(state=State.PREPARING).count(), } - def get_items(self, parent: 'Model', item: typing.Optional[str]) -> types.rest.GetItemsResult['MetaItem']: + def get_items(self, parent: 'Model', item: typing.Optional[str]) -> types.rest.ItemsResult['MetaItem']: parent = ensure.is_instance(parent, models.MetaPool) try: if not item: @@ -177,7 +177,7 @@ class MetaAssignedService(DetailHandler[UserServiceItem]): except Exception: raise self.invalid_item_response() - def get_items(self, parent: 'Model', item: typing.Optional[str]) -> types.rest.GetItemsResult[UserServiceItem]: + def get_items(self, parent: 'Model', item: typing.Optional[str]) -> types.rest.ItemsResult[UserServiceItem]: parent = ensure.is_instance(parent, models.MetaPool) def _assigned_userservices_for_pools() -> ( typing.Generator[ diff --git a/server/src/uds/REST/methods/mfas.py b/server/src/uds/REST/methods/mfas.py index f4555072c..6879173d9 100644 --- a/server/src/uds/REST/methods/mfas.py +++ b/server/src/uds/REST/methods/mfas.py @@ -51,7 +51,7 @@ logger = logging.getLogger(__name__) # Enclosed methods under /item path -class MFAItem(types.rest.ItemDictType): +class MFAItem(types.rest.BaseRestItem): id: str name: str remember_device: int diff --git a/server/src/uds/REST/methods/networks.py b/server/src/uds/REST/methods/networks.py index b3d0a46f4..23742bfd0 100644 --- a/server/src/uds/REST/methods/networks.py +++ b/server/src/uds/REST/methods/networks.py @@ -49,7 +49,7 @@ logger = logging.getLogger(__name__) # Enclosed methods under /item path -class NetworkItem(types.rest.ItemDictType): +class NetworkItem(types.rest.BaseRestItem): id: str name: str tags: list[str] diff --git a/server/src/uds/REST/methods/notifiers.py b/server/src/uds/REST/methods/notifiers.py index 003126395..6a08b0863 100644 --- a/server/src/uds/REST/methods/notifiers.py +++ b/server/src/uds/REST/methods/notifiers.py @@ -53,7 +53,7 @@ logger = logging.getLogger(__name__) # Enclosed methods under /item path -class NotifierItem(types.rest.ItemDictType): +class NotifierItem(types.rest.BaseRestItem): id: str name: str level: str diff --git a/server/src/uds/REST/methods/op_calendars.py b/server/src/uds/REST/methods/op_calendars.py index 8dc008497..38fe0f6b4 100644 --- a/server/src/uds/REST/methods/op_calendars.py +++ b/server/src/uds/REST/methods/op_calendars.py @@ -52,7 +52,7 @@ logger = logging.getLogger(__name__) ALLOW = 'ALLOW' DENY = 'DENY' -class AccessCalendarItem(types.rest.ItemDictType): +class AccessCalendarItem(types.rest.BaseRestItem): id: str calendar_id: str calendar: str @@ -70,7 +70,7 @@ class AccessCalendars(DetailHandler[AccessCalendarItem]): 'priority': item.priority, } - def get_items(self, parent: 'Model', item: typing.Optional[str]) -> types.rest.GetItemsResult[AccessCalendarItem]: + def get_items(self, parent: 'Model', item: typing.Optional[str]) -> types.rest.ItemsResult[AccessCalendarItem]: # parent can be a ServicePool or a metaPool parent = typing.cast(typing.Union['models.ServicePool', 'models.MetaPool'], parent) @@ -133,7 +133,7 @@ class AccessCalendars(DetailHandler[AccessCalendarItem]): log.log(parent, types.log.LogLevel.INFO, log_str, types.log.LogSource.ADMIN) -class ActionCalendarItem(types.rest.ItemDictType): +class ActionCalendarItem(types.rest.BaseRestItem): id: str calendar_id: str calendar: str @@ -174,7 +174,7 @@ class ActionsCalendars(DetailHandler[ActionCalendarItem]): 'last_execution': item.last_execution, } - def get_items(self, parent: 'Model', item: typing.Optional[str]) -> types.rest.GetItemsResult[ActionCalendarItem]: + def get_items(self, parent: 'Model', item: typing.Optional[str]) -> types.rest.ItemsResult[ActionCalendarItem]: parent = ensure.is_instance(parent, models.ServicePool) try: if item is None: @@ -267,7 +267,7 @@ class ActionsCalendars(DetailHandler[ActionCalendarItem]): logger.debug('Launching action') uuid = process_uuid(item) calendar_action: models.CalendarAction = models.CalendarAction.objects.get(uuid=uuid) - self.ensure_has_access(calendar_action, types.permissions.PermissionType.MANAGEMENT) + self.check_access(calendar_action, types.permissions.PermissionType.MANAGEMENT) log_str = ( f'Launched scheduled action "{calendar_action.calendar.name},' diff --git a/server/src/uds/REST/methods/osmanagers.py b/server/src/uds/REST/methods/osmanagers.py index af9e70631..361b96c1b 100644 --- a/server/src/uds/REST/methods/osmanagers.py +++ b/server/src/uds/REST/methods/osmanagers.py @@ -38,7 +38,7 @@ from django.utils.translation import gettext, gettext_lazy as _ from uds.core import exceptions, osmanagers, types from uds.core.environment import Environment -from uds.core.util import ensure, permissions +from uds.core.util import ensure, permissions, ui as ui_utils from uds.models import OSManager from uds.REST.model import ModelHandler @@ -50,7 +50,7 @@ logger = logging.getLogger(__name__) # Enclosed methods under /osm path -class OsManagerItem(types.rest.ManagedObjectDictType): +class OsManagerItem(types.rest.ManagedObjectItem): id: str name: str tags: list[str] @@ -116,13 +116,11 @@ class OsManagers(ModelHandler[OsManagerItem]): raise exceptions.rest.NotFound('OS Manager type not found') with Environment.temporary_environment() as env: osmanager = osmanager_type(env, None) - return self.compose_gui( - [ - types.rest.stock.StockField.NAME, - types.rest.stock.StockField.COMMENTS, - types.rest.stock.StockField.TAGS, - ], - *osmanager.gui_description(), - ) + return ui_utils.GuiBuilder( + types.rest.stock.StockField.NAME, + types.rest.stock.StockField.COMMENTS, + types.rest.stock.StockField.TAGS, + gui=osmanager.gui_description(), + ).build() except: raise exceptions.rest.NotFound('type not found') diff --git a/server/src/uds/REST/methods/providers.py b/server/src/uds/REST/methods/providers.py index 10c57ce65..60eea1093 100644 --- a/server/src/uds/REST/methods/providers.py +++ b/server/src/uds/REST/methods/providers.py @@ -40,7 +40,7 @@ from django.utils.translation import gettext_lazy as _ import uds.core.types.permissions from uds.core import exceptions, services, types from uds.core.environment import Environment -from uds.core.util import ensure, permissions +from uds.core.util import ensure, permissions, ui as ui_utils from uds.core.types.states import State from uds.models import Provider, Service, UserService from uds.REST.model import ModelHandler @@ -55,14 +55,14 @@ if typing.TYPE_CHECKING: # Helper class for Provider offers -class OfferItem(types.rest.ItemDictType): +class OfferItem(types.rest.BaseRestItem): name: str type: str description: str icon: str -class ProviderItem(types.rest.ManagedObjectDictType): +class ProviderItem(types.rest.ManagedObjectItem): id: str name: str tags: list[str] @@ -151,17 +151,16 @@ class Providers(ModelHandler[ProviderItem]): if provider_type: with Environment.temporary_environment() as env: provider = provider_type(env, None) - return self.compose_gui( - [ - types.rest.stock.StockField.NAME, - types.rest.stock.StockField.COMMENTS, - types.rest.stock.StockField.TAGS, - ], - *provider.gui_description(), - ) + return ui_utils.GuiBuilder( + types.rest.stock.StockField.NAME, + types.rest.stock.StockField.COMMENTS, + types.rest.stock.StockField.TAGS, + gui=provider.gui_description(), + ).build() + raise exceptions.rest.NotFound('Type not found!') - def allservices(self) -> typing.Generator[types.rest.ItemDictType, None, None]: + def allservices(self) -> typing.Generator[types.rest.BaseRestItem, None, None]: """ Custom method that returns "all existing services", no mater who's his daddy :) """ @@ -173,26 +172,26 @@ class Providers(ModelHandler[ProviderItem]): except Exception: logger.exception('Passed service cause type is unknown') - def service(self) -> types.rest.ItemDictType: + def service(self) -> types.rest.BaseRestItem: """ Custom method that returns a service by its uuid, no matter who's his daddy """ try: service = Service.objects.get(uuid=self._args[1]) - self.ensure_has_access(service.provider, uds.core.types.permissions.PermissionType.READ) + self.check_access(service.provider, uds.core.types.permissions.PermissionType.READ) perm = self.get_permissions(service.provider) return DetailServices.service_to_dict(service, perm, True) except Exception: # logger.exception('Exception') return {} - def maintenance(self, item: 'Model') -> types.rest.ItemDictType: + def maintenance(self, item: 'Model') -> types.rest.BaseRestItem: """ Custom method that swaps maintenance mode state for a provider :param item: """ item = ensure.is_instance(item, Provider) - self.ensure_has_access(item, uds.core.types.permissions.PermissionType.MANAGEMENT) + self.check_access(item, uds.core.types.permissions.PermissionType.MANAGEMENT) item.maintenance_mode = not item.maintenance_mode item.save() return self.item_as_dict(item) diff --git a/server/src/uds/REST/methods/reports.py b/server/src/uds/REST/methods/reports.py index 027691d3f..6332dbd70 100644 --- a/server/src/uds/REST/methods/reports.py +++ b/server/src/uds/REST/methods/reports.py @@ -58,7 +58,7 @@ VALID_PARAMS = ( ) -class ReportItem(types.rest.ItemDictType): +class ReportItem(types.rest.BaseRestItem): id: str mime_type: str encoded: bool @@ -116,7 +116,7 @@ class Reports(model.BaseModelHandler[ReportItem]): ((consts.rest.OVERVIEW,), lambda: list(self.get_items())), ( (consts.rest.TABLEINFO,), - lambda: self.process_table_fields( + lambda: self.table_description( str(self.table_title), self.table_fields, self.table_row_style ), ), diff --git a/server/src/uds/REST/methods/servers_management.py b/server/src/uds/REST/methods/servers_management.py index 0b488d62d..c537eeed8 100644 --- a/server/src/uds/REST/methods/servers_management.py +++ b/server/src/uds/REST/methods/servers_management.py @@ -49,7 +49,7 @@ if typing.TYPE_CHECKING: logger = logging.getLogger(__name__) -class TokenItem(types.rest.ItemDictType): +class TokenItem(types.rest.BaseRestItem): id: str name: str stamp: datetime.datetime @@ -110,7 +110,7 @@ class ServersTokens(ModelHandler[TokenItem]): if len(self._args) != 1: raise RequestError('Delete need one and only one argument') - self.ensure_has_access( + self.check_access( self.model(), types.permissions.PermissionType.ALL, root=True ) # Must have write permissions to delete @@ -122,7 +122,7 @@ class ServersTokens(ModelHandler[TokenItem]): return consts.OK -class ServerItem(types.rest.ItemDictType): +class ServerItem(types.rest.BaseRestItem): id: str hostname: str ip: str @@ -138,7 +138,7 @@ class ServersServers(DetailHandler[ServerItem]): custom_methods = ['maintenance', 'importcsv'] - def get_items(self, parent: 'Model', item: typing.Optional[str]) -> types.rest.GetItemsResult[ServerItem]: + def get_items(self, parent: 'Model', item: typing.Optional[str]) -> types.rest.ItemsResult[ServerItem]: parent = typing.cast('models.ServerGroup', parent) # We will receive for sure try: if item is None: @@ -337,7 +337,7 @@ class ServersServers(DetailHandler[ServerItem]): :param item: """ item = models.Server.objects.get(uuid=process_uuid(id)) - self.ensure_has_access(item, types.permissions.PermissionType.MANAGEMENT) + self.check_access(item, types.permissions.PermissionType.MANAGEMENT) item.maintenance_mode = not item.maintenance_mode item.save() return 'ok' @@ -414,7 +414,7 @@ class ServersServers(DetailHandler[ServerItem]): return import_errors -class GroupItem(types.rest.ItemDictType): +class GroupItem(types.rest.BaseRestItem): id: str name: str comments: str @@ -519,7 +519,7 @@ class ServersGroups(ModelHandler[GroupItem]): """ Processes a DELETE request """ - self.ensure_has_access( + self.check_access( self.model(), permissions.PermissionType.ALL, root=True ) # Must have write permissions to delete diff --git a/server/src/uds/REST/methods/services.py b/server/src/uds/REST/methods/services.py index a1b70e680..89410af12 100644 --- a/server/src/uds/REST/methods/services.py +++ b/server/src/uds/REST/methods/services.py @@ -58,7 +58,7 @@ if typing.TYPE_CHECKING: logger = logging.getLogger(__name__) -class ServiceItem(types.rest.ManagedObjectDictType): +class ServiceItem(types.rest.ManagedObjectItem): id: str name: str tags: list[str] @@ -71,7 +71,7 @@ class ServiceItem(types.rest.ManagedObjectDictType): info: typing.NotRequired['ServiceInfo'] -class ServiceInfo(types.rest.ItemDictType): +class ServiceInfo(types.rest.BaseRestItem): icon: str needs_publication: bool max_deployed: int @@ -86,7 +86,7 @@ class ServiceInfo(types.rest.ItemDictType): can_list_assignables: bool -class ServicePoolResumeItem(types.rest.ItemDictType): +class ServicePoolResumeItem(types.rest.BaseRestItem): id: str name: str thumb: str @@ -148,7 +148,7 @@ class Services(DetailHandler[ServiceItem]): # pylint: disable=too-many-public-m return ret_value - def get_items(self, parent: 'Model', item: typing.Optional[str]) -> types.rest.GetItemsResult[ServiceItem]: + def get_items(self, parent: 'Model', item: typing.Optional[str]) -> types.rest.ItemsResult[ServiceItem]: parent = ensure.is_instance(parent, models.Provider) # Check what kind of access do we have to parent provider perm = permissions.effective_permissions(self._user, parent) @@ -380,7 +380,7 @@ class Services(DetailHandler[ServiceItem]): # pylint: disable=too-many-public-m res: list[ServicePoolResumeItem] = [] for i in service.deployedServices.all(): try: - self.ensure_has_access( + self.check_access( i, uds.core.types.permissions.PermissionType.READ ) # Ensures access before listing... res.append( diff --git a/server/src/uds/REST/methods/services_pool_groups.py b/server/src/uds/REST/methods/services_pool_groups.py index ccc08240f..f73d9a6d1 100644 --- a/server/src/uds/REST/methods/services_pool_groups.py +++ b/server/src/uds/REST/methods/services_pool_groups.py @@ -50,7 +50,7 @@ if typing.TYPE_CHECKING: # Enclosed methods under /item path -class ServicePoolGroupItem(types.rest.ItemDictType): +class ServicePoolGroupItem(types.rest.BaseRestItem): id: str name: str comments: str diff --git a/server/src/uds/REST/methods/services_pools.py b/server/src/uds/REST/methods/services_pools.py index cdce2469b..db417cade 100644 --- a/server/src/uds/REST/methods/services_pools.py +++ b/server/src/uds/REST/methods/services_pools.py @@ -59,7 +59,7 @@ if typing.TYPE_CHECKING: logger = logging.getLogger(__name__) -class ServicePoolItem(types.rest.ItemDictType): +class ServicePoolItem(types.rest.BaseRestItem): id: str name: str short_name: str @@ -605,7 +605,7 @@ class ServicesPools(ModelHandler[ServicePoolItem]): # Set fallback status def set_fallback_access(self, item: 'Model') -> typing.Any: item = ensure.is_instance(item, ServicePool) - self.ensure_has_access(item, types.permissions.PermissionType.MANAGEMENT) + self.check_access(item, types.permissions.PermissionType.MANAGEMENT) fallback = self._params.get('fallbackAccess', self.params.get('fallback', None)) if fallback: diff --git a/server/src/uds/REST/methods/services_usage.py b/server/src/uds/REST/methods/services_usage.py index bba382164..a3ddcfdc9 100644 --- a/server/src/uds/REST/methods/services_usage.py +++ b/server/src/uds/REST/methods/services_usage.py @@ -51,7 +51,7 @@ if typing.TYPE_CHECKING: logger = logging.getLogger(__name__) -class ServicesUsageItem(types.rest.ItemDictType): +class ServicesUsageItem(types.rest.BaseRestItem): id: str state_date: datetime.datetime creation_date: datetime.datetime @@ -111,7 +111,7 @@ class ServicesUsage(DetailHandler[ServicesUsageItem]): def get_items( self, parent: 'Model', item: typing.Optional[str] - ) -> types.rest.GetItemsResult[ServicesUsageItem]: + ) -> types.rest.ItemsResult[ServicesUsageItem]: parent = ensure.is_instance(parent, Provider) try: if item is None: diff --git a/server/src/uds/REST/methods/transports.py b/server/src/uds/REST/methods/transports.py index e675719bb..3682226fe 100644 --- a/server/src/uds/REST/methods/transports.py +++ b/server/src/uds/REST/methods/transports.py @@ -52,7 +52,7 @@ logger = logging.getLogger(__name__) # Enclosed methods under /item path -class TransportItem(types.rest.ManagedObjectDictType): +class TransportItem(types.rest.ManagedObjectItem): id: str name: str tags: list[str] diff --git a/server/src/uds/REST/methods/tunnels_management.py b/server/src/uds/REST/methods/tunnels_management.py index 7f30d8e25..8374a391e 100644 --- a/server/src/uds/REST/methods/tunnels_management.py +++ b/server/src/uds/REST/methods/tunnels_management.py @@ -49,7 +49,7 @@ if typing.TYPE_CHECKING: logger = logging.getLogger(__name__) -class TunnelServerItem(types.rest.ItemDictType): +class TunnelServerItem(types.rest.BaseRestItem): id: str hostname: str ip: str @@ -63,7 +63,7 @@ class TunnelServers(DetailHandler[TunnelServerItem]): def get_items( self, parent: 'Model', item: typing.Optional[str] - ) -> types.rest.GetItemsResult[TunnelServerItem]: + ) -> types.rest.ItemsResult[TunnelServerItem]: parent = ensure.is_instance(parent, models.ServerGroup) try: multi = False @@ -139,13 +139,13 @@ class TunnelServers(DetailHandler[TunnelServerItem]): """ parent = ensure.is_instance(parent, models.ServerGroup) item = models.Server.objects.get(uuid=process_uuid(id)) - self.ensure_has_access(item, uds.core.types.permissions.PermissionType.MANAGEMENT) + self.check_access(item, uds.core.types.permissions.PermissionType.MANAGEMENT) item.maintenance_mode = not item.maintenance_mode item.save() return 'ok' -class TunnelItem(types.rest.ItemDictType): +class TunnelItem(types.rest.BaseRestItem): id: str name: str comments: str @@ -235,7 +235,7 @@ class Tunnels(ModelHandler[TunnelItem]): def assign(self, parent: 'Model') -> typing.Any: parent = ensure.is_instance(parent, models.ServerGroup) - self.ensure_has_access(parent, uds.core.types.permissions.PermissionType.MANAGEMENT) + self.check_access(parent, uds.core.types.permissions.PermissionType.MANAGEMENT) server: typing.Optional['models.Server'] = None # Avoid warning on reference before assignment @@ -246,7 +246,7 @@ class Tunnels(ModelHandler[TunnelItem]): try: server = models.Server.objects.get(uuid=process_uuid(item)) - self.ensure_has_access(server, uds.core.types.permissions.PermissionType.READ) + self.check_access(server, uds.core.types.permissions.PermissionType.READ) parent.servers.add(server) except Exception: raise self.invalid_item_response() from None diff --git a/server/src/uds/REST/methods/user_services.py b/server/src/uds/REST/methods/user_services.py index 69f1f98ca..3ce914002 100644 --- a/server/src/uds/REST/methods/user_services.py +++ b/server/src/uds/REST/methods/user_services.py @@ -51,7 +51,7 @@ if typing.TYPE_CHECKING: logger = logging.getLogger(__name__) -class UserServiceItem(types.rest.ItemDictType): +class UserServiceItem(types.rest.BaseRestItem): id: str id_deployed_service: str unique_id: str @@ -149,7 +149,7 @@ class AssignedUserService(DetailHandler[UserServiceItem]): def get_items( self, parent: 'Model', item: typing.Optional[str] - ) -> types.rest.GetItemsResult['UserServiceItem']: + ) -> types.rest.ItemsResult['UserServiceItem']: parent = ensure.is_instance(parent, models.ServicePool) try: @@ -313,7 +313,7 @@ class CachedService(AssignedUserService): def get_items( self, parent: 'Model', item: typing.Optional[str] - ) -> types.rest.GetItemsResult['UserServiceItem']: + ) -> types.rest.ItemsResult['UserServiceItem']: parent = ensure.is_instance(parent, models.ServicePool) try: @@ -369,7 +369,7 @@ class CachedService(AssignedUserService): except Exception: raise self.invalid_item_response() from None -class GroupItem(types.rest.ItemDictType): +class GroupItem(types.rest.BaseRestItem): id: str auth_id: str name: str @@ -461,7 +461,7 @@ class Groups(DetailHandler[GroupItem]): types.log.LogSource.ADMIN, ) -class TransportItem(types.rest.ItemDictType): +class TransportItem(types.rest.BaseRestItem): id: str name: str type: types.rest.TypeInfoDict @@ -483,7 +483,7 @@ class Transports(DetailHandler[TransportItem]): def get_type(trans: 'models.Transport') -> types.rest.TypeInfoDict: try: - return self.type_as_dict(trans.get_type()) + return self.as_typeinfo(trans.get_type()) except Exception: # No type found raise self.invalid_item_response() @@ -538,7 +538,7 @@ class Transports(DetailHandler[TransportItem]): types.log.LogSource.ADMIN, ) -class PublicationItem(types.rest.ItemDictType): +class PublicationItem(types.rest.BaseRestItem): id: str revision: int publish_date: datetime.datetime @@ -648,7 +648,7 @@ class Publications(DetailHandler[PublicationItem]): def get_row_style(self, parent: 'Model') -> types.ui.RowStyleInfo: return types.ui.RowStyleInfo(prefix='row-state-', field='state') -class ChangelogItem(types.rest.ItemDictType): +class ChangelogItem(types.rest.BaseRestItem): revision: int stamp: datetime.datetime log: str diff --git a/server/src/uds/REST/methods/users_groups.py b/server/src/uds/REST/methods/users_groups.py index edf993388..b056ece51 100644 --- a/server/src/uds/REST/methods/users_groups.py +++ b/server/src/uds/REST/methods/users_groups.py @@ -78,7 +78,7 @@ def get_service_pools_for_groups( yield servicepool -class UserItem(types.rest.ItemDictType): +class UserItem(types.rest.BaseRestItem): id: str name: str real_name: str @@ -359,7 +359,7 @@ class GroupItem(typing.TypedDict): class Groups(DetailHandler[GroupItem]): custom_methods = ['services_pools', 'users'] - def get_items(self, parent: 'Model', item: typing.Optional[str]) -> types.rest.GetItemsResult['GroupItem']: + def get_items(self, parent: 'Model', item: typing.Optional[str]) -> types.rest.ItemsResult['GroupItem']: parent = ensure.is_instance(parent, Authenticator) try: multi = False diff --git a/server/src/uds/REST/model/base.py b/server/src/uds/REST/model/base.py index dbd2fea5f..6eca63e5a 100644 --- a/server/src/uds/REST/model/base.py +++ b/server/src/uds/REST/model/base.py @@ -30,7 +30,6 @@ Author: Adolfo Gómez, dkmaster at dkmon dot com """ -import inspect import logging import typing @@ -59,103 +58,7 @@ class BaseModelHandler(Handler, typing.Generic[types.rest.T_Item]): Base Handler for Master & Detail Handlers """ - def add_field( - self, gui: list[typing.Any], field: typing.Union[types.rest.FieldType, list[types.rest.FieldType]] - ) -> list[typing.Any]: - """ - Add a field to a "gui" description. - This method checks that every required field element is in there. - If not, defaults are assigned - :param gui: List of "gui" items where the field will be added - :param field: Field to be added (dictionary) - """ - if isinstance(field, list): - for i in field: - gui = self.add_field(gui, i) - else: - if 'values' in field: - caller = inspect.stack()[1] - logger.warning( - 'Field %s has "values" attribute, this is deprecated and will be removed in future versions. Use "choices" instead. Called from %s:%s', - field.get('name', ''), - caller.filename, - caller.lineno, - ) - choices = field['values'] - else: - choices = field.get('choices', None) - # Build gui with non empty values - gui_description: dict[str, typing.Any] = {} - # First, mandatory fields - for fld in ('name', 'type'): - if fld not in field: - caller = inspect.stack()[1] - logger.error( - 'Field %s does not have mandatory field %s. Called from %s:%s', - field.get('name', ''), - fld, - caller.filename, - caller.lineno, - ) - raise exceptions.rest.RequestError( - f'Field {fld} is mandatory on {field.get("name", "")} field.' - ) - - if choices: - gui_description['choices'] = choices - # "fillable" fields (optional and mandatory on gui) - for fld in ( - 'type', - 'default', - 'required', - 'min_value', - 'max_value', - 'length', - 'lines', - 'tooltip', - 'readonly', - ): - if fld in field and field[fld] is not None: - gui_description[fld] = field[fld] - - # Order and label optional, but must be present on gui - gui_description['order'] = field.get('order', 0) - gui_description['label'] = field.get('label', field['name']) - - v: dict[str, typing.Any] = { - 'name': field.get('name', ''), - 'value': field.get('value', ''), - 'gui': gui_description, - } - if field.get('tab', None): - v['gui']['tab'] = _(str(field['tab'])) - gui.append(v) - return gui - - def compose_gui( - self, - stock_fields: list[types.rest.stock.StockField], - *gui: types.ui.GuiElement, - ) -> list[types.ui.GuiElement]: - """ - Adds default fields (based in a list) to a "gui" description - - Args: - gui: Gui list where the "default" fielsds will be added - stock_fields: List of StockField to be added. Valid values are 'name', ' - - returns: - The updated gui list with the new fields appended - """ - the_gui: list[types.ui.GuiElement] = [ - gui_field for field in stock_fields for gui_field in field.get_fields() - ] - for field in gui: - the_gui.append(field) - - return the_gui - - def ensure_has_access( + def check_access( self, obj: models.Model, permission: 'types.permissions.PermissionType', @@ -174,7 +77,7 @@ class BaseModelHandler(Handler, typing.Generic[types.rest.T_Item]): """ return None - def type_as_dict(self, type_: type['Module']) -> types.rest.TypeInfoDict: + def as_typeinfo(self, type_: type['Module']) -> types.rest.TypeInfoDict: """ Returns a dictionary describing the type (the name, the icon, description, etc...) """ @@ -189,7 +92,7 @@ class BaseModelHandler(Handler, typing.Generic[types.rest.T_Item]): return res - def process_table_fields( + def table_description( self, title: str, fields: list[typing.Any], @@ -242,7 +145,7 @@ class BaseModelHandler(Handler, typing.Generic[types.rest.T_Item]): return args @staticmethod - def fill_instance_type(item: 'models.Model', dct: types.rest.ManagedObjectDictType) -> None: + def fill_instance_type(item: 'models.Model', dct: types.rest.ManagedObjectItem) -> None: """ For Managed Objects (db element that contains a serialized object), fills a dictionary with the "type" and "type_name" parameters values. For non managed objects, it does nothing @@ -258,7 +161,7 @@ class BaseModelHandler(Handler, typing.Generic[types.rest.T_Item]): typing.cast(dict[str, typing.Any], dct)['type_name'] = kind.mod_name() @staticmethod - def fill_instance_fields(item: 'models.Model', dct: types.rest.ItemDictType) -> None: + def fill_instance_fields(item: 'models.Model', dct: types.rest.BaseRestItem) -> None: """ For Managed Objects (db element that contains a serialized object), fills a dictionary with the "field" parameters values. For non managed objects, it does nothing @@ -271,7 +174,7 @@ class BaseModelHandler(Handler, typing.Generic[types.rest.T_Item]): # Cast to allow override typing if isinstance(item, ManagedObjectModel): - res = typing.cast(types.rest.ManagedObjectDictType, dct) + res = typing.cast(types.rest.ManagedObjectItem, dct) i = item.get_instance() i.init_gui() # Defaults & stuff fields = i.get_fields_as_dict() diff --git a/server/src/uds/REST/model/detail.py b/server/src/uds/REST/model/detail.py index 08685410c..ad451fd1b 100644 --- a/server/src/uds/REST/model/detail.py +++ b/server/src/uds/REST/model/detail.py @@ -163,7 +163,7 @@ class DetailHandler(BaseModelHandler[types.rest.T_Item], typing.Generic[types.re raise self.invalid_request_response() case consts.rest.TABLEINFO: if num_args == 1: - return self.process_table_fields( + return self.table_description( self.get_title(parent), self.get_fields(parent), self.get_row_style(parent), @@ -255,7 +255,7 @@ class DetailHandler(BaseModelHandler[types.rest.T_Item], typing.Generic[types.re # Default (as sample) get_items def get_items( self, parent: models.Model, item: typing.Optional[str] - ) -> types.rest.GetItemsResult[types.rest.T_Item]: + ) -> types.rest.ItemsResult[types.rest.T_Item]: """ This MUST be overridden by derived classes Excepts to return a list of dictionaries or a single dictionary, depending on "item" param diff --git a/server/src/uds/REST/model/model.py b/server/src/uds/REST/model/model.py index 9539507a3..5c53f7bd4 100644 --- a/server/src/uds/REST/model/model.py +++ b/server/src/uds/REST/model/model.py @@ -137,7 +137,7 @@ class ModelHandler(BaseModelHandler[types.rest.T_Item], typing.Generic[types.res self, *args: typing.Any, **kwargs: typing.Any ) -> typing.Generator[types.rest.TypeInfoDict, None, None]: for type_ in self.enum_types(): - yield self.type_as_dict(type_) + yield self.as_typeinfo(type_) def get_type(self, type_: str) -> types.rest.TypeInfoDict: found = None @@ -154,7 +154,7 @@ class ModelHandler(BaseModelHandler[types.rest.T_Item], typing.Generic[types.res # log related def get_logs(self, item: models.Model) -> list[dict[typing.Any, typing.Any]]: - self.ensure_has_access(item, types.permissions.PermissionType.READ) + self.check_access(item, types.permissions.PermissionType.READ) try: return log.get_logs(item) except Exception as e: @@ -336,7 +336,7 @@ class ModelHandler(BaseModelHandler[types.rest.T_Item], typing.Generic[types.res case [consts.rest.OVERVIEW, *_fails]: raise self.invalid_request_response() case [consts.rest.TABLEINFO]: - return self.process_table_fields( + return self.table_description( self.table_title, self.table_fields, self.table_row_style, @@ -360,7 +360,7 @@ class ModelHandler(BaseModelHandler[types.rest.T_Item], typing.Generic[types.res if number_of_args == 1: try: item = self.model.objects.get(uuid__iexact=self._args[0].lower()) - self.ensure_has_access(item, types.permissions.PermissionType.READ) + self.check_access(item, types.permissions.PermissionType.READ) res = self.item_as_dict(item) self.fill_instance_fields(item, res) return res @@ -408,7 +408,7 @@ class ModelHandler(BaseModelHandler[types.rest.T_Item], typing.Generic[types.res return self.process_detail() # Here, self.model() indicates an "django model object with default params" - self.ensure_has_access( + self.check_access( self.model(), types.permissions.PermissionType.ALL, root=True ) # Must have write permissions to create, modify, etc.. @@ -497,7 +497,7 @@ class ModelHandler(BaseModelHandler[types.rest.T_Item], typing.Generic[types.res if len(self._args) != 1: raise exceptions.rest.RequestError('Delete need one and only one argument') - self.ensure_has_access( + self.check_access( self.model(), types.permissions.PermissionType.ALL, root=True ) # Must have write permissions to delete diff --git a/server/src/uds/core/types/rest/__init__.py b/server/src/uds/core/types/rest/__init__.py index 4a3834626..e1c37b9f7 100644 --- a/server/src/uds/core/types/rest/__init__.py +++ b/server/src/uds/core/types/rest/__init__.py @@ -41,10 +41,11 @@ from . import stock if typing.TYPE_CHECKING: from uds.REST.handlers import Handler + from uds.core import types # Type related definitions -TypeInfoDict = dict[str, typing.Any] # Alias for type info dict +TypeInfoDict: typing.TypeAlias = dict[str, typing.Any] # Alias for type info dict class ExtraTypeInfo(abc.ABC): @@ -111,11 +112,11 @@ class ModelCustomMethod: # Note that for this item to work with documentation # no forward references can be used (that is, do not use quotes around the inner field types) -class ItemDictType(typing.TypedDict): +class BaseRestItem(typing.TypedDict): pass -class ManagedObjectDictType(typing.TypedDict): +class ManagedObjectItem(BaseRestItem): """ Represents a managed object type, with its name and type. This is used to represent the type of a managed object in the REST API. @@ -128,13 +129,31 @@ class ManagedObjectDictType(typing.TypedDict): # Alias for item type # ItemDictType = dict[str, typing.Any] -T_Item = typing.TypeVar("T_Item", bound=ItemDictType) +T_Item = typing.TypeVar("T_Item", bound=BaseRestItem) # Alias for get_items return type -GetItemsResult: typing.TypeAlias = list[T_Item] | ItemDictType | typing.Iterator[T_Item] +ItemsResult: typing.TypeAlias = list[T_Item] | BaseRestItem | typing.Iterator[T_Item] -# -FieldType = collections.abc.Mapping[str, typing.Any] + +@dataclasses.dataclass +class TableInfo: + """ + Represents the table info for a REST API endpoint. + This is used to describe the table fields and row style. + """ + + title: str + fields: list[dict[str, dict[str, typing.Any]]] + row_style: 'types.ui.RowStyleInfo' + subtitle: typing.Optional[str] = None + + def as_dict(self) -> dict[str, typing.Any]: + return { + 'title': self.title, + 'fields': self.fields, + 'row-style': self.row_style.as_dict(), + 'subtitle': self.subtitle or '', + } @dataclasses.dataclass(frozen=True)