Skip to main content
Access is expressed as roles, and a role is a set of grants, entity by entity.

No inheritance, no wildcards

An entity a role doesn’t name is an entity that role can’t touch. There’s no *, and no role extends another. It’s more typing. What you get for it is that reading one role file tells you exactly what that role can do, without resolving a chain to find out, and a new entity isn’t silently readable by everyone the moment somebody adds it.
Adding an entity doesn’t add it to any role. Until you grant it, nobody but the owner and a platform administrator can see it. That’s the direction we’d rather fail in.

What enforcement actually does

Grants are checked on every operation, through the API as well as through the interface. Two behaviours are worth knowing because they surprise people. Read responses drop fields the role may not read. Not an error, just absent. A list silently drops a filter on a hidden field, but an aggregate refuses it. Those look inconsistent. A list returning fewer rows leaks nothing, while an aggregate that quietly ignored a groupBy would hand back a confidently wrong number, so it answers 403 instead.

404 and 403 mean different things

no relationship
You have no relationship to this app at all. It answers exactly as a missing app does. Whether an app exists isn’t something an unrelated caller gets to learn.
the role denies it
You have a relationship, and the roles it grants don’t permit this operation.

Writing them

upsert_role and remove_behaviour are the operations. Or edit roles/*.cordango.yaml directly.

In a generated application

A standalone application generated by cordango build carries its own users, authentication and permissions, built from the same role definitions. The enforcement there is a second implementation of the same rules, pinned by hand-written fixtures that both products assert against. Same roles, same grants, no shared code path. That’s on purpose. Two implementations that have to agree catch mistakes a single one can’t.