1package adminui
2
3import (
4 "context"
5 "errors"
6
7 "tug/internal/service/admin"
8 "tug/internal/transport/web"
9)
10
11type RefreshCounter interface {
12 RefreshTokenCount(ctx context.Context, userID string) (int, error)
13}
14
15type Backend struct {
16 service *admin.Service
17 refresh RefreshCounter
18}
19
20var _ web.AdminBackend = Backend{}
21
22func New(service *admin.Service, refresh RefreshCounter) Backend {
23 if service == nil || refresh == nil {
24 panic("adminui: the backend needs the admin service and a refresh-token counter")
25 }
26 return Backend{service: service, refresh: refresh}
27}
28
29var problems = map[admin.Problem]web.AdminProblem{
30 admin.ProblemUsernameGrammar: web.AdminUsernameGrammar,
31 admin.ProblemUsernameTaken: web.AdminUsernameTaken,
32 admin.ProblemDisplayNameInvalid: web.AdminDisplayNameInvalid,
33 admin.ProblemGroupNameGrammar: web.AdminGroupNameGrammar,
34 admin.ProblemGroupNameTaken: web.AdminGroupNameTaken,
35 admin.ProblemDescriptionInvalid: web.AdminDescriptionInvalid,
36 admin.ProblemUnknownUser: web.AdminUnknownUser,
37 admin.ProblemUnknownGroup: web.AdminUnknownGroup,
38 admin.ProblemAlreadyMember: web.AdminAlreadyMember,
39 admin.ProblemNotMember: web.AdminNotMember,
40 admin.ProblemPrimaryGroup: web.AdminPrimaryGroup,
41 admin.ProblemSelf: web.AdminSelf,
42 admin.ProblemLastAdmin: web.AdminLastAdmin,
43 admin.ProblemReferencedGroup: web.AdminReferencedGroup,
44 admin.ProblemInvitationClosed: web.AdminInvitationClosed,
45 admin.ProblemGoogleNotLinked: web.AdminGoogleNotLinked,
46 admin.ProblemResetLinkDisabled: web.AdminResetLinkDisabled,
47 admin.ProblemResetLinkPending: web.AdminResetLinkPending,
48 admin.ProblemLabelInvalid: web.AdminLabelInvalid,
49 admin.ProblemExpiryInvalid: web.AdminExpiryInvalid,
50 admin.ProblemAdminGroupGrant: web.AdminAdminGroupGrant,
51 admin.ProblemBatchSize: web.AdminBatchSize,
52 admin.ProblemUserPending: web.AdminUserPending,
53 admin.ProblemUnknownBatch: web.AdminUnknownBatch,
54 admin.ProblemSSHKeyDisabled: web.AdminSSHKeyDisabled,
55 admin.ProblemSSHKeyPending: web.AdminSSHKeyPending,
56}
57
58var states = map[admin.InvitationState]web.AdminInvitationState{
59 admin.InvitationPending: web.AdminInvitationPending,
60 admin.InvitationUsed: web.AdminInvitationUsed,
61 admin.InvitationRevoked: web.AdminInvitationRevoked,
62 admin.InvitationExpired: web.AdminInvitationExpired,
63}
64
65func mapError(err error) error {
66 if err == nil {
67 return nil
68 }
69 if errors.Is(err, admin.ErrNotFound) {
70 return web.ErrAdminNotFound
71 }
72 var p admin.Problem
73 if errors.As(err, &p) {
74 if q, ok := problems[p]; ok {
75 return q
76 }
77 }
78 return err
79}
80
81func audit(entries []admin.AuditEntry) []web.AdminAuditEntry {
82 out := make([]web.AdminAuditEntry, 0, len(entries))
83 for _, e := range entries {
84 out = append(out, web.AdminAuditEntry(e))
85 }
86 return out
87}
88
89func groupRefs(refs []admin.GroupRef) []web.AdminGroupRef {
90 out := make([]web.AdminGroupRef, 0, len(refs))
91 for _, g := range refs {
92 out = append(out, web.AdminGroupRef(g))
93 }
94 return out
95}
96
97func invitation(i admin.Invitation) (web.AdminInvitation, error) {
98 state, ok := states[i.State]
99 if !ok {
100 return web.AdminInvitation{}, errors.New("adminui: unknown invitation state")
101 }
102 out := web.AdminInvitation{ID: i.ID, CreatedBy: i.CreatedBy, CreatedAt: i.CreatedAt, ExpiresAt: i.ExpiresAt, State: state, Label: i.Label, UsedBy: i.UsedBy, UsedByID: i.UsedByID, UsedByPending: i.UsedByPending, UsedByDeleted: i.State == admin.InvitationUsed && i.UsedBy == "", Batch: i.Batch}
103 for _, g := range i.Groups {
104 out.Groups = append(out.Groups, web.AdminInvitationGroup{Name: g.Name, Deleted: g.Deleted})
105 }
106 return out, nil
107}
108
109func (b Backend) Dashboard(ctx context.Context) (web.AdminDashboard, error) {
110 d, err := b.service.Dashboard(ctx)
111 if err != nil {
112 return web.AdminDashboard{}, mapError(err)
113 }
114 return web.AdminDashboard{
115 Users: d.Users,
116 DisabledUsers: d.DisabledUsers,
117 Groups: d.Groups,
118 PendingInvitations: d.PendingInvitations,
119 LiveSessions: d.LiveSessions,
120 Recent: audit(d.Recent),
121 }, nil
122}
123
124func (b Backend) ListUsers(ctx context.Context, query string, page int) (web.AdminUserList, error) {
125 l, err := b.service.ListUsers(ctx, query, page)
126 if err != nil {
127 return web.AdminUserList{}, mapError(err)
128 }
129 users := make([]web.AdminUserRow, 0, len(l.Users))
130 for _, u := range l.Users {
131 users = append(users, web.AdminUserRow(u))
132 }
133 return web.AdminUserList{Users: users, Total: l.Total}, nil
134}
135
136func (b Backend) User(ctx context.Context, id string) (web.AdminUser, error) {
137 u, err := b.service.User(ctx, id)
138 if err != nil {
139 return web.AdminUser{}, mapError(err)
140 }
141 tokens, err := b.refresh.RefreshTokenCount(ctx, u.ID)
142 if err != nil {
143 return web.AdminUser{}, err
144 }
145 out := web.AdminUser{
146 ID: u.ID,
147 Username: u.Username,
148 DisplayName: u.DisplayName,
149 UIDNumber: u.UIDNumber,
150 Disabled: u.Disabled,
151 Pending: u.Pending,
152 Admin: u.Admin,
153 CreatedAt: u.CreatedAt,
154 LastLoginAt: u.LastLoginAt,
155 Groups: groupRefs(u.Groups),
156 GroupCount: u.GroupCount,
157 Sessions: u.Sessions,
158 RefreshTokens: tokens,
159 Activity: audit(u.Activity),
160 SSHKeys: sshKeys(u.SSHKeys),
161 }
162 if u.Google != nil {
163 out.Google = &web.AdminGoogleLink{LinkedAt: u.Google.LinkedAt}
164 }
165 return out, nil
166}
167
168func (b Backend) CreateUser(ctx context.Context, actor string, u web.AdminNewUser) (string, error) {
169 id, err := b.service.CreateUser(ctx, actor, admin.NewUser{Username: u.Username, DisplayName: u.DisplayName})
170 return id, mapError(err)
171}
172
173func (b Backend) UpdateProfile(ctx context.Context, actor, id string, p web.AdminProfile) error {
174 return mapError(b.service.UpdateProfile(ctx, actor, id, admin.Profile{DisplayName: p.DisplayName}))
175}
176
177func (b Backend) DisableUser(ctx context.Context, actor, id string) error {
178 return mapError(b.service.DisableUser(ctx, actor, id))
179}
180
181func (b Backend) EnableUser(ctx context.Context, actor, id string) error {
182 return mapError(b.service.EnableUser(ctx, actor, id))
183}
184
185func (b Backend) RevokeAllSessions(ctx context.Context, actor, id string) error {
186 return mapError(b.service.RevokeAllSessions(ctx, actor, id))
187}
188
189func (b Backend) DeleteUser(ctx context.Context, actor, id string) error {
190 return mapError(b.service.DeleteUser(ctx, actor, id))
191}
192
193func (b Backend) Memberships(ctx context.Context, userID string, page int) (web.AdminMemberships, error) {
194 m, err := b.service.Memberships(ctx, userID, page)
195 if err != nil {
196 return web.AdminMemberships{}, mapError(err)
197 }
198 return web.AdminMemberships{Username: m.Username, Groups: groupRefs(m.Groups), Total: m.Total, Candidates: m.Candidates}, nil
199}
200
201func (b Backend) AddMembership(ctx context.Context, actor, userID, groupName string) error {
202 return mapError(b.service.AddMembership(ctx, actor, userID, groupName))
203}
204
205func (b Backend) AddMember(ctx context.Context, actor, groupID, username string) error {
206 return mapError(b.service.AddMember(ctx, actor, groupID, username))
207}
208
209func (b Backend) RemoveMembership(ctx context.Context, actor, userID, groupID string) error {
210 return mapError(b.service.RemoveMembership(ctx, actor, userID, groupID))
211}
212
213func (b Backend) ListGroups(ctx context.Context, page int) (web.AdminGroupList, error) {
214 l, err := b.service.ListGroups(ctx, page)
215 if err != nil {
216 return web.AdminGroupList{}, mapError(err)
217 }
218 groups := make([]web.AdminGroupRow, 0, len(l.Groups))
219 for _, g := range l.Groups {
220 groups = append(groups, web.AdminGroupRow(g))
221 }
222 return web.AdminGroupList{Groups: groups, Total: l.Total}, nil
223}
224
225func (b Backend) Group(ctx context.Context, id string, page int) (web.AdminGroup, error) {
226 g, err := b.service.Group(ctx, id, page)
227 if err != nil {
228 return web.AdminGroup{}, mapError(err)
229 }
230 return web.AdminGroup{
231 ID: g.ID,
232 Name: g.Name,
233 Description: g.Description,
234 GIDNumber: g.GIDNumber,
235 Primary: g.Primary,
236 Referenced: g.Referenced,
237 Members: adminMembers(g.Members),
238 Total: g.Total,
239 Disabled: adminMembers(g.Disabled),
240 DisabledTotal: g.DisabledTotal,
241 }, nil
242}
243
244func (b Backend) CreateGroup(ctx context.Context, actor string, g web.AdminNewGroup) (string, error) {
245 id, err := b.service.CreateGroup(ctx, actor, admin.NewGroup(g))
246 return id, mapError(err)
247}
248
249func (b Backend) DescribeGroup(ctx context.Context, actor, id, description string) error {
250 return mapError(b.service.DescribeGroup(ctx, actor, id, description))
251}
252
253func (b Backend) DeleteGroup(ctx context.Context, actor, id string) error {
254 return mapError(b.service.DeleteGroup(ctx, actor, id))
255}
256
257func (b Backend) ListInvitations(ctx context.Context, page int) (web.AdminInvitationList, error) {
258 l, err := b.service.ListInvitations(ctx, page)
259 if err != nil {
260 return web.AdminInvitationList{}, mapError(err)
261 }
262 out := make([]web.AdminInvitation, 0, len(l.Invitations))
263 for _, i := range l.Invitations {
264 v, err := invitation(i)
265 if err != nil {
266 return web.AdminInvitationList{}, err
267 }
268 out = append(out, v)
269 }
270 return web.AdminInvitationList{Invitations: out, Total: l.Total}, nil
271}
272
273func (b Backend) Invitation(ctx context.Context, id string) (web.AdminInvitation, error) {
274 i, err := b.service.Invitation(ctx, id)
275 if err != nil {
276 return web.AdminInvitation{}, mapError(err)
277 }
278 return invitation(i)
279}
280
281func (b Backend) CreateInvitations(ctx context.Context, actor string, n web.AdminNewInvitations) (web.AdminCreatedInvitations, error) {
282 created, err := b.service.CreateInvitations(ctx, actor, admin.NewInvitations{Labels: n.Labels, GroupIDs: n.GroupIDs, GroupNames: n.GroupNames, Expiry: n.Expiry})
283 if err != nil {
284 return web.AdminCreatedInvitations{}, mapError(err)
285 }
286 out := web.AdminCreatedInvitations{ExpiresAt: created.ExpiresAt, Groups: created.Groups}
287 for _, i := range created.Invitations {
288 out.Invitations = append(out.Invitations, web.AdminIssuedInvitation{Label: i.Label, URL: i.URL})
289 }
290 return out, nil
291}
292
293func (b Backend) InvitationGroupChoices(ctx context.Context) (web.AdminGroupChoices, error) {
294 c, err := b.service.InvitationGroupChoices(ctx)
295 if err != nil {
296 return web.AdminGroupChoices{}, mapError(err)
297 }
298 out := web.AdminGroupChoices{More: c.More, Defaults: c.Defaults}
299 for _, g := range c.Groups {
300 out.Groups = append(out.Groups, web.AdminGroupChoice{ID: g.ID, Name: g.Name})
301 }
302 return out, nil
303}
304
305func (b Backend) RevokeInvitation(ctx context.Context, actor, id string) error {
306 return mapError(b.service.RevokeInvitation(ctx, actor, id))
307}
308
309func (b Backend) RevokeInvitationBatch(ctx context.Context, actor, batch string) error {
310 _, err := b.service.RevokeInvitationBatch(ctx, actor, batch)
311 return mapError(err)
312}
313
314func adminMembers(in []admin.Member) []web.AdminMember {
315 out := make([]web.AdminMember, 0, len(in))
316 for _, m := range in {
317 out = append(out, web.AdminMember(m))
318 }
319 return out
320}
321
322func (b Backend) GroupCandidates(ctx context.Context, groupID, query string) ([]web.AdminMember, error) {
323 found, err := b.service.GroupCandidates(ctx, groupID, query)
324 if err != nil {
325 return nil, mapError(err)
326 }
327 return adminMembers(found), nil
328}