"""Sharing: what it grants, what it does not, and what it leaves behind. Half of this file is about grants that outlive what they name. `Share` carries no foreign key in either direction — `principal_id` points at a user *or* a group and `resource_id` at one of four tables, neither of which SQLite can express — so nothing cascades, and every delete has to say so explicitly. `sharing.forget_principal` existed for exactly this and was called by nobody. """ from __future__ import annotations import pytest from fastapi.testclient import TestClient from sqlalchemy import select from lembas.db.models import ( ROLE_USER, Group, Note, Report, Share, User, ) from lembas.security import permissions from lembas.services import reports as reports_service from lembas.services import sharing from lembas.services.library import notes as notes_service @pytest.fixture def owner(db, registered) -> User: return db.scalars(select(User).order_by(User.created_at)).first() @pytest.fixture def reader(db) -> User: person = User( email="sam@shire.test", name="Sam", password_hash="x", role=ROLE_USER # noqa: S106 ) db.add(person) db.commit() return person # --- Dangling grants ------------------------------------------------------------ def test_deleting_a_group_drops_the_shares_naming_it(db, client, registered, owner, reader): """It never did. A group id is a random hex string that nothing reissues today and nothing promises not to reissue tomorrow.""" group = Group(name="team") group.users.append(reader) db.add(group) db.commit() note = notes_service.create(db, owner=owner, title="Secret", body="mellon") sharing.set_grants(db, note, user_ids=[], group_ids=[group.id]) assert db.scalars(select(Share)).all() client.post(f"/admin/groups/{group.id}/delete", follow_redirects=False) db.expire_all() assert db.scalars(select(Share)).all() == [] def test_deleting_an_account_drops_both_halves(db, client, registered, owner, reader): """Grants **to** them, and grants **of** their own work. The second is the one nothing else could catch: their rows cascade, and the shares of those rows have nothing to cascade from.""" theirs = notes_service.create(db, owner=reader, title="Theirs", body="x") sharing.set_grants(db, theirs, user_ids=[owner.id], group_ids=[]) mine = notes_service.create(db, owner=owner, title="Mine", body="y") sharing.set_grants(db, mine, user_ids=[reader.id], group_ids=[]) assert len(db.scalars(select(Share)).all()) == 2 client.post(f"/admin/users/{reader.id}/delete", follow_redirects=False) db.expire_all() assert db.scalars(select(Share)).all() == [] def test_deleting_a_report_drops_its_grants(db, owner, reader): report = reports_service.create(db, owner=owner, title="Findings", body="x") sharing.set_grants(db, report, user_ids=[reader.id], group_ids=[]) reports_service.delete(db, report) db.expire_all() assert db.scalars(select(Share)).all() == [] # --- Reports --------------------------------------------------------------------- def test_a_shared_report_is_visible_and_not_deletable(db, owner, reader): """Sharing grants reading. Being able to see a report is not being able to delete it out from under the person who filed it.""" report = reports_service.create(db, owner=owner, title="Findings", body="x") sharing.set_grants(db, report, user_ids=[reader.id], group_ids=[]) assert reports_service.get(db, report.id, reader) is not None assert reports_service.owned(db, report.id, reader) is None assert reports_service.owned(db, report.id, owner) is not None def test_an_unshared_report_stays_invisible(db, owner, reader): report = reports_service.create(db, owner=owner, title="Findings", body="x") assert reports_service.get(db, report.id, reader) is None assert list(db.scalars(reports_service.visible(reader))) == [] def test_a_shared_report_appears_in_the_feed_and_the_filter(db, owner, reader): report = reports_service.create(db, owner=owner, title="Findings", body="x") theirs = reports_service.create(db, owner=reader, title="Mine", body="y") sharing.set_grants(db, report, user_ids=[reader.id], group_ids=[]) everything = {r.id for r in db.scalars(reports_service.visible(reader))} only_shared = { r.id for r in db.scalars(select(Report).where(sharing.only_shared(Report, reader))) } assert everything == {report.id, theirs.id} assert only_shared == {report.id} def test_reading_somebody_elses_report_does_not_clear_their_dot(db, client, registered, owner): """`unread` is the owner's notification. Somebody it was shared with opening it would silence a dot meant for a person who has not seen it.""" other = User( email="sam@shire.test", name="Sam", password_hash="x", role=ROLE_USER # noqa: S106 ) db.add(other) db.commit() report = reports_service.create(db, owner=other, title="Theirs", body="x") report.unread = True db.commit() sharing.set_grants(db, report, user_ids=[owner.id], group_ids=[]) assert client.get(f"/reports/{report.id}").status_code == 200 db.refresh(report) assert report.unread is True # --- Shared with me -------------------------------------------------------------- def test_only_shared_excludes_your_own(db, owner, reader): mine = notes_service.create(db, owner=reader, title="Mine", body="x") theirs = notes_service.create(db, owner=owner, title="Theirs", body="y") sharing.set_grants(db, theirs, user_ids=[reader.id], group_ids=[]) rows = {n.id for n in db.scalars(select(Note).where(sharing.only_shared(Note, reader)))} assert rows == {theirs.id} assert mine.id not in rows def test_the_filter_is_a_url_you_can_keep(db, client, registered): for path in ("/library/notes", "/library/skills", "/library/knowledge", "/reports"): page = client.get(f"{path}?shared=1") assert page.status_code == 200, path assert "Shared with me" in page.text, path # --- The panel ------------------------------------------------------------------- def test_a_grant_is_stored_the_moment_it_is_made(db, client, registered, owner, reader): """It used to ride along with the resource's save form, so ticking a box and navigating away did nothing — silently.""" note = notes_service.create(db, owner=owner, title="Note", body="x") response = client.post( f"/api/library/share/note/{note.id}", data={"principal_type": "user", "principal_id": reader.id, "on": "true"}, ) assert response.status_code == 200 assert sharing.can_read(db, note, reader) is True # And the answer is the panel, showing what is now true. assert "Shared with" in response.text def test_a_grant_can_be_taken_back(db, client, registered, owner, reader): note = notes_service.create(db, owner=owner, title="Note", body="x") sharing.set_grants(db, note, user_ids=[reader.id], group_ids=[]) client.post( f"/api/library/share/note/{note.id}", data={"principal_type": "user", "principal_id": reader.id, "on": "false"}, ) db.expire_all() assert sharing.can_read(db, note, reader) is False def test_the_panel_searches_rather_than_listing_everybody(db, client, registered, owner): """It rendered every group and every account on the instance, unpaginated, on every detail page.""" for n in range(30): db.add( User( email=f"p{n}@shire.test", name=f"Person {n}", password_hash="x", # noqa: S106 ) ) db.commit() note = notes_service.create(db, owner=owner, title="Note", body="x") everything = client.get(f"/api/library/share/note/{note.id}").text assert everything.count('name="principal_id"') == 0 # values ride in hx-vals assert "Person 29" not in everything found = client.get(f"/api/library/share/note/{note.id}?q=Person+29").text assert "Person 29" in found def test_an_existing_grant_stays_listed_whatever_the_search_says( db, client, registered, owner, reader ): """Otherwise the only way to remove a grant would be to search for the name it was given to.""" note = notes_service.create(db, owner=owner, title="Note", body="x") sharing.set_grants(db, note, user_ids=[reader.id], group_ids=[]) page = client.get(f"/api/library/share/note/{note.id}?q=nobody-matches-this").text assert "sam@shire.test" in page def test_somebody_it_was_shared_with_cannot_share_it_on(db, client, registered, owner, reader): """What keeps "who can see this?" answerable by asking one person.""" note = notes_service.create(db, owner=owner, title="Note", body="x") sharing.set_grants(db, note, user_ids=[reader.id], group_ids=[]) other = TestClient(client.app) other.post( "/auth/register", data={"name": "Merry", "email": "merry@shire.test", "password": "a-fine-second-breakfast"}, follow_redirects=False, ) # Signed in as somebody else entirely: the panel is not theirs to open. assert other.get(f"/api/library/share/note/{note.id}").status_code == 404 def test_a_grant_naming_nothing_is_refused(db, client, registered, owner): """A crafted id would write a grant that is invisible in the panel and unremovable from it.""" note = notes_service.create(db, owner=owner, title="Note", body="x") client.post( f"/api/library/share/note/{note.id}", data={"principal_type": "user", "principal_id": "not-a-real-id", "on": "true"}, ) assert db.scalars(select(Share)).all() == [] def test_sharing_is_on_by_default(db, registered, owner): """It was off, which meant sharing shipped documented as done and unreachable: the panel only renders for somebody who holds this.""" assert permissions.has(db, owner, "library.share") is True assert permissions.DEFAULT_PERMISSIONS["library.share"] is True # --- Read and write, split ------------------------------------------------------- def test_a_reader_can_search_notes_and_not_write_them(db, reader): from lembas.db.models import Chat, Connection, Model from lembas.services import settings_store from lembas.services import tools as tools_service from lembas.services.crypto import encrypt connection = Connection(name="c", base_url="http://127.0.0.1:1", api_key_encrypted=encrypt("")) db.add(connection) db.commit() db.add(Model(connection_id=connection.id, model_id="m", capabilities_json={"tools": True})) db.commit() chat = Chat(user_id=reader.id, model_id="m", connection_id=connection.id) db.add(chat) db.commit() settings_store.update(db, {"default_permissions": {"tools.notes.write": False}}) offered = {tool.name for tool in tools_service.resolve_tools(db, chat, reader).defs} assert "notes_search" in offered assert "notes_get" in offered assert "notes_create" not in offered assert "notes_edit" not in offered assert "notes_delete" not in offered def test_the_write_half_defaults_on(db, reader): """An instance that never looks behaves exactly as it did.""" for key in ("tools.notes.write", "tools.memory.write", "tools.skills.write"): assert permissions.DEFAULT_PERMISSIONS[key] is True