Files
kopia/internal/user/user_manager.go
Julio López 09b88d3860 chore(general): minor cleanups and other nits (#4507)
* use uint8 for clarity
* unexport writeContentAsyncAndVerify
* fix typo in test function name
* remove commented interface functions
* use atomic.Int32
* cleanups in socket server activation test
* leverage stdlib's maps and slices packages
  replace uses of `golang.org/x/exp/maps`
* nit: leverage `maps.Values`
2025-04-16 23:25:01 -07:00

185 lines
5.1 KiB
Go

// Package user provides management of user accounts.
package user
import (
"context"
"maps"
"regexp"
"slices"
"strings"
"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")
// ErrUserAlreadyExists indicates that a user already exist in the system when attempting to create a new one.
var ErrUserAlreadyExists = errors.New("user already exists")
// 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) {
users, err := LoadProfileMap(ctx, rep, nil)
if err != nil {
return nil, err
}
profs := slices.SortedFunc(maps.Values(users), func(p1, p2 *Profile) int {
return strings.Compare(p1.Username, p2.Username)
})
return profs, nil
}
// GetUserProfile returns the user profile with a given username.
// Returns ErrUserNotFound when the user does not exist.
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
}
// GetNewProfile returns a profile for a new user with the given username.
// Returns ErrUserAlreadyExists when the user already exists.
func GetNewProfile(ctx context.Context, r repo.Repository, username string) (*Profile, error) {
if err := ValidateUsername(username); err != nil {
return nil, err
}
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(ErrUserAlreadyExists, username)
}
return &Profile{
Username: username,
PasswordHashVersion: defaultPasswordHashVersion,
},
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.New("username is required")
}
if !validUsernameRegexp.MatchString(name) {
return errors.New("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
}
id, err := w.ReplaceManifests(ctx, map[string]string{
manifest.TypeLabelKey: ManifestType,
UsernameAtHostnameLabel: p.Username,
}, p)
if err != nil {
return errors.Wrap(err, "error looking for user profile")
}
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.New("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
}