TessariDBDocs
  • 0.34.2-betalatest
  • 0.34.1-beta
  • 0.34.0-beta
  • 0.33.4-beta
  • 0.33.3-beta
  • 0.33.2-beta
  • 0.33.1-beta
  • 0.33.0-beta
  • 0.32.0-beta

Users

An open store, the moment it closes, three roles, and the door that does not exist.

A store with no user is open, and anything may run against it. That is a decision and not an oversight: requiring a sign-in against a store with no accounts locks everybody out of it with no way in to fix that.

Declaring the first user is what closes it.

DEFINE NAMESPACE prod;
USE NAMESPACE prod;
DEFINE DATABASE orders;
USE DATABASE orders;
DEFINE COLLECTION notes;
CREATE notes = { body: 'before there were users' };
DEFINE USER ada ON prod.orders ROLE editor PASSWORD 'correct horse';

Everything before that statement ran anonymously. Everything after it does not:

SELECT * FROM notes;

Refused — NotSignedIn, and note that it is a read. A closed store has no anonymous access at all, not merely no anonymous writes. An application serving public pages out of one therefore needs an account to read with; there is no "public" level below viewer.

Closing it without a person there

That order works at a terminal and not in a container: nothing types the first DEFINE USER into a node that comes up unattended. So a node reads two variables on the way up, and declares that user as a store-wide owner when the store has no users at all:

TESSARIDB_INITIAL_USER=root
TESSARIDB_INITIAL_PASSWORD=a long enough one

In a compose file that is the whole of it:

services:
  database:
    image: tessaridb
    command: ["/data", "--serve", "0.0.0.0:7654"]
    environment:
      TESSARIDB_INITIAL_USER: root
      TESSARIDB_INITIAL_PASSWORD: ${DB_PASSWORD:?set this}

Four things are worth knowing before you rely on it.

Both or neither. Setting one without the other stops the node rather than starting it. The alternative is a node that came up open, on a network, because a variable name was misspelled — and looking exactly like one that came up correctly.

It runs once. A store that already has users ignores both variables and says so in its log. That is what makes them safe to leave in a compose file across every restart, and it also means they are not a way to reset a password: once the store is closed they do nothing. A lost owner password is still a restore from backup.

The user owns the store, not a database. Anything narrower could not declare the users that come after it, and could not take a backup.

A variable is not a secret store. It is readable by anything that can inspect the container or the process. That is a smaller circle than a command line and it is not nothing, so treat it the way you would treat any other credential in a deployment file.

There is no back door

DROP USER ada;

NotSignedIn. An anonymous session cannot re-open a store by removing its last account, and no flag, environment variable or single-user mode does it either.

The consequence is worth knowing before you need it: a lost owner password is a restore from backup, not a recovery. Nobody can let you back in, because the design is that nobody can.

The three roles

These are declared by a signed-in owner, which is why they are shown here rather than run against the anonymous session above:

DEFINE USER reader ON prod.orders ROLE viewer PASSWORD 'a long enough one';
DEFINE USER writer ON prod.orders ROLE editor PASSWORD 'another long one';
DEFINE USER root ROLE owner PASSWORD 'the longest one of all';
rolemay
viewerread what the tenancy holds, and nothing else
editorread and write records, and define structure
ownereverything, including declaring users

Only an owner may declare a user. That is what forces the order at deployment time: the owner is declared first, by the anonymous session that still can; the owner then signs in and declares everyone else. A start-up script that declares the editor first finds it cannot declare the owner afterwards.

Scope

ON prod.orders puts the user in one database. Left off, the user belongs to the store:

writtenreaches
DEFINE USER ada ON prod.orders ROLE editor …that one database
DEFINE USER root ROLE owner …the whole store

A store-scoped owner is what a backup needs, because BACKUP is the one statement whose subject is the store rather than the selected namespace — there is no table for a grant to name.

Authority is a role and a reach

The role says what you may do. The scope says where. Neither is the whole answer on its own, and the second one is what bounds you when the thing you are acting on is another user.

An owner may administer users — but only those in the tenancy they administer, and containment is asymmetric:

you areyou may administer
an owner with no scopeeveryone
an owner of prod.ordersusers of prod.orders
an owner of prod.ordersnot a user of prod.billing, and not a store-scoped owner

There are exactly two reaches, and it is worth saying so plainly: the whole store, or one database. ON takes a table reference, so ON prod names the database prod in the selected namespace rather than the namespace — there is no syntax for a user who owns a namespace and everything under it.

Nothing contains a sibling. Two owners at the same depth are invisible to each other, which is the case that would otherwise leak quietly.

This bounds five statements, not one:

statementrefused when
INFO FOR USER <name>the subject is outside your tenancy
INFO FOR USERSyou administer nothing — it refuses rather than narrowing
ALTER USER …the subject is outside your tenancy
DROP USER …the subject is outside your tenancy
GRANT / REVOKE … TO <name>the user is outside your tenancy
DEFINE USER …the reach being asked for is wider than your own

The last two are the ones worth reading twice.

A grant narrows. A user with even one grant is reduced to exactly what they were granted — so aiming a grant upwards takes authority away rather than giving it, and the first thing to go is the store owner's BACKUP. It is refused for that reason.

DEFINE USER is bounded by the reach it names, not by the user it creates. An owner of one database declaring an owner with no scope would be minting a node administrator and then signing in as them, which is the way round every other rule on this page. The refusal names it:

DEFINE USER mallory ROLE owner PASSWORD 'a long enough one';

Run by an owner of prod.orders, that is refused — "mallory would reach further than you do; name a tenancy inside your own".

The one exemption is the moment the store closes: an anonymous session on an open store may declare a store-wide owner, because otherwise no store could ever be closed at all.

Some statements are about the store, not a tenancy

Most statements name a namespace, and naming one you do not hold is refused. A few name nothing — their subject is the whole store, or the machine serving it — so there is no name for that check to look at. Those need an owner with no tenancy of their own:

statementneedswhy
BACKUPowner, store-wideevery record in every namespace, past every grant
INFO FOR NODE · SELECT … FROM $nodeowner, store-widethis machine's roles, endpoints and peers
DEFINE NODE · DEFINE REPLICAowner, store-widewhat this machine is, and which others hold the data
a peer collecting the logreplicate, store-wide onlythe log carries users and their credential hashes, so a namespace owner must not be able to admit a follower
DEFINE NAMESPACEeditor, store-widea namespace is a sibling, and nothing contains a sibling

Role and reach are two separate questions, and that last row is where the difference shows. A namespace is less than the databases and tables an editor can already define, so declaring one does not need a raised role — it needs a store-wide reach. A store-wide editor may; an editor of one database may not, because for them a new namespace is a sibling of the one they are in.

An owner of prod.orders is refused all four — "owns a part of this store, and this needs an owner of the whole store". That refusal is deliberately not the one a viewer gets: being told you are not an owner when you are one sends you to ask for the wrong thing.

The familiar exemption still holds: an anonymous session on an open store may declare the first namespace, because otherwise no store could be set up at all.

Changing your own password

Everything above is about acting on somebody else. Your own password is different, and any role may change it:

$ curl -s -u ada -X POST --data-binary 'a different long one' \
    http://localhost:8000/password
{"changed":true,"tokens_ended":true}

The current password is asked for, and checked. Being signed in is not proof of it: a token can be copied off a plaintext connection or read out of a log, and if holding one were enough to set a new password, a stolen token would be a permanent takeover rather than a temporary one. For the same reason the route takes Basic and refuses a bearer token.

It changes the caller and nobody else — there is no name to give it — which is why it needs no ownership at all. A viewer whose password may have leaked should not have to ask an owner to choose them a new one.

Every token that user holds stops working, this session included.

Changing a user afterwards

DEFINE USER refuses a name that is already taken — a rotation spelled as a re-declaration fails rather than half-succeeding. ALTER USER is the statement that changes one:

ALTER USER reader SET PASSWORD 'a different long one';
ALTER USER reader SET ROLE editor;

One field per statement, so a password rotation cannot silently reset a role by restating it wrongly, and a role correction cannot invalidate a password. There is no SET ON: a reach is fixed when the user is declared, for the same reason DEFINE USER is bounded by it.

Removing one is DROP USER, and it takes the user's grants with it. A new user of the same name is a different user and inherits none of them.

Passwords

A password is never an argument on a command line. It arrives in TESSARIDB_PASSWORD, because an argument is in the process table where any user on the machine can read it, and a password that has been in a process table has been published.

$ TESSARIDB_PASSWORD='correct horse' tessaridb --at 127.0.0.1:7654 --user ada

In a statement it is written out, and it prints back as <redacted> — the value does not reach a log through a debug rendering, because the type that holds it has no rendering that shows it.

A user can also be declared from the hash the store keeps, which is how a state script brings users back without anybody knowing their passwords:

DEFINE USER ada ON prod.orders ROLE editor PASSHASH '$argon2id$v=19$m=19456,t=2,p=1$…';

The hash is taken only if this store could have made it — Argon2id, at parameters no weaker than its own — so it is not a way to plant a credential cheaper to guess than any the store would write.

Guessing is made slow, not impossible

Repeated failures against one identity are throttled rather than locked out. The numbers are fixed and not configurable:

3 consecutive failures are freesomeone mistyping a password twice is ordinary; a third consecutive miss is where a person stops guessing and a program starts
then 250 ms, doubling each further failurea fixed delay is a fixed guess rate, and a fixed guess rate is a budget an attacker can plan against
up to 30 s between attemptsdoubling without a ceiling reaches days, and a real user who mistyped six times should not be locked out for one

A delay rather than a lockout, deliberately: a lockout on a name anyone can guess is a way to deny service to the person who owns it. The refusal is SignInThrottled, which is distinct from SignInRefused — one says not now and the other says not this password.

On a cluster the budget belongs to the cluster, not to each node (from 0.21.0-beta): a clustered node asks the leader whether a name may try a password and reports how the try went, so three nodes are not three allowances. A node that cannot reach the leader in time falls back to its own count.

Separately, the process runs at most 24 password verifications at once. A password hash is expensive on purpose, so that bound is what keeps a burst of sign-ins from becoming the node's whole CPU budget; it is not the connection ceiling, which is larger and counts something else.

Signing in once

That bound is also why a password should be spent once rather than on every request. Over HTTP, POST /session exchanges one for a token:

$ curl -s -u ada -X POST http://localhost:8000/session
{"token":"5f3a…","expires_in":43200}

Every request afterwards carries the token instead:

$ curl -s -H 'Authorization: Bearer 5f3a…' -d 'SELECT * FROM orders;' \
    http://localhost:8000/script

The difference is not small. A password costs an Argon2 hash — nineteen mebibytes and tens of milliseconds, deliberately — and a token costs a lookup. Sending the password every time also means at most 24 of those requests are in flight at once, whatever the connection ceiling says.

DELETE /session hands a token back. A token also expires on its own after twelve hours, and a node's tokens live only in that node's memory, so a restart ends every session it had issued.

A token stops standing for anybody the moment the account behind it changes. Rotating a password, correcting a role, or removing the user kills every token that user held, on the next request:

what you runwhat happens to their tokens
ALTER USER ada SET PASSWORD '…'all dead
ALTER USER ada SET ROLE viewerall dead
DROP USER adaall dead

That is what makes a rotation worth doing. A token that outlived the password it came from would make changing a password a note in the catalog rather than a change in what somebody may do.

Grants are the exception, and deliberately: a GRANT or REVOKE narrows a token-carrying session on its very next statement rather than ending it. Taking authority away should take effect sooner than giving it back, not later.

A token is a bearer credential — whoever holds the text is that user until it dies. It travels in a header exactly as a password does, over a connection this node does not encrypt. What it is better at is that it can be thrown away and it expires; a password does neither.

The wire protocol needs none of this: a connection is a session there, so the sign-in happens once when it opens.

Narrowing further

A role says which verbs a user may use. A grant says which tables they may use them on, and a user with even one grant is reduced to exactly what they were granted.

Users — TessariDB