Raw
  1package admin
  2
  3import (
  4	"context"
  5	"errors"
  6	"strings"
  7
  8	"tug/internal/service/password"
  9	"tug/internal/storage"
 10)
 11
 12func (s *Service) ListUsers(ctx context.Context, query string, page int) (UserList, error) {
 13	users, err := s.store.ListUsers(ctx)
 14	if err != nil {
 15		return UserList{}, failed("list users", err)
 16	}
 17	needle := strings.ToLower(strings.TrimSpace(query))
 18	matched := make([]storage.User, 0, len(users))
 19	for _, u := range users {
 20		if matches(u, needle) {
 21			matched = append(matched, u)
 22		}
 23	}
 24	shown := window(matched, page, UsersPerPage)
 25	ids := make([]string, len(shown))
 26	for i, u := range shown {
 27		ids[i] = u.ID
 28	}
 29	groups, err := s.store.GroupsForUsers(ctx, ids)
 30	if err != nil {
 31		return UserList{}, failed("list users' groups", err)
 32	}
 33	primary, err := s.primary(ctx)
 34	if err != nil {
 35		return UserList{}, err
 36	}
 37	pending, err := s.pending(ctx)
 38	if err != nil {
 39		return UserList{}, err
 40	}
 41	rows := make([]UserRow, 0, len(shown))
 42	for _, u := range shown {
 43		badges := refs(groups[u.ID], primary)
 44		names := make([]string, len(badges))
 45		for i, b := range badges {
 46			names[i] = b.Name
 47		}
 48		rows = append(rows, UserRow{
 49			ID:          u.ID,
 50			Username:    u.Username,
 51			DisplayName: u.DisplayName,
 52			Disabled:    u.Disabled,
 53			Pending:     isPendingIn(pending, u.ID),
 54			Groups:      names,
 55			GroupCount:  len(badges),
 56		})
 57	}
 58	return UserList{Users: rows, Total: len(matched)}, nil
 59}
 60
 61func matches(u storage.User, needle string) bool {
 62	if needle == "" {
 63		return true
 64	}
 65	for _, field := range []string{u.Username, u.DisplayName} {
 66		if strings.Contains(strings.ToLower(field), needle) {
 67			return true
 68		}
 69	}
 70	return false
 71}
 72
 73func (s *Service) User(ctx context.Context, id string) (User, error) {
 74	u, err := s.store.UserByID(ctx, id)
 75	if err != nil {
 76		return User{}, target("read user", err)
 77	}
 78	groups, err := s.store.GroupsForUser(ctx, u.ID)
 79	if err != nil {
 80		return User{}, failed("read user's groups", err)
 81	}
 82	primary, err := s.primary(ctx)
 83	if err != nil {
 84		return User{}, err
 85	}
 86	var google *GoogleLink
 87	link, err := s.store.GoogleLinkByUser(ctx, u.ID)
 88	switch {
 89	case err == nil:
 90		google = &GoogleLink{LinkedAt: link.LinkedAt}
 91	case !errors.Is(err, storage.ErrNotFound):
 92		return User{}, failed("read user's Google link", err)
 93	}
 94	live, err := s.store.LiveForwardSessionsByUser(ctx, u.ID, s.now())
 95	if err != nil {
 96		return User{}, failed("read user's sessions", err)
 97	}
 98	activity, err := s.activity(ctx, u.ID)
 99	if err != nil {
100		return User{}, err
101	}
102	pending, err := s.pending(ctx)
103	if err != nil {
104		return User{}, err
105	}
106	keys, err := s.sshKeys(ctx, u.ID)
107	if err != nil {
108		return User{}, err
109	}
110	badges := refs(groups, primary)
111	isAdmin := false
112	for _, g := range groups {
113		isAdmin = isAdmin || g.Name == s.adminGroup
114	}
115	return User{
116		ID:          u.ID,
117		Username:    u.Username,
118		DisplayName: u.DisplayName,
119		UIDNumber:   u.UIDNumber,
120		Disabled:    u.Disabled,
121		Pending:     isPendingIn(pending, u.ID),
122		Admin:       isAdmin,
123		CreatedAt:   u.CreatedAt,
124		LastLoginAt: u.LastLoginAt,
125		Groups:      badges,
126		GroupCount:  len(badges),
127		Google:      google,
128		Sessions:    len(live),
129		Activity:    activity,
130		SSHKeys:     keys,
131	}, nil
132}
133
134func (s *Service) CreateUser(ctx context.Context, actor string, u NewUser) (string, error) {
135	username, err := storage.NormalizeUsername(u.Username)
136	if err != nil {
137		return "", ProblemUsernameGrammar
138	}
139	if storage.CheckDisplayName(u.DisplayName) != nil {
140		return "", ProblemDisplayNameInvalid
141	}
142	defaults, _ := s.configuredGroups()
143	created, err := s.store.CreateUserAudited(ctx, storage.CreateUserInput{
144		Username:     username,
145		DisplayName:  u.DisplayName,
146		PasswordHash: password.Unusable(),
147	}, defaults, actor)
148	switch {
149	case errors.Is(err, storage.ErrUsernameTaken):
150		return "", ProblemUsernameTaken
151	case err != nil:
152		return "", failed("create user", err)
153	}
154	if err := s.invites.Sweep(ctx); err != nil {
155		s.logger.Error("bootstrap invitation not swept after a user was created; the next start removes it", "username", created.Username, "err", err)
156	}
157	return created.ID, nil
158}
159
160func (s *Service) UpdateProfile(ctx context.Context, actor, id string, p Profile) error {
161	if storage.CheckDisplayName(p.DisplayName) != nil {
162		return ProblemDisplayNameInvalid
163	}
164	_, err := s.store.UpdateProfileAudited(ctx, id, p.DisplayName, actor)
165	return userWrite("update profile", err)
166}
167
168func (s *Service) DisableUser(ctx context.Context, actor, id string) error {
169	if actor == id {
170		return ProblemSelf
171	}
172	_, err := s.store.SetUserDisabledAudited(ctx, id, true, actor, s.adminGroup)
173	return userWrite("disable user", err)
174}
175
176func (s *Service) EnableUser(ctx context.Context, actor, id string) error {
177	_, err := s.store.SetUserDisabledAudited(ctx, id, false, actor, s.adminGroup)
178	return userWrite("enable user", err)
179}
180
181func (s *Service) RevokeAllSessions(ctx context.Context, actor, id string) error {
182	_, err := s.store.RevokeUserCredentialsAudited(ctx, id, "", storage.AppendAuditInput{Actor: actor, Action: "session.revoke_all", Subject: id})
183	return userWrite("revoke all sessions", err)
184}
185
186func (s *Service) DeleteUser(ctx context.Context, actor, id string) error {
187	if actor == id {
188		return ProblemSelf
189	}
190	_, err := s.store.DeleteUserAudited(ctx, id, actor, s.adminGroup)
191	return userWrite("delete user", err)
192}
193
194func userWrite(operation string, err error) error {
195	switch {
196	case err == nil:
197		return nil
198	case errors.Is(err, storage.ErrLastAdmin):
199		return ProblemLastAdmin
200	case errors.Is(err, storage.ErrUserPending):
201		return ProblemUserPending
202	}
203	return target(operation, err)
204}