package accountserver import ( "context" "encoding/json" "fmt" "strings" "time" "git.autistici.org/ai3/go-common/pwhash" ) // Possible values for user status. const ( UserStatusActive = "active" UserStatusInactive = "inactive" ) // The password hashing algorithm to use when updating credentials. var DefaultPasswordHash pwhash.PasswordHash func init() { DefaultPasswordHash = pwhash.DefaultEncryptAlgorithm } // User information, public: includes data *about* credentials, but // not the credentials themselves. Every user has a unique // identifier, which may be an email address. type User struct { // Name of the user. Also its email. Name string `json:"name"` // Preferred language. Lang string `json:"lang"` // UNIX user id. UID int `json:"uid"` // Timestamp of last password change. This is serialized as a // RFC3339 string in JSON. LastPasswordChangeStamp time.Time `json:"last_password_change_stamp"` // User status. Status string `json:"status"` // Shard for temporary resources (must match the email resources). Shard string `json:"shard"` // Has2FA is true if the user has a second-factor authentication // mechanism properly set up. In practice, this is the case if either // HasOTP is true, or len(U2FRegistrations) > 0. Has2FA bool `json:"has_2fa"` // HasOTP is true if TOTP is set up. HasOTP bool `json:"has_otp"` // HasEncryptionKeys is true if encryption keys are properly set up for // this user. HasEncryptionKeys bool `json:"has_encryption_keys"` // The recovery hint for this account (empty if unset). AccountRecoveryHint string `json:"account_recovery_hint"` // List of application-specific passwords (metadata only). AppSpecificPasswords []*AppSpecificPasswordInfo `json:"app_specific_passwords,omitempty"` // List of U2F registrations. U2FRegistrations []*U2FRegistration `json:"u2f_registrations,omitempty"` // All the resources owned by this user. Resources []*Resource `json:"resources,omitempty"` } // GetResourceByID returns the resource with the specified ID, or nil // if not found. func (u *User) GetResourceByID(id ResourceID) *Resource { for _, r := range u.Resources { if r.ID.Equal(id) { return r } } return nil } // GetResourcesByType returns all resources with the specified type. func (u *User) GetResourcesByType(resourceType string) []*Resource { var out []*Resource for _, r := range u.Resources { if r.Type == resourceType { out = append(out, r) } } return out } // GetSingleResourceByType returns a single resource of the specified // type. If there are none, returns nil. func (u *User) GetSingleResourceByType(resourceType string) *Resource { for _, r := range u.Resources { if r.Type == resourceType { return r } } return nil } // GetResourcesByGroup returns all resources belonging to the specified group. func (u *User) GetResourcesByGroup(group string) []*Resource { var out []*Resource for _, r := range u.Resources { if r.Group == group { out = append(out, r) } } return out } // AllEmailAddrs is a convenience function that returns all // (non-inactive) email addresses for this User. func (u *User) AllEmailAddrs() []string { var addrs []string for _, r := range u.Resources { if r.Type == ResourceTypeEmail && r.Status != ResourceStatusInactive { addrs = append(addrs, r.Name) if r.Email != nil && len(r.Email.Aliases) > 0 { addrs = append(addrs, r.Email.Aliases...) } } } return addrs } // RawUser extends User with private information (as stored in the // database) that we have a direct use for. // // Its methods manipulate authentication-related data and enforce its // consistency, so they may have side effects such as maintaining // encryption keys up to date, or disabling secondary authentication // mechanisms. In any case both the database and the underlying User // object are kept in sync. // // The separation between User and RawUser makes it easier to prevent // private data from being served over the API. type RawUser struct { User // Password for local authentication of privileged actions // (these are encrypted!). Password string RecoveryPassword string // Encryption keys need to change whenever there is a change // in authentication parameters, so keep them around. Keys encryptedKeyList } // Disable 2FA for a user account. func (u *RawUser) disable2FA(ctx context.Context, tx TX) error { // Disable OTP. if u.HasOTP { if err := tx.DeleteUserTOTPSecret(ctx, &u.User); err != nil { return newBackendError(err) } u.HasOTP = false } // Disable U2F. if len(u.U2FRegistrations) > 0 { u.U2FRegistrations = nil if err := tx.UpdateUser(ctx, &u.User); err != nil { return newBackendError(err) } } u.Has2FA = false return u.deleteAllApplicationSpecificPasswords(ctx, tx) } // Disable OTP. func (u *RawUser) disableOTP(ctx context.Context, tx TX) error { if u.HasOTP { if err := tx.DeleteUserTOTPSecret(ctx, &u.User); err != nil { return newBackendError(err) } u.HasOTP = false } return u.check2FAState(ctx, tx) } // Enable OTP with the specified secret. Overwrites the current one, if set. func (u *RawUser) setTOTPSecret(ctx context.Context, tx TX, totpSecret string) error { if err := tx.SetUserTOTPSecret(ctx, &u.User, totpSecret); err != nil { return err } u.HasOTP = true u.Has2FA = true return nil } // Update the list of U2F registrations for the user. The list may be empty. func (u *RawUser) setU2FRegistrations(ctx context.Context, tx TX, regs []*U2FRegistration) error { u.U2FRegistrations = regs if err := tx.UpdateUser(ctx, &u.User); err != nil { return err } return u.check2FAState(ctx, tx) } // Whenever one of OTP or U2F is modified, we'd like to check if it was the // last 2FA method available: in that case, 2FA has been disabled and we also // want to clear all application-specific passwords. func (u *RawUser) check2FAState(ctx context.Context, tx TX) error { if u.HasOTP || len(u.U2FRegistrations) > 0 { u.Has2FA = true return nil } // 2FA has been disabled, so drop all app-specific passwords along with it. u.Has2FA = false return u.deleteAllApplicationSpecificPasswords(ctx, tx) } // Set the primary password for the user. When encryption keys are present, // requires a valid unlockPassword. func (u *RawUser) setPrimaryPassword(ctx context.Context, tx TX, unlockPassword, password string, enableOpportunisticEncryption bool) error { if u.HasEncryptionKeys { l, err := u.Keys.add(UserEncryptionKeyMainID, unlockPassword, password) if err != nil { return err } u.Keys = l if err := tx.SetUserEncryptionKeys(ctx, &u.User, u.Keys); err != nil { return err } } else if enableOpportunisticEncryption { if err := u.initEncryption(ctx, tx, password); err != nil { return err } } enc := DefaultPasswordHash.Encrypt(password) u.Password = enc return tx.SetUserPassword(ctx, &u.User, enc) } // Set the password recovery hint for the user. When encryption keys are // present, requires a valid unlockPassword. func (u *RawUser) setAccountRecoveryHint(ctx context.Context, tx TX, unlockPassword, hint, response string) error { if u.HasEncryptionKeys { l, err := u.Keys.add(UserEncryptionKeyRecoveryID, unlockPassword, response) if err != nil { return err } u.Keys = l if err := tx.SetUserEncryptionKeys(ctx, &u.User, u.Keys); err != nil { return err } } enc := DefaultPasswordHash.Encrypt(response) u.RecoveryPassword = enc return tx.SetAccountRecoveryHint(ctx, &u.User, hint, enc) } // Initialize encryption keys for this user, given the primary authentication // password. If encryption keys are already present, they are discarded. All // secondary authentication tokens are cleared. func (u *RawUser) initEncryption(ctx context.Context, tx TX, password string) error { // Disable all secondary credentials, as we only have the // primary password to initialize encryption so all other // credentials would not be able to unlock the keys. for _, asp := range u.AppSpecificPasswords { if err := tx.DeleteApplicationSpecificPassword(ctx, &u.User, asp.ID); err != nil { return err } } if u.AccountRecoveryHint != "" { if err := tx.DeleteAccountRecoveryHint(ctx, &u.User); err != nil { return err } u.AccountRecoveryHint = "" } // Initialize a new key storage with the given primary password. pub, keys, err := newEncryptionKeys(password) if err != nil { return err } if err := tx.SetUserEncryptionPublicKey(ctx, &u.User, pub); err != nil { return err } u.Keys = keys u.HasEncryptionKeys = true return tx.SetUserEncryptionKeys(ctx, &u.User, keys) } // Reset the primary password for the user. When encryption keys are present, // this will disable all other secondary authentication mechanisms (including // recovery), as keys would be unreadable otherwise. func (u *RawUser) resetPassword(ctx context.Context, tx TX, password string) error { // If a user has associated encryption keys, we need to // disable all secondary authentication credentials as we are // going to wipe the existing keys clean. if u.HasEncryptionKeys { if err := u.initEncryption(ctx, tx, password); err != nil { return err } } enc := DefaultPasswordHash.Encrypt(password) return tx.SetUserPassword(ctx, &u.User, enc) } // Add a new application-specific password. func (u *RawUser) addApplicationSpecificPassword(ctx context.Context, tx TX, unlockPassword, password string, asp *AppSpecificPasswordInfo) error { if u.HasEncryptionKeys { l, err := u.Keys.add(aspKeyID(asp.ID), unlockPassword, password) if err != nil { return err } u.Keys = l if err := tx.SetUserEncryptionKeys(ctx, &u.User, u.Keys); err != nil { return err } } enc := DefaultPasswordHash.Encrypt(password) return tx.SetApplicationSpecificPassword(ctx, &u.User, asp, enc) } // Delete an existing application-specific password. func (u *RawUser) deleteApplicationSpecificPassword(ctx context.Context, tx TX, aspID string) error { if u.HasEncryptionKeys { u.Keys = u.Keys.deleteByID(aspKeyID(aspID)) if err := tx.SetUserEncryptionKeys(ctx, &u.User, u.Keys); err != nil { return err } } return tx.DeleteApplicationSpecificPassword(ctx, &u.User, aspID) } // Wipe all app-specific passwords and their associated encryption keys. func (u *RawUser) deleteAllApplicationSpecificPasswords(ctx context.Context, tx TX) error { for _, asp := range u.AppSpecificPasswords { if err := tx.DeleteApplicationSpecificPassword(ctx, &u.User, asp.ID); err != nil { return err } if u.HasEncryptionKeys { u.Keys = u.Keys.deleteByID(aspKeyID(asp.ID)) } } u.AppSpecificPasswords = nil if u.HasEncryptionKeys { if err := tx.SetUserEncryptionKeys(ctx, &u.User, u.Keys); err != nil { return err } } return nil } // AppSpecificPasswordInfo stores public information about an // app-specific password. type AppSpecificPasswordInfo struct { ID string `json:"id"` Service string `json:"service"` Comment string `json:"comment"` } // Well-known user encryption key types, corresponding to primary and // secondary passwords. const ( UserEncryptionKeyMainID = "main" UserEncryptionKeyRecoveryID = "recovery" ) // Resource types. const ( ResourceTypeEmail = "email" ResourceTypeMailingList = "list" ResourceTypeWebsite = "web" ResourceTypeDomain = "domain" ResourceTypeDAV = "dav" ResourceTypeDatabase = "db" ) // Resource status values. const ( ResourceStatusActive = "active" ResourceStatusInactive = "inactive" ResourceStatusReadonly = "readonly" ResourceStatusArchived = "archived" ) // Returns true if the given status is valid for the given resource type. func isValidStatusByResourceType(rtype, rstatus string) bool { switch rtype { case ResourceTypeEmail, ResourceTypeMailingList: switch rstatus { case ResourceStatusActive, ResourceStatusInactive, ResourceStatusReadonly: return true } case ResourceTypeWebsite, ResourceTypeDomain, ResourceTypeDAV, ResourceTypeDatabase: switch rstatus { case ResourceStatusActive, ResourceStatusInactive, ResourceStatusReadonly, ResourceStatusArchived: return true } } return false } // ResourceID is an opaque ID that uniquely identifies a resource in // the backend database. These should normally not be visible to users. type ResourceID string // Equal returns true if the two IDs are the same. func (i ResourceID) Equal(other ResourceID) bool { return i == other } // Empty returns true if the ResourceID has the nil value. func (i ResourceID) Empty() bool { return i == "" } func (i ResourceID) String() string { return string(i) } // MarshalJSON serializes a resource ID to JSON. func (i ResourceID) MarshalJSON() ([]byte, error) { return json.Marshal(string(i)) } // UnmarshalJSON deserializes a resource ID from JSON. func (i *ResourceID) UnmarshalJSON(data []byte) error { var s string err := json.Unmarshal(data, &s) if err != nil { return err } *i = ResourceID(s) return nil } // ParseResourceID parses a string representation of a ResourceID. func ParseResourceID(s string) (ResourceID, error) { return ResourceID(s), nil } // Resource represents a somewhat arbitrary resource, identified by a // unique name/type combination (a.k.a. its ID). A resource contains // some common properties related to sharding and state, plus // type-specific attributes. type Resource struct { // ID is a unique primary key in the resources space, with a // path-like representation. It must make sense to the // database backend and be reversible (i.e. there must be a // bidirectional mapping between database objects and resource // IDs). ID ResourceID `json:"id"` // Resource type. Type string `json:"type"` // Name of the resource, used for display purposes. Name string `json:"name"` // Optional attribute for hierarchical resources. ParentID ResourceID `json:"parent_id,omitempty"` // Optional attribute for resources that have a status. Status string `json:"status,omitempty"` // Optional attributes for sharded resources. Shard string `json:"shard,omitempty"` OriginalShard string `json:"original_shard,omitempty"` // Resources can be 'grouped' together, for various reasons // (display purposes, service integrity). All resources in the // same group should have the same Shard. Group names can be // arbitrary strings. Group string `json:"group,omitempty"` // Details about the specific type (only one of these can be // set, depending on the value of 'type'). Email *Email `json:"email,omitempty"` List *MailingList `json:"list,omitempty"` Website *Website `json:"website,omitempty"` DAV *WebDAV `json:"dav,omitempty"` Database *Database `json:"database,omitempty"` } // Copy the resource (makes a deep copy). func (r *Resource) Copy() *Resource { rr := *r switch { case r.Email != nil: e := *r.Email rr.Email = &e case r.Website != nil: w := *r.Website rr.Website = &w case r.List != nil: l := *r.List rr.List = &l case r.DAV != nil: d := *r.DAV rr.DAV = &d case r.Database != nil: d := *r.Database rr.Database = &d } return &rr } // String returns a short handle for the resource, useful for debugging. func (r *Resource) String() string { s := fmt.Sprintf("%s/%s", r.Type, r.Name) if !r.ID.Empty() { s += fmt.Sprintf("(%s)", r.ID.String()) } return s } // A RawResource associates a Resource with its (optional) owner. type RawResource struct { Resource Owner string `json:"owner"` } // Email resource attributes. type Email struct { Aliases []string `json:"aliases,omitempty"` Maildir string `json:"maildir"` QuotaLimit int `json:"quota_limit"` QuotaUsage int `json:"quota_usage"` } // MailingList resource attributes. type MailingList struct { Admins []string `json:"admins"` Public bool `json:"public"` } // WebDAV represents a hosting account. type WebDAV struct { UID int `json:"uid"` Homedir string `json:"homedir"` } // Website resource attributes. type Website struct { URL string `json:"url,omitempty"` UID int `json:"uid"` ParentDomain string `json:"parent_domain,omitempty"` AcceptMail bool `json:"accept_mail"` Options []string `json:"options,omitempty"` Categories []string `json:"categories,omitempty"` Description map[string]string `json:"description,omitempty"` QuotaUsage int `json:"quota_usage"` DocumentRoot string `json:"document_root"` CMSInfo map[string]*CMSInfo `json:"cms_info,omitempty"` VulnerabilityInfo map[string]*VulnInfo `json:"vulnerability_info,omitempty"` } // Database resource attributes. type Database struct { DBUser string `json:"db_user"` } // CMSInfo holds CMS-specific information. type CMSInfo struct { Name string `json:"name"` Version string `json:"version"` Status string `json:"status"` } // VulnInfo stores information about vulnerabilities detected by our // automated scanners. type VulnInfo struct { Name string `json:"name"` Path string `json:"path"` DetectedAt time.Time `json:"detected_at"` } // Blog resource attributes. type Blog struct { Name string `json:"name"` URL string `json:"url"` } const hardcodedWebRoot = "/home/users/investici.org" // Group web-related resources into groups. // // This is a very specific function meant to address a peculiar characteristic // of the A/I legacy data model, where DAV accounts and websites do not have an // explicit relation. // // TODO: Ideally we should be able to do this without hard-coding the webroot. func (u *User) groupWebResources() { // Set the group name to be the 'hostingDir' for sites and DAV // accounts. Keep a reference of websites by ID so we can later fix the // group for databases too, via their ParentID. webs := make(map[string]*Resource) for _, r := range u.Resources { switch r.Type { case ResourceTypeWebsite, ResourceTypeDomain: r.Group = getHostingDir(r.Website.DocumentRoot, hardcodedWebRoot) webs[r.ID.String()] = r case ResourceTypeDAV: r.Group = getHostingDir(r.DAV.Homedir, hardcodedWebRoot) } } // Fix databases in a second pass. for _, r := range u.Resources { if r.Type == ResourceTypeDatabase && !r.ParentID.Empty() { r.Group = webs[r.ParentID.String()].Group } } } // The hosting directory for a website is the path component immediately after // siteRoot. This works also for sites with nested documentRoots. func getHostingDir(path, siteRoot string) string { path = strings.TrimPrefix(strings.TrimPrefix(path, siteRoot), "/") if i := strings.Index(path, "/"); i > 0 { return path[:i] } return path } // U2FRegistration stores information on a single U2F device registration. // // This mirrors closely compositetypes.U2FRegistration, with the very // important difference that the data here is base64-encoded! Can't // reliably push arbitrary binary data through JSON otherwise. type U2FRegistration struct { KeyHandle string `json:"key_handle"` PublicKey string `json:"public_key"` }