Files
Warp/server/internal/storage/admin.go
T
tajniak81andClaude Opus 5.5 a94794b866
CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Test (windows-latest) (push) Canceled after 0s
CI / Storage tests on PostgreSQL (push) Canceled after 0s
CI / Complement (federation off) (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
CI / Smoke test the binary (push) Canceled after 0s
Phase 9: application services and admin completion
Implements PLAN.md Phase 9. internal/appservice reads registration files
(appservice.config_files), matches namespaces the way Synapse does
(anchored at the start), and delivers events to each service as
transactions read from the roomserver's event stream at a cursor kept in
appservice_state. A transaction in flight is recorded, so after a restart
it is resent under the same ID; failures back off from 2s to 5m, and a
successful ping cuts the backoff short.

In the Client-Server API an as_token acts as the service's sender or, with
?user_id=, as any existing user in its namespace. Services register and
log in their users with m.login.application_service; exclusive user and
alias namespaces are refused to everybody else with M_EXCLUSIVE. Unknown
aliases and users in a namespace are asked of the service first. The
ping endpoint reports M_URL_NOT_SET, M_BAD_STATUS, M_CONNECTION_FAILED
and M_CONNECTION_TIMEOUT. Services' own requests, and their users' when
rate_limited is false, skip the rate limiter.

Users can change their password (POST /account/password) and deactivate
their account (POST /account/deactivate), both behind the password.
Deactivation revokes tokens, deletes devices, optionally erases the
profile and leaves every room through a new userapi.AccountObserver.

The admin API is complete: user detail, the admin flag, deactivation and
reactivation, password resets, devices, media quotas
(media.max_user_storage_bytes plus per-user overrides), registration
tokens with use limits and expiry (registration.require_token), server
notices (internal/servernotices, server_notices.*), an editor for the
federation allow/deny lists persisted in server_settings, a delivery view
of the application services, and an audit log of every change.

Migration 0011 adds the registration token, server notice room, media
quota, audit log and appservice cursor tables. Policy lists are now
returned sorted. The Complement entrypoint loads the registrations
Complement mounts in /complement/appservice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 12:58:28 +02:00

157 lines
5.2 KiB
Go

package storage
import (
"context"
"errors"
)
// Errors of the admin store.
var (
// ErrNoSuchRegistrationToken is an unknown registration token — or,
// from UseRegistrationToken, one that is known but expired or used
// up. A client is told the same thing either way.
ErrNoSuchRegistrationToken = errors.New("no such registration token")
// ErrRegistrationTokenExists is CreateRegistrationToken's answer for a
// token that is already there.
ErrRegistrationTokenExists = errors.New("registration token already exists")
// ErrNoServerNoticeRoom is a user who has never been sent a server
// notice.
ErrNoServerNoticeRoom = errors.New("no server notice room")
)
// RegistrationToken is a token made through the admin API.
type RegistrationToken struct {
Token string
// UsesAllowed is how many registrations may spend the token; nil is
// no limit.
UsesAllowed *int
// Completed is how many registrations have spent it.
Completed int
// ExpiresAtMS is when the token stops working, or 0 for never.
ExpiresAtMS int64
CreatedAtMS int64
}
// Usable reports whether the token can still be spent at nowMS.
func (t RegistrationToken) Usable(nowMS int64) bool {
if t.ExpiresAtMS != 0 && t.ExpiresAtMS <= nowMS {
return false
}
return t.UsesAllowed == nil || t.Completed < *t.UsesAllowed
}
// AuditEntry is one change made through the admin API.
type AuditEntry struct {
// ID orders the log; it is assigned by AppendAudit.
ID int64
TSMS int64
// Actor is the full user ID of the admin.
Actor string
// Action names what was done, e.g. "user.deactivate".
Action string
// Target is what it was done to, or empty for a server-wide change.
Target string
// Details is a JSON object with whatever else is worth keeping: the
// new value of a switch, whether a deactivation erased the profile.
Details []byte
}
// AuditFilter narrows and pages the audit log, newest first.
type AuditFilter struct {
// Before returns entries with an ID below it; 0 starts at the newest.
Before int64
// Limit caps the returned entries; 0 means the store's own default.
Limit int
// Actor and Action, when set, must match exactly.
Actor string
Action string
}
// AdminStore keeps the state of the admin API's own: registration tokens,
// the room each user's server notices go to, and the audit log.
type AdminStore interface {
// CreateRegistrationToken stores a new token, returning
// ErrRegistrationTokenExists when it is taken.
CreateRegistrationToken(ctx context.Context, t RegistrationToken) error
// RegistrationToken reads one token.
RegistrationToken(ctx context.Context, token string) (RegistrationToken, error)
// RegistrationTokens lists every token, ordered by token.
RegistrationTokens(ctx context.Context) ([]RegistrationToken, error)
// UpdateRegistrationToken replaces a token's use limit and expiry; the
// count of uses it has had stays.
UpdateRegistrationToken(ctx context.Context, token string, usesAllowed *int, expiresAtMS int64) error
// DeleteRegistrationToken removes a token.
DeleteRegistrationToken(ctx context.Context, token string) error
// UseRegistrationToken spends one use of a token, if it is usable at
// nowMS, in a single statement: two registrations racing for the last
// use cannot both have it. A token that is not usable is
// ErrNoSuchRegistrationToken.
UseRegistrationToken(ctx context.Context, token string, nowMS int64) error
// ReleaseRegistrationToken gives back a use taken by a registration
// that then failed.
ReleaseRegistrationToken(ctx context.Context, token string) error
// AppendAudit records an entry and returns its ID.
AppendAudit(ctx context.Context, e AuditEntry) (int64, error)
// AuditLog reads a page of the log, newest first.
AuditLog(ctx context.Context, f AuditFilter) ([]AuditEntry, error)
// ServerNoticeRoom returns the room a user's server notices go to,
// or ErrNoServerNoticeRoom.
ServerNoticeRoom(ctx context.Context, localpart string) (string, error)
// SetServerNoticeRoom records it, replacing any earlier one.
SetServerNoticeRoom(ctx context.Context, localpart, roomID string) error
}
// AppServiceState is where an application service is in the event stream.
type AppServiceState struct {
ID string
// StreamPos is the last position whose events have been delivered or
// skipped.
StreamPos int64
// TxnID is the ID of the last transaction begun. While TxnTo is
// non-zero that transaction is in flight and holds the events in
// (StreamPos, TxnTo].
TxnID int64
TxnTo int64
}
// AppServiceStore keeps the application services' delivery cursors.
type AppServiceStore interface {
// AppServiceState reads an application service's cursor, creating it
// at startPos the first time the service is seen: a newly registered
// service is sent what happens from now on, not the server's history.
AppServiceState(ctx context.Context, id string, startPos int64) (AppServiceState, error)
// BeginTransaction records a transaction as in flight.
BeginTransaction(ctx context.Context, id string, txnID, to int64) error
// CompleteTransaction moves the cursor to `to` and clears the
// transaction in flight. It is also how a stretch of the stream with
// nothing of interest in it is skipped.
CompleteTransaction(ctx context.Context, id string, to int64) error
}