44import operator
55from collections .abc import Callable
66from datetime import UTC , datetime
7- from typing import cast
7+ from typing import Any , TypedDict , cast
88
99import arrow
1010from fastapi import FastAPI
11+ from models_library .groups import GroupID
12+ from models_library .products import ProductName
1113from models_library .services import ServiceMetaDataPublished
1214from models_library .services_types import ServiceKey , ServiceVersion
1315from packaging .version import Version
1416from pydantic .types import PositiveInt
1517from sqlalchemy .ext .asyncio import AsyncEngine
1618
1719from ..api ._dependencies .director import get_director_client
18- from ..models .services_db import ServiceAccessRightsDB
20+ from ..models .services_db import ServiceAccessRightsDB , ServiceMetaDataDBGet
1921from ..repository .groups import GroupsRepository
2022from ..repository .services import ServicesRepository
2123from ..utils .versioning import as_version , is_patch_release
2527_LEGACY_SERVICES_DATE : datetime = datetime (year = 2020 , month = 8 , day = 19 , tzinfo = UTC )
2628
2729
30+ class InheritedData (TypedDict ):
31+ access_rights : list [ServiceAccessRightsDB ]
32+ metadata_updates : dict [str , Any ]
33+
34+
2835def _is_frontend_service (service : ServiceMetaDataPublished ) -> bool :
2936 return "/frontend/" in service .key
3037
@@ -41,36 +48,46 @@ async def _is_old_service(app: FastAPI, service: ServiceMetaDataPublished) -> bo
4148 return bool (service_build_data < _LEGACY_SERVICES_DATE )
4249
4350
44- async def evaluate_default_policy (
45- app : FastAPI , service : ServiceMetaDataPublished
46- ) -> tuple [PositiveInt | None , list [ServiceAccessRightsDB ]]:
47- """Given a service, it returns the owner's group-id (gid) and a list of access rights following
48- default access-rights policies
51+ async def evaluate_default_service_ownership_and_rights (
52+ app : FastAPI , * , service : ServiceMetaDataPublished , product_name : ProductName
53+ ) -> tuple [GroupID | None , list [ServiceAccessRightsDB ]]:
54+ """Evaluates the owner (group_id) and the access rights for a service
4955
50- - DEFAULT Access Rights policies:
51- 1. All services published in osparc prior 19.08.2020 will be visible to everyone (refered as 'old service').
52- 2. Services published after 19.08.2020 will be visible ONLY to his/her owner
53- 3. Front-end services are have execute-access to everyone
56+ This function determines:
57+ 1. Who owns the service (based on contact or author email)
58+ 2. Who can access the service based on the following rules:
59+ - All services published before August 19, 2020 (_LEGACY_SERVICES_DATE) are accessible to everyone
60+ - Services published after August 19, 2020 are only accessible to their owner
61+ - Frontend services are accessible to everyone regardless of publication date
5462
63+ Returns:
64+ A tuple containing:
65+ - The owner's group ID (gid) if found, None otherwise
66+ - A list of ServiceAccessRightsDB objects representing the default access rights
67+ for the service, including who can execute and/or modify the service
5568
5669 Raises:
57- HTTPException: from calls to director 's rest API. Maps director errors into catalog's server error
58- SQLAlchemyError: from access to pg database
59- ValidationError: from pydantic model errors
70+ HTTPException: If there 's an error communicating with the director API
71+ SQLAlchemyError: If there's an error accessing the database
72+ ValidationError: If there's an error validating the Pydantic models
6073 """
6174 db_engine : AsyncEngine = app .state .engine
6275
6376 groups_repo = GroupsRepository (db_engine )
6477 owner_gid = None
6578 group_ids : list [PositiveInt ] = []
6679
80+ # 1. If service is old or frontend, we add the everyone group
6781 if _is_frontend_service (service ) or await _is_old_service (app , service ):
6882 everyone_gid = (await groups_repo .get_everyone_group ()).gid
69- _logger .debug ("service %s:%s is old or frontend" , service .key , service .version )
70- # let's make that one available to everyone
71- group_ids .append (everyone_gid )
83+ group_ids .append (everyone_gid ) # let's make that one available to everyone
84+ _logger .debug (
85+ "service %s:%s is old or frontend. Set available to everyone" ,
86+ service .key ,
87+ service .version ,
88+ )
7289
73- # try to find the owner
90+ # 2. Deducing the owner gid
7491 possible_owner_email = [service .contact ] + [
7592 author .email for author in service .authors
7693 ]
@@ -80,78 +97,129 @@ async def evaluate_default_policy(
8097 if possible_gid and not owner_gid :
8198 owner_gid = possible_gid
8299 if not owner_gid :
83- _logger .warning ("service %s:%s has no owner" , service .key , service .version )
100+ _logger .warning ("Service %s:%s has no owner" , service .key , service .version )
84101 else :
85102 group_ids .append (owner_gid )
86103
87- # we add the owner with full rights, unless it's everyone
104+ # 3. Aplying default access rights
88105 default_access_rights = [
89106 ServiceAccessRightsDB (
90107 key = service .key ,
91108 version = service .version ,
92109 gid = gid ,
93110 execute_access = True ,
94- write_access = (gid == owner_gid ),
95- product_name = app .state .default_product_name ,
111+ write_access = (
112+ gid == owner_gid
113+ ), # we add the owner with full rights, unless it's everyone
114+ product_name = product_name ,
96115 )
97116 for gid in set (group_ids )
98117 ]
99118
100119 return (owner_gid , default_access_rights )
101120
102121
103- async def evaluate_auto_upgrade_policy (
104- service_metadata : ServiceMetaDataPublished , services_repo : ServicesRepository
105- ) -> list [ServiceAccessRightsDB ]:
106- # AUTO-UPGRADE PATCH policy:
107- #
108- # - Any new patch released, inherits the access rights from previous compatible version
109- # - IDEA: add as option in the publication contract, i.e. in ServiceDockerData?
110- # - Does NOT apply to front-end services
111- #
112- # SEE https://github.com/ITISFoundation/osparc-simcore/issues/2244)
113- #
122+ async def _find_latest_patch_compatible_release (
123+ services_repo : ServicesRepository , * , service_metadata : ServiceMetaDataPublished
124+ ) -> ServiceMetaDataDBGet | None :
125+ """
126+ Finds the previous patched release for a service.
127+
128+ Args:
129+ services_repo: Instance of ServicesRepository for database access.
130+ service_metadata: Metadata of the service being evaluated.
131+
132+ Returns:
133+ Latest patch release of the service if it exists, otherwise None.
134+ """
114135 if _is_frontend_service (service_metadata ):
115- return []
136+ return None
116137
117- service_access_rights = []
118138 new_version : Version = as_version (service_metadata .version )
119- latest_releases = await services_repo .list_service_releases (
139+ patch_releases_latest_first = await services_repo .list_service_releases (
120140 service_metadata .key ,
121141 major = new_version .major ,
122142 minor = new_version .minor ,
123143 )
124144
125- previous_release = None
126- for release in latest_releases :
127- # NOTE: latest_release is sorted from newer to older
128- # Here we search for the previous version patched by new-version
145+ # latest_releases is sorted from newer to older
146+ for release in patch_releases_latest_first :
147+ # COMPATIBILITY RULE:
148+ # - a patch release is compatible with the previous patch release
149+ # - WARNING: this does not account for custom compatibility policies!!!!
129150 if is_patch_release (new_version , release .version ):
130- previous_release = release
131- break
151+ return release
152+
153+ return None
154+
155+
156+ async def inherit_from_latest_compatible_release (
157+ services_repo : ServicesRepository , * , service_metadata : ServiceMetaDataPublished
158+ ) -> InheritedData :
159+ """
160+ Inherits metadata and access rights from a previous compatible release.
161+
162+ This function applies inheritance policies:
163+ - AUTO-UPGRADE PATCH policy: new patch releases inherit access rights from previous compatible versions
164+ - Metadata inheritance: icon and thumbnail fields are inherited if not specified in the new version
165+
166+ Args:
167+ services_repo: Instance of ServicesRepository for database access.
168+ service_metadata: Metadata of the service being evaluated.
132169
133- if previous_release :
134- previous_access_rights = await services_repo .get_service_access_rights (
135- previous_release .key , previous_release .version
170+ Returns:
171+ An InheritedData object containing:
172+ - access_rights: List of ServiceAccessRightsDB objects inherited from the previous release
173+ - metadata_updates: Dict of metadata fields that should be updated in the new service
174+
175+ Notes:
176+ - The policy is described in https://github.com/ITISFoundation/osparc-simcore/issues/2244
177+ - Inheritance is only for patch releases (i.e., same major and minor version).
178+ """
179+ inherited_data : InheritedData = {
180+ "access_rights" : [],
181+ "metadata_updates" : {},
182+ }
183+
184+ previous_release = await _find_latest_patch_compatible_release (
185+ services_repo , service_metadata = service_metadata
186+ )
187+
188+ if not previous_release :
189+ return inherited_data
190+
191+ # 1. ACCESS-RIGHTS:
192+ # Inherit access rights (from all products) from the previous release
193+ previous_access_rights = await services_repo .get_service_access_rights (
194+ previous_release .key , previous_release .version
195+ )
196+
197+ inherited_data ["access_rights" ] = [
198+ access .model_copy (
199+ update = {"version" : service_metadata .version },
200+ deep = True ,
136201 )
202+ for access in previous_access_rights
203+ ]
204+
205+ # 2. ServiceMetaDataPublished
206+ # Inherit some fields if not specified in the new service
207+ if not service_metadata .icon and previous_release .icon :
208+ inherited_data ["metadata_updates" ]["icon" ] = previous_release .icon
209+ if not service_metadata .thumbnail and previous_release .thumbnail :
210+ inherited_data ["metadata_updates" ]["thumbnail" ] = previous_release .thumbnail
137211
138- service_access_rights = [
139- access .model_copy (
140- update = {"version" : service_metadata .version },
141- deep = True ,
142- )
143- for access in previous_access_rights
144- ]
145- return service_access_rights
212+ return inherited_data
146213
147214
148215def reduce_access_rights (
149216 access_rights : list [ServiceAccessRightsDB ],
150217 reduce_operation : Callable = operator .ior ,
151218) -> list [ServiceAccessRightsDB ]:
152- """
153- Reduces a list of access-rights per target
219+ """Reduces a list of access-rights per target
220+
154221 By default, the reduction is OR (i.e. preserves True flags)
222+
155223 """
156224 # TODO: probably a lot of room to optimize
157225 # helper functions to simplify operation of access rights
0 commit comments