async def create_service( self, new_service: ServiceMetaDataAtDB, new_service_access_rights: List[ServiceAccessRightsAtDB], ) -> ServiceMetaDataAtDB: for access_rights in new_service_access_rights: if ( access_rights.key != new_service.key or access_rights.version != new_service.version ): raise ValueError( f"{access_rights} does not correspond to service {new_service.key}:{new_service.version}" ) async with self.db_engine.acquire() as conn: # NOTE: this ensure proper rollback in case of issue async with conn.begin() as _transaction: row: RowProxy = await ( await conn.execute( # pylint: disable=no-value-for-parameter services_meta_data.insert() .values(**new_service.dict(by_alias=True)) .returning(literal_column("*")) ) ).first() created_service = ServiceMetaDataAtDB(**row) for access_rights in new_service_access_rights: insert_stmt = pg_insert(services_access_rights).values( **access_rights.dict(by_alias=True) ) await conn.execute(insert_stmt) return created_service
def _prepare_service_details( service_in_registry: Dict[str, Any], service_in_db: ServiceMetaDataAtDB, service_access_rights_in_db: List[ServiceAccessRightsAtDB], service_owner: Optional[str], ) -> Optional[ServiceOut]: # compose service from registry and DB composed_service = service_in_registry composed_service.update( service_in_db.dict(exclude_unset=True, exclude={"owner"}), access_rights={rights.gid: rights for rights in service_access_rights_in_db}, owner=service_owner if service_owner else None, ) # validate the service validated_service = None try: validated_service = ServiceOut(**composed_service) except ValidationError as exc: logger.warning( "could not validate service [%s:%s]: %s", composed_service.get("key"), composed_service.get("version"), exc, ) return validated_service
async def update_service( self, patched_service: ServiceMetaDataAtDB ) -> ServiceMetaDataAtDB: # update the services_meta_data table async with self.db_engine.acquire() as conn: row: RowProxy = await ( await conn.execute( # pylint: disable=no-value-for-parameter services_meta_data.update() .where( (services_meta_data.c.key == patched_service.key) & (services_meta_data.c.version == patched_service.version) ) .values(**patched_service.dict(by_alias=True, exclude_unset=True)) .returning(literal_column("*")) ) ).first() updated_service = ServiceMetaDataAtDB(**row) return updated_service
async def _create_services_in_db( app: FastAPI, connection: SAConnection, service_keys: Set[Tuple[ServiceKey, ServiceVersion]], services: Dict[Tuple[ServiceKey, ServiceVersion], ServiceDockerData], ) -> None: services_repo = ServicesRepository(connection) for service_key, service_version in service_keys: service: ServiceDockerData = services[(service_key, service_version)] # find the service owner owner_gid, service_access_rights = await _create_service_default_access_rights( app, service, connection) # set the service in the DB await services_repo.create_service( ServiceMetaDataAtDB(**service.dict(), owner=owner_gid), service_access_rights, )
async def list_service_releases( self, key: str, *, major: Optional[int] = None, minor: Optional[int] = None, limit_count: Optional[int] = None, ) -> List[ServiceMetaDataAtDB]: """Lists LAST n releases of a given service, sorted from latest first major, minor is used to filter as major.minor.* or major.* limit_count limits returned value. None or non-positive values returns all matches """ if minor is not None and major is None: raise ValueError("Expected only major.*.* or major.minor.*") search_condition = services_meta_data.c.key == key if major is not None: if minor is not None: # All patches search_condition &= services_meta_data.c.version.like( f"{major}.{minor}.%" ) else: # All minor and patches search_condition &= services_meta_data.c.version.like(f"{major}.%") query = ( sa.select([services_meta_data]) .where(search_condition) .order_by(sa.desc(services_meta_data.c.version)) ) if limit_count and limit_count > 0: query = query.limit(limit_count) releases = [] async with self.db_engine.acquire() as conn: async for row in conn.execute(query): releases.append(ServiceMetaDataAtDB(**row)) return releases
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 get_service( self, key: str, version: str, *, gids: Optional[List[int]] = None, execute_access: Optional[bool] = None, write_access: Optional[bool] = None, product_name: Optional[str] = None, ) -> Optional[ServiceMetaDataAtDB]: query = sa.select([services_meta_data]).where( (services_meta_data.c.key == key) & (services_meta_data.c.version == version) ) if gids or execute_access or write_access: query = ( sa.select([services_meta_data]) .select_from(services_meta_data.join(services_access_rights)) .where( and_( (services_meta_data.c.key == key), (services_meta_data.c.version == version), or_(*[services_access_rights.c.gid == gid for gid in gids]) if gids else True, services_access_rights.c.execute_access if execute_access else True, services_access_rights.c.write_access if write_access else True, (services_access_rights.c.product_name == product_name) if product_name else True, ) ) ) async with self.db_engine.acquire() as conn: row: RowProxy = await (await conn.execute(query)).first() if row: return ServiceMetaDataAtDB(**row)
async def list_services( self, *, gids: Optional[List[int]] = None, execute_access: Optional[bool] = None, write_access: Optional[bool] = None, combine_access_with_and: Optional[bool] = True, product_name: Optional[str] = None, ) -> List[ServiceMetaDataAtDB]: services_in_db = [] async with self.db_engine.acquire() as conn: async for row in conn.execute( _make_list_services_query( gids, execute_access, write_access, combine_access_with_and, product_name, ) ): services_in_db.append(ServiceMetaDataAtDB(**row)) return services_in_db
async def _create_services_in_db( app: FastAPI, service_keys: Set[Tuple[ServiceKey, ServiceVersion]], services_in_registry: Dict[Tuple[ServiceKey, ServiceVersion], ServiceDockerData], ) -> None: """Adds a new service in the database Determines the access rights of each service and adds it to the database""" services_repo = ServicesRepository(app.state.engine) sorted_services = sorted(service_keys, key=lambda t: Version(t[1])) for service_key, service_version in sorted_services: service_metadata: ServiceDockerData = services_in_registry[( service_key, service_version)] # DEFAULT policies ( owner_gid, service_access_rights, ) = await access_rights.evaluate_default_policy(app, service_metadata) # AUTO-UPGRADE PATCH policy inherited_access_rights = await access_rights.evaluate_auto_upgrade_policy( service_metadata, services_repo) service_access_rights += inherited_access_rights service_access_rights = access_rights.reduce_access_rights( service_access_rights) # set the service in the DB await services_repo.create_service( ServiceMetaDataAtDB(**service_metadata.dict(), owner=owner_gid), service_access_rights, )
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_frontend_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, )