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}