mirror of
https://github.com/kopia/kopia.git
synced 2026-01-28 00:08:04 -05:00
* 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
173 lines
4.6 KiB
Go
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
|
|
}
|