package admin import ( "context" "errors" "strings" "tug/internal/service/password" "tug/internal/storage" ) func (s *Service) ListUsers(ctx context.Context, query string, page int) (UserList, error) { users, err := s.store.ListUsers(ctx) if err != nil { return UserList{}, failed("list users", err) } needle := strings.ToLower(strings.TrimSpace(query)) matched := make([]storage.User, 0, len(users)) for _, u := range users { if matches(u, needle) { matched = append(matched, u) } } shown := window(matched, page, UsersPerPage) ids := make([]string, len(shown)) for i, u := range shown { ids[i] = u.ID } groups, err := s.store.GroupsForUsers(ctx, ids) if err != nil { return UserList{}, failed("list users' groups", err) } primary, err := s.primary(ctx) if err != nil { return UserList{}, err } pending, err := s.pending(ctx) if err != nil { return UserList{}, err } rows := make([]UserRow, 0, len(shown)) for _, u := range shown { badges := refs(groups[u.ID], primary) names := make([]string, len(badges)) for i, b := range badges { names[i] = b.Name } rows = append(rows, UserRow{ ID: u.ID, Username: u.Username, DisplayName: u.DisplayName, Disabled: u.Disabled, Pending: isPendingIn(pending, u.ID), Groups: names, GroupCount: len(badges), }) } return UserList{Users: rows, Total: len(matched)}, nil } func matches(u storage.User, needle string) bool { if needle == "" { return true } for _, field := range []string{u.Username, u.DisplayName} { if strings.Contains(strings.ToLower(field), needle) { return true } } return false } func (s *Service) User(ctx context.Context, id string) (User, error) { u, err := s.store.UserByID(ctx, id) if err != nil { return User{}, target("read user", err) } groups, err := s.store.GroupsForUser(ctx, u.ID) if err != nil { return User{}, failed("read user's groups", err) } primary, err := s.primary(ctx) if err != nil { return User{}, err } var google *GoogleLink link, err := s.store.GoogleLinkByUser(ctx, u.ID) switch { case err == nil: google = &GoogleLink{LinkedAt: link.LinkedAt} case !errors.Is(err, storage.ErrNotFound): return User{}, failed("read user's Google link", err) } live, err := s.store.LiveForwardSessionsByUser(ctx, u.ID, s.now()) if err != nil { return User{}, failed("read user's sessions", err) } activity, err := s.activity(ctx, u.ID) if err != nil { return User{}, err } pending, err := s.pending(ctx) if err != nil { return User{}, err } keys, err := s.sshKeys(ctx, u.ID) if err != nil { return User{}, err } badges := refs(groups, primary) isAdmin := false for _, g := range groups { isAdmin = isAdmin || g.Name == s.adminGroup } return User{ ID: u.ID, Username: u.Username, DisplayName: u.DisplayName, UIDNumber: u.UIDNumber, Disabled: u.Disabled, Pending: isPendingIn(pending, u.ID), Admin: isAdmin, CreatedAt: u.CreatedAt, LastLoginAt: u.LastLoginAt, Groups: badges, GroupCount: len(badges), Google: google, Sessions: len(live), Activity: activity, SSHKeys: keys, }, nil } func (s *Service) CreateUser(ctx context.Context, actor string, u NewUser) (string, error) { username, err := storage.NormalizeUsername(u.Username) if err != nil { return "", ProblemUsernameGrammar } if storage.CheckDisplayName(u.DisplayName) != nil { return "", ProblemDisplayNameInvalid } defaults, _ := s.configuredGroups() created, err := s.store.CreateUserAudited(ctx, storage.CreateUserInput{ Username: username, DisplayName: u.DisplayName, PasswordHash: password.Unusable(), }, defaults, actor) switch { case errors.Is(err, storage.ErrUsernameTaken): return "", ProblemUsernameTaken case err != nil: return "", failed("create user", err) } if err := s.invites.Sweep(ctx); err != nil { s.logger.Error("bootstrap invitation not swept after a user was created; the next start removes it", "username", created.Username, "err", err) } return created.ID, nil } func (s *Service) UpdateProfile(ctx context.Context, actor, id string, p Profile) error { if storage.CheckDisplayName(p.DisplayName) != nil { return ProblemDisplayNameInvalid } _, err := s.store.UpdateProfileAudited(ctx, id, p.DisplayName, actor) return userWrite("update profile", err) } func (s *Service) DisableUser(ctx context.Context, actor, id string) error { if actor == id { return ProblemSelf } _, err := s.store.SetUserDisabledAudited(ctx, id, true, actor, s.adminGroup) return userWrite("disable user", err) } func (s *Service) EnableUser(ctx context.Context, actor, id string) error { _, err := s.store.SetUserDisabledAudited(ctx, id, false, actor, s.adminGroup) return userWrite("enable user", err) } func (s *Service) RevokeAllSessions(ctx context.Context, actor, id string) error { _, err := s.store.RevokeUserCredentialsAudited(ctx, id, "", storage.AppendAuditInput{Actor: actor, Action: "session.revoke_all", Subject: id}) return userWrite("revoke all sessions", err) } func (s *Service) DeleteUser(ctx context.Context, actor, id string) error { if actor == id { return ProblemSelf } _, err := s.store.DeleteUserAudited(ctx, id, actor, s.adminGroup) return userWrite("delete user", err) } func userWrite(operation string, err error) error { switch { case err == nil: return nil case errors.Is(err, storage.ErrLastAdmin): return ProblemLastAdmin case errors.Is(err, storage.ErrUserPending): return ProblemUserPending } return target(operation, err) }