ExampleDifficulty: Intermediate2 min

Proxy Example: Checking Permissions Before a Document Is Deleted

The protection proxy lets an editor delete and tells a viewer no.

A permission check is a natural Proxy, and this example maps the template onto it as a protection proxy. A document store can read and delete documents. Anyone may read, and only an editor may delete. The store itself knows nothing about users; a proxy that holds the user's role stands in front of it.

The template's participants in the permissions example
TemplatePermissions example
SubjectDocumentStore, with read and delete
RealSubjectDatabaseDocumentStore, which holds the documents
ProxyProtectedDocumentStore, which holds the store and a role
ClientThe loop that acts as an editor and then as a viewer
py
from abc import ABC, abstractmethod


class DocumentStore(ABC):
    @abstractmethod
    def read(self, doc_id: str) -> str: ...

    @abstractmethod
    def delete(self, doc_id: str) -> None: ...


class DatabaseDocumentStore(DocumentStore):
    def __init__(self) -> None:
        self._documents = {"plan": "Q4 plan", "notes": "Meeting notes"}

    def read(self, doc_id: str) -> str:
        return self._documents[doc_id]

    def delete(self, doc_id: str) -> None:
        del self._documents[doc_id]
        print(f"store: deleted {doc_id}")


class PermissionDenied(Exception):
    pass


class ProtectedDocumentStore(DocumentStore):
    def __init__(self, store: DocumentStore, role: str) -> None:
        self._store = store
        self._role = role

    def read(self, doc_id: str) -> str:
        return self._store.read(doc_id)

    def delete(self, doc_id: str) -> None:
        if self._role != "editor":
            raise PermissionDenied(f"a {self._role} cannot delete {doc_id}")
        self._store.delete(doc_id)


store = DatabaseDocumentStore()
for role in ["editor", "viewer"]:
    documents = ProtectedDocumentStore(store, role)
    print(f"{role} reads: {documents.read('notes')}")
    try:
        documents.delete("plan" if role == "editor" else "notes")
    except PermissionDenied as error:
        print(f"refused: {error}")
OutputRan with python:3.12-slim (Python 3.12.13)
editor reads: Meeting notes
store: deleted plan
viewer reads: Meeting notes
refused: a viewer cannot delete notes
`read` is forwarded unchanged; `delete` is forwarded only for an editor.

Both users read the notes, because the proxy forwards read without a check. The editor's delete reaches the store, which prints that it deleted the plan. The viewer's delete never reaches it: the proxy raises PermissionDenied, and the store's own line does not appear.

The store can now be tested with no users at all, and a different rule, such as letting owners delete their own documents, is a change to the proxy alone. The caller still holds a DocumentStore, so code written before permissions existed works with the proxy unchanged.