Files
kopia/internal/user/user_manager.go
Jarek Kowalski cbcd59f18e Added repository user authorization support + server flag refactoring + refresh (#890)
* nit: replaced harcoded string constants with named constants

* acl: added management of ACL entries

* auth: implemented DefaultAuthorizer which uses ACLs if any entries are found in the system and falls back to LegacyAuthorizer if not

* cli: switch to DefaultAuthorizer when starting server

* cli: added ACL management

* server: refactored authenticator + added refresh

Authenticator is now an interface which also supports Refresh.

* authz: refactored authorizer to be an interface + added Refresh()

* server: refresh authentication and authorizer

* e2e tests for ACLs

* server: handling of SIGHUP to refresh authn/authz caches

* server: reorganized flags to specify auth options:

- removed '--allow-repository-users' - it's always on
- one of --without-password, --server-password or --random-password
  can be specified to specify password for the UI user
- htpasswd-file - can be specified to provide password for UI or remote
  users

* cli: moved 'kopia user' to 'kopia server user'

* server: allow all UI actions if no authenticator is set

* acl: removed priority until we have a better understood use case for it

* acl: added validation of allowed labels when adding ACL entries

* site: added docs for ACLs
2021-03-18 23:03:27 -07:00

173 lines
4.6 KiB
Go

// Package user provides management of user accounts.
package user
import (
"context"
"regexp"
"sort"
"github.com/pkg/errors"
"github.com/kopia/kopia/repo"
"github.com/kopia/kopia/repo/manifest"
)
// ManifestType is the type of the manifest used to represent user accounts.
const ManifestType = "user"
// UsernameAtHostnameLabel is the manifest label identifying users by username@hostname.
const UsernameAtHostnameLabel = "username"
// ErrUserNotFound is returned to indicate that a user was not found in the system.
var ErrUserNotFound = errors.New("user not found")
// LoadProfileMap returns the map of all users profiles in the repository by username, using old map as a cache.
func LoadProfileMap(ctx context.Context, rep repo.Repository, old map[string]*Profile) (map[string]*Profile, error) {
if rep == nil {
return nil, nil
}
entries, err := rep.FindManifests(ctx, map[string]string{manifest.TypeLabelKey: ManifestType})
if err != nil {
return nil, errors.Wrap(err, "error listing user manifests")
}
result := map[string]*Profile{}
for _, m := range manifest.DedupeEntryMetadataByLabel(entries, UsernameAtHostnameLabel) {
user := m.Labels[UsernameAtHostnameLabel]
// same user info as before
if o := old[user]; o != nil && o.ManifestID == m.ID {
result[user] = o
continue
}
p := &Profile{}
if _, err := rep.GetManifest(ctx, m.ID, p); err != nil {
return nil, errors.Wrapf(err, "error loading user manifest %v", user)
}
p.ManifestID = m.ID
result[user] = p
}
return result, nil
}
// ListUserProfiles gets the list of all user profiles in the system.
func ListUserProfiles(ctx context.Context, rep repo.Repository) ([]*Profile, error) {
var result []*Profile
users, err := LoadProfileMap(ctx, rep, nil)
if err != nil {
return nil, err
}
for _, v := range users {
result = append(result, v)
}
sort.Slice(result, func(i, j int) bool {
return result[i].Username < result[j].Username
})
return result, nil
}
// GetUserProfile returns the user profile with a given username.
func GetUserProfile(ctx context.Context, r repo.Repository, username string) (*Profile, error) {
manifests, err := r.FindManifests(ctx, map[string]string{
manifest.TypeLabelKey: ManifestType,
UsernameAtHostnameLabel: username,
})
if err != nil {
return nil, errors.Wrap(err, "error looking for user profile")
}
if len(manifests) == 0 {
return nil, errors.Wrap(ErrUserNotFound, username)
}
p := &Profile{}
if _, err := r.GetManifest(ctx, manifest.PickLatestID(manifests), p); err != nil {
return nil, errors.Wrap(err, "error loading user profile")
}
return p, nil
}
// validUsernameRegexp matches username@hostname where both username and hostname consist of
// lowercase letters, digits or dashes, underscores or period characters.
var validUsernameRegexp = regexp.MustCompile(`^[a-z0-9\-_.]+@[a-z0-9\-_.]+$`)
// ValidateUsername returns an error if the given username is invalid.
func ValidateUsername(name string) error {
if name == "" {
return errors.Errorf("username is required")
}
if !validUsernameRegexp.MatchString(name) {
return errors.Errorf("username must be specified as lowercase 'user@hostname'")
}
return nil
}
// SetUserProfile creates or updates user profile.
func SetUserProfile(ctx context.Context, w repo.RepositoryWriter, p *Profile) error {
if err := ValidateUsername(p.Username); err != nil {
return err
}
manifests, err := w.FindManifests(ctx, map[string]string{
manifest.TypeLabelKey: ManifestType,
UsernameAtHostnameLabel: p.Username,
})
if err != nil {
return errors.Wrap(err, "error looking for user profile")
}
id, err := w.PutManifest(ctx, map[string]string{
manifest.TypeLabelKey: ManifestType,
UsernameAtHostnameLabel: p.Username,
}, p)
if err != nil {
return errors.Wrap(err, "error writing user profile")
}
for _, m := range manifests {
if err := w.DeleteManifest(ctx, m.ID); err != nil {
return errors.Wrapf(err, "error deleting user profile %v", p.Username)
}
}
p.ManifestID = id
return nil
}
// DeleteUserProfile removes user profile with a given username.
func DeleteUserProfile(ctx context.Context, w repo.RepositoryWriter, username string) error {
if username == "" {
return errors.Errorf("username is required")
}
manifests, err := w.FindManifests(ctx, map[string]string{
manifest.TypeLabelKey: ManifestType,
UsernameAtHostnameLabel: username,
})
if err != nil {
return errors.Wrap(err, "error looking for user profile")
}
for _, m := range manifests {
if err := w.DeleteManifest(ctx, m.ID); err != nil {
return errors.Wrapf(err, "error deleting user profile %v", username)
}
}
return nil
}