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

SpiNNakerManchester / JavaSpiNNaker / 13005837453

28 Jan 2025 07:48AM UTC coverage: 38.562% (+1.3%) from 37.299%
13005837453

push

github

web-flow
Merge pull request #1214 from SpiNNakerManchester/fix_password_reset

Fix password reset

9 of 24 new or added lines in 3 files covered. (37.5%)

13 existing lines in 6 files now uncovered.

9135 of 23689 relevant lines covered (38.56%)

1.07 hits per line

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

40.94
/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 {
3✔
62
        private static final Logger log = getLogger(UserControl.class);
3✔
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 class UserCheckSQL extends AbstractSQL {
3✔
89
                private final Query userCheck = conn.query(GET_USER_ID);
3✔
90

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

94
                private final Query getMembershipsOfUser =
3✔
95
                                conn.query(GET_MEMBERSHIPS_OF_USER);
3✔
96

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

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

111
                Function<UserRecord, UserRecord>
112
                                populateMemberships(Function<MemberRecord, URI> urlGen) {
113
                        if (isNull(urlGen)) {
3✔
114
                                return identity();
3✔
115
                        }
116
                        return user -> {
3✔
117
                                user.setGroups(Row.stream(getMembershipsOfUser.call(
3✔
118
                                                UserControl::member, user.getUserId()))
3✔
119
                                                .toMap(TreeMap::new,
3✔
120
                                                                MemberRecord::getGroupName, urlGen));
121
                                return user;
3✔
122
                        };
123
                }
124
        }
125

126
        private final class CreateSQL extends UserCheckSQL {
3✔
127
                private final Update createUser = conn.update(CREATE_USER);
3✔
128

129
                @Override
130
                public void close() {
131
                        createUser.close();
3✔
132
                        super.close();
3✔
133
                }
3✔
134

135
                Optional<Integer> createUser(String name, String encPass,
136
                                TrustLevel trustLevel, boolean disabled, String openIdSubject) {
137
                        return createUser.key(name, encPass, trustLevel, disabled,
3✔
138
                                        openIdSubject);
139
                }
140
        }
141

142
        private final class UpdateAllSQL extends UserCheckSQL {
3✔
143
                private final Update setUserName = conn.update(SET_USER_NAME);
3✔
144

145
                private final Update setUserPass = conn.update(SET_USER_PASS);
3✔
146

147
                private final Update setUserDisabled = conn.update(SET_USER_DISABLED);
3✔
148

149
                private final Update setUserLocked = conn.update(SET_USER_LOCKED);
3✔
150

151
                private final Update setUserTrust = conn.update(SET_USER_TRUST);
3✔
152

153
                @Override
154
                public void close() {
155
                        setUserName.close();
3✔
156
                        setUserPass.close();
3✔
157
                        setUserDisabled.close();
3✔
158
                        setUserLocked.close();
3✔
159
                        setUserTrust.close();
3✔
160
                        super.close();
3✔
161
                }
3✔
162
        }
163

164
        private final class UpdatePassSQL extends UserCheckSQL {
×
165
                private final Query getPasswordedUser =
×
166
                                conn.query(GET_LOCAL_USER_DETAILS);
×
167

168
                private final Update setPassword = conn.update(SET_USER_PASS);
×
169

170
                @Override
171
                public void close() {
172
                        getPasswordedUser.close();
×
173
                        setPassword.close();
×
174
                        super.close();
×
175
                }
×
176
        }
177

178
        private final class DeleteUserSQL extends UserCheckSQL {
×
179
                private final Query getUserName = conn.query(GET_USER_DETAILS);
×
180

181
                private final Update deleteUser = conn.update(DELETE_USER);
×
182

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

191
        private final class GroupsSQL extends AbstractSQL {
3✔
192
                private final Query listGroups = conn.query(LIST_ALL_GROUPS);
3✔
193

194
                private final Query listGroupsOfType =
3✔
195
                                conn.query(LIST_ALL_GROUPS_OF_TYPE);
3✔
196

197
                private final Query getUsers = conn.query(GET_USERS_OF_GROUP);
3✔
198

199
                private final Query getGroupId = conn.query(GET_GROUP_BY_ID);
3✔
200

201
                private final Query getGroupName = conn.query(GET_GROUP_BY_NAME);
3✔
202

203
                private final Update insertGroup = conn.update(CREATE_GROUP);
3✔
204

205
                private final Update updateGroup = conn.update(UPDATE_GROUP);
3✔
206

207
                private final Update deleteGroup = conn.update(DELETE_GROUP);
3✔
208

209
                @Override
210
                public void close() {
211
                        listGroups.close();
3✔
212
                        getUsers.close();
3✔
213
                        getGroupId.close();
3✔
214
                        getGroupName.close();
3✔
215
                        insertGroup.close();
3✔
216
                        updateGroup.close();
3✔
217
                        deleteGroup.close();
3✔
218
                        super.close();
3✔
219
                }
3✔
220

221
                List<GroupRecord> listGroups() {
222
                        return listGroups.call(GroupRecord::new);
×
223
                }
224

225
                List<GroupRecord> listGroups(GroupType type) {
226
                        return listGroupsOfType.call(GroupRecord::new, type);
×
227
                }
228

229
                Optional<GroupRecord> getGroupId(int id) {
230
                        return getGroupId.call1(GroupRecord::new, id);
3✔
231
                }
232

233
                Optional<GroupRecord> getGroupName(String name) {
234
                        return getGroupName.call1(GroupRecord::new, name);
×
235
                }
236

237
                Optional<Integer> insertGroup(String name, Optional<Long> quota,
238
                                GroupType groupType) {
239
                        return insertGroup.key(name, quota, groupType);
3✔
240
                }
241

242
                public Optional<GroupRecord> updateGroup(int id, String name,
243
                                Optional<Long> quota) {
244
                        if (updateGroup.call(name, quota.orElse(null), id) == 0) {
×
245
                                return Optional.empty();
×
246
                        }
247
                        return getGroupId(id);
×
248
                }
249

250
                Optional<String> deleteGroup(int id) {
251
                        return getGroupId.call1(row -> {
×
252
                                // Order matters! Get the name before the delete
253
                                var groupName = row.getString("group_name");
×
254
                                return deleteGroup.call(id) == 1 ? groupName : null;
×
255
                        }, id);
×
256
                }
257

258
                Function<GroupRecord, GroupRecord>
259
                                populateMemberships(Function<MemberRecord, URI> urlGen) {
260
                        if (isNull(urlGen)) {
×
261
                                return identity();
×
262
                        }
263
                        return group -> {
×
264
                                group.setMembers(Row.stream(getUsers.call(
×
265
                                                UserControl::member, group.getGroupId()))
×
266
                                                .toMap(TreeMap::new,
×
267
                                                                MemberRecord::getUserName, urlGen));
268
                                return group;
×
269
                        };
270
                }
271
        }
272

273
        private static MemberRecord member(Row row) {
274
                var m = new MemberRecord();
3✔
275
                m.setId(row.getInt("membership_id"));
3✔
276
                m.setGroupId(row.getInt("group_id"));
3✔
277
                m.setGroupName(row.getString("group_name"));
3✔
278
                m.setUserId(row.getInt("user_id"));
3✔
279
                m.setUserName(row.getString("user_name"));
3✔
280
                return m;
3✔
281
        }
282

283
        /**
284
         * List the users in the database.
285
         *
286
         * @return List of users. Only {@link UserRecord#userId} and
287
         *         {@link UserRecord#userName} fields are inflated.
288
         */
289
        public List<UserRecord> listUsers() {
290
                try (var sql = new AllUsersSQL()) {
×
291
                        return sql.transactionRead(() -> sql.allUsers());
×
292
                }
293
        }
294

295
        /**
296
         * List the users of a type in the database.
297
         *
298
         * @param internal
299
         *            Whether to get the internal users. If not, get the OpenID
300
         *            users.
301
         * @return List of users. Only {@link UserRecord#userId} and
302
         *         {@link UserRecord#userName} fields are inflated.
303
         */
304
        public List<UserRecord> listUsers(boolean internal) {
305
                try (var sql = new AllUsersSQL()) {
×
306
                        return sql.transactionRead(() -> sql.allUsers(internal));
×
307
                }
308
        }
309

310
        /**
311
         * List the users in the database.
312
         *
313
         * @param uriMapper
314
         *            How to construct a URL for the user.
315
         * @return Map of users to URLs.
316
         */
317
        public Map<String, URI> listUsers(Function<UserRecord, URI> uriMapper) {
318
                try (var sql = new AllUsersSQL()) {
×
319
                        return sql.transactionRead(
×
320
                                        () -> Row.stream(sql.allUsers()).toMap(
×
321
                                                        TreeMap::new, UserRecord::getUserName, uriMapper));
322
                }
323
        }
324

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

343
        private static UserRecord sketchUser(Row row) {
344
                var userSketch = new UserRecord();
×
345
                userSketch.setUserId(row.getInt("user_id"));
×
346
                userSketch.setUserName(row.getString("user_name"));
×
347
                userSketch.setOpenIdSubject(row.getString("openid_subject"));
×
348
                return userSketch;
×
349
        }
350

351
        /**
352
         * Create a user.
353
         *
354
         * @param user
355
         *            The description of the user to create.
356
         * @param urlGen
357
         *            How to construct the URL for a group membership in the
358
         *            response. If {@code null}, the memberships will be omitted.
359
         * @return A description of the created user, or {@link Optional#empty()} if
360
         *         the user exists already.
361
         */
362
        public Optional<UserRecord> createUser(UserRecord user,
363
                        Function<MemberRecord, URI> urlGen) {
364
                // This is a slow operation; don't hold a database transaction
365
                var encPass = passServices.encodePassword(user.getPassword());
3✔
366
                try (var sql = new CreateSQL()) {
3✔
367
                        return sql.transaction(
3✔
368
                                        () -> createUser(user, encPass, urlGen, sql));
3✔
369
                }
370
        }
371

372
        private Optional<UserRecord> createUser(UserRecord user, String encPass,
373
                        Function<MemberRecord, URI> urlGen, CreateSQL sql) {
374
                var result =  sql.createUser(user.getUserName(), encPass,
3✔
375
                                user.getTrustLevel(), !user.getEnabled(),
3✔
376
                                user.getOpenIdSubject());
3✔
377
                if (result.isEmpty()) {
3✔
NEW
378
                        return Optional.empty();
×
379
                }
380
                return getUser(result.get(), urlGen);
3✔
381
        }
382

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

401
        private Optional<UserRecord> getUser(int id,
402
                        Function<MemberRecord, URI> urlGen, UserCheckSQL sql) {
403
                return sql.getUserDetails.call1(UserRecord::new, id)
3✔
404
                                .map(sql.populateMemberships(urlGen));
3✔
405
        }
406

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

425
        private Optional<UserRecord> getUser(String user,
426
                        Function<MemberRecord, URI> urlGen, UserCheckSQL sql) {
427
                return sql.getUserDetailsByName.call1(UserRecord::new, user)
3✔
428
                                .map(sql.populateMemberships(urlGen));
3✔
429
        }
430

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

456
        // Use this for looking up the current user, who should exist!
457
        private static int getCurrentUserId(UserCheckSQL sql, String userName) {
458
                return sql.userCheck.call1(integer("user_id"), userName)
3✔
459
                                .orElseThrow(() -> new RuntimeException(
3✔
460
                                                "current user has unexpectedly vanshed"));
461
        }
462

463
        private Optional<UserRecord> updateUser(int id, UserRecord user,
464
                        String adminUser, String encPass,
465
                        Function<MemberRecord, URI> urlGen, UpdateAllSQL sql) {
466
                int adminId = getCurrentUserId(sql, adminUser);
3✔
467

468
                var oldUser = getUser(id, null, sql)
3✔
469
                                .orElseThrow(() -> new RuntimeException(
3✔
470
                                                "current user has unexpectedly vanshed"));
471

472
                if (nonNull(user.getUserName())
3✔
473
                                && !oldUser.getUserName().equals(user.getUserName())) {
3✔
474
                        if (sql.setUserName.call(user.getUserName(), id) > 0) {
×
475
                                log.info("setting user {} to name '{}'", id,
×
476
                                                user.getUserName());
×
477
                        }
478
                }
479

480
                if (!oldUser.isExternallyAuthenticated() && nonNull(user.getPassword())
3✔
481
                                && !user.getPassword().isBlank()) {
3✔
482
                        if (sql.setUserPass.call(encPass, id) > 0) {
3✔
483
                                log.info("setting user {} to have password", id);
3✔
484
                        }
485
                }
486

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

507
                if (nonNull(user.getTrustLevel())
3✔
508
                                && oldUser.getTrustLevel() != user.getTrustLevel()
3✔
509
                                && adminId != id) {
510
                        // Admins can't change their own trust level
511
                        if (sql.setUserTrust.call(user.getTrustLevel(), id) > 0) {
×
512
                                log.info("setting user {} to {}", id, user.getTrustLevel());
×
513
                        }
514
                }
515

516
                return getUser(id, urlGen);
3✔
517
        }
518

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

546
        private static PasswordChangeRecord passChange(Row row) {
547
                return new PasswordChangeRecord(row.getInt("user_id"),
×
548
                                row.getString("user_name"));
×
549
        }
550

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

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

595
        /**
596
         * Just a tuple extracted from a row. Only used in
597
         * {@link #updateUser(Principal,PasswordChangeRecord,UpdatePassSQL)}; it's
598
         * only not a local class to work around <a href=
599
         * "https://bugs.openjdk.java.net/browse/JDK-8144673">JDK-8144673</a> (fixed
600
         * by Java 11).
601
         */
602
        private static class GetUserResult {
603
                final PasswordChangeRecord baseUser;
604

605
                final String oldEncPass;
606

607
                GetUserResult(Row row) {
×
608
                        baseUser = passChange(row);
×
609
                        oldEncPass = row.getString("encrypted_password");
×
610
                }
×
611
        }
612

613
        /**
614
         * Back end of {@link #updateUser(Principal,PasswordChangeRecord)}.
615
         * <p>
616
         * <strong>Do not hold a transaction when calling this!</strong> This is a
617
         * slow method as it validates and encodes passwords using bcrypt.
618
         *
619
         * @param principal
620
         *            Current user
621
         * @param user
622
         *            What to update
623
         * @param sql
624
         *            How to touch the DB
625
         * @return What was updated
626
         */
627
        private PasswordChangeRecord updateUser(Principal principal,
628
                        PasswordChangeRecord user, UpdatePassSQL sql) {
629
                var result = sql
×
630
                                .transaction(() -> sql.getPasswordedUser
×
631
                                                .call1(GetUserResult::new, principal.getName()))
×
632
                                .orElseThrow(
×
633
                                                // OpenID-authenticated user; go away
634
                                                () -> new AuthenticationServiceException(
×
635
                                                                "user is managed externally; cannot "
636
                                                                                + "change password here"));
637

638
                // This is a SLOW operation; must not hold transaction here
639
                if (!passServices.matchPassword(user.getOldPassword(),
×
640
                                result.oldEncPass)) {
641
                        throw new BadCredentialsException("bad password");
×
642
                }
643

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

659
        /**
660
         * List the groups in the database. Does not include membership data.
661
         *
662
         * @return List of groups.
663
         */
664
        public List<GroupRecord> listGroups() {
665
                try (var sql = new GroupsSQL()) {
×
666
                        return sql.transactionRead(() -> sql.listGroups());
×
667
                }
668
        }
669

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

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

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

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

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

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

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

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

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

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

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

882
        /**
883
         * Describe the details of a particular group membership.
884
         *
885
         * @param memberId
886
         *            The ID of the membership record.
887
         * @param groupUriGen
888
         *            How to generate the URL for the group. Ignored if
889
         *            {@code null}.
890
         * @param userUriGen
891
         *            How to generate the URL for the user. Ignored if {@code null}.
892
         * @return The membership description
893
         */
894
        public Optional<MemberRecord> describeMembership(int memberId,
895
                        Function<MemberRecord, URI> groupUriGen,
896
                        Function<MemberRecord, URI> userUriGen) {
897
                Optional<MemberRecord> mr;
898
                try (var c = getConnection();
×
899
                                var q = c.query(GET_MEMBERSHIP)) {
×
900
                        mr = c.transaction(false,
×
901
                                        () -> q.call1(UserControl::member, memberId));
×
902
                }
903
                mr.ifPresent(member -> {
×
904
                        if (nonNull(groupUriGen)) {
×
905
                                member.setGroupUrl(groupUriGen.apply(member));
×
906
                        }
907
                        if (nonNull(userUriGen)) {
×
908
                                member.setUserUrl(userUriGen.apply(member));
×
909
                        }
910
                });
×
911
                return mr;
×
912
        }
913
}
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

© 2026 Coveralls, Inc