// Package store holds the server's authoritative metadata: users, nested // groups, databases and the grants that connect them. // // Glossary: // // User an account, authenticated by password+TOTP or by OIDC. // Group a node in a single tree of groups. Grants hang off groups. // Membership a user's place in one group, as member or admin. // Database one KDBX file tracked by the server. Owned by one group. // Grant a (group, database, permission) triple. // Invite a single- or multi-use registration token. // Session a bearer token held by a client device. package store import "time" // Perm is a permission on a database, ordered read < write < admin. type Perm string const ( PermNone Perm = "" PermRead Perm = "read" PermWrite Perm = "write" PermAdmin Perm = "admin" ) var permRank = map[Perm]int{PermNone: 0, PermRead: 1, PermWrite: 2, PermAdmin: 3} // Rank orders permissions so the strongest of several grants can be picked. func (p Perm) Rank() int { return permRank[p] } // AtLeast reports whether p is as strong as want. func (p Perm) AtLeast(want Perm) bool { return p.Rank() >= want.Rank() } // Valid reports whether p is a permission that may be stored. func (p Perm) Valid() bool { return p == PermRead || p == PermWrite || p == PermAdmin } // Role is a user's role inside a group. type Role string const ( RoleMember Role = "member" RoleAdmin Role = "admin" ) // Valid reports whether r is a role that may be stored. func (r Role) Valid() bool { return r == RoleMember || r == RoleAdmin } // Strategy decides what happens when a client pushes a database version that // is no longer current. type Strategy string const ( // StrategyServerWins rejects the stale push; the client takes the server // copy and keeps its own as a conflict file. This is the default. StrategyServerWins Strategy = "server-wins" // StrategyClientWins lets the client overwrite the server copy. StrategyClientWins Strategy = "client-wins" // StrategyManual rejects the stale push and leaves both copies in place // for a human to reconcile. StrategyManual Strategy = "manual" ) // Valid reports whether s is a strategy that may be stored. func (s Strategy) Valid() bool { return s == StrategyServerWins || s == StrategyClientWins || s == StrategyManual } type User struct { ID string `json:"id"` Username string `json:"username"` Email string `json:"email,omitempty"` PassHash string `json:"pass_hash,omitempty"` OIDCIssuer string `json:"oidc_issuer,omitempty"` OIDCSubject string `json:"oidc_subject,omitempty"` TOTPSecret string `json:"totp_secret,omitempty"` TOTPConfirmed bool `json:"totp_confirmed"` ServerAdmin bool `json:"server_admin"` Disabled bool `json:"disabled"` CreatedAt time.Time `json:"created_at"` } type Group struct { ID string `json:"id"` Name string `json:"name"` ParentID string `json:"parent_id,omitempty"` CreatedAt time.Time `json:"created_at"` } type Membership struct { UserID string `json:"user_id"` GroupID string `json:"group_id"` Role Role `json:"role"` } type Database struct { ID string `json:"id"` Name string `json:"name"` OwnerGroupID string `json:"owner_group_id"` MergeStrategy Strategy `json:"merge_strategy"` Version int64 `json:"version"` SHA256 string `json:"sha256"` Size int64 `json:"size"` UpdatedAt time.Time `json:"updated_at"` UpdatedBy string `json:"updated_by,omitempty"` } type Grant struct { GroupID string `json:"group_id"` DatabaseID string `json:"database_id"` Perm Perm `json:"perm"` } type Invite struct { Token string `json:"token"` GroupID string `json:"group_id,omitempty"` Role Role `json:"role"` CreatedBy string `json:"created_by"` CreatedAt time.Time `json:"created_at"` ExpiresAt time.Time `json:"expires_at"` MaxUses int `json:"max_uses"` Uses int `json:"uses"` } // Usable reports whether the invite may still be redeemed at time now. func (i *Invite) Usable(now time.Time) bool { return now.Before(i.ExpiresAt) && (i.MaxUses <= 0 || i.Uses < i.MaxUses) } type Session struct { Token string `json:"token"` UserID string `json:"user_id"` Label string `json:"label,omitempty"` CreatedAt time.Time `json:"created_at"` ExpiresAt time.Time `json:"expires_at"` } // DeviceAuth backs the browser-assisted login used for OIDC on desktop and // Android: the client shows a short user code, the browser approves it, the // client polls until a session token appears. type DeviceAuth struct { DeviceCode string `json:"device_code"` UserCode string `json:"user_code"` Label string `json:"label,omitempty"` SessionToken string `json:"session_token,omitempty"` OIDCState string `json:"oidc_state,omitempty"` CreatedAt time.Time `json:"created_at"` ExpiresAt time.Time `json:"expires_at"` } // State is the whole metadata set, serialized as one JSON document. type State struct { Users map[string]*User `json:"users"` Groups map[string]*Group `json:"groups"` Databases map[string]*Database `json:"databases"` Invites map[string]*Invite `json:"invites"` Sessions map[string]*Session `json:"sessions"` Devices map[string]*DeviceAuth `json:"devices"` Memberships []Membership `json:"memberships"` Grants []Grant `json:"grants"` } func newState() *State { return &State{ Users: map[string]*User{}, Groups: map[string]*Group{}, Databases: map[string]*Database{}, Invites: map[string]*Invite{}, Sessions: map[string]*Session{}, Devices: map[string]*DeviceAuth{}, } } func (s *State) init() { if s.Users == nil { s.Users = map[string]*User{} } if s.Groups == nil { s.Groups = map[string]*Group{} } if s.Databases == nil { s.Databases = map[string]*Database{} } if s.Invites == nil { s.Invites = map[string]*Invite{} } if s.Sessions == nil { s.Sessions = map[string]*Session{} } if s.Devices == nil { s.Devices = map[string]*DeviceAuth{} } } // UserByName finds a user by exact username. func (s *State) UserByName(name string) *User { for _, u := range s.Users { if u.Username == name { return u } } return nil } // UserByOIDC finds a user previously linked to an OIDC issuer and subject. func (s *State) UserByOIDC(issuer, subject string) *User { for _, u := range s.Users { if u.OIDCIssuer == issuer && u.OIDCSubject == subject { return u } } return nil } // DeviceByUserCode finds a pending device login by the code shown to the user. func (s *State) DeviceByUserCode(code string) *DeviceAuth { for _, d := range s.Devices { if d.UserCode == code { return d } } return nil }