rucio 37.1.0.post1__py3-none-any.whl → 37.3.0__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.
Potentially problematic release.
This version of rucio might be problematic. Click here for more details.
- rucio/common/plugins.py +1 -1
- rucio/common/utils.py +17 -10
- rucio/core/did.py +32 -4
- rucio/core/did_meta_plugins/did_column_meta.py +1 -1
- rucio/core/replica.py +1 -1
- rucio/db/sqla/util.py +1 -1
- rucio/gateway/authentication.py +58 -88
- rucio/gateway/config.py +63 -75
- rucio/gateway/credential.py +11 -17
- rucio/gateway/did.py +245 -329
- rucio/gateway/dirac.py +33 -34
- rucio/gateway/exporter.py +27 -30
- rucio/gateway/heartbeat.py +15 -19
- rucio/gateway/importer.py +12 -14
- rucio/gateway/lifetime_exception.py +16 -24
- rucio/gateway/lock.py +27 -40
- rucio/gateway/meta_conventions.py +19 -28
- rucio/gateway/quarantined_replica.py +24 -27
- rucio/gateway/replica.py +223 -226
- rucio/gateway/rse.py +191 -218
- rucio/gateway/rule.py +115 -146
- rucio/gateway/scope.py +18 -25
- rucio/gateway/trace.py +48 -0
- rucio/gateway/vo.py +32 -37
- rucio/vcsversion.py +3 -3
- rucio/web/rest/flaskapi/v1/accounts.py +2 -2
- rucio/web/rest/flaskapi/v1/auth.py +15 -0
- rucio/web/rest/flaskapi/v1/common.py +20 -0
- rucio/web/rest/flaskapi/v1/config.py +7 -7
- rucio/web/rest/flaskapi/v1/credentials.py +20 -13
- rucio/web/rest/flaskapi/v1/dids.py +55 -55
- rucio/web/rest/flaskapi/v1/dirac.py +2 -2
- rucio/web/rest/flaskapi/v1/export.py +1 -1
- rucio/web/rest/flaskapi/v1/heartbeats.py +3 -3
- rucio/web/rest/flaskapi/v1/import.py +1 -1
- rucio/web/rest/flaskapi/v1/lifetime_exceptions.py +5 -5
- rucio/web/rest/flaskapi/v1/locks.py +4 -4
- rucio/web/rest/flaskapi/v1/main.py +17 -10
- rucio/web/rest/flaskapi/v1/meta_conventions.py +3 -3
- rucio/web/rest/flaskapi/v1/replicas.py +31 -30
- rucio/web/rest/flaskapi/v1/rses.py +37 -37
- rucio/web/rest/flaskapi/v1/rules.py +15 -15
- rucio/web/rest/flaskapi/v1/scopes.py +3 -3
- rucio/web/rest/flaskapi/v1/traces.py +75 -77
- rucio/web/rest/flaskapi/v1/vos.py +5 -7
- {rucio-37.1.0.post1.dist-info → rucio-37.3.0.dist-info}/METADATA +1 -2
- {rucio-37.1.0.post1.dist-info → rucio-37.3.0.dist-info}/RECORD +106 -105
- {rucio-37.1.0.post1.dist-info → rucio-37.3.0.dist-info}/WHEEL +1 -1
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/data/rucio/etc/alembic.ini.template +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/data/rucio/etc/alembic_offline.ini.template +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/data/rucio/etc/globus-config.yml.template +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/data/rucio/etc/ldap.cfg.template +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/data/rucio/etc/mail_templates/rule_approval_request.tmpl +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/data/rucio/etc/mail_templates/rule_approved_admin.tmpl +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/data/rucio/etc/mail_templates/rule_approved_user.tmpl +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/data/rucio/etc/mail_templates/rule_denied_admin.tmpl +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/data/rucio/etc/mail_templates/rule_denied_user.tmpl +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/data/rucio/etc/mail_templates/rule_ok_notification.tmpl +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/data/rucio/etc/rse-accounts.cfg.template +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/data/rucio/etc/rucio.cfg.atlas.client.template +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/data/rucio/etc/rucio.cfg.template +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/data/rucio/etc/rucio_multi_vo.cfg.template +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/data/rucio/requirements.server.txt +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/data/rucio/tools/bootstrap.py +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/data/rucio/tools/merge_rucio_configs.py +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/data/rucio/tools/reset_database.py +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/scripts/rucio +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/scripts/rucio-abacus-account +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/scripts/rucio-abacus-collection-replica +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/scripts/rucio-abacus-rse +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/scripts/rucio-admin +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/scripts/rucio-atropos +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/scripts/rucio-auditor +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/scripts/rucio-automatix +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/scripts/rucio-bb8 +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/scripts/rucio-cache-client +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/scripts/rucio-cache-consumer +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/scripts/rucio-conveyor-finisher +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/scripts/rucio-conveyor-poller +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/scripts/rucio-conveyor-preparer +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/scripts/rucio-conveyor-receiver +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/scripts/rucio-conveyor-stager +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/scripts/rucio-conveyor-submitter +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/scripts/rucio-conveyor-throttler +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/scripts/rucio-dark-reaper +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/scripts/rucio-dumper +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/scripts/rucio-follower +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/scripts/rucio-hermes +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/scripts/rucio-judge-cleaner +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/scripts/rucio-judge-evaluator +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/scripts/rucio-judge-injector +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/scripts/rucio-judge-repairer +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/scripts/rucio-kronos +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/scripts/rucio-minos +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/scripts/rucio-minos-temporary-expiration +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/scripts/rucio-necromancer +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/scripts/rucio-oauth-manager +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/scripts/rucio-reaper +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/scripts/rucio-replica-recoverer +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/scripts/rucio-rse-decommissioner +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/scripts/rucio-storage-consistency-actions +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/scripts/rucio-transmogrifier +0 -0
- {rucio-37.1.0.post1.data → rucio-37.3.0.data}/scripts/rucio-undertaker +0 -0
- {rucio-37.1.0.post1.dist-info → rucio-37.3.0.dist-info}/licenses/AUTHORS.rst +0 -0
- {rucio-37.1.0.post1.dist-info → rucio-37.3.0.dist-info}/licenses/LICENSE +0 -0
- {rucio-37.1.0.post1.dist-info → rucio-37.3.0.dist-info}/top_level.txt +0 -0
rucio/gateway/vo.py
CHANGED
|
@@ -12,23 +12,19 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
|
-
from typing import
|
|
15
|
+
from typing import Any, Optional
|
|
16
16
|
|
|
17
17
|
from rucio.common import exception
|
|
18
18
|
from rucio.common.schema import validate_schema
|
|
19
19
|
from rucio.common.types import InternalAccount
|
|
20
20
|
from rucio.core import identity
|
|
21
21
|
from rucio.core import vo as vo_core
|
|
22
|
-
from rucio.db.sqla.constants import IdentityType
|
|
23
|
-
from rucio.db.sqla.session import
|
|
22
|
+
from rucio.db.sqla.constants import DatabaseOperationType, IdentityType
|
|
23
|
+
from rucio.db.sqla.session import db_session
|
|
24
24
|
from rucio.gateway.permission import has_permission
|
|
25
25
|
|
|
26
|
-
if TYPE_CHECKING:
|
|
27
|
-
from sqlalchemy.orm import Session
|
|
28
26
|
|
|
29
|
-
|
|
30
|
-
@transactional_session
|
|
31
|
-
def add_vo(new_vo: str, issuer: str, description: Optional[str] = None, email: Optional[str] = None, vo: str = 'def', *, session: "Session") -> None:
|
|
27
|
+
def add_vo(new_vo: str, issuer: str, description: Optional[str] = None, email: Optional[str] = None, vo: str = 'def') -> None:
|
|
32
28
|
'''
|
|
33
29
|
Add a new VO.
|
|
34
30
|
|
|
@@ -37,38 +33,38 @@ def add_vo(new_vo: str, issuer: str, description: Optional[str] = None, email: O
|
|
|
37
33
|
:param email: A contact for the VO.
|
|
38
34
|
:param issuer: The user issuing the command.
|
|
39
35
|
:param vo: The vo of the user issuing the command.
|
|
40
|
-
:param session: The database session in use.
|
|
41
36
|
'''
|
|
42
37
|
|
|
43
38
|
new_vo = vo_core.map_vo(new_vo)
|
|
44
39
|
validate_schema('vo', new_vo, vo=vo)
|
|
45
40
|
|
|
46
41
|
kwargs = {}
|
|
47
|
-
auth_result = has_permission(issuer=issuer, action='add_vo', kwargs=kwargs, vo=vo, session=session)
|
|
48
|
-
if not auth_result.allowed:
|
|
49
|
-
raise exception.AccessDenied('Account {} cannot add a VO. {}'.format(issuer, auth_result.message))
|
|
50
42
|
|
|
51
|
-
|
|
43
|
+
with db_session(DatabaseOperationType.WRITE) as session:
|
|
44
|
+
auth_result = has_permission(issuer=issuer, action='add_vo', kwargs=kwargs, vo=vo, session=session)
|
|
45
|
+
if not auth_result.allowed:
|
|
46
|
+
raise exception.AccessDenied('Account {} cannot add a VO. {}'.format(issuer, auth_result.message))
|
|
47
|
+
|
|
48
|
+
vo_core.add_vo(vo=new_vo, description=description, email=email, session=session)
|
|
52
49
|
|
|
53
50
|
|
|
54
|
-
|
|
55
|
-
def list_vos(issuer: str, vo: str = 'def', *, session: "Session") -> list[dict[str, Any]]:
|
|
51
|
+
def list_vos(issuer: str, vo: str = 'def') -> list[dict[str, Any]]:
|
|
56
52
|
'''
|
|
57
53
|
List the VOs.
|
|
58
54
|
|
|
59
55
|
:param issuer: The user issuing the command.
|
|
60
56
|
:param vo: The vo of the user issuing the command.
|
|
61
|
-
:param session: The database session in use.
|
|
62
57
|
'''
|
|
63
58
|
kwargs = {}
|
|
64
|
-
auth_result = has_permission(issuer=issuer, action='list_vos', kwargs=kwargs, vo=vo, session=session)
|
|
65
|
-
if not auth_result.allowed:
|
|
66
|
-
raise exception.AccessDenied('Account {} cannot list VOs. {}'.format(issuer, auth_result.message))
|
|
67
59
|
|
|
68
|
-
|
|
60
|
+
with db_session(DatabaseOperationType.READ) as session:
|
|
61
|
+
auth_result = has_permission(issuer=issuer, action='list_vos', kwargs=kwargs, vo=vo, session=session)
|
|
62
|
+
if not auth_result.allowed:
|
|
63
|
+
raise exception.AccessDenied('Account {} cannot list VOs. {}'.format(issuer, auth_result.message))
|
|
64
|
+
|
|
65
|
+
return vo_core.list_vos(session=session)
|
|
69
66
|
|
|
70
67
|
|
|
71
|
-
@transactional_session
|
|
72
68
|
def recover_vo_root_identity(
|
|
73
69
|
root_vo: str,
|
|
74
70
|
identity_key: str,
|
|
@@ -78,8 +74,6 @@ def recover_vo_root_identity(
|
|
|
78
74
|
default: bool = False,
|
|
79
75
|
password: Optional[str] = None,
|
|
80
76
|
vo: str = 'def',
|
|
81
|
-
*,
|
|
82
|
-
session: "Session"
|
|
83
77
|
) -> None:
|
|
84
78
|
"""
|
|
85
79
|
Adds a membership association between identity and the root account for given VO.
|
|
@@ -92,22 +86,22 @@ def recover_vo_root_identity(
|
|
|
92
86
|
:param default: If True, the account should be used by default with the provided identity.
|
|
93
87
|
:param password: Password if id_type is userpass.
|
|
94
88
|
:param vo: the VO to act on.
|
|
95
|
-
:param session: The database session in use.
|
|
96
89
|
"""
|
|
97
90
|
kwargs = {}
|
|
98
91
|
root_vo = vo_core.map_vo(root_vo)
|
|
99
|
-
auth_result = has_permission(issuer=issuer, vo=vo, action='recover_vo_root_identity', kwargs=kwargs, session=session)
|
|
100
|
-
if not auth_result.allowed:
|
|
101
|
-
raise exception.AccessDenied('Account %s can not recover root identity. %s' % (issuer, auth_result.message))
|
|
102
92
|
|
|
103
|
-
|
|
93
|
+
with db_session(DatabaseOperationType.WRITE) as session:
|
|
94
|
+
auth_result = has_permission(issuer=issuer, vo=vo, action='recover_vo_root_identity', kwargs=kwargs, session=session)
|
|
95
|
+
if not auth_result.allowed:
|
|
96
|
+
raise exception.AccessDenied('Account %s can not recover root identity. %s' % (issuer, auth_result.message))
|
|
104
97
|
|
|
105
|
-
|
|
106
|
-
email=email, account=account, password=password, session=session)
|
|
98
|
+
account = InternalAccount('root', vo=root_vo)
|
|
107
99
|
|
|
100
|
+
return identity.add_account_identity(identity=identity_key, type_=IdentityType[id_type.upper()], default=default,
|
|
101
|
+
email=email, account=account, password=password, session=session)
|
|
108
102
|
|
|
109
|
-
|
|
110
|
-
def update_vo(updated_vo: str, parameters: dict[str, Any], issuer: str, vo: str = 'def'
|
|
103
|
+
|
|
104
|
+
def update_vo(updated_vo: str, parameters: dict[str, Any], issuer: str, vo: str = 'def') -> None:
|
|
111
105
|
"""
|
|
112
106
|
Update VO properties (email, description).
|
|
113
107
|
|
|
@@ -115,12 +109,13 @@ def update_vo(updated_vo: str, parameters: dict[str, Any], issuer: str, vo: str
|
|
|
115
109
|
:param parameters: A dictionary with the new properties.
|
|
116
110
|
:param issuer: The user issuing the command.
|
|
117
111
|
:param vo: The VO of the user issusing the command.
|
|
118
|
-
:param session: The database session in use.
|
|
119
112
|
"""
|
|
120
113
|
kwargs = {}
|
|
121
114
|
updated_vo = vo_core.map_vo(updated_vo)
|
|
122
|
-
auth_result = has_permission(issuer=issuer, action='update_vo', kwargs=kwargs, vo=vo, session=session)
|
|
123
|
-
if not auth_result.allowed:
|
|
124
|
-
raise exception.AccessDenied('Account {} cannot update VO. {}'.format(issuer, auth_result.message))
|
|
125
115
|
|
|
126
|
-
|
|
116
|
+
with db_session(DatabaseOperationType.WRITE) as session:
|
|
117
|
+
auth_result = has_permission(issuer=issuer, action='update_vo', kwargs=kwargs, vo=vo, session=session)
|
|
118
|
+
if not auth_result.allowed:
|
|
119
|
+
raise exception.AccessDenied('Account {} cannot update VO. {}'.format(issuer, auth_result.message))
|
|
120
|
+
|
|
121
|
+
return vo_core.update_vo(vo=updated_vo, parameters=parameters, session=session)
|
rucio/vcsversion.py
CHANGED
|
@@ -4,8 +4,8 @@ This file is automatically generated; Do not edit it. :)
|
|
|
4
4
|
'''
|
|
5
5
|
VERSION_INFO = {
|
|
6
6
|
'final': True,
|
|
7
|
-
'version': '37.
|
|
7
|
+
'version': '37.3.0',
|
|
8
8
|
'branch_nick': 'release-37',
|
|
9
|
-
'revision_id': '
|
|
10
|
-
'revno':
|
|
9
|
+
'revision_id': '874ee3e820d3ff64ab941ccfe8a02906b5540066',
|
|
10
|
+
'revno': 13716
|
|
11
11
|
}
|
|
@@ -217,7 +217,7 @@ class Scopes(ErrorHandlingMethodView):
|
|
|
217
217
|
description: Not acceptable
|
|
218
218
|
"""
|
|
219
219
|
try:
|
|
220
|
-
scopes = get_scopes(account, vo=request.environ
|
|
220
|
+
scopes = get_scopes(account, vo=request.environ['vo'])
|
|
221
221
|
except AccountNotFound as error:
|
|
222
222
|
return generate_http_error_flask(404, error)
|
|
223
223
|
|
|
@@ -264,7 +264,7 @@ class Scopes(ErrorHandlingMethodView):
|
|
|
264
264
|
description: Scope already exists.
|
|
265
265
|
"""
|
|
266
266
|
try:
|
|
267
|
-
add_scope(scope, account, issuer=request.environ
|
|
267
|
+
add_scope(scope, account, issuer=request.environ['issuer'], vo=request.environ['vo'])
|
|
268
268
|
except InvalidObject as error:
|
|
269
269
|
return generate_http_error_flask(400, error)
|
|
270
270
|
except AccessDenied as error:
|
|
@@ -916,6 +916,9 @@ class GSS(ErrorHandlingMethodView):
|
|
|
916
916
|
appid = request.headers.get('X-Rucio-AppID', default='unknown')
|
|
917
917
|
ip = request.headers.get('X-Forwarded-For', default=request.remote_addr)
|
|
918
918
|
|
|
919
|
+
if not account or not gsscred:
|
|
920
|
+
return generate_http_error_flask(400, ValueError.__name__, 'Account and REMOTE_USER must be set.')
|
|
921
|
+
|
|
919
922
|
try:
|
|
920
923
|
result = get_auth_token_gss(account, gsscred, appid, ip, vo=vo)
|
|
921
924
|
except AccessDenied:
|
|
@@ -1212,6 +1215,9 @@ class SSH(ErrorHandlingMethodView):
|
|
|
1212
1215
|
appid = request.headers.get('X-Rucio-AppID', default='unknown')
|
|
1213
1216
|
ip = request.headers.get('X-Forwarded-For', default=request.remote_addr)
|
|
1214
1217
|
|
|
1218
|
+
if not account or not signature:
|
|
1219
|
+
return generate_http_error_flask(400, ValueError.__name__, 'Account and SSH signature must be set.')
|
|
1220
|
+
|
|
1215
1221
|
try:
|
|
1216
1222
|
result = get_auth_token_ssh(account, signature, appid, ip, vo=vo)
|
|
1217
1223
|
except AccessDenied:
|
|
@@ -1332,6 +1338,9 @@ class SSHChallengeToken(ErrorHandlingMethodView):
|
|
|
1332
1338
|
appid = request.headers.get('X-Rucio-AppID', default='unknown')
|
|
1333
1339
|
ip = request.headers.get('X-Forwarded-For', default=request.remote_addr)
|
|
1334
1340
|
|
|
1341
|
+
if not account:
|
|
1342
|
+
return generate_http_error_flask(400, ValueError.__name__, 'Account must be set.')
|
|
1343
|
+
|
|
1335
1344
|
result = get_ssh_challenge_token(account, appid, ip, vo=vo)
|
|
1336
1345
|
|
|
1337
1346
|
if not result:
|
|
@@ -1452,6 +1461,9 @@ class SAML(ErrorHandlingMethodView):
|
|
|
1452
1461
|
appid = request.headers.get('X-Rucio-AppID', default='unknown')
|
|
1453
1462
|
ip = request.headers.get('X-Forwarded-For', default=request.remote_addr)
|
|
1454
1463
|
|
|
1464
|
+
if not account:
|
|
1465
|
+
return generate_http_error_flask(400, ValueError.__name__, 'Account must be set.')
|
|
1466
|
+
|
|
1455
1467
|
if saml_nameid:
|
|
1456
1468
|
try:
|
|
1457
1469
|
result = get_auth_token_saml(account, saml_nameid, appid, ip, vo=vo)
|
|
@@ -1592,6 +1604,9 @@ class Validate(ErrorHandlingMethodView):
|
|
|
1592
1604
|
|
|
1593
1605
|
token = request.headers.get('X-Rucio-Auth-Token', default=None)
|
|
1594
1606
|
|
|
1607
|
+
if not token:
|
|
1608
|
+
return generate_http_error_flask(400, ValueError.__name__, 'Token must be set.')
|
|
1609
|
+
|
|
1595
1610
|
result = validate_auth_token(token)
|
|
1596
1611
|
if not result:
|
|
1597
1612
|
return generate_http_error_flask(
|
|
@@ -15,11 +15,13 @@
|
|
|
15
15
|
import itertools
|
|
16
16
|
import json
|
|
17
17
|
import logging
|
|
18
|
+
import os
|
|
18
19
|
import re
|
|
19
20
|
from configparser import NoOptionError, NoSectionError
|
|
20
21
|
from functools import wraps
|
|
21
22
|
from time import time
|
|
22
23
|
from typing import TYPE_CHECKING, Any, Literal, Optional, TypeVar, Union
|
|
24
|
+
from urllib.parse import unquote_plus
|
|
23
25
|
|
|
24
26
|
import flask
|
|
25
27
|
from flask.views import MethodView
|
|
@@ -46,6 +48,9 @@ if TYPE_CHECKING:
|
|
|
46
48
|
|
|
47
49
|
ResponseTypeVar = TypeVar('ResponseTypeVar', bound=flask.wrappers.Response)
|
|
48
50
|
|
|
51
|
+
RUCIO_HTTPD_ENCODED_SLASHES_NO_DECODE = os.environ.get('RUCIO_HTTPD_ENCODED_SLASHES_NO_DECODE',
|
|
52
|
+
'false').lower() == 'true'
|
|
53
|
+
|
|
49
54
|
|
|
50
55
|
class CORSMiddleware:
|
|
51
56
|
"""
|
|
@@ -143,6 +148,9 @@ def request_auth_env() -> Optional['ResponseReturnValue']:
|
|
|
143
148
|
|
|
144
149
|
auth_token = flask.request.headers.get('X-Rucio-Auth-Token', default=None)
|
|
145
150
|
|
|
151
|
+
if not auth_token:
|
|
152
|
+
return generate_http_error_flask(400, ValueError.__name__, 'Token must be set.')
|
|
153
|
+
|
|
146
154
|
try:
|
|
147
155
|
auth = validate_auth_token(auth_token)
|
|
148
156
|
except CannotAuthenticate:
|
|
@@ -211,6 +219,18 @@ def parse_scope_name(scope_name: str, vo: Optional[str]) -> tuple[str, ...]:
|
|
|
211
219
|
:returns: a (scope, name) tuple.
|
|
212
220
|
"""
|
|
213
221
|
|
|
222
|
+
if RUCIO_HTTPD_ENCODED_SLASHES_NO_DECODE:
|
|
223
|
+
if scope_name.count('/') != 1:
|
|
224
|
+
# scope and name are always separated by a single slash ('/', unencoded) in the request.
|
|
225
|
+
# If the server is configured with the 'NoDecode' option, other slashes will be encoded.
|
|
226
|
+
# This is just a sanity check that should never happen.
|
|
227
|
+
raise ValueError(f"Could not parse '{scope_name}' ({scope_name=}) with encoded '/' into scope and name.")
|
|
228
|
+
|
|
229
|
+
scope, name = scope_name.split('/', 1)
|
|
230
|
+
name = unquote_plus(name)
|
|
231
|
+
|
|
232
|
+
return scope, name
|
|
233
|
+
|
|
214
234
|
if not vo:
|
|
215
235
|
vo = 'def'
|
|
216
236
|
|
|
@@ -46,9 +46,9 @@ class Config(ErrorHandlingMethodView):
|
|
|
46
46
|
description: Not acceptable
|
|
47
47
|
"""
|
|
48
48
|
res = {}
|
|
49
|
-
for section in config.sections(issuer=request.environ
|
|
49
|
+
for section in config.sections(issuer=request.environ['issuer'], vo=request.environ['vo']):
|
|
50
50
|
res[section] = {}
|
|
51
|
-
for item in config.items(section, issuer=request.environ
|
|
51
|
+
for item in config.items(section, issuer=request.environ['issuer'], vo=request.environ['vo']):
|
|
52
52
|
res[section][item[0]] = item[1]
|
|
53
53
|
|
|
54
54
|
return jsonify(res), 200
|
|
@@ -87,7 +87,7 @@ class Config(ErrorHandlingMethodView):
|
|
|
87
87
|
return generate_http_error_flask(400, ValueError.__name__, '')
|
|
88
88
|
for option, value in section_config.items():
|
|
89
89
|
try:
|
|
90
|
-
config.set(section=section, option=option, value=value, issuer=request.environ
|
|
90
|
+
config.set(section=section, option=option, value=value, issuer=request.environ['issuer'], vo=request.environ['vo'])
|
|
91
91
|
except ConfigurationError:
|
|
92
92
|
return generate_http_error_flask(400, 'ConfigurationError', f"Could not set value '{value}' for section '{section}' option '{option}'")
|
|
93
93
|
return 'Created', 201
|
|
@@ -137,7 +137,7 @@ class Section(ErrorHandlingMethodView):
|
|
|
137
137
|
description: Not acceptable
|
|
138
138
|
"""
|
|
139
139
|
res = {}
|
|
140
|
-
for item in config.items(section, issuer=request.environ
|
|
140
|
+
for item in config.items(section, issuer=request.environ['issuer'], vo=request.environ['vo']):
|
|
141
141
|
res[item[0]] = item[1]
|
|
142
142
|
|
|
143
143
|
if res == {}:
|
|
@@ -190,7 +190,7 @@ class OptionGetDel(ErrorHandlingMethodView):
|
|
|
190
190
|
description: Not acceptable
|
|
191
191
|
"""
|
|
192
192
|
try:
|
|
193
|
-
result = config.get(section=section, option=option, issuer=request.environ
|
|
193
|
+
result = config.get(section=section, option=option, issuer=request.environ['issuer'], vo=request.environ['vo'])
|
|
194
194
|
return jsonify(result), 200
|
|
195
195
|
except AccessDenied as error:
|
|
196
196
|
return generate_http_error_flask(401, error, f"Access to '{section}' option '{option}' denied")
|
|
@@ -223,7 +223,7 @@ class OptionGetDel(ErrorHandlingMethodView):
|
|
|
223
223
|
401:
|
|
224
224
|
description: Invalid Auth Token
|
|
225
225
|
"""
|
|
226
|
-
config.remove_option(section=section, option=option, issuer=request.environ
|
|
226
|
+
config.remove_option(section=section, option=option, issuer=request.environ['issuer'], vo=request.environ['vo'])
|
|
227
227
|
return '', 200
|
|
228
228
|
|
|
229
229
|
|
|
@@ -275,7 +275,7 @@ class OptionSet(ErrorHandlingMethodView):
|
|
|
275
275
|
enum: ['Could not set value {} for section {} option {}']
|
|
276
276
|
"""
|
|
277
277
|
try:
|
|
278
|
-
config.set(section=section, option=option, value=value, issuer=request.environ
|
|
278
|
+
config.set(section=section, option=option, value=value, issuer=request.environ['issuer'], vo=request.environ['vo'])
|
|
279
279
|
return 'Created', 201
|
|
280
280
|
except ConfigurationError as error:
|
|
281
281
|
return generate_http_error_flask(500, error, f"Could not set value '{value}' for section '{section}' option '{option}'")
|
|
@@ -12,12 +12,12 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
|
-
from typing import TYPE_CHECKING
|
|
15
|
+
from typing import TYPE_CHECKING, cast
|
|
16
16
|
|
|
17
17
|
from flask import Flask, request
|
|
18
18
|
from werkzeug.datastructures import Headers
|
|
19
19
|
|
|
20
|
-
from rucio.common.constants import RSE_BASE_SUPPORTED_PROTOCOL_OPERATIONS, SUPPORTED_SIGN_URL_SERVICES
|
|
20
|
+
from rucio.common.constants import RSE_BASE_SUPPORTED_PROTOCOL_OPERATIONS, RSE_BASE_SUPPORTED_PROTOCOL_OPERATIONS_LITERAL, SUPPORTED_SIGN_URL_SERVICES, SUPPORTED_SIGN_URL_SERVICES_LITERAL
|
|
21
21
|
from rucio.common.exception import CannotAuthenticate
|
|
22
22
|
from rucio.gateway.credential import get_signed_url
|
|
23
23
|
from rucio.web.rest.flaskapi.authenticated_bp import AuthenticatedBlueprint
|
|
@@ -163,28 +163,35 @@ class SignURL(ErrorHandlingMethodView):
|
|
|
163
163
|
headers = self.get_headers()
|
|
164
164
|
vo = extract_vo(request.headers)
|
|
165
165
|
account = request.headers.get('X-Rucio-Account', default=None)
|
|
166
|
-
appid = request.headers.get('X-Rucio-AppID', default='unknown')
|
|
167
|
-
ip = request.headers.get('X-Forwarded-For', default=request.remote_addr)
|
|
168
166
|
|
|
169
|
-
if
|
|
170
|
-
return generate_http_error_flask(400, ValueError.__name__, 'Parameter "
|
|
167
|
+
if account is None:
|
|
168
|
+
return generate_http_error_flask(400, ValueError.__name__, 'Parameter "account" not found.', headers=headers)
|
|
169
|
+
|
|
171
170
|
rse = request.args.get('rse')
|
|
172
171
|
|
|
172
|
+
if rse is None:
|
|
173
|
+
return generate_http_error_flask(400, ValueError.__name__, 'Parameter "rse" not found', headers=headers)
|
|
174
|
+
|
|
175
|
+
url = request.args.get('url')
|
|
176
|
+
|
|
177
|
+
if url is None:
|
|
178
|
+
return generate_http_error_flask(400, ValueError.__name__, 'Parameter "url" not found', headers=headers)
|
|
179
|
+
|
|
173
180
|
lifetime = request.args.get('lifetime', type=int, default=600)
|
|
174
181
|
service = request.args.get('svc', default='gcs')
|
|
175
182
|
operation = request.args.get('op', default='read')
|
|
176
183
|
|
|
177
|
-
if 'url' not in request.args:
|
|
178
|
-
return generate_http_error_flask(400, ValueError.__name__, 'Parameter "url" not found', headers=headers)
|
|
179
|
-
url = request.args.get('url')
|
|
180
|
-
|
|
181
184
|
if service not in SUPPORTED_SIGN_URL_SERVICES:
|
|
182
|
-
return generate_http_error_flask(400, ValueError.__name__, 'Parameter "svc" must be either empty(
|
|
185
|
+
return generate_http_error_flask(400, ValueError.__name__, 'Parameter "svc" must be either empty (which defaults to "gcs"), "gcs", "s3" or "swift"', headers=headers)
|
|
186
|
+
|
|
187
|
+
service = cast("SUPPORTED_SIGN_URL_SERVICES_LITERAL", service)
|
|
183
188
|
|
|
184
189
|
if operation not in RSE_BASE_SUPPORTED_PROTOCOL_OPERATIONS:
|
|
185
|
-
return generate_http_error_flask(400, ValueError.__name__, 'Parameter "op" must be either empty(
|
|
190
|
+
return generate_http_error_flask(400, ValueError.__name__, 'Parameter "op" must be either empty (which defaults to "read"), "read", "write", or "delete".', headers=headers)
|
|
191
|
+
|
|
192
|
+
operation = cast("RSE_BASE_SUPPORTED_PROTOCOL_OPERATIONS_LITERAL", service)
|
|
186
193
|
|
|
187
|
-
result = get_signed_url(account,
|
|
194
|
+
result = get_signed_url(account, rse=rse, service=service, operation=operation, url=url, lifetime=lifetime, vo=vo)
|
|
188
195
|
|
|
189
196
|
if not result:
|
|
190
197
|
return generate_http_error_flask(401, CannotAuthenticate.__name__, f'Cannot generate signed URL for account {account}', headers=headers)
|