• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

SpiNNakerManchester / JavaSpiNNaker / 6233274834

19 Sep 2023 08:46AM UTC coverage: 36.409% (-0.6%) from 36.982%
6233274834

Pull #658

github

dkfellows
Merge branch 'master' into java-17
Pull Request #658: Update Java version to 17

1656 of 1656 new or added lines in 260 files covered. (100.0%)

8373 of 22997 relevant lines covered (36.41%)

0.36 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

10.22
/SpiNNaker-allocserv/src/main/java/uk/ac/manchester/spinnaker/alloc/admin/UserControl.java
1
/*
2
 * Copyright (c) 2021 The University of Manchester
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *     https://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16
package uk.ac.manchester.spinnaker.alloc.admin;
17

18
import static java.util.Objects.isNull;
19
import static java.util.Objects.nonNull;
20
import static java.util.function.Function.identity;
21
import static org.slf4j.LoggerFactory.getLogger;
22
import static uk.ac.manchester.spinnaker.alloc.db.Row.integer;
23

24
import java.net.URI;
25
import java.security.Principal;
26
import java.util.List;
27
import java.util.Map;
28
import java.util.Objects;
29
import java.util.Optional;
30
import java.util.TreeMap;
31
import java.util.function.Function;
32

33
import org.slf4j.Logger;
34
import org.springframework.beans.factory.annotation.Autowired;
35
import org.springframework.security.authentication.AuthenticationServiceException;
36
import org.springframework.security.authentication.BadCredentialsException;
37
import org.springframework.security.authentication.InternalAuthenticationServiceException;
38
import org.springframework.security.core.AuthenticationException;
39
import org.springframework.stereotype.Service;
40

41
import uk.ac.manchester.spinnaker.alloc.db.DatabaseAPI.Query;
42
import uk.ac.manchester.spinnaker.alloc.db.DatabaseAPI.Update;
43
import uk.ac.manchester.spinnaker.alloc.db.DatabaseAwareBean;
44
import uk.ac.manchester.spinnaker.alloc.db.Row;
45
import uk.ac.manchester.spinnaker.alloc.db.SQLQueries;
46
import uk.ac.manchester.spinnaker.alloc.model.GroupRecord;
47
import uk.ac.manchester.spinnaker.alloc.model.GroupRecord.GroupType;
48
import uk.ac.manchester.spinnaker.alloc.model.MemberRecord;
49
import uk.ac.manchester.spinnaker.alloc.model.PasswordChangeRecord;
50
import uk.ac.manchester.spinnaker.alloc.model.UserRecord;
51
import uk.ac.manchester.spinnaker.alloc.security.PasswordServices;
52
import uk.ac.manchester.spinnaker.alloc.security.TrustLevel;
53
import uk.ac.manchester.spinnaker.utils.UsedInJavadocOnly;
54

55
/**
56
 * User and group administration DAO.
57
 *
58
 * @author Donal Fellows
59
 */
60
@Service
61
public class UserControl extends DatabaseAwareBean {
1✔
62
        private static final Logger log = getLogger(UserControl.class);
1✔
63

64
        @Autowired
65
        private PasswordServices passServices;
66

67
        private final class AllUsersSQL extends AbstractSQL {
×
68
                private final Query allUsers = conn.query(LIST_ALL_USERS);
×
69

70
                private final Query allUsersOfType = conn.query(LIST_ALL_USERS_OF_TYPE);
×
71

72
                @Override
73
                public void close() {
74
                        allUsers.close();
×
75
                        super.close();
×
76
                }
×
77

78
                List<UserRecord> allUsers() {
79
                        return allUsers.call(UserControl::sketchUser);
×
80
                }
81

82
                List<UserRecord> allUsers(boolean internal) {
83
                        return allUsersOfType.call(UserControl::sketchUser, internal);
×
84
                }
85
        }
86

87
        @UsedInJavadocOnly(SQLQueries.class)
88
        private sealed class UserCheckSQL extends AbstractSQL
×
89
                        permits CreateSQL, DeleteUserSQL, UpdateAllSQL, UpdatePassSQL {
90
                private final Query userCheck = conn.query(GET_USER_ID);
×
91

92
                /** See {@link SQLQueries#GET_USER_DETAILS}. */
93
                private final Query getUserDetails = conn.query(GET_USER_DETAILS);
×
94

95
                private final Query getMembershipsOfUser =
×
96
                                conn.query(GET_MEMBERSHIPS_OF_USER);
×
97

98
                /** See {@link SQLQueries#GET_USER_DETAILS_BY_NAME}. */
99
                private final Query getUserDetailsByName =
×
100
                                conn.query(GET_USER_DETAILS_BY_NAME);
×
101

102
                @Override
103
                public void close() {
104
                        userCheck.close();
×
105
                        getUserDetails.close();
×
106
                        getMembershipsOfUser.close();
×
107
                        getUserDetails.close();
×
108
                        getUserDetailsByName.close();
×
109
                        super.close();
×
110
                }
×
111

112
                /**
113
                 * Get a user.
114
                 *
115
                 * @param id
116
                 *            User ID
117
                 * @return Database row, if user exists.
118
                 * @see SQLQueries#GET_USER_DETAILS
119
                 */
120
                Optional<UserRecord> getUser(int id) {
121
                        return getUserDetails.call1(UserControl::sketchUser, id);
×
122
                }
123

124
                Function<UserRecord, UserRecord>
125
                                populateMemberships(Function<MemberRecord, URI> urlGen) {
126
                        if (isNull(urlGen)) {
×
127
                                return identity();
×
128
                        }
129
                        return user -> {
×
130
                                user.setGroups(Row.stream(getMembershipsOfUser.call(
×
131
                                                UserControl::member, user.getUserId()))
×
132
                                                .toMap(TreeMap::new,
×
133
                                                                MemberRecord::getGroupName, urlGen));
134
                                return user;
×
135
                        };
136
                }
137
        }
138

139
        private final class CreateSQL extends UserCheckSQL {
×
140
                private final Update createUser = conn.update(CREATE_USER);
×
141

142
                @Override
143
                public void close() {
144
                        createUser.close();
×
145
                        super.close();
×
146
                }
×
147

148
                Optional<Integer> createUser(String name, String encPass,
149
                                TrustLevel trustLevel, boolean disabled, String openIdSubject) {
150
                        return createUser.key(name, encPass, trustLevel, disabled,
×
151
                                        openIdSubject);
152
                }
153
        }
154

155
        private final class UpdateAllSQL extends UserCheckSQL {
×
156
                private final Update setUserName = conn.update(SET_USER_NAME);
×
157

158
                private final Update setUserPass = conn.update(SET_USER_PASS);
×
159

160
                private final Update setUserDisabled = conn.update(SET_USER_DISABLED);
×
161

162
                private final Update setUserLocked = conn.update(SET_USER_LOCKED);
×
163

164
                private final Update setUserTrust = conn.update(SET_USER_TRUST);
×
165

166
                @Override
167
                public void close() {
168
                        setUserName.close();
×
169
                        setUserPass.close();
×
170
                        setUserDisabled.close();
×
171
                        setUserLocked.close();
×
172
                        setUserTrust.close();
×
173
                        super.close();
×
174
                }
×
175
        }
176

177
        private final class UpdatePassSQL extends UserCheckSQL {
×
178
                private final Query getPasswordedUser =
×
179
                                conn.query(GET_LOCAL_USER_DETAILS);
×
180

181
                private final Update setPassword = conn.update(SET_USER_PASS);
×
182

183
                @Override
184
                public void close() {
185
                        getPasswordedUser.close();
×
186
                        setPassword.close();
×
187
                        super.close();
×
188
                }
×
189
        }
190

191
        private final class DeleteUserSQL extends UserCheckSQL {
×
192
                private final Query getUserName = conn.query(GET_USER_DETAILS);
×
193

194
                private final Update deleteUser = conn.update(DELETE_USER);
×
195

196
                @Override
197
                public void close() {
198
                        getUserName.close();
×
199
                        deleteUser.close();
×
200
                        super.close();
×
201
                }
×
202
        }
203

204
        private final class GroupsSQL extends AbstractSQL {
1✔
205
                private final Query listGroups = conn.query(LIST_ALL_GROUPS);
1✔
206

207
                private final Query listGroupsOfType =
1✔
208
                                conn.query(LIST_ALL_GROUPS_OF_TYPE);
1✔
209

210
                private final Query getUsers = conn.query(GET_USERS_OF_GROUP);
1✔
211

212
                private final Query getGroupId = conn.query(GET_GROUP_BY_ID);
1✔
213

214
                private final Query getGroupName = conn.query(GET_GROUP_BY_NAME);
1✔
215

216
                private final Update insertGroup = conn.update(CREATE_GROUP);
1✔
217

218
                private final Update updateGroup = conn.update(UPDATE_GROUP);
1✔
219

220
                private final Update deleteGroup = conn.update(DELETE_GROUP);
1✔
221

222
                @Override
223
                public void close() {
224
                        listGroups.close();
1✔
225
                        getUsers.close();
1✔
226
                        getGroupId.close();
1✔
227
                        getGroupName.close();
1✔
228
                        insertGroup.close();
1✔
229
                        updateGroup.close();
1✔
230
                        deleteGroup.close();
1✔
231
                        super.close();
1✔
232
                }
1✔
233

234
                List<GroupRecord> listGroups() {
235
                        return listGroups.call(GroupRecord::new);
×
236
                }
237

238
                List<GroupRecord> listGroups(GroupType type) {
239
                        return listGroupsOfType.call(GroupRecord::new, type);
×
240
                }
241

242
                Optional<GroupRecord> getGroupId(int id) {
243
                        return getGroupId.call1(GroupRecord::new, id);
1✔
244
                }
245

246
                Optional<GroupRecord> getGroupName(String name) {
247
                        return getGroupName.call1(GroupRecord::new, name);
×
248
                }
249

250
                Optional<Integer> insertGroup(String name, Optional<Long> quota,
251
                                GroupType groupType) {
252
                        return insertGroup.key(name, quota, groupType);
1✔
253
                }
254

255
                public Optional<GroupRecord> updateGroup(int id, String name,
256
                                Optional<Long> quota) {
257
                        if (updateGroup.call(name, quota.orElse(null), id) == 0) {
×
258
                                return Optional.empty();
×
259
                        }
260
                        return getGroupId(id);
×
261
                }
262

263
                Optional<String> deleteGroup(int id) {
264
                        return getGroupId.call1(row -> {
×
265
                                // Order matters! Get the name before the delete
266
                                var groupName = row.getString("group_name");
×
267
                                return deleteGroup.call(id) == 1 ? groupName : null;
×
268
                        }, id);
×
269
                }
270

271
                Function<GroupRecord, GroupRecord>
272
                                populateMemberships(Function<MemberRecord, URI> urlGen) {
273
                        if (isNull(urlGen)) {
×
274
                                return identity();
×
275
                        }
276
                        return group -> {
×
277
                                group.setMembers(Row.stream(getUsers.call(
×
278
                                                UserControl::member, group.getGroupId()))
×
279
                                                .toMap(TreeMap::new,
×
280
                                                                MemberRecord::getUserName, urlGen));
281
                                return group;
×
282
                        };
283
                }
284
        }
285

286
        private static MemberRecord member(Row row) {
287
                var m = new MemberRecord();
×
288
                m.setId(row.getInt("membership_id"));
×
289
                m.setGroupId(row.getInt("group_id"));
×
290
                m.setGroupName(row.getString("group_name"));
×
291
                m.setUserId(row.getInt("user_id"));
×
292
                m.setUserName(row.getString("user_name"));
×
293
                return m;
×
294
        }
295

296
        /**
297
         * List the users in the database.
298
         *
299
         * @return List of users. Only {@link UserRecord#userId} and
300
         *         {@link UserRecord#userName} fields are inflated.
301
         */
302
        public List<UserRecord> listUsers() {
303
                try (var sql = new AllUsersSQL()) {
×
304
                        return sql.transactionRead(sql::allUsers);
×
305
                }
306
        }
307

308
        /**
309
         * List the users of a type in the database.
310
         *
311
         * @param internal
312
         *            Whether to get the internal users. If not, get the OpenID
313
         *            users.
314
         * @return List of users. Only {@link UserRecord#userId} and
315
         *         {@link UserRecord#userName} fields are inflated.
316
         */
317
        public List<UserRecord> listUsers(boolean internal) {
318
                try (var sql = new AllUsersSQL()) {
×
319
                        return sql.transactionRead(() -> sql.allUsers(internal));
×
320
                }
321
        }
322

323
        /**
324
         * List the users in the database.
325
         *
326
         * @param uriMapper
327
         *            How to construct a URL for the user.
328
         * @return Map of users to URLs.
329
         */
330
        public Map<String, URI> listUsers(Function<UserRecord, URI> uriMapper) {
331
                try (var sql = new AllUsersSQL()) {
×
332
                        return sql.transactionRead(
×
333
                                        () -> Row.stream(sql.allUsers()).toMap(
×
334
                                                        TreeMap::new, UserRecord::getUserName, uriMapper));
335
                }
336
        }
337

338
        /**
339
         * List the users of a type in the database.
340
         *
341
         * @param internal
342
         *            Whether to get the internal users. If not, get the OpenID
343
         *            users.
344
         * @param uriMapper
345
         *            How to construct a URL for the user.
346
         * @return Map of users to URLs.
347
         */
348
        public Map<String, URI> listUsers(boolean internal,
349
                        Function<UserRecord, URI> uriMapper) {
350
                try (var sql = new AllUsersSQL()) {
×
351
                        return sql.transactionRead(() -> Row.stream(sql.allUsers(internal))
×
352
                                        .toMap(TreeMap::new, UserRecord::getUserName, uriMapper));
×
353
                }
354
        }
355

356
        private static UserRecord sketchUser(Row row) {
357
                var userSketch = new UserRecord();
×
358
                userSketch.setUserId(row.getInt("user_id"));
×
359
                userSketch.setUserName(row.getString("user_name"));
×
360
                userSketch.setOpenIdSubject(row.getString("openid_subject"));
×
361
                return userSketch;
×
362
        }
363

364
        /**
365
         * Create a user.
366
         *
367
         * @param user
368
         *            The description of the user to create.
369
         * @return A description of the created user, or {@link Optional#empty()} if
370
         *         the user exists already.
371
         */
372
        public Optional<UserRecord> createUser(UserRecord user) {
373
                // This is a slow operation; don't hold a database transaction
374
                var encPass = passServices.encodePassword(user.getPassword());
×
375
                try (var sql = new CreateSQL()) {
×
376
                        return sql.transaction(() -> createUser(user, encPass, sql));
×
377
                }
378
        }
379

380
        private Optional<UserRecord> createUser(UserRecord user, String encPass,
381
                        CreateSQL sql) {
382
                return sql
×
383
                                .createUser(user.getUserName(), encPass, user.getTrustLevel(),
×
384
                                                !user.getEnabled(), user.getOpenIdSubject())
×
385
                                .flatMap(sql::getUser);
×
386
        }
387

388
        /**
389
         * Get a description of a user.
390
         *
391
         * @param id
392
         *            The ID of the user.
393
         * @param urlGen
394
         *            How to construct the URL for a group membership in the
395
         *            response. If {@code null}, the memberships will be omitted.
396
         * @return A description of the user, or {@link Optional#empty()} if the
397
         *         user doesn't exist.
398
         */
399
        public Optional<UserRecord> getUser(int id,
400
                        Function<MemberRecord, URI> urlGen) {
401
                try (var sql = new UserCheckSQL()) {
×
402
                        return sql.transaction(() -> getUser(id, urlGen, sql));
×
403
                }
404
        }
405

406
        private Optional<UserRecord> getUser(int id,
407
                        Function<MemberRecord, URI> urlGen, UserCheckSQL sql) {
408
                return sql.getUserDetails.call1(UserRecord::new, id)
×
409
                                .map(sql.populateMemberships(urlGen));
×
410
        }
411

412
        /**
413
         * Get a description of a user.
414
         *
415
         * @param user
416
         *            The name of the user.
417
         * @param urlGen
418
         *            How to construct the URL for a group membership in the
419
         *            response. If {@code null}, the memberships will be omitted.
420
         * @return A description of the user, or {@link Optional#empty()} if the
421
         *         user doesn't exist.
422
         */
423
        public Optional<UserRecord> getUser(String user,
424
                        Function<MemberRecord, URI> urlGen) {
425
                try (var sql = new UserCheckSQL()) {
×
426
                        return sql.transaction(() -> getUser(user, urlGen, sql));
×
427
                }
428
        }
429

430
        private Optional<UserRecord> getUser(String user,
431
                        Function<MemberRecord, URI> urlGen, UserCheckSQL sql) {
432
                return sql.getUserDetailsByName.call1(UserRecord::new, user)
×
433
                                .map(sql.populateMemberships(urlGen));
×
434
        }
435

436
        /**
437
         * Updates a user.
438
         *
439
         * @param id
440
         *            The ID of the user
441
         * @param user
442
         *            The description of what to update.
443
         * @param adminUser
444
         *            The <em>name</em> of the current user doing this call. Used to
445
         *            prohibit any admin from doing major damage to themselves.
446
         * @param urlGen
447
         *            How to construct the URL for a group membership in the
448
         *            response. If {@code null}, the memberships will be omitted.
449
         * @return The updated user
450
         */
451
        public Optional<UserRecord> updateUser(int id, UserRecord user,
452
                        String adminUser, Function<MemberRecord, URI> urlGen) {
453
                // Encode the password outside of any transaction; this is a slow op!
454
                var encPass = passServices.encodePassword(user.getPassword());
×
455
                try (var sql = new UpdateAllSQL()) {
×
456
                        return sql.transaction(() -> updateUser(id, user, adminUser,
×
457
                                        encPass, urlGen, sql));
458
                }
459
        }
460

461
        // Use this for looking up the current user, who should exist!
462
        private static int getCurrentUserId(UserCheckSQL sql, String userName) {
463
                return sql.userCheck.call1(integer("user_id"), userName)
×
464
                                .orElseThrow(() -> new RuntimeException(
×
465
                                                "current user has unexpectedly vanshed"));
466
        }
467

468
        private Optional<UserRecord> updateUser(int id, UserRecord user,
469
                        String adminUser, String encPass,
470
                        Function<MemberRecord, URI> urlGen, UpdateAllSQL sql) {
471
                int adminId = getCurrentUserId(sql, adminUser);
×
472

473
                var oldUser = getUser(id, null, sql)
×
474
                                .orElseThrow(() -> new RuntimeException(
×
475
                                                "current user has unexpectedly vanshed"));
476

477
                if (nonNull(user.getUserName())
×
478
                                && !oldUser.getUserName().equals(user.getUserName())) {
×
479
                        if (sql.setUserName.call(user.getUserName(), id) > 0) {
×
480
                                log.info("setting user {} to name '{}'", id,
×
481
                                                user.getUserName());
×
482
                        }
483
                }
484

485
                if (!oldUser.isExternallyAuthenticated() && nonNull(user.getPassword())
×
486
                                && !user.getPassword().isBlank()) {
×
487
                        if (sql.setUserPass.call(encPass, id) > 0) {
×
488
                                log.info("setting user {} to have password", id);
×
489
                        }
490
                }
491

492
                if (nonNull(user.getEnabled())
×
493
                                && !Objects.equals(oldUser.getEnabled(), user.getEnabled())
×
494
                                && (adminId != id)) {
495
                        // Admins can't change their own disable state
496
                        if (sql.setUserDisabled.call(!user.getEnabled(), id) > 0) {
×
497
                                log.info("setting user {} to {}", id,
×
498
                                                user.getEnabled() ? "enabled" : "disabled");
×
499
                        }
500
                }
501
                if (nonNull(user.getLocked())
×
502
                                && !Objects.equals(oldUser.getLocked(), user.getLocked())
×
503
                                && !user.getLocked() && adminId != id) {
×
504
                        // Admins can't change their own locked state
505
                        // Locked can't be set via this API, only reset
506
                        if (sql.setUserLocked.call(user.getLocked(), id) > 0) {
×
507
                                log.info("setting user {} to {}", id,
×
508
                                                user.getLocked() ? "locked" : "unlocked");
×
509
                        }
510
                }
511

512
                if (nonNull(user.getTrustLevel())
×
513
                                && oldUser.getTrustLevel() != user.getTrustLevel()
×
514
                                && adminId != id) {
515
                        // Admins can't change their own trust level
516
                        if (sql.setUserTrust.call(user.getTrustLevel(), id) > 0) {
×
517
                                log.info("setting user {} to {}", id, user.getTrustLevel());
×
518
                        }
519
                }
520

521
                return sql.getUser(id).map(sql.populateMemberships(urlGen));
×
522
        }
523

524
        /**
525
         * Deletes a user.
526
         *
527
         * @param id
528
         *            The ID of the user to delete.
529
         * @param adminUser
530
         *            The <em>name</em> of the current user doing this call. Used to
531
         *            prohibit anyone from deleting themselves.
532
         * @return The name of the deleted user if things succeeded, or
533
         *         {@link Optional#empty()} on failure.
534
         */
535
        public Optional<String> deleteUser(int id, String adminUser) {
536
                try (var sql = new DeleteUserSQL()) {
×
537
                        return sql.transaction(() -> {
×
538
                                if (getCurrentUserId(sql, adminUser) == id) {
×
539
                                        // May not delete yourself!
540
                                        return Optional.empty();
×
541
                                }
542
                                return sql.getUserName.call1(row -> {
×
543
                                        // Order matters! Get the name before the delete
544
                                        var userName = row.getString("user_name");
×
545
                                        return sql.deleteUser.call(id) == 1 ? userName : null;
×
546
                                }, id);
×
547
                        });
548
                }
549
        }
550

551
        private static PasswordChangeRecord passChange(Row row) {
552
                return new PasswordChangeRecord(row.getInt("user_id"),
×
553
                                row.getString("user_name"));
×
554
        }
555

556
        /**
557
         * Get a model for updating the local password of the current user.
558
         *
559
         * @param principal
560
         *            The current user
561
         * @return User model object. Password fields are unfilled.
562
         * @throws AuthenticationException
563
         *             If the user cannot change their password here for some
564
         *             reason.
565
         */
566
        public PasswordChangeRecord getUser(Principal principal)
567
                        throws AuthenticationException {
568
                try (var c = getConnection();
×
569
                                var q = c.query(GET_LOCAL_USER_DETAILS)) {
×
570
                        return c.transaction(false,
×
571
                                        () -> q.call1(UserControl::passChange, principal.getName())
×
572
                                        .orElseThrow(
×
573
                                                        // OpenID-authenticated user; go away
574
                                                        () -> new AuthenticationServiceException(
×
575
                                                                        "user is managed externally; "
576
                                                                                        + "cannot manage password here")));
577
                }
578
        }
579

580
        /**
581
         * Update the local password of the current user based on a filled out model
582
         * previously provided.
583
         *
584
         * @param principal
585
         *            The current user
586
         * @param user
587
         *            Valid user model object with password fields filled.
588
         * @return Replacement user model object. Password fields are unfilled.
589
         * @throws AuthenticationException
590
         *             If the user cannot change their password here for some
591
         *             reason.
592
         */
593
        public PasswordChangeRecord updateUser(Principal principal,
594
                        PasswordChangeRecord user) throws AuthenticationException {
595
                try (var sql = new UpdatePassSQL()) {
×
596
                        return updateUser(principal, user, sql);
×
597
                }
598
        }
599

600
        /**
601
         * Back end of {@link #updateUser(Principal,PasswordChangeRecord)}.
602
         * <p>
603
         * <strong>Do not hold a transaction when calling this!</strong> This is a
604
         * slow method as it validates and encodes passwords using bcrypt.
605
         *
606
         * @param principal
607
         *            Current user
608
         * @param user
609
         *            What to update
610
         * @param sql
611
         *            How to touch the DB
612
         * @return What was updated, <em>without</em> the actual password fields
613
         *         filled out.
614
         */
615
        private PasswordChangeRecord updateUser(Principal principal,
616
                        PasswordChangeRecord user, UpdatePassSQL sql) {
617
                /**
618
                 * Record extracted from a row of the {@code user_info} table.
619
                 *
620
                 * @param baseUser
621
                 *            The user's password change record, <em>without</em> the
622
                 *            actual password fields filled out.
623
                 * @param oldEncPass
624
                 *            Old encoded password.
625
                 * @see UserControl#updateUser(Principal, PasswordChangeRecord,
626
                 *      UpdatePassSQL)
627
                 */
628
                record GetUserResult(PasswordChangeRecord baseUser, String oldEncPass) {
×
629
                        private GetUserResult(Row row) {
630
                                this(passChange(row), row.getString("encrypted_password"));
×
631
                        }
×
632
                }
633

634
                var result = sql
×
635
                                .transaction(() -> sql.getPasswordedUser
×
636
                                                .call1(GetUserResult::new, principal.getName()))
×
637
                                .orElseThrow(
×
638
                                                // OpenID-authenticated user; go away
639
                                                () -> new AuthenticationServiceException(
×
640
                                                                "user is managed externally; cannot "
641
                                                                                + "change password here"));
642

643
                // This is a SLOW operation; must not hold transaction here
644
                if (!passServices.matchPassword(user.getOldPassword(),
×
645
                                result.oldEncPass())) {
×
646
                        throw new BadCredentialsException("bad password");
×
647
                }
648

649
                // Validate change; this should never fail but...
650
                if (!user.isNewPasswordMatched()) {
×
651
                        throw new BadCredentialsException("bad password");
×
652
                }
653
                var newEncPass = passServices.encodePassword(user.getNewPassword());
×
654
                return sql.transaction(() -> {
×
655
                        if (sql.setPassword.call(newEncPass,
×
656
                                        result.baseUser().getUserId()) != 1) {
×
657
                                throw new InternalAuthenticationServiceException(
×
658
                                                "failed to update database");
659
                        }
660
                        return result.baseUser();
×
661
                });
662
        }
663

664
        /**
665
         * List the groups in the database. Does not include membership data.
666
         *
667
         * @return List of groups.
668
         */
669
        public List<GroupRecord> listGroups() {
670
                try (var sql = new GroupsSQL()) {
×
671
                        return sql.transactionRead(sql::listGroups);
×
672
                }
673
        }
674

675
        /**
676
         * List the groups of a type in the database. Does not include membership
677
         * data.
678
         *
679
         * @param type
680
         *            The type of groups to get.
681
         * @return List of groups.
682
         */
683
        public List<GroupRecord> listGroups(GroupType type) {
684
                try (var sql = new GroupsSQL()) {
×
685
                        return sql.transactionRead(() -> sql.listGroups(type));
×
686
                }
687
        }
688

689
        /**
690
         * List the groups in the database.
691
         *
692
         * @param uriMapper
693
         *            How to construct a URL for the group.
694
         * @return Map of group names to URLs.
695
         */
696
        public Map<String, URI> listGroups(Function<GroupRecord, URI> uriMapper) {
697
                try (var sql = new GroupsSQL()) {
×
698
                        return sql.transactionRead(() -> Row.stream(sql.listGroups())
×
699
                                        .toMap(TreeMap::new, GroupRecord::getGroupName, uriMapper));
×
700
                }
701
        }
702

703
        /**
704
         * List the groups of a type in the database.
705
         *
706
         * @param type
707
         *            The type of groups to get.
708
         * @param uriMapper
709
         *            How to construct a URL for the group.
710
         * @return Map of group names to URLs.
711
         */
712
        public Map<String, URI> listGroups(GroupType type,
713
                        Function<GroupRecord, URI> uriMapper) {
714
                try (var sql = new GroupsSQL()) {
×
715
                        return sql.transactionRead(() -> Row.stream(sql.listGroups(type))
×
716
                                        .toMap(TreeMap::new, GroupRecord::getGroupName, uriMapper));
×
717
                }
718
        }
719

720
        /**
721
         * Get a description of a group. Includes group membership data.
722
         *
723
         * @param id
724
         *            The ID of the group.
725
         * @param urlGen
726
         *            How to construct the URL for a group membership. If
727
         *            {@code null}, the memberships will be omitted.
728
         * @return A description of the group, or {@link Optional#empty()} if the
729
         *         group doesn't exist.
730
         */
731
        public Optional<GroupRecord> getGroup(int id,
732
                        Function<MemberRecord, URI> urlGen) {
733
                try (var sql = new GroupsSQL()) {
×
734
                        return sql.transactionRead(
×
735
                                        () -> sql.getGroupId(id)
×
736
                                                        .map(sql.populateMemberships(urlGen)));
×
737
                }
738
        }
739

740
        /**
741
         * Get a description of a group. Includes group membership data.
742
         *
743
         * @param name
744
         *            The name of the group.
745
         * @param urlGen
746
         *            How to construct the URL for a group membership. If
747
         *            {@code null}, the memberships will be omitted.
748
         * @return A description of the group, or {@link Optional#empty()} if the
749
         *         group doesn't exist.
750
         */
751
        public Optional<GroupRecord> getGroup(String name,
752
                        Function<MemberRecord, URI> urlGen) {
753
                try (var sql = new GroupsSQL()) {
×
754
                        return sql.transactionRead(
×
755
                                        () -> sql.getGroupName(name)
×
756
                                                        .map(sql.populateMemberships(urlGen)));
×
757
                }
758
        }
759

760
        /**
761
         * Create a group from a supplied group.
762
         *
763
         * @param groupTemplate
764
         *            Description of what the group should look like. Only the
765
         *            {@code groupName} and the {@code quota} properties are used.
766
         * @param type
767
         *            What type of group is this; internal groups hold internal
768
         *            users, external groups hold external users and come in two
769
         *            kinds.
770
         * @return The full group description, assuming all went well.
771
         */
772
        public Optional<GroupRecord> createGroup(GroupRecord groupTemplate,
773
                        GroupType type) {
774
                try (var sql = new GroupsSQL()) {
1✔
775
                        return sql.transaction(() -> sql
1✔
776
                                        .insertGroup(groupTemplate.getGroupName(),
1✔
777
                                                        groupTemplate.getQuota(), type)
1✔
778
                                        .flatMap(sql::getGroupId));
1✔
779
                }
780
        }
781

782
        /**
783
         * Update a group from a supplied description.
784
         *
785
         * @param id
786
         *            The ID of the group to update
787
         * @param group
788
         *            The template of what the group is to be updated to.
789
         * @param urlGen
790
         *            How to construct the URL for a group membership in the
791
         *            response. If {@code null}, the memberships will be omitted.
792
         * @return A description of the updated group, or {@link Optional#empty()}
793
         *         if the group doesn't exist.
794
         */
795
        public Optional<GroupRecord> updateGroup(int id, GroupRecord group,
796
                        Function<MemberRecord, URI> urlGen) {
797
                try (var sql = new GroupsSQL()) {
×
798
                        return sql.transaction(() -> sql
×
799
                                        .updateGroup(id, group.getGroupName(), group.getQuota())
×
800
                                        .map(sql.populateMemberships(urlGen)));
×
801
                }
802
        }
803

804
        /**
805
         * Delete a group. This removes all users from that group automatically.
806
         *
807
         * @param groupId
808
         *            The ID of the group to delete.
809
         * @return The deleted group name on success; {@link Optional#empty()} on
810
         *         failure.
811
         */
812
        public Optional<String> deleteGroup(int groupId) {
813
                try (var sql = new GroupsSQL()) {
×
814
                        return sql.transaction(
×
815
                                        () -> sql.deleteGroup(groupId));
×
816
                }
817
        }
818

819
        /**
820
         * Adds a user to a group.
821
         *
822
         * @param user
823
         *            What user to add.
824
         * @param group
825
         *            What group to add to.
826
         * @return Description of the created membership, or empty if adding failed.
827
         *         Note that this doesn't set the URLs.
828
         */
829
        public Optional<MemberRecord> addUserToGroup(UserRecord user,
830
                        GroupRecord group) {
831
                try (var c = getConnection();
×
832
                                var insert = c.update(ADD_USER_TO_GROUP)) {
×
833
                        return c.transaction(
×
834
                                        () -> insert.key(user.getUserId(), group.getGroupId()))
×
835
                                        .map(id -> {
×
836
                                                var mr = new MemberRecord();
×
837
                                                // Don't need to fetch this stuff; already have it!
838
                                                mr.setId(id);
×
839
                                                mr.setGroupId(group.getGroupId());
×
840
                                                mr.setGroupName(group.getGroupName());
×
841
                                                mr.setUserId(user.getUserId());
×
842
                                                mr.setUserName(user.getUserName());
×
843
                                                return mr;
×
844
                                        });
845
                }
846
        }
847

848
        /**
849
         * Removes a user from a group.
850
         *
851
         * @param user
852
         *            What user to remove.
853
         * @param group
854
         *            What group to remove from.
855
         * @return Whether the removing succeeded.
856
         */
857
        public boolean removeUserFromGroup(UserRecord user, GroupRecord group) {
858
                try (var c = getConnection();
×
859
                                var delete = c.update(REMOVE_USER_FROM_GROUP)) {
×
860
                        return c.transaction(() -> delete.call(user.getUserId(),
×
861
                                        group.getGroupId())) > 0;
×
862
                }
863
        }
864

865
        /**
866
         * Removes a user from a group.
867
         *
868
         * @param member
869
         *            What membership to remove.
870
         * @param group
871
         *            What group to remove from.
872
         * @return Whether the removing succeeded.
873
         */
874
        public boolean removeMembershipOfGroup(MemberRecord member,
875
                        GroupRecord group) {
876
                if (member.getGroupId() != group.getGroupId()) {
×
877
                        // Sanity check
878
                        return false;
×
879
                }
880
                try (var c = getConnection();
×
881
                                var delete = c.update(REMOVE_USER_FROM_GROUP)) {
×
882
                        return c.transaction(() -> delete.call(member.getUserId(),
×
883
                                        group.getGroupId())) > 0;
×
884
                }
885
        }
886

887
        /**
888
         * Describe the details of a particular group membership.
889
         *
890
         * @param memberId
891
         *            The ID of the membership record.
892
         * @param groupUriGen
893
         *            How to generate the URL for the group. Ignored if
894
         *            {@code null}.
895
         * @param userUriGen
896
         *            How to generate the URL for the user. Ignored if {@code null}.
897
         * @return The membership description
898
         */
899
        public Optional<MemberRecord> describeMembership(int memberId,
900
                        Function<MemberRecord, URI> groupUriGen,
901
                        Function<MemberRecord, URI> userUriGen) {
902
                Optional<MemberRecord> mr;
903
                try (var c = getConnection();
×
904
                                var q = c.query(GET_MEMBERSHIP)) {
×
905
                        mr = c.transaction(false,
×
906
                                        () -> q.call1(UserControl::member, memberId));
×
907
                }
908
                mr.ifPresent(member -> {
×
909
                        if (nonNull(groupUriGen)) {
×
910
                                member.setGroupUrl(groupUriGen.apply(member));
×
911
                        }
912
                        if (nonNull(userUriGen)) {
×
913
                                member.setUserUrl(userUriGen.apply(member));
×
914
                        }
915
                });
×
916
                return mr;
×
917
        }
918
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc