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
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>
230 lines
7.8 KiB
Go
230 lines
7.8 KiB
Go
package storage
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
)
|
|
|
|
// Errors the user store returns for the cases a caller has to tell apart.
|
|
// Everything else comes back wrapped and becomes a 500.
|
|
var (
|
|
// ErrUserExists is returned by CreateAccount for a taken localpart.
|
|
ErrUserExists = errors.New("user already exists")
|
|
|
|
// ErrNoSuchUser is returned for an unknown localpart.
|
|
ErrNoSuchUser = errors.New("no such user")
|
|
|
|
// ErrNoSuchDevice is returned for an unknown device of a known user.
|
|
ErrNoSuchDevice = errors.New("no such device")
|
|
|
|
// ErrNoSuchToken is returned for an access or refresh token that is
|
|
// unknown or has already been rotated away.
|
|
ErrNoSuchToken = errors.New("no such token")
|
|
|
|
// ErrNoSuchAccountData is returned for an account data type the user
|
|
// has never set.
|
|
ErrNoSuchAccountData = errors.New("no such account data")
|
|
)
|
|
|
|
// Account is a local user.
|
|
//
|
|
// Only the localpart is stored: the server name is fixed for the lifetime of
|
|
// a deployment — it is baked into every ID — so repeating it on every row
|
|
// would buy nothing. Virtual hosting, if it ever happens, is a migration.
|
|
//
|
|
// Timestamps throughout the user store are milliseconds since the Unix
|
|
// epoch, the unit the Client-Server API uses on the wire. Nothing needs
|
|
// converting on the way in or out, and both SQL drivers hold the same value.
|
|
type Account struct {
|
|
Localpart string
|
|
DisplayName string
|
|
AvatarURL string
|
|
Admin bool
|
|
Deactivated bool
|
|
CreatedAtMS int64
|
|
}
|
|
|
|
// NewAccount describes an account to create.
|
|
type NewAccount struct {
|
|
Localpart string
|
|
|
|
// PasswordHash is an encoded argon2id hash, or empty for an account
|
|
// that cannot log in with a password.
|
|
PasswordHash string
|
|
|
|
DisplayName string
|
|
Admin bool
|
|
CreatedAtMS int64
|
|
}
|
|
|
|
// Device is one login session's device record.
|
|
type Device struct {
|
|
Localpart string
|
|
ID string
|
|
DisplayName string
|
|
CreatedAtMS int64
|
|
|
|
LastSeenMS int64
|
|
LastSeenIP string
|
|
UserAgent string
|
|
}
|
|
|
|
// NewSession is a device plus the tokens issued for it: one login or one
|
|
// registration. The store writes all of it in a single transaction, so a
|
|
// client never sees a device without a working token, or the reverse.
|
|
//
|
|
// Tokens are stored as hashes. They are bearer credentials that by default
|
|
// never expire; hashing costs one SHA-256 per request and keeps a leaked
|
|
// database dump from handing over live sessions.
|
|
type NewSession struct {
|
|
Localpart string
|
|
DeviceID string
|
|
DisplayName string
|
|
CreatedAtMS int64
|
|
|
|
AccessTokenHash string
|
|
|
|
// AccessTokenExpiresAtMS is 0 for a token that never expires.
|
|
AccessTokenExpiresAtMS int64
|
|
|
|
// RefreshTokenHash is empty when the client did not ask for one.
|
|
RefreshTokenHash string
|
|
RefreshTokenExpiresAtMS int64
|
|
|
|
LastSeenIP string
|
|
UserAgent string
|
|
}
|
|
|
|
// RefreshedSession rotates the token pair of an existing device.
|
|
type RefreshedSession struct {
|
|
// RefreshTokenHash identifies the token being spent.
|
|
RefreshTokenHash string
|
|
|
|
NewAccessTokenHash string
|
|
AccessTokenExpiresAtMS int64
|
|
NewRefreshTokenHash string
|
|
RefreshTokenExpiresAtMS int64
|
|
RefreshedAtMS int64
|
|
}
|
|
|
|
// TokenInfo is everything the auth middleware needs from an access token, in
|
|
// one query: who owns it, on which device, and whether the account may still
|
|
// be used.
|
|
type TokenInfo struct {
|
|
Localpart string
|
|
DeviceID string
|
|
Admin bool
|
|
Deactivated bool
|
|
|
|
// ExpiresAtMS is 0 for a token that never expires.
|
|
ExpiresAtMS int64
|
|
}
|
|
|
|
// AccountFilter narrows and pages the admin user list.
|
|
type AccountFilter struct {
|
|
// Search matches a substring of the localpart or the display name,
|
|
// case-insensitively. Empty matches everything.
|
|
Search string
|
|
|
|
// Limit caps the returned rows; 0 means the store's own default.
|
|
Limit int
|
|
|
|
// Offset skips rows, for paging.
|
|
Offset int
|
|
}
|
|
|
|
// Stats are the counts the admin dashboard shows.
|
|
type Stats struct {
|
|
Users int
|
|
Admins int
|
|
Deactivated int
|
|
Devices int
|
|
}
|
|
|
|
// UserStore persists accounts, devices, tokens and account data: the
|
|
// storage half of the userapi component.
|
|
//
|
|
// Every method takes a localpart the caller has already normalised
|
|
// (lower-cased and validated); the store does no interpretation of its own.
|
|
type UserStore interface {
|
|
// CreateAccount inserts an account, returning ErrUserExists when the
|
|
// localpart is taken.
|
|
CreateAccount(ctx context.Context, acc NewAccount) (Account, error)
|
|
|
|
// Account reads one account, returning ErrNoSuchUser if it is absent.
|
|
Account(ctx context.Context, localpart string) (Account, error)
|
|
|
|
// PasswordHash returns the stored argon2id hash, which is empty for an
|
|
// account without a password.
|
|
PasswordHash(ctx context.Context, localpart string) (string, error)
|
|
|
|
// SetPasswordHash replaces the password hash.
|
|
SetPasswordHash(ctx context.Context, localpart, hash string) error
|
|
|
|
// SetAdmin grants or revokes the admin flag.
|
|
SetAdmin(ctx context.Context, localpart string, admin bool) error
|
|
|
|
// SetDeactivated sets or clears the deactivated flag.
|
|
SetDeactivated(ctx context.Context, localpart string, deactivated bool) error
|
|
|
|
// SetDisplayName updates the profile display name.
|
|
SetDisplayName(ctx context.Context, localpart, displayName string) error
|
|
|
|
// SetAvatarURL updates the profile avatar.
|
|
SetAvatarURL(ctx context.Context, localpart, avatarURL string) error
|
|
|
|
// ListAccounts returns a page of accounts ordered by localpart, plus
|
|
// the total number of accounts matching the filter.
|
|
ListAccounts(ctx context.Context, filter AccountFilter) ([]Account, int, error)
|
|
|
|
// Stats returns the aggregate counts for the admin dashboard.
|
|
Stats(ctx context.Context) (Stats, error)
|
|
|
|
// CreateSession creates or replaces a device and issues its tokens.
|
|
// Re-using an existing device ID revokes the tokens that device had,
|
|
// which is what the spec requires of a login naming a device already
|
|
// in use.
|
|
CreateSession(ctx context.Context, s NewSession) (Device, error)
|
|
|
|
// RefreshSession spends a refresh token and issues a new pair,
|
|
// returning the owner of the device. It returns ErrNoSuchToken if the
|
|
// token is unknown or has already been spent.
|
|
RefreshSession(ctx context.Context, r RefreshedSession) (TokenInfo, error)
|
|
|
|
// TokenInfo resolves an access token hash to its owner, returning
|
|
// ErrNoSuchToken when there is no such token. An expired token is
|
|
// returned with its expiry rather than rejected here, so the caller can
|
|
// answer with the soft logout the spec asks for.
|
|
TokenInfo(ctx context.Context, tokenHash string) (TokenInfo, error)
|
|
|
|
// TouchDevice records the last-seen timestamp, address and user agent
|
|
// of a device. It runs on authenticated requests, so it has to stay a
|
|
// single cheap statement.
|
|
TouchDevice(ctx context.Context, localpart, deviceID, ip, userAgent string, seenAtMS int64) error
|
|
|
|
// Device reads one device, returning ErrNoSuchDevice if it is absent.
|
|
Device(ctx context.Context, localpart, deviceID string) (Device, error)
|
|
|
|
// Devices lists a user's devices, ordered by device ID.
|
|
Devices(ctx context.Context, localpart string) ([]Device, error)
|
|
|
|
// SetDeviceDisplayName renames a device, returning ErrNoSuchDevice if
|
|
// it is absent.
|
|
SetDeviceDisplayName(ctx context.Context, localpart, deviceID, displayName string) error
|
|
|
|
// DeleteDevices removes devices and every token issued to them,
|
|
// returning how many existed. Unknown device IDs are ignored.
|
|
DeleteDevices(ctx context.Context, localpart string, deviceIDs []string) (int, error)
|
|
|
|
// DeleteAllDevices removes every device of a user and their tokens.
|
|
DeleteAllDevices(ctx context.Context, localpart string) (int, error)
|
|
|
|
// AccountData reads one account data blob; roomID is empty for global
|
|
// account data. It returns ErrNoSuchAccountData when unset.
|
|
AccountData(ctx context.Context, localpart, roomID, dataType string) ([]byte, error)
|
|
|
|
// SetAccountData writes one account data blob, overwriting any previous
|
|
// value.
|
|
SetAccountData(ctx context.Context, localpart, roomID, dataType string, content []byte) error
|
|
}
|