Users and access
The office has no open registration: whoever has an active row in the organtia.acceso_usuarios
table gets in. The list is managed in Settings › People, which only the owner and
administrators see. Everything else —the form, the cookie, signing out— exists around that table.
Who can sign in
Section titled “Who can sign in”Each installation has its own list of users. There is no shared gate between clients and there are
no guests: if you are not in the table, or your row has activo = false, the gate answers
“Incorrect username or password” and does not tell your case apart from a mistyped password.
| Situation | What happens |
|---|---|
A user in the table with activo = true and the right password |
They get in and ultimo_acceso is updated. |
| A user that does not exist | A hash is computed anyway (so as not to reveal which users exist) and the attempt is counted. |
A user with activo = false |
Treated as if they did not exist. |
No ACCESO_CLAVE in the environment (or fewer than 32 characters) |
Nobody gets in: the gate fails closed, not open. |
Account types: there is only one
Section titled “Account types: there is only one”Better said before you go looking for what is not there. There are no per-user roles or permissions. The table has no role column, the application checks no different permissions and everyone with an account sees and does the same: the whole office, with its pages, its approvals and its settings.
What does tell people apart is the trail: the audit stores the owner’s name or the gate’s username
in the actor field of each operation (see
/docs/administracion/auditoria/). That is traceability, not access
control.
The CEO and the directors are not users in this table: they are roles, they live in
organtia.puestos and they do not come in through the gate.
The session
Section titled “The session”On sign-in a cookie called oficina_sesion is created, shaped usuario|expira|firma. The
signature is an HMAC-SHA256 with the ACCESO_CLAVE key; the expiry is checked by the server on its
own, because the browser can lie about the date.
The cookie is issued with HttpOnly, Secure and SameSite=Lax. It lasts whatever ACCESO_DIAS
says:
| Variable | Default | What it does |
|---|---|---|
ACCESO_CLAVE |
no value | Key the cookies are signed with. It must be 32 characters or more. If it changes, every open session stops being valid and everyone has to sign in again. |
ACCESO_DIAS |
7 |
Days the cookie is valid for. 1 to 90 is accepted; a value out of range or non-numeric stays at 7. |
Caddy knows nothing about passwords: before serving the office it asks /acceso/authz whether
there is a session. If there is, it gets a 200 and the Remote-User header with the username,
which is what the application shows in the top bar and what it uses to attribute requests.
Creating a user
Section titled “Creating a user”It is not created from the web. It is created with a script that ships inside the container:
docker compose exec -T api python scripts/crear_usuario_oficina.py pedroIt asks for the password twice on the keyboard, with no arguments: whatever is written on the
command line ends up in the shell history and in ps. The username is stored in lowercase and the
display name is filled in with the first letter capitalised.
Ways to use it:
| Command | What it does |
|---|---|
... crear_usuario_oficina.py pedro |
Creates the user, or changes their password if they already exist. |
... crear_usuario_oficina.py --listar |
Lists username, whether they are active, last access and name. |
... crear_usuario_oficina.py --desactivar pedro |
Sets activo = false. It does not delete the row or their data. |
... crear_usuario_oficina.py pedro --por-tuberia |
Reads the password from standard input, two lines. For automation only. |
Password requirements: 8 characters minimum. There are no rules on capitals, symbols or expiry: the minimum is low on purpose, so as not to get in the owner’s way.
Changing the password
Section titled “Changing the password”Users change it themselves at /acceso/clave, with their session open. It asks for the current
one, the new one and the repeat, and checks that the new one is not the same as the current one.
Saving it renews the cookie in that browser.
The password is not stored anywhere: what is stored is scrypt(key, salt) with cost N=2^14, r=8,
p=1 and a different salt per user. The comparison is in constant time, and a user that does not
exist pays the same computation as a real one.
Signing out
Section titled “Signing out”The sign-out button in the top bar sends you to /acceso/salir, which deletes the cookie and
returns to the form.
The interface also watches for inactivity: after 30 minutes without touching anything (keyboard, mouse, wheel or touchscreen) it shows a notice with “Your session will close in a minute if you do nothing” and a “Stay here” button. That notice does not close the session by itself: the cookie is still what rules. What it does is, when you come back to the tab, ask the gate whether it still accepts the session; if it no longer does, “The session has been closed for security. Nothing has been lost” appears with a button to sign in again.
Failed attempts
Section titled “Failed attempts”Five attempts per user every five minutes. On the sixth, the gate answers the same as for a wrong password and writes a notice to the process log:
Acceso a la oficina bloqueado temporalmente: pedroThe counter lives in memory: if the service restarts, it is forgotten. That is deliberate —what was wrong was having nothing at all— but it is worth knowing before counting on it as lasting defence.
Deactivating someone
Section titled “Deactivating someone”docker compose exec -T api python scripts/crear_usuario_oficina.py --desactivar pedroThey stop being able to sign in immediately. What it does not do is cut the session they
already have open: the cookie is validated by signature and date, without querying the database on
every request, so a browser already authenticated stays in until it expires (up to ACCESO_DIAS
days). If you need to cut it now, change ACCESO_CLAVE and restart the API: that invalidates every
cookie for every person.
For the same reason, changing someone’s password does not close their other sessions.
The table
Section titled “The table”organtia.acceso_usuarios (migration 028_usuarios_oficina.sql):
| Field | Type | What it means |
|---|---|---|
id |
bigserial | Internal identifier. |
usuario |
text, unique | The name you sign in with, in lowercase. It is not the email. |
nombre |
text | Display name. The script fills it in with the capitalised username; it is kept if the password is updated without giving a name. |
hash |
text | The scrypt result in hexadecimal. Never the password. |
sal |
text | A 16-byte salt in hexadecimal, different per user. |
activo |
boolean | If it is false, the gate ignores them. Saving the password again sets it back to true. |
creado_en |
timestamptz | Creation. |
ultimo_acceso |
timestamptz | Last successful sign-in. Every authentication writes it; null means they have never signed in. |
The table has no two-factor column yet. The code leaves it ready to add it on top without touching the rest.
What there is not
Section titled “What there is not”So you do not go looking:
- No sign-up or removal from the interface: only the script.
- No password recovery by email and no security question. If someone loses it, you change it with the script.
- No two-factor of its own.
- No roles, groups or per-person permissions.
- No password expiry and no history of previous passwords.
- Sign-in attempts do not go to the audit table: they go to the process log.
Typical problems
Section titled “Typical problems”| What you see | Likely cause | What to do |
|---|---|---|
| “Incorrect username or password” with the right password | User deactivated, or a trailing space when pasting it | Check --listar and type it again by hand. |
| You sign in and get sent back to the form in a loop | The browser is not storing the cookie, or you are coming in over http:// instead of https:// (the cookie is Secure) |
Use the public address with HTTPS and accept cookies for the site. |
| After five failed attempts it will not let you in even with the right one | Five-minute temporary block | Wait five minutes or restart the API. |
| The gate throws a 500 error and shows no form | ACCESO_CLAVE is missing or shorter than 32 characters |
Set the variable in .env and recreate the api service. |
| You changed the password and they are still signing in from somewhere else | That browser’s cookie has not expired | Change ACCESO_CLAVE if you need to cut every session. |
