Authorization code is security code. This page explains how Licet handles untrusted input and what remains your responsibility.

Your responsibilities

Authenticate first. Licet answers whether a subject has a permission. It doesn't know who the current user is. Build the subject string from the authenticated user, such as "user:#{current_user.id}", never from request parameters.

Fail closed. Treat {:error, _} from a check as a denial. A check can fail because of a timeout, a depth limit, or a database problem, and none of those should grant access.

case Licet.check(MyApp.Licet, resource, "edit", subject) do
  {:ok, :allowed} -> :ok
  _denied_or_error -> {:error, :forbidden}
end

Check on every action that changes data. A LiveView that checked on mount should check again before handling an event that writes. See Phoenix integration.

Choose the consistency mode deliberately. The default can be a few seconds behind. Revoking access and then immediately relying on the revocation needs a token or :full. See Consistency.

Pick the instance name in code. The instance name is an atom chosen by your application, never derived from user input.

Untrusted input

Licet treats schema text, object ids, relationship text, tokens, and cursors as untrusted data.

  • No atoms. Type, relation, and permission names stay strings. Licet never creates atoms from input, so it can't exhaust the atom table.
  • No term decoding. Tokens and cursors are base64-encoded JSON, validated by shape, and tokens have a size limit. Neither is ever passed to :erlang.binary_to_term/1.
  • Parameterized SQL. Every value is a query parameter. The only interpolated identifier is the table prefix, which is validated against ^[a-z_][a-z0-9_]{0,62}$ when the instance starts.
  • Validated ids. An object id is * or 1 to 1024 characters from [A-Za-z0-9/_|=\-+]. A resource can't be a wildcard.
  • Safe YAML. Validation files are read with atoms: false.

Tokens and cursors aren't secret and aren't signed. A forged token can at most ask for a snapshot that exists or fail with :token_invalid; it can't grant access.

Resource limits

Every query and write is bounded by the instance options, so one request can't monopolize the database:

OptionBoundsDefault
max_updatesupdates in one Licet.write/31,000
max_importrelationships in one Licet.import/310,000
max_preconditionspreconditions on one write1,000
max_deptharrow and userset hops in one check50
max_lookupresults computed by one lookup10,000
timeoutmilliseconds for one check or lookup5,000

Lower them if your schema doesn't need the headroom.

Data integrity

  • Schema changes are checked. Licet.write_schema/3, and starting an instance with a new schema, fail with :unsafe_schema_change when the new schema would ignore relationships that are still stored, such as removing a relation that has data. Delete the relationships first, then change the schema.
  • Cache answers are scoped to a snapshot. A cached answer is only reused for the exact snapshot it was computed at. Errors aren't cached, and checks inside a transaction or with :full don't use the cache.
  • History is kept for gc_window. Garbage collection keeps every transaction newer than gc_window seconds, so a token from that window can still be read exactly.

Reporting a vulnerability

Report security issues privately to the maintainers listed in the repository, before opening a public issue.