Skip to content

Commit dbb042f

Browse files
Merge pull request #630 from danielballan/acp-fix
Fix Access Policy + Catalog
2 parents 0e3346c + 5b82e73 commit dbb042f

File tree

3 files changed

+89
-6
lines changed

3 files changed

+89
-6
lines changed

tiled/_tests/test_catalog.py

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@
2121
from ..client import Context, from_context
2222
from ..client.xarray import write_xarray_dataset
2323
from ..queries import Eq, Key
24-
from ..server.app import build_app
24+
from ..server.app import build_app, build_app_from_config
2525
from ..server.schemas import Asset, DataSource
2626
from ..structures.core import StructureFamily
27+
from .utils import enter_password
2728

2829

2930
@pytest_asyncio.fixture
@@ -342,3 +343,71 @@ async def test_delete_tree(tmpdir):
342343
assert len(data_sources_after_delete) == 0
343344
assets_after_delete = (await tree.context.execute("SELECT * from assets")).all()
344345
assert len(assets_after_delete) == 0
346+
347+
348+
@pytest.mark.asyncio
349+
async def test_access_control(tmpdir):
350+
config = {
351+
"authentication": {
352+
"allow_anonymous_access": True,
353+
"secret_keys": ["SECRET"],
354+
"providers": [
355+
{
356+
"provider": "toy",
357+
"authenticator": "tiled.authenticators:DictionaryAuthenticator",
358+
"args": {
359+
"users_to_passwords": {
360+
"alice": "secret1",
361+
"bob": "secret2",
362+
"admin": "admin",
363+
}
364+
},
365+
}
366+
],
367+
},
368+
"database": {
369+
"uri": "sqlite+aiosqlite://", # in-memory
370+
},
371+
"trees": [
372+
{
373+
"tree": "catalog",
374+
"path": "/",
375+
"args": {
376+
"uri": f"sqlite+aiosqlite:///{tmpdir}/catalog.db",
377+
"writable_storage": str(tmpdir / "data"),
378+
"init_if_not_exists": True,
379+
},
380+
"access_control": {
381+
"access_policy": "tiled.access_policies:SimpleAccessPolicy",
382+
"args": {
383+
"provider": "toy",
384+
"access_lists": {
385+
"alice": ["outer_x"],
386+
"bob": ["outer_y"],
387+
},
388+
"admins": ["admin"],
389+
"public": ["outer_z"],
390+
},
391+
},
392+
},
393+
],
394+
}
395+
396+
app = build_app_from_config(config)
397+
with Context.from_app(app) as context:
398+
with enter_password("admin"):
399+
admin_client = from_context(context, username="admin")
400+
for key in ["outer_x", "outer_y", "outer_z"]:
401+
container = admin_client.create_container(key)
402+
container.write_array([1, 2, 3], key="inner")
403+
admin_client.logout()
404+
with enter_password("secret1"):
405+
alice_client = from_context(context, username="alice")
406+
alice_client["outer_x"]["inner"].read()
407+
with pytest.raises(KeyError):
408+
alice_client["outer_y"]
409+
alice_client.logout()
410+
public_client = from_context(context)
411+
public_client["outer_z"]["inner"].read()
412+
with pytest.raises(KeyError):
413+
public_client["outer_x"]

tiled/access_policies.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,15 @@ def _get_id(self, principal):
6464
def allowed_scopes(self, node, principal):
6565
# If this is being called, filter_access has let us get this far.
6666
if principal is SpecialUsers.public:
67-
return PUBLIC_SCOPES
68-
if self._get_id(principal) in self.admins:
69-
return ALL_SCOPES
67+
allowed = PUBLIC_SCOPES
68+
elif self._get_id(principal) in self.admins:
69+
allowed = ALL_SCOPES
7070
# The simple policy does not provide for different Principals to
7171
# have different scopes on different Nodes. If the Principal has access,
7272
# they have the same hard-coded access everywhere.
73-
return self.scopes
73+
else:
74+
allowed = self.scopes
75+
return allowed
7476

7577
def filters(self, node, principal, scopes):
7678
queries = []

tiled/catalog/adapter.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ def writable(self):
294294
return bool(self.context.writable_storage)
295295

296296
def __repr__(self):
297-
return f"<{type(self).__name__} {self.segments}>"
297+
return f"<{type(self).__name__} /{'/'.join(self.segments)}>"
298298

299299
async def __aiter__(self):
300300
statement = select(orm.Node.key).filter(orm.Node.ancestors == self.segments)
@@ -335,6 +335,18 @@ async def lookup_adapter(
335335
if not segments:
336336
return self
337337
*ancestors, key = segments
338+
if self.conditions and len(segments) > 1:
339+
# There are some conditions (i.e. WHERE clauses) applied to
340+
# this node, either via user search queries or via access
341+
# control policy queries. Look up first the _direct_ child of this
342+
# node, if it exists within the filtered results.
343+
first_level = await self.lookup_adapter(segments[:1])
344+
if first_level is None:
345+
return None
346+
# Now proceed to traverse further down the tree, if needed.
347+
# Search queries and access controls apply only at the top level.
348+
assert not first_level.conditions
349+
return await first_level.lookup_adapter(segments[1:])
338350
statement = select(orm.Node).filter(
339351
orm.Node.ancestors == self.segments + ancestors
340352
)

0 commit comments

Comments
 (0)