dspace-api-toolkit 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dspace_api/__init__.py +75 -0
- dspace_api/_base.py +407 -0
- dspace_api/_containers.py +217 -0
- dspace_api/_version.py +4 -0
- dspace_api/auth.py +467 -0
- dspace_api/authorizations.py +486 -0
- dspace_api/bundles.py +1193 -0
- dspace_api/collections.py +1069 -0
- dspace_api/communities.py +363 -0
- dspace_api/config.py +254 -0
- dspace_api/discovery.py +920 -0
- dspace_api/entity_types.py +127 -0
- dspace_api/epersons.py +647 -0
- dspace_api/exceptions.py +110 -0
- dspace_api/groups.py +599 -0
- dspace_api/items.py +549 -0
- dspace_api/metadata.py +742 -0
- dspace_api/permissions.py +1052 -0
- dspace_api/projection.py +292 -0
- dspace_api/relationships.py +694 -0
- dspace_api/session.py +538 -0
- dspace_api/site.py +98 -0
- dspace_api/utils.py +519 -0
- dspace_api/versions.py +292 -0
- dspace_api/vocabulary.py +365 -0
- dspace_api/workflow.py +893 -0
- dspace_api/workspaces.py +1404 -0
- dspace_api_toolkit-0.1.1.dist-info/METADATA +211 -0
- dspace_api_toolkit-0.1.1.dist-info/RECORD +32 -0
- dspace_api_toolkit-0.1.1.dist-info/WHEEL +5 -0
- dspace_api_toolkit-0.1.1.dist-info/licenses/LICENSE +28 -0
- dspace_api_toolkit-0.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
# -*- coding: UTF-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Authorization checks for DSpace API (DSpace 7+ / DSpace-CRIS): "may this
|
|
4
|
+
user do X on object Y?" - the same checks the Angular UI uses to show or
|
|
5
|
+
hide buttons, so scripts can test permissions before acting.
|
|
6
|
+
|
|
7
|
+
GET /authz/authorizations/{id}[/eperson|/feature|/object]
|
|
8
|
+
GET /authz/authorizations/search/object?uri=&eperson=&feature=
|
|
9
|
+
GET /authz/authorizations/search/objects?uuid=..&type=&eperson=&feature=.. (batch)
|
|
10
|
+
GET /authz/features[/{id}] (admin only)
|
|
11
|
+
GET /authz/features/search/resourcetype?type= (admin only)
|
|
12
|
+
|
|
13
|
+
auth = client.authorizations
|
|
14
|
+
auth.can(item, Feature.CAN_EDIT_METADATA) # current user
|
|
15
|
+
auth.can(collection, Feature.ADMINISTRATOR_OF, eperson=jane) # someone else (admin, or jane herself)
|
|
16
|
+
auth.features_on(item) # {"canEditMetadata", "withdrawItem", ...}
|
|
17
|
+
auth.check(item, [Feature.WITHDRAW_ITEM, Feature.CAN_DELETE]) # {"withdrawItem": True, "canDelete": False}
|
|
18
|
+
auth.require(item, Feature.CAN_MOVE) # raises AuthorizationError if not
|
|
19
|
+
auth.can_many(items, Feature.CAN_EDIT_METADATA) # {uuid: bool}, one request per type
|
|
20
|
+
auth.is_site_admin()
|
|
21
|
+
|
|
22
|
+
Objects can be DSpace object dicts (with _links.self or type + uuid), REST
|
|
23
|
+
URLs, or (type, id) tuples such as ("item", uuid) or ("workspaceitem", 42).
|
|
24
|
+
|
|
25
|
+
Without eperson the server evaluates the logged-in user (anonymous when not
|
|
26
|
+
logged in); asking about another eperson needs admin rights.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
import logging
|
|
30
|
+
from typing import Any, Dict, Iterable, List, Optional, Sequence, Set, Tuple, Union
|
|
31
|
+
|
|
32
|
+
from ._base import BaseManager
|
|
33
|
+
from .exceptions import AuthorizationError, DSpaceError
|
|
34
|
+
from .metadata import DSO_PATHS
|
|
35
|
+
from .projection import EmbedArg
|
|
36
|
+
from .utils import validate_uuid
|
|
37
|
+
|
|
38
|
+
# Logger for this module
|
|
39
|
+
logger = logging.getLogger(__name__)
|
|
40
|
+
|
|
41
|
+
# API endpoints for authorizations and features
|
|
42
|
+
AUTHORIZATIONS = "/authz/authorizations"
|
|
43
|
+
FEATURES = "/authz/features"
|
|
44
|
+
|
|
45
|
+
# object type -> REST collection path (DSO_PATHS plus non-DSpace-object resources)
|
|
46
|
+
TYPE_PATHS = dict(DSO_PATHS, **{
|
|
47
|
+
"workspaceitem": "/submission/workspaceitems",
|
|
48
|
+
"workflowitem": "/workflow/workflowitems",
|
|
49
|
+
"version": "/versioning/versions",
|
|
50
|
+
"versionhistory": "/versioning/versionhistories",
|
|
51
|
+
"resourcepolicy": "/authz/resourcepolicies",
|
|
52
|
+
"process": "/system/processes",
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
# Type aliases for object and eperson references
|
|
56
|
+
ObjectRef = Union[str, Dict[str, Any], Tuple[str, Any]]
|
|
57
|
+
EPersonRef = Union[None, str, Dict[str, Any]]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Feature:
|
|
61
|
+
"""
|
|
62
|
+
Feature ids known from DSpace 7.6 - 9 (Angular FeatureID). Which ones a
|
|
63
|
+
server offers depends on version and configuration; list_features()
|
|
64
|
+
(admin) shows the real set.
|
|
65
|
+
"""
|
|
66
|
+
ADMINISTRATOR_OF = "administratorOf"
|
|
67
|
+
IS_COLLECTION_ADMIN = "isCollectionAdmin" # on the site: admin of at least one collection
|
|
68
|
+
IS_COMMUNITY_ADMIN = "isCommunityAdmin" # on the site: admin of at least one community
|
|
69
|
+
LOGIN_ON_BEHALF_OF = "loginOnBehalfOf"
|
|
70
|
+
CAN_CHANGE_PASSWORD = "canChangePassword"
|
|
71
|
+
CAN_MANAGE_GROUP = "canManageGroup"
|
|
72
|
+
CAN_MANAGE_GROUPS = "canManageGroups"
|
|
73
|
+
CAN_MANAGE_POLICIES = "canManagePolicies"
|
|
74
|
+
CAN_MANAGE_MAPPINGS = "canManageMappings"
|
|
75
|
+
CAN_MANAGE_RELATIONSHIPS = "canManageRelationships"
|
|
76
|
+
CAN_MANAGE_BITSTREAM_BUNDLES = "canManageBitstreamBundles"
|
|
77
|
+
CAN_MANAGE_VERSIONS = "canManageVersions"
|
|
78
|
+
CAN_CREATE_VERSION = "canCreateVersion"
|
|
79
|
+
CAN_EDIT_VERSION = "canEditVersion"
|
|
80
|
+
CAN_DELETE_VERSION = "canDeleteVersion"
|
|
81
|
+
CAN_EDIT_ITEM = "canEditItem"
|
|
82
|
+
CAN_EDIT_METADATA = "canEditMetadata"
|
|
83
|
+
CAN_DELETE = "canDelete"
|
|
84
|
+
CAN_MOVE = "canMove"
|
|
85
|
+
CAN_MAKE_PRIVATE = "canMakePrivate"
|
|
86
|
+
WITHDRAW_ITEM = "withdrawItem"
|
|
87
|
+
REINSTATE_ITEM = "reinstateItem"
|
|
88
|
+
CAN_REORDER_BITSTREAMS = "canReorderBitstreams"
|
|
89
|
+
CAN_CREATE_BITSTREAM = "canCreateBitstream"
|
|
90
|
+
CAN_CREATE_BUNDLE = "canCreateBundle"
|
|
91
|
+
CAN_DOWNLOAD = "canDownload"
|
|
92
|
+
CAN_REQUEST_A_COPY = "canRequestACopy"
|
|
93
|
+
CAN_REGISTER_DOI = "canRegisterDOI"
|
|
94
|
+
CAN_SUBSCRIBE = "canSubscribeDso"
|
|
95
|
+
CAN_VIEW_USAGE_STATISTICS = "canViewUsageStatistics"
|
|
96
|
+
CAN_SEND_FEEDBACK = "canSendFeedback"
|
|
97
|
+
CAN_CLAIM_ITEM = "canClaimItem"
|
|
98
|
+
SHOW_CLAIM_ITEM = "showClaimItem"
|
|
99
|
+
CAN_SYNCHRONIZE_WITH_ORCID = "canSynchronizeWithORCID"
|
|
100
|
+
EPERSON_REGISTRATION = "epersonRegistration"
|
|
101
|
+
EPERSON_FORGOT_PASSWORD = "epersonForgotPassword"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def parse_authorization_id(authorization_id: str) -> Dict[str, Optional[str]]:
|
|
105
|
+
"""
|
|
106
|
+
Parse an authorization ID into its components.
|
|
107
|
+
|
|
108
|
+
'[eperson-uuid_]feature_category.model_object-id' ->
|
|
109
|
+
{"eperson": uuid|None, "feature": ..., "object_type": "core.item", "object_id": ...}
|
|
110
|
+
|
|
111
|
+
Parameters:
|
|
112
|
+
authorization_id (str): The authorization ID string to parse.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Dict[str, Optional[str]]: A dictionary with keys "eperson", "feature", "object_type", and "object_id".
|
|
116
|
+
"""
|
|
117
|
+
parts = str(authorization_id).split("_")
|
|
118
|
+
eperson = parts[0] if len(parts) >= 4 and validate_uuid(parts[0]) else None
|
|
119
|
+
rest = parts[1:] if eperson else parts
|
|
120
|
+
if len(rest) < 3:
|
|
121
|
+
raise ValueError(f"Not an authorization id: {authorization_id!r}")
|
|
122
|
+
return {"eperson": eperson, "feature": rest[0], "object_type": rest[1], "object_id": "_".join(rest[2:])}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def feature_of(authorization: Dict[str, Any]) -> Optional[str]:
|
|
126
|
+
"""Feature id of an authorization (embedded feature, else parsed from the id).
|
|
127
|
+
|
|
128
|
+
Parameters:
|
|
129
|
+
authorization (Dict[str, Any]): The authorization object to extract the feature from.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Optional[str]: The feature ID if available, None otherwise.
|
|
133
|
+
"""
|
|
134
|
+
embedded = (authorization.get("_embedded") or {}).get("feature") or {}
|
|
135
|
+
if embedded.get("id"):
|
|
136
|
+
return embedded["id"]
|
|
137
|
+
try:
|
|
138
|
+
return parse_authorization_id(authorization.get("id", ""))["feature"]
|
|
139
|
+
except ValueError:
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class AuthorizationManager(BaseManager):
|
|
144
|
+
"""Read-only permission checks via /authz/authorizations and /authz/features"""
|
|
145
|
+
|
|
146
|
+
# ------------------------------------------------------------------
|
|
147
|
+
# Object references
|
|
148
|
+
# ------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
def object_uri(self, obj: ObjectRef) -> str:
|
|
151
|
+
"""Absolute REST URL of the object an authorization check is about.
|
|
152
|
+
|
|
153
|
+
Parameters:
|
|
154
|
+
obj (ObjectRef): The object reference to build the URI for.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
str: The absolute REST URL of the object.
|
|
158
|
+
"""
|
|
159
|
+
if isinstance(obj, tuple):
|
|
160
|
+
kind, ident = obj
|
|
161
|
+
if kind not in TYPE_PATHS:
|
|
162
|
+
raise ValueError(f"Unknown object type {kind!r}; known: {sorted(TYPE_PATHS)}")
|
|
163
|
+
return self._url(TYPE_PATHS[kind], f"/{ident}")
|
|
164
|
+
if isinstance(obj, dict):
|
|
165
|
+
href = ((obj.get("_links") or {}).get("self") or {}).get("href")
|
|
166
|
+
if href:
|
|
167
|
+
return self.resolve_href(href)
|
|
168
|
+
ident = obj.get("uuid") or obj.get("id")
|
|
169
|
+
if obj.get("type") and ident is not None:
|
|
170
|
+
return self.object_uri((obj["type"], ident))
|
|
171
|
+
raise ValueError("Object dict needs _links.self.href or type + uuid/id")
|
|
172
|
+
if isinstance(obj, str) and obj.startswith(("http://", "https://", "/")):
|
|
173
|
+
return self.resolve_href(obj)
|
|
174
|
+
raise ValueError(f"Cannot build an object URI from {obj!r} - pass a dict, URL or (type, id)")
|
|
175
|
+
|
|
176
|
+
def _eperson_param(self, eperson: EPersonRef) -> Optional[str]:
|
|
177
|
+
"""Get the UUID of an eperson reference.
|
|
178
|
+
|
|
179
|
+
Parameters:
|
|
180
|
+
eperson (EPersonRef): The eperson reference to extract the UUID from.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Optional[str]: The UUID of the eperson if available, None otherwise.
|
|
184
|
+
"""
|
|
185
|
+
if eperson is None:
|
|
186
|
+
return None
|
|
187
|
+
uuid = self._uuid_of(eperson)
|
|
188
|
+
if not uuid:
|
|
189
|
+
raise ValueError(f"eperson must be a UUID or eperson dict, not {eperson!r}")
|
|
190
|
+
return uuid
|
|
191
|
+
|
|
192
|
+
# ------------------------------------------------------------------
|
|
193
|
+
# Authorizations
|
|
194
|
+
# ------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
def get_authorization(self, authorization_id: str, embed: EmbedArg = None) -> Dict[str, Any]:
|
|
197
|
+
"""Get an authorization by its ID.
|
|
198
|
+
|
|
199
|
+
Parameters:
|
|
200
|
+
authorization_id (str): The ID of the authorization to retrieve.
|
|
201
|
+
embed (EmbedArg, optional): Embed related resources. Defaults to None.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
Dict[str, Any]: The authorization object.
|
|
205
|
+
"""
|
|
206
|
+
return self._get_json(self._url(AUTHORIZATIONS, f"/{authorization_id}"),
|
|
207
|
+
f"Get authorization {authorization_id}", embed=embed)
|
|
208
|
+
|
|
209
|
+
def search_object(self, obj: ObjectRef, feature: Optional[str] = None, eperson: EPersonRef = None,
|
|
210
|
+
embed: EmbedArg = None) -> List[Dict[str, Any]]:
|
|
211
|
+
"""All authorizations (optionally one feature) the user has on one object.
|
|
212
|
+
|
|
213
|
+
Parameters:
|
|
214
|
+
obj (ObjectRef): The object reference to check authorizations on.
|
|
215
|
+
feature (str, optional): The feature to filter authorizations by. Defaults to None.
|
|
216
|
+
eperson (EPersonRef, optional): The eperson reference to check authorizations for. Defaults to None.
|
|
217
|
+
embed (EmbedArg, optional): Embed related resources. Defaults to None.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
List[Dict[str, Any]]: A list of authorization objects.
|
|
221
|
+
"""
|
|
222
|
+
params = [("uri", self.object_uri(obj))]
|
|
223
|
+
if feature:
|
|
224
|
+
params.append(("feature", feature))
|
|
225
|
+
eperson_uuid = self._eperson_param(eperson)
|
|
226
|
+
if eperson_uuid:
|
|
227
|
+
params.append(("eperson", eperson_uuid))
|
|
228
|
+
url = self._url(AUTHORIZATIONS, "/search/object")
|
|
229
|
+
return list(self._iter_embedded(url, "authorizations", "Search authorizations", params, embed=embed))
|
|
230
|
+
|
|
231
|
+
def features_on(self, obj: ObjectRef, eperson: EPersonRef = None) -> Set[str]:
|
|
232
|
+
"""Every feature id the user has on the object (one request).
|
|
233
|
+
|
|
234
|
+
Parameters:
|
|
235
|
+
obj (ObjectRef): The object reference to check features on.
|
|
236
|
+
eperson (EPersonRef, optional): The eperson reference to check features for. Defaults to None.
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
Set[str]: A set of feature IDs the user has on the object.
|
|
240
|
+
"""
|
|
241
|
+
return {f for f in (feature_of(a) for a in self.search_object(obj, eperson=eperson, embed="feature")) if f}
|
|
242
|
+
|
|
243
|
+
def can(self, obj: ObjectRef, feature: str, eperson: EPersonRef = None) -> bool:
|
|
244
|
+
"""Check if the user has a specific feature on an object.
|
|
245
|
+
|
|
246
|
+
Parameters:
|
|
247
|
+
obj (ObjectRef): The object reference to check the feature on.
|
|
248
|
+
feature (str): The feature to check.
|
|
249
|
+
eperson (EPersonRef, optional): The eperson reference to check the feature for. Defaults to None.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
bool: True if the feature is granted, False otherwise.
|
|
253
|
+
"""
|
|
254
|
+
return bool(self.search_object(obj, feature=feature, eperson=eperson))
|
|
255
|
+
|
|
256
|
+
def check(self, obj: ObjectRef, features: Iterable[str], eperson: EPersonRef = None) -> Dict[str, bool]:
|
|
257
|
+
"""{feature: bool} for several features on one object, in one request.
|
|
258
|
+
|
|
259
|
+
Parameters:
|
|
260
|
+
obj (ObjectRef): The object reference to check features on.
|
|
261
|
+
features (Iterable[str]): The features to check.
|
|
262
|
+
eperson (EPersonRef, optional): The eperson reference to check features for. Defaults to None.
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
Dict[str, bool]: A dictionary mapping feature IDs to their granted status.
|
|
266
|
+
"""
|
|
267
|
+
granted = self.features_on(obj, eperson)
|
|
268
|
+
return {feature: feature in granted for feature in features}
|
|
269
|
+
|
|
270
|
+
def require(self, obj: ObjectRef, feature: str, eperson: EPersonRef = None) -> None:
|
|
271
|
+
"""Raise AuthorizationError unless the feature is granted - a pre-flight check for scripts.
|
|
272
|
+
|
|
273
|
+
Parameters:
|
|
274
|
+
obj (ObjectRef): The object reference to check the feature on.
|
|
275
|
+
feature (str): The feature to check.
|
|
276
|
+
eperson (EPersonRef, optional): The eperson reference to check the feature for. Defaults to None.
|
|
277
|
+
|
|
278
|
+
Raises:
|
|
279
|
+
AuthorizationError: If the feature is not granted.
|
|
280
|
+
"""
|
|
281
|
+
if not self.can(obj, feature, eperson):
|
|
282
|
+
who = f"eperson {self._eperson_param(eperson)}" if eperson else "the current user"
|
|
283
|
+
raise AuthorizationError(f"{who} lacks {feature!r} on {self.object_uri(obj)}", status_code=403)
|
|
284
|
+
|
|
285
|
+
# ------------------------------------------------------------------
|
|
286
|
+
# Batch checks
|
|
287
|
+
# ------------------------------------------------------------------
|
|
288
|
+
|
|
289
|
+
@staticmethod
|
|
290
|
+
def _rest_type(path: str) -> str:
|
|
291
|
+
"""
|
|
292
|
+
Convert a REST API path to the type parameter used in search/objects.
|
|
293
|
+
|
|
294
|
+
Parameters:
|
|
295
|
+
path (str): The REST API path.
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
str: The corresponding type parameter.
|
|
299
|
+
|
|
300
|
+
Example:
|
|
301
|
+
'/core/items' -> 'core.items' (the type parameter of search/objects)."""
|
|
302
|
+
return ".".join(path.strip("/").split("/"))
|
|
303
|
+
|
|
304
|
+
def _group_by_type(self, objects: Iterable[ObjectRef]) -> Dict[str, List[Tuple[str, ObjectRef]]]:
|
|
305
|
+
"""
|
|
306
|
+
Group objects by their REST API path type.
|
|
307
|
+
|
|
308
|
+
Parameters:
|
|
309
|
+
objects (Iterable[ObjectRef]): The objects to group.
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
Dict[str, List[Tuple[str, ObjectRef]]]: A dictionary mapping path types to lists of (identifier, object) tuples.
|
|
313
|
+
"""
|
|
314
|
+
grouped: Dict[str, List[Tuple[str, ObjectRef]]] = {}
|
|
315
|
+
for obj in objects:
|
|
316
|
+
uri = self.object_uri(obj)
|
|
317
|
+
path, _, ident = uri[len(self.config.dspace_url):].rpartition("/")
|
|
318
|
+
grouped.setdefault(path, []).append((ident, obj))
|
|
319
|
+
return grouped
|
|
320
|
+
|
|
321
|
+
def features_on_many(self, objects: Sequence[ObjectRef], features: Union[str, Iterable[str], None] = None,
|
|
322
|
+
eperson: EPersonRef = None, batch_size: int = 50) -> Dict[str, Set[str]]:
|
|
323
|
+
"""
|
|
324
|
+
Get granted features for many objects using search/objects.
|
|
325
|
+
|
|
326
|
+
Parameters:
|
|
327
|
+
objects (Sequence[ObjectRef]): The objects to check features on.
|
|
328
|
+
features (Union[str, Iterable[str], None], optional): The features to check. Defaults to None.
|
|
329
|
+
eperson (EPersonRef, optional): The eperson reference to check features for. Defaults to None.
|
|
330
|
+
batch_size (int, optional): The batch size for search/objects requests. Defaults to 50.
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
Dict[str, Set[str]]: A dictionary mapping object IDs to sets of granted features.
|
|
334
|
+
|
|
335
|
+
Notes:
|
|
336
|
+
{object id: granted features} for many objects with search/objects
|
|
337
|
+
(DSpace 7.6+; one request per type and batch). Without `features`
|
|
338
|
+
(search/objects returns nothing then), on servers without that
|
|
339
|
+
search, and when it fails server-side, it falls back to one
|
|
340
|
+
search/object request per object.
|
|
341
|
+
"""
|
|
342
|
+
wanted = [features] if isinstance(features, str) else list(features or [])
|
|
343
|
+
eperson_uuid = self._eperson_param(eperson)
|
|
344
|
+
result: Dict[str, Set[str]] = {}
|
|
345
|
+
url = self._url(AUTHORIZATIONS, "/search/objects")
|
|
346
|
+
for path, entries in self._group_by_type(objects).items():
|
|
347
|
+
for ident, _ in entries:
|
|
348
|
+
result[ident] = set()
|
|
349
|
+
if not wanted:
|
|
350
|
+
# search/objects needs feature names - it answers an empty page without them
|
|
351
|
+
for ident, obj in entries:
|
|
352
|
+
result[ident] = self.features_on(obj, eperson)
|
|
353
|
+
continue
|
|
354
|
+
for start in range(0, len(entries), batch_size):
|
|
355
|
+
batch = entries[start:start + batch_size]
|
|
356
|
+
params = [("type", self._rest_type(path))] + [("uuid", ident) for ident, _ in batch]
|
|
357
|
+
params += [("feature", f) for f in wanted]
|
|
358
|
+
if eperson_uuid:
|
|
359
|
+
params.append(("eperson", eperson_uuid))
|
|
360
|
+
try:
|
|
361
|
+
found = list(self._iter_embedded(url, "authorizations", "Batch authorization search", params,
|
|
362
|
+
embed="feature"))
|
|
363
|
+
except DSpaceError as exc:
|
|
364
|
+
status = getattr(exc, "status_code", None)
|
|
365
|
+
if status not in (400, 404, 405, 422) and not (status or 0) >= 500:
|
|
366
|
+
raise
|
|
367
|
+
logger.warning("search/objects failed (HTTP %s: %s), checking one by one", status,
|
|
368
|
+
(exc.response_text or "")[:200])
|
|
369
|
+
for ident, obj in batch:
|
|
370
|
+
granted = self.features_on(obj, eperson)
|
|
371
|
+
result[ident] = {f for f in granted if not wanted or f in wanted}
|
|
372
|
+
continue
|
|
373
|
+
for authorization in found:
|
|
374
|
+
try:
|
|
375
|
+
ident = parse_authorization_id(authorization.get("id", ""))["object_id"]
|
|
376
|
+
except ValueError:
|
|
377
|
+
continue
|
|
378
|
+
feature = feature_of(authorization)
|
|
379
|
+
if ident in result and feature and (not wanted or feature in wanted):
|
|
380
|
+
result[ident].add(feature)
|
|
381
|
+
return result
|
|
382
|
+
|
|
383
|
+
def can_many(self, objects: Sequence[ObjectRef], feature: str, eperson: EPersonRef = None) -> Dict[str, bool]:
|
|
384
|
+
"""
|
|
385
|
+
Get a dictionary mapping object IDs to a boolean indicating if the feature is granted.
|
|
386
|
+
|
|
387
|
+
Parameters:
|
|
388
|
+
objects (Sequence[ObjectRef]): The objects to check the feature on.
|
|
389
|
+
feature (str): The feature to check.
|
|
390
|
+
eperson (EPersonRef, optional): The eperson reference to check the feature for. Defaults to None.
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
Dict[str, bool]: A dictionary mapping object IDs to a boolean indicating if the feature is granted.
|
|
394
|
+
"""
|
|
395
|
+
return {ident: feature in granted
|
|
396
|
+
for ident, granted in self.features_on_many(objects, feature, eperson).items()}
|
|
397
|
+
|
|
398
|
+
# ------------------------------------------------------------------
|
|
399
|
+
# Convenience
|
|
400
|
+
# ------------------------------------------------------------------
|
|
401
|
+
|
|
402
|
+
def site_uri(self) -> str:
|
|
403
|
+
"""
|
|
404
|
+
Get the URI of the site object.
|
|
405
|
+
|
|
406
|
+
Returns:
|
|
407
|
+
str: The URI of the site object.
|
|
408
|
+
|
|
409
|
+
Raises:
|
|
410
|
+
ValueError: If no site object is found.
|
|
411
|
+
"""
|
|
412
|
+
from .site import SiteManager
|
|
413
|
+
site = SiteManager(self.auth).get_site(all_languages=False)
|
|
414
|
+
if not site:
|
|
415
|
+
raise ValueError("No Site object found")
|
|
416
|
+
return self._url(DSO_PATHS["site"], f"/{site['uuid']}")
|
|
417
|
+
|
|
418
|
+
def is_site_admin(self, eperson: EPersonRef = None) -> bool:
|
|
419
|
+
"""
|
|
420
|
+
Check if the eperson is a site administrator.
|
|
421
|
+
|
|
422
|
+
Parameters:
|
|
423
|
+
eperson (EPersonRef, optional): The eperson reference to check. Defaults to None.
|
|
424
|
+
|
|
425
|
+
Returns:
|
|
426
|
+
bool: True if the eperson is a site administrator, False otherwise.
|
|
427
|
+
"""
|
|
428
|
+
return self.can(self.site_uri(), Feature.ADMINISTRATOR_OF, eperson)
|
|
429
|
+
|
|
430
|
+
def is_admin_of(self, obj: ObjectRef, eperson: EPersonRef = None) -> bool:
|
|
431
|
+
"""Admin of this community/collection/item (directly, inherited, or site admin)."""
|
|
432
|
+
return self.can(obj, Feature.ADMINISTRATOR_OF, eperson)
|
|
433
|
+
|
|
434
|
+
def admin_scope(self, eperson: EPersonRef = None) -> Dict[str, bool]:
|
|
435
|
+
"""
|
|
436
|
+
Get the administrative scope of the eperson.
|
|
437
|
+
|
|
438
|
+
Parameters:
|
|
439
|
+
eperson (EPersonRef, optional): The eperson reference to check the admin scope for. Defaults to None.
|
|
440
|
+
|
|
441
|
+
Returns:
|
|
442
|
+
Dict[str, bool]: A dictionary indicating which admin menus apply.
|
|
443
|
+
Keys are "site", "any_community", and "any_collection".
|
|
444
|
+
"""
|
|
445
|
+
granted = self.features_on(self.site_uri(), eperson)
|
|
446
|
+
return {"site": Feature.ADMINISTRATOR_OF in granted,
|
|
447
|
+
"any_community": Feature.IS_COMMUNITY_ADMIN in granted,
|
|
448
|
+
"any_collection": Feature.IS_COLLECTION_ADMIN in granted}
|
|
449
|
+
|
|
450
|
+
# ------------------------------------------------------------------
|
|
451
|
+
# Features (admin only)
|
|
452
|
+
# ------------------------------------------------------------------
|
|
453
|
+
|
|
454
|
+
def list_features(self) -> List[Dict[str, Any]]:
|
|
455
|
+
"""
|
|
456
|
+
List all authorization features.
|
|
457
|
+
|
|
458
|
+
Returns:
|
|
459
|
+
List[Dict[str, Any]]: A list of all authorization features.
|
|
460
|
+
"""
|
|
461
|
+
return list(self._iter_embedded(self._url(FEATURES), "features", "List authorization features"))
|
|
462
|
+
|
|
463
|
+
def get_feature(self, feature_id: str) -> Dict[str, Any]:
|
|
464
|
+
"""
|
|
465
|
+
Get details of a specific authorization feature.
|
|
466
|
+
|
|
467
|
+
Parameters:
|
|
468
|
+
feature_id (str): The ID of the feature to retrieve.
|
|
469
|
+
|
|
470
|
+
Returns:
|
|
471
|
+
Dict[str, Any]: The details of the specified feature.
|
|
472
|
+
"""
|
|
473
|
+
return self._get_json(self._url(FEATURES, f"/{feature_id}"), f"Get feature {feature_id}")
|
|
474
|
+
|
|
475
|
+
def features_for_type(self, resource_type: str) -> List[Dict[str, Any]]:
|
|
476
|
+
"""
|
|
477
|
+
Get features that apply to a specific resource type.
|
|
478
|
+
|
|
479
|
+
Parameters:
|
|
480
|
+
resource_type (str): The resource type to get features for, e.g. 'core.item', 'core.collection', 'core.site'.
|
|
481
|
+
|
|
482
|
+
Returns:
|
|
483
|
+
List[Dict[str, Any]]: A list of features applicable to the specified resource type.
|
|
484
|
+
"""
|
|
485
|
+
url = self._url(FEATURES, "/search/resourcetype")
|
|
486
|
+
return list(self._iter_embedded(url, "features", f"Features for {resource_type}", [("type", resource_type)]))
|