def db_services(registry_services: List[ServiceOut], user_groups: List[GroupAtDB]) -> List[ServiceAccessRightsAtDB]: return [ ServiceAccessRightsAtDB( key=s.key, version=s.version, gid=user_groups[0].gid, execute_access=True, product_name="osparc", ) for s in registry_services ]
async def evaluate_default_policy( app: FastAPI, service: ServiceDockerData ) -> Tuple[Optional[PositiveInt], List[ServiceAccessRightsAtDB]]: """Given a service, it returns the owner's group-id (gid) and a list of access rights following default access-rights policies - DEFAULT Access Rights policies: 1. All services published in osparc prior 19.08.2020 will be visible to everyone (refered as 'old service'). 2. Services published after 19.08.2020 will be visible ONLY to his/her owner 3. Front-end services are have execute-access to everyone """ db_engine: AsyncEngine = app.state.engine groups_repo = GroupsRepository(db_engine) owner_gid = None group_ids: List[PositiveInt] = [] if _is_frontend_service(service) or await _is_old_service(app, service): everyone_gid = (await groups_repo.get_everyone_group()).gid logger.debug("service %s:%s is old or frontend", service.key, service.version) # let's make that one available to everyone group_ids.append(everyone_gid) # try to find the owner possible_owner_email = [service.contact ] + [author.email for author in service.authors] for user_email in possible_owner_email: possible_gid = await groups_repo.get_user_gid_from_email(user_email) if possible_gid: if not owner_gid: owner_gid = possible_gid if not owner_gid: logger.warning("service %s:%s has no owner", service.key, service.version) else: group_ids.append(owner_gid) # we add the owner with full rights, unless it's everyone default_access_rights = [ ServiceAccessRightsAtDB( key=service.key, version=service.version, gid=gid, execute_access=True, write_access=(gid == owner_gid), product_name=app.state.settings. CATALOG_ACCESS_RIGHTS_DEFAULT_PRODUCT_NAME, ) for gid in set(group_ids) ] return (owner_gid, default_access_rights)
def test_reduce_access_rights(): sample = ServiceAccessRightsAtDB.parse_obj({ "key": "simcore/services/dynamic/sim4life", "version": "1.0.9", "gid": 8, "execute_access": True, "write_access": True, "product_name": "osparc", }) # fixture with overrides and with other products reduced = reduce_access_rights([ sample.copy(deep=True), sample.copy(deep=True), sample.copy(update={"execute_access": False}, deep=True), sample.copy(update={"product_name": "s4l"}, deep=True), ]) # two products with the same flags assert len(reduced) == 2 assert reduced[0].dict(include={"execute_access", "write_access"}) == { "execute_access": True, "write_access": True, } assert reduced[1].dict(include={"execute_access", "write_access"}) == { "execute_access": True, "write_access": True, } # two gids with the different falgs reduced = reduce_access_rights([ sample.copy(deep=True), sample.copy( update={ "gid": 1, "execute_access": True, "write_access": False }, deep=True, ), ]) assert len(reduced) == 2 assert reduced[0].dict(include={"execute_access", "write_access"}) == { "execute_access": True, "write_access": True, } assert reduced[1].dict(include={"execute_access", "write_access"}) == { "execute_access": True, "write_access": False, }
def reduce_access_rights( access_rights: List[ServiceAccessRightsAtDB], reduce_operation: Callable = operator.ior, ) -> List[ServiceAccessRightsAtDB]: """ Reduces a list of access-rights per target By default, the reduction is OR (i.e. preserves True flags) """ # TODO: probably a lot of room to optimize # helper functions to simplify operation of access rights def get_target( access: ServiceAccessRightsAtDB) -> Tuple[Union[str, int], ...]: """Hashable identifier of the resource the access rights apply to""" return tuple( [access.key, access.version, access.gid, access.product_name]) def get_flags(access: ServiceAccessRightsAtDB) -> Dict[str, bool]: """Extracts only""" return access.dict(include={"execute_access", "write_access"}) access_flags_map = {} for access in access_rights: target = get_target(access) access_flags = access_flags_map.get(target) if access_flags: # applies reduction on flags for key, value in get_flags(access).items(): access_flags[key] = reduce_operation(access_flags[key], value) # a |= b else: access_flags_map[target] = get_flags(access) reduced_access_rights = [] for target in access_flags_map: reduced_access_rights.append( ServiceAccessRightsAtDB( key=target[0], version=target[1], gid=target[2], product_name=target[3], **access_flags_map[target], )) return reduced_access_rights
async def test_create_services(services_repo: ServicesRepository, service_catalog_faker: Callable): # creates fake data fake_service, *fake_access_rights = service_catalog_faker( "simcore/services/dynamic/jupyterlab", "1.0.0", team_access=None, everyone_access=None, ) # validation service = ServiceMetaDataAtDB.parse_obj(fake_service) service_access_rights = [ ServiceAccessRightsAtDB.parse_obj(a) for a in fake_access_rights ] new_service = await services_repo.create_service(service, service_access_rights) assert new_service.dict(include=set(fake_service.keys())) == service.dict()
async def _ensure_published_templates_accessible( db_engine: Engine, default_product_name: str ) -> None: # Rationale: if a project template was published, its services must be available to everyone. # a published template has a column Published that is set to True projects_repo = ProjectsRepository(db_engine) published_services: Set[Tuple[str, str]] = { (service.key, service.version) for service in await projects_repo.list_services_from_published_templates() } groups_repo = GroupsRepository(db_engine) everyone_gid = (await groups_repo.get_everyone_group()).gid services_repo = ServicesRepository(db_engine) available_services: Set[Tuple[str, str]] = { (service.key, service.version) for service in await services_repo.list_services( gids=[everyone_gid], execute_access=True ) } missing_services = published_services - available_services missing_services_access_rights = [ ServiceAccessRightsAtDB( key=service[0], version=service[1], gid=everyone_gid, execute_access=True, product_name=default_product_name, ) for service in missing_services ] if missing_services_access_rights: logger.info( "Adding access rights for published templates\n: %s", missing_services_access_rights, ) await services_repo.upsert_service_access_rights(missing_services_access_rights)
async def list_services_access_rights( self, key_versions: List[Tuple[str, str]], product_name: Optional[str] = None ) -> Dict[Tuple[str, str], List[ServiceAccessRightsAtDB]]: """Batch version of get_service_access_rights""" service_to_access_rights = defaultdict(list) query = sa.select([services_access_rights]).where( tuple_(services_access_rights.c.key, services_access_rights.c.version).in_( key_versions ) & (services_access_rights.c.product_name == product_name) if product_name else True ) async with self.db_engine.acquire() as conn: async for row in conn.execute(query): service_to_access_rights[ ( row[services_access_rights.c.key], row[services_access_rights.c.version], ) ].append(ServiceAccessRightsAtDB(**row)) return service_to_access_rights
async def get_service_access_rights( self, key: str, version: str, product_name: Optional[str] = None, ) -> List[ServiceAccessRightsAtDB]: """ - If product_name is not specificed, then all are considered in the query """ services_in_db = [] search_expression = (services_access_rights.c.key == key) & ( services_access_rights.c.version == version ) if product_name: search_expression &= services_access_rights.c.product_name == product_name query = sa.select([services_access_rights]).where(search_expression) async with self.db_engine.acquire() as conn: async for row in conn.execute(query): services_in_db.append(ServiceAccessRightsAtDB(**row)) return services_in_db
async def modify_service( # pylint: disable=too-many-arguments user_id: int, service_key: constr(regex=KEY_RE), service_version: constr(regex=VERSION_RE), updated_service: ServiceUpdate, director_client: DirectorApi = Depends(get_director_api), groups_repository: GroupsRepository = Depends( get_repository(GroupsRepository)), services_repo: ServicesRepository = Depends( get_repository(ServicesRepository)), x_simcore_products_name: str = Header(None), ): if is_function_service(service_key): # NOTE: this is a temporary decision after discussing with OM raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Cannot update front-end services", ) # check the service exists await director_client.get( f"/services/{urllib.parse.quote_plus(service_key)}/{service_version}") # the director client already raises an exception if not found # get the user groups user_groups = await groups_repository.list_user_groups(user_id) if not user_groups: # deny access raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="You have unsufficient rights to access the service", ) # check the user has write access to this service writable_service = await services_repo.get_service( service_key, service_version, gids=[group.gid for group in user_groups], write_access=True, product_name=x_simcore_products_name, ) if not writable_service: # deny access raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="You have unsufficient rights to modify the service", ) # let's modify the service then await services_repo.update_service( ServiceMetaDataAtDB( key=service_key, version=service_version, **updated_service.dict(exclude_unset=True), )) # let's modify the service access rights (they can be added/removed/modified) current_gids_in_db = [ r.gid for r in await services_repo.get_service_access_rights( service_key, service_version, product_name=x_simcore_products_name) ] if updated_service.access_rights: # start by updating/inserting new entries new_access_rights = [ ServiceAccessRightsAtDB( key=service_key, version=service_version, gid=gid, execute_access=rights.execute_access, write_access=rights.write_access, product_name=x_simcore_products_name, ) for gid, rights in updated_service.access_rights.items() ] await services_repo.upsert_service_access_rights(new_access_rights) # then delete the ones that were removed removed_gids = [ gid for gid in current_gids_in_db if gid not in updated_service.access_rights ] deleted_access_rights = [ ServiceAccessRightsAtDB( key=service_key, version=service_version, gid=gid, product_name=x_simcore_products_name, ) for gid in removed_gids ] await services_repo.delete_service_access_rights(deleted_access_rights) # now return the service return await get_service( user_id, service_key, service_version, director_client, groups_repository, services_repo, x_simcore_products_name, )
def get_flags(access: ServiceAccessRightsAtDB) -> Dict[str, bool]: """Extracts only""" return access.dict(include={"execute_access", "write_access"})