Skip to content

Commit 7337225

Browse files
authored
Merge pull request #407 from natthan-pigoux/feat_sb_dl_permissions
feat: Ensure SBAccessPolicy verifies the user owns the sandbox
2 parents a374382 + dde7d06 commit 7337225

File tree

5 files changed

+84
-6
lines changed

5 files changed

+84
-6
lines changed

diracx-db/src/diracx/db/sql/sandbox_metadata/db.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ async def get_owner_id(self, user: UserInfo) -> int | None:
3030
)
3131
return (await self.conn.execute(stmt)).scalar_one_or_none()
3232

33+
async def get_sandbox_owner_id(self, pfn: str) -> int | None:
34+
"""Get the id of the owner of a sandbox."""
35+
stmt = select(SBOwners.OwnerID).where(
36+
SBOwners.OwnerID == SandBoxes.OwnerId,
37+
SandBoxes.SEPFN == pfn,
38+
)
39+
return (await self.conn.execute(stmt)).scalar_one_or_none()
40+
3341
async def insert_owner(self, user: UserInfo) -> int:
3442
stmt = insert(SBOwners).values(
3543
Owner=user.preferred_username,

diracx-db/tests/jobs/test_sandbox_metadata.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,3 +186,25 @@ async def test_assign_and_unsassign_sandbox_to_jobs(
186186
res = await sandbox_metadata_db.conn.execute(stmt)
187187
res_sb_id = [row.SBId for row in res]
188188
assert sb_id_1 not in res_sb_id
189+
190+
191+
async def test_get_sandbox_owner_id(sandbox_metadata_db: SandboxMetadataDB):
192+
user_info = UserInfo(
193+
sub="vo:sub", preferred_username="user1", dirac_group="group1", vo="vo"
194+
)
195+
pfn = secrets.token_hex()
196+
sandbox_se = "SandboxSE"
197+
# Insert the sandbox
198+
async with sandbox_metadata_db:
199+
owner_id = await sandbox_metadata_db.insert_owner(user_info)
200+
await sandbox_metadata_db.insert_sandbox(owner_id, sandbox_se, pfn, 100)
201+
202+
async with sandbox_metadata_db:
203+
sb_owner_id = await sandbox_metadata_db.get_sandbox_owner_id(pfn)
204+
205+
assert owner_id == 1
206+
assert sb_owner_id == owner_id
207+
208+
async with sandbox_metadata_db:
209+
sb_owner_id = await sandbox_metadata_db.get_sandbox_owner_id("not_found")
210+
assert sb_owner_id is None

diracx-routers/src/diracx/routers/jobs/access_policies.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from fastapi import Depends, HTTPException, status
88

99
from diracx.core.properties import JOB_ADMINISTRATOR, NORMAL_USER
10-
from diracx.db.sql import JobDB
10+
from diracx.db.sql import JobDB, SandboxMetadataDB
1111
from diracx.routers.access_policies import BaseAccessPolicy
1212
from diracx.routers.utils.users import AuthorizedUserInfo
1313

@@ -108,10 +108,12 @@ async def policy(
108108
/,
109109
*,
110110
action: ActionType | None = None,
111+
sandbox_metadata_db: SandboxMetadataDB | None = None,
111112
pfns: list[str] | None = None,
112113
required_prefix: str | None = None,
113114
):
114115
assert action, "action is a mandatory parameter"
116+
assert sandbox_metadata_db, "sandbox_metadata_db is a mandatory parameter"
115117

116118
if action == ActionType.CREATE:
117119

@@ -137,6 +139,14 @@ async def policy(
137139
status_code=status.HTTP_403_FORBIDDEN,
138140
detail=f"Invalid PFN. PFN must start with {required_prefix}",
139141
)
142+
# Checking if the user owns the sandbox
143+
owner_id = await sandbox_metadata_db.get_owner_id(user_info)
144+
sandbox_owner_id = await sandbox_metadata_db.get_sandbox_owner_id(pfn)
145+
if not owner_id or owner_id != sandbox_owner_id:
146+
raise HTTPException(
147+
status_code=status.HTTP_403_FORBIDDEN,
148+
detail=f"{user_info.preferred_username} is not the owner of the sandbox",
149+
)
140150

141151

142152
CheckSandboxPolicyCallable = Annotated[Callable, Depends(SandboxAccessPolicy.check)]

diracx-routers/src/diracx/routers/jobs/sandboxes.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,9 @@ async def initiate_sandbox_upload(
5555
If the sandbox does not exist in the database then the "url" and "fields"
5656
should be used to upload the sandbox to the storage backend.
5757
"""
58-
await check_permissions(action=ActionType.CREATE)
58+
await check_permissions(
59+
action=ActionType.CREATE, sandbox_metadata_db=sandbox_metadata_db
60+
)
5961

6062
try:
6163
sandbox_upload_response = await initiate_sandbox_upload_bl(
@@ -73,6 +75,7 @@ async def initiate_sandbox_upload(
7375
async def get_sandbox_file(
7476
pfn: Annotated[str, Query(max_length=256, pattern=SANDBOX_PFN_REGEX)],
7577
settings: SandboxStoreSettings,
78+
sandbox_metadata_db: SandboxMetadataDB,
7679
user_info: Annotated[AuthorizedUserInfo, Depends(verify_dirac_access_token)],
7780
check_permissions: CheckSandboxPolicyCallable,
7881
) -> SandboxDownloadResponse:
@@ -92,6 +95,7 @@ async def get_sandbox_file(
9295
)
9396
await check_permissions(
9497
action=ActionType.READ,
98+
sandbox_metadata_db=sandbox_metadata_db,
9599
pfns=[short_pfn],
96100
required_prefix=required_prefix,
97101
)

diracx-routers/tests/jobs/test_wms_access_policy.py

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,23 @@
2323
}
2424

2525

26-
class FakeDB:
26+
class FakeJobDB:
2727
async def summary(self, *args): ...
2828

2929

30+
class FakeSBMetadataDB:
31+
async def get_owner_id(self, *args): ...
32+
async def get_sandbox_owner_id(self, *args): ...
33+
34+
3035
@pytest.fixture
3136
def job_db():
32-
yield FakeDB()
37+
yield FakeJobDB()
38+
39+
40+
@pytest.fixture
41+
def sandbox_metadata_db():
42+
yield FakeSBMetadataDB()
3343

3444

3545
WMS_POLICY_NAME = "WMSAccessPolicy_AlthoughItDoesNotMatter"
@@ -220,7 +230,7 @@ async def summary_other_vo(*args):
220230
)
221231

222232

223-
async def test_sandbox_access_policy_create():
233+
async def test_sandbox_access_policy_create(sandbox_metadata_db):
224234

225235
admin_user = AuthorizedUserInfo(properties=[JOB_ADMINISTRATOR], **base_payload)
226236
normal_user = AuthorizedUserInfo(properties=[NORMAL_USER], **base_payload)
@@ -230,6 +240,7 @@ async def test_sandbox_access_policy_create():
230240
await SandboxAccessPolicy.policy(
231241
SANDBOX_POLICY_NAME,
232242
normal_user,
243+
sandbox_metadata_db=sandbox_metadata_db,
233244
)
234245

235246
# An admin cannot create any resource
@@ -238,6 +249,7 @@ async def test_sandbox_access_policy_create():
238249
SANDBOX_POLICY_NAME,
239250
admin_user,
240251
action=ActionType.CREATE,
252+
sandbox_metadata_db=sandbox_metadata_db,
241253
pfns=[USER_SANDBOX_PFN],
242254
)
243255

@@ -246,13 +258,14 @@ async def test_sandbox_access_policy_create():
246258
SANDBOX_POLICY_NAME,
247259
normal_user,
248260
action=ActionType.CREATE,
261+
sandbox_metadata_db=sandbox_metadata_db,
249262
pfns=[USER_SANDBOX_PFN],
250263
)
251264

252265
##############
253266

254267

255-
async def test_sandbox_access_policy_read():
268+
async def test_sandbox_access_policy_read(sandbox_metadata_db, monkeypatch):
256269

257270
admin_user = AuthorizedUserInfo(properties=[JOB_ADMINISTRATOR], **base_payload)
258271
normal_user = AuthorizedUserInfo(properties=[NORMAL_USER], **base_payload)
@@ -261,6 +274,7 @@ async def test_sandbox_access_policy_read():
261274
SANDBOX_POLICY_NAME,
262275
admin_user,
263276
action=ActionType.READ,
277+
sandbox_metadata_db=sandbox_metadata_db,
264278
pfns=[USER_SANDBOX_PFN],
265279
required_prefix=SANDBOX_PREFIX,
266280
)
@@ -269,6 +283,7 @@ async def test_sandbox_access_policy_read():
269283
SANDBOX_POLICY_NAME,
270284
admin_user,
271285
action=ActionType.READ,
286+
sandbox_metadata_db=sandbox_metadata_db,
272287
pfns=[OTHER_USER_SANDBOX_PFN],
273288
required_prefix=SANDBOX_PREFIX,
274289
)
@@ -279,24 +294,43 @@ async def test_sandbox_access_policy_read():
279294
SANDBOX_POLICY_NAME,
280295
normal_user,
281296
action=ActionType.READ,
297+
sandbox_metadata_db=sandbox_metadata_db,
282298
pfns=[USER_SANDBOX_PFN],
283299
)
284300

285301
# User can act on his own sandbox
302+
async def get_owner_id(*args):
303+
return 1
304+
305+
async def get_sandbox_owner_id(*args):
306+
return 1
307+
308+
monkeypatch.setattr(sandbox_metadata_db, "get_owner_id", get_owner_id)
309+
monkeypatch.setattr(
310+
sandbox_metadata_db, "get_sandbox_owner_id", get_sandbox_owner_id
311+
)
312+
286313
await SandboxAccessPolicy.policy(
287314
SANDBOX_POLICY_NAME,
288315
normal_user,
289316
action=ActionType.READ,
317+
sandbox_metadata_db=sandbox_metadata_db,
290318
pfns=[USER_SANDBOX_PFN],
291319
required_prefix=SANDBOX_PREFIX,
292320
)
293321

294322
# User cannot act on others
323+
async def get_owner_id(*args):
324+
return 2
325+
326+
monkeypatch.setattr(sandbox_metadata_db, "get_owner_id", get_owner_id)
327+
295328
with pytest.raises(HTTPException):
296329
await SandboxAccessPolicy.policy(
297330
SANDBOX_POLICY_NAME,
298331
normal_user,
299332
action=ActionType.READ,
333+
sandbox_metadata_db=sandbox_metadata_db,
300334
pfns=[OTHER_USER_SANDBOX_PFN],
301335
required_prefix=SANDBOX_PREFIX,
302336
)

0 commit comments

Comments
 (0)