1package admin
2
3import (
4 "context"
5 "errors"
6 "slices"
7
8 "tug/internal/storage"
9)
10
11func (s *Service) Memberships(ctx context.Context, userID string, page int) (Memberships, error) {
12 u, err := s.store.UserByID(ctx, userID)
13 if err != nil {
14 return Memberships{}, target("read user", err)
15 }
16 groups, err := s.store.GroupsForUser(ctx, u.ID)
17 if err != nil {
18 return Memberships{}, failed("read user's groups", err)
19 }
20 primary, err := s.primary(ctx)
21 if err != nil {
22 return Memberships{}, err
23 }
24 candidates, err := s.candidates(ctx, groups)
25 if err != nil {
26 return Memberships{}, err
27 }
28 badges := refs(groups, primary)
29 return Memberships{Username: u.Username, Groups: window(badges, page, MembersPerPage), Total: len(badges), Candidates: candidates}, nil
30}
31
32func (s *Service) AddMembership(ctx context.Context, actor, userID, groupName string) error {
33 if _, err := s.store.UserByID(ctx, userID); err != nil {
34 return target("read user", err)
35 }
36 group, err := s.store.GroupByName(ctx, groupName)
37 switch {
38 case errors.Is(err, storage.ErrNotFound), errors.Is(err, storage.ErrInvalid):
39 return ProblemUnknownGroup
40 case err != nil:
41 return failed("read group", err)
42 }
43 return s.add(ctx, actor, userID, group)
44}
45
46func (s *Service) AddMember(ctx context.Context, actor, groupID, username string) error {
47 group, err := s.store.GroupByID(ctx, groupID)
48 if err != nil {
49 return target("read group", err)
50 }
51 if group.Name == s.primaryGroup {
52 return ProblemPrimaryGroup
53 }
54 u, err := s.store.UserByUsername(ctx, username)
55 switch {
56 case errors.Is(err, storage.ErrNotFound), errors.Is(err, storage.ErrInvalid):
57 return ProblemUnknownUser
58 case err != nil:
59 return failed("read user", err)
60 }
61 return s.add(ctx, actor, u.ID, group)
62}
63
64func (s *Service) add(ctx context.Context, actor, userID string, group storage.Group) error {
65 if group.Name == s.primaryGroup {
66 return ProblemPrimaryGroup
67 }
68 err := s.store.AddMembershipAudited(ctx, userID, group.ID, actor)
69 if errors.Is(err, storage.ErrConflict) {
70 return ProblemAlreadyMember
71 }
72 if err != nil {
73 return target("add membership", err)
74 }
75 return nil
76}
77
78func (s *Service) RemoveMembership(ctx context.Context, actor, userID, groupID string) error {
79 group, err := s.store.GroupByID(ctx, groupID)
80 if err != nil {
81 return target("read group", err)
82 }
83 if _, err := s.store.UserByID(ctx, userID); err != nil {
84 return target("read user", err)
85 }
86 if group.Name == s.primaryGroup {
87 return ProblemPrimaryGroup
88 }
89 if actor == userID && group.Name == s.adminGroup {
90 return ProblemSelf
91 }
92 err = s.store.RemoveMembershipAudited(ctx, userID, group.ID, actor, s.adminGroup)
93 switch {
94 case err == nil:
95 return nil
96 case errors.Is(err, storage.ErrLastAdmin):
97 return ProblemLastAdmin
98 case errors.Is(err, storage.ErrNotFound):
99 return ProblemNotMember
100 }
101 return failed("remove membership", err)
102}
103
104func (s *Service) ListGroups(ctx context.Context, page int) (GroupList, error) {
105 groups, err := s.store.ListGroups(ctx)
106 if err != nil {
107 return GroupList{}, failed("list groups", err)
108 }
109 shown := window(groups, page, GroupsPerPage)
110 rows := make([]GroupRow, 0, len(shown))
111 for _, g := range shown {
112 members, err := s.members(ctx, g)
113 if err != nil {
114 return GroupList{}, err
115 }
116 enabled, _ := split(members)
117 rows = append(rows, GroupRow{ID: g.ID, Name: g.Name, Description: g.Description, Members: len(enabled)})
118 }
119 return GroupList{Groups: rows, Total: len(groups)}, nil
120}
121
122func (s *Service) Group(ctx context.Context, id string, page int) (Group, error) {
123 g, err := s.store.GroupByID(ctx, id)
124 if err != nil {
125 return Group{}, target("read group", err)
126 }
127 users, err := s.members(ctx, g)
128 if err != nil {
129 return Group{}, err
130 }
131 enabled, disabled := split(users)
132 pending, err := s.pending(ctx)
133 if err != nil {
134 return Group{}, err
135 }
136 _, referenced := s.configuredGroups()
137 return Group{
138 ID: g.ID,
139 Name: g.Name,
140 Description: g.Description,
141 GIDNumber: g.GIDNumber,
142 Primary: g.Name == s.primaryGroup,
143 Referenced: slices.Contains(referenced, g.Name),
144 Members: membersOf(window(enabled, page, MembersPerPage)),
145 Total: len(enabled),
146 Disabled: markPending(membersOf(disabled[:min(len(disabled), DisabledShown)]), pending),
147 DisabledTotal: len(disabled),
148 }, nil
149}
150
151func (s *Service) members(ctx context.Context, g storage.Group) ([]storage.User, error) {
152 var users []storage.User
153 var err error
154 if g.Name == s.primaryGroup {
155 users, err = s.store.ListUsers(ctx)
156 } else {
157 users, err = s.store.UsersInGroup(ctx, g.ID)
158 }
159 if err != nil {
160 return nil, failed("read group members", err)
161 }
162 return users, nil
163}
164
165func (s *Service) CreateGroup(ctx context.Context, actor string, g NewGroup) (string, error) {
166 name, err := storage.NormalizeGroupName(g.Name)
167 if err != nil {
168 return "", ProblemGroupNameGrammar
169 }
170 if storage.CheckDisplayName(g.Description) != nil {
171 return "", ProblemDescriptionInvalid
172 }
173 created, err := s.store.CreateGroupAudited(ctx, storage.CreateGroupInput{Name: name, Description: g.Description}, actor)
174 switch {
175 case errors.Is(err, storage.ErrGroupTaken):
176 return "", ProblemGroupNameTaken
177 case err != nil:
178 return "", failed("create group", err)
179 }
180 return created.ID, nil
181}
182
183func (s *Service) DescribeGroup(ctx context.Context, actor, id, description string) error {
184 if storage.CheckDisplayName(description) != nil {
185 return ProblemDescriptionInvalid
186 }
187 if _, err := s.store.UpdateGroupDescriptionAudited(ctx, id, description, actor); err != nil {
188 return target("describe group", err)
189 }
190 return nil
191}
192
193func (s *Service) DeleteGroup(ctx context.Context, actor, id string) error {
194 _, referenced := s.configuredGroups()
195 err := s.store.DeleteGroupAudited(ctx, id, actor, referenced)
196 switch {
197 case err == nil:
198 return nil
199 case errors.Is(err, storage.ErrReferencedGroup):
200 return ProblemReferencedGroup
201 }
202 return target("delete group", err)
203}