Compare commits

...

235 Commits
master ... test

Author SHA1 Message Date
fc2aa31d77 Merge branch 'master' into test 2025-03-13 15:11:54 +01:00
29166b0b39 refactor(navigation): fix merge 2025-02-20 15:20:13 +01:00
3949a60828 refactor(migration): fix merge 2025-02-20 15:04:57 +01:00
7bdd9b648a refactor(settings): fix merge 2025-02-20 15:03:36 +01:00
51164abc54 refactor(settings): add USERAUTH_MODE env var option to override user-auth.protocol 2025-02-20 15:03:21 +01:00
d6607b4432 refactor(model): fix User merge 2025-02-20 15:02:35 +01:00
988dddc0a2 Merge branch '145-build-system-rewrite' into test 2025-02-20 13:34:46 +01:00
4eb28c3c5b Merge branch 'master' into test 2025-02-20 11:01:15 +01:00
cac16b4e1c chore(release): v27.4.59-test-h0.0.18 2025-02-19 13:52:25 +01:00
dfa7746ff6 build: update stack.yaml.lock 2025-02-19 13:36:43 +01:00
be250afce5 chore(release): v27.4.59-test-h0.0.17 2025-02-19 12:31:38 +01:00
81278d92a1 Merge branch '145-build-system-rewrite' into test 2025-02-19 12:29:54 +01:00
Sarah Vaupel
6693bbe166 chore(release): 28.1.1 2024-04-22 10:44:24 +02:00
Sarah Vaupel
1696135096 Merge branch '128-remove-nodejs' into test 2024-04-22 10:42:50 +02:00
c764182a6d bugfix(gitlab-ci): refined pattern matching for not matching manifest.json 2024-04-21 05:34:52 +02:00
Sarah Vaupel
a112ef2eca chore(release): 28.1.0 2024-04-19 00:34:33 +02:00
Sarah Vaupel
7e28517b82 Merge branch 'test' into 55-oauth2-single-sign-on 2024-04-19 00:34:07 +02:00
Sarah Vaupel
3080ab995a chore(release): 28.0.10 2024-04-19 00:32:24 +02:00
Sarah Vaupel
7a510b315d fix(auth): use appsettings for azure tenant id; refactor azure lookup url methods 2024-04-18 22:27:51 +02:00
Sarah Vaupel
dc701e5c49 chore: fix tests 2024-04-18 02:07:04 +02:00
233e9ca92f chore(gitlab-ci): Add debug print to container sanitation. 2024-04-18 01:29:05 +02:00
Sarah Vaupel
e1a25cdd31 feat(middleware): allow Cross Origin Resource Sharing (CORS) 2024-04-17 02:52:11 +02:00
Sarah Vaupel
5be23c0d52 refactor: move makeMiddleware and dependencies to separate module; refactor Application imports 2024-04-17 01:30:22 +02:00
Sarah Vaupel
de8cf11d4d chore(release): 28.0.9 2024-04-08 19:06:07 +02:00
Sarah Vaupel
666a50e163 chore(release): 28.0.8 2024-04-07 01:56:07 +02:00
0bd256cb09 Merge branch '128-remove-nodejs' into 'test'
chore(gitlab-ci): query nodejs roots in nix store if nix store delete fails,...

See merge request fradrive/fradrive!30
2024-04-06 23:45:52 +00:00
0d46802862 chore(gitlab-ci): query nodejs roots in nix store if nix store delete fails,... 2024-04-06 23:45:51 +00:00
b190e25c88 chore(release): 28.0.0 2024-03-21 17:48:37 +01:00
8290f9dd23 chore(changelog): add entry for new OAuth2 support 2024-03-21 17:48:24 +01:00
22e57dc075 chore(oauth2): fix DEVELOPMENT imports 2024-03-21 17:40:29 +01:00
aa6406f949 Merge branch 'oauth2' into 'test'
Implement OAuth2 (AzureADv2) support

See merge request fradrive/fradrive!29
2024-03-21 16:00:37 +00:00
663ad01740 chore(oauth2): remove unused imports and defs 2024-03-21 14:14:33 +01:00
619c5975aa chore(oauth2): remove unused import 2024-03-21 14:06:15 +01:00
795c707a1f chore(oauth2): remove unused loadPlugin function 2024-03-21 09:16:43 +01:00
0599ec2512 chore(oauth2): fix type 2024-03-21 00:27:43 +01:00
b1cb45ac7e chore(oauth2): fix !develop syntax contd 2024-03-20 18:24:39 +01:00
274c86a820 chore(oauth2): fix conf constructors in !develop 2024-03-20 15:56:30 +01:00
3119dff6fe Merge branch 'test' into oauth2 2024-03-19 22:51:37 +01:00
f7f3532b30 Merge branch 'master' into test 2024-03-19 22:47:59 +01:00
1dd83af6aa chore(oauth2): fix syntax 2024-03-19 22:45:04 +01:00
cea64da34d chore(oauth2): downgrade yesod-auth-oauth2 to v0.6.3.4 2024-03-19 16:27:57 +01:00
cba9cadb41 chore: update backend dependency sources 2024-03-19 15:11:19 +01:00
9428bc05cc chore: revert to previous flake inputs 2024-03-18 15:28:02 +01:00
94d45c1f17 chore: update stack.yaml.lock 2024-03-18 12:54:30 +01:00
8be3e2ea78 chore: use previous oauth2 lib 2024-03-18 12:54:05 +01:00
7e33d9e5de chore: update stack(-flake).yaml (fix fork urls, add inputs, revert to previous oauth2 lib) 2024-03-18 12:53:37 +01:00
923166b592 chore: update package.yaml 2024-03-18 12:51:19 +01:00
dbfd3657a0 chore(flake): remove redundant inputs 2024-03-18 12:50:59 +01:00
Sarah Vaupel
864175284d Merge branch 'master' into test 2024-03-15 10:44:43 +01:00
a4eda81436 chore: work on flakey oauth2 yesod plugin input for v0.7.2 specifically 2024-03-15 10:40:07 +01:00
4db44733ca chore: fix haskell inputs 2024-03-15 10:12:33 +01:00
1fc43a8727 chore: update flake 2024-03-14 22:14:53 +01:00
6cd1d829b6 chore(nix): fix backend build target 2024-03-14 21:59:15 +01:00
85dc1fa0b5 chore: depromote debug logErrorS calls 2024-03-14 19:26:16 +01:00
2aa64f7360 feat(sso): redirect to login when auto-sign-on is enabled and user is not authenticated 2024-03-14 19:20:37 +01:00
f3da2ac630 chore(sso): add bare auto-sign-out setting 2024-03-14 14:07:17 +01:00
d44b903b3e chore: fix tests 2024-03-14 13:07:22 +01:00
c4501f1d08 chore: hlint 2024-03-14 13:06:58 +01:00
560d1adf5f chore(sso): disable sso by default (i.e. for develop) 2024-03-14 12:47:04 +01:00
acd6a3c11c chore: hlint 2024-03-14 12:42:10 +01:00
2787bde8da Merge branch '142-userdata-oauth-mode' into 'oauth2'
Resolve "Benutzerdaten-Abfrage: Mehrere Modi & OAuth-Modus"

See merge request fradrive/fradrive!26
2024-03-13 16:24:04 +00:00
6b82c26268 chore(migration): fix oauth2 migration contd 2024-03-13 12:24:25 +01:00
770c2f3182 chore(migration): fix oauth2 migration 2024-03-13 10:20:10 +01:00
843e6dbba2 chore(migration): add oauth2 migration 2024-03-12 18:09:18 +01:00
3607a9da6d Merge branch 'oauth2' into 142-userdata-oauth-mode 2024-03-12 15:08:20 +01:00
608bea5199 Merge branch '139-single-sign-on-sso-routing-anpassen' into 'oauth2'
Resolve "Single sign-on (SSO): Routing anpassen"

See merge request fradrive/fradrive!28
2024-03-11 14:49:41 +00:00
07dd91665c chore: fix auth plugin refs 2024-03-11 15:20:24 +01:00
5662a2d1f1 chore: fix merge oopsie contd 2024-03-11 15:09:33 +01:00
72938e41ba chore: fix merge oopsie 2024-03-11 15:07:50 +01:00
Sarah Vaupel
cf6ae898c4 Merge branch '139-single-sign-on-sso-routing-anpassen' into 142-userdata-oauth-mode 2024-03-11 14:50:07 +01:00
05acba8cbe chore(foundation): ditch redirectToReferrer in favour of SSOut 2024-03-11 14:30:44 +01:00
9856272734 chore(login): do not login via modal 2024-03-11 14:23:35 +01:00
504490f593 chore(admin): switch to generic Aeson Value for oauth response parsing 2024-03-11 11:09:59 +01:00
David Mosbach
4c109538ee chore(auth): new 'Account' section 2024-03-10 22:15:20 +00:00
David Mosbach
1e5c4df163 chore(auth): fix single sign out redirect route 2024-03-10 19:43:54 +00:00
e1ebd528b8 chore(auth): use available sources in AuthIsExternal access pred 2024-03-08 21:16:16 +01:00
708320e067 chore(auth): change user identification to UserIdent for ExternalUser entries 2024-03-08 20:04:19 +01:00
51298ba726 chore: make fetch and upsert results Maybe 2024-03-08 19:05:58 +01:00
96e3eb613d chore(admin): merge external-user handlers (ldap, oauth2) 2024-03-08 12:10:26 +01:00
a2903da109 refactor(auth): UserConversionException -> DecodeUserException 2024-03-08 10:40:49 +01:00
c9fa627651 chore(admin): generalize admin ldap handler for all source types (TODO: rename) 2024-03-08 09:56:54 +01:00
969cc4df63 chore(jobs): use userLookupAndUpsert for synchronise user job 2024-03-08 09:56:27 +01:00
2480efc345 chore: userLookupAndUpsert contd 2024-03-08 09:55:51 +01:00
8c4ec00c35 chore(ldap): ldapSearch for arbitrary number of results 2024-03-08 09:54:30 +01:00
78a8442d07 chore(auth): userLookupAndUpsert 2024-03-07 23:24:41 +01:00
95803db3a0 chore(auth): fix fetchUserData 2024-03-07 15:32:07 +01:00
d71ff014ea chore(ldap): derive more json instances 2024-03-07 15:30:48 +01:00
aca5a79de2 chore(auth): implement fetchUserData, generalized version of azureUser and ldapUser 2024-03-07 05:38:39 +01:00
4feb05a02e chore(foundation): tweak UpsertUserData fields 2024-03-07 05:37:27 +01:00
77a9100b2e chore(auth): refactor; add util function 2024-03-07 05:36:03 +01:00
David Mosbach
b947037ea2 feat(auth): implemented single sign out 2024-03-07 03:31:17 +00:00
David Mosbach
d88acf4634 chore(auth): updated mock server 2024-03-06 04:26:47 +00:00
David Mosbach
fbe0e37d28 feat(auth): oidc based sso for auth protected routes 2024-03-05 23:57:10 +00:00
bb03d28b7d chore(auth): actually use user-auth config for determining auth plugins to load 2024-03-03 06:16:53 +01:00
2196e89208 chore(settings): define more sane default values in settings.yml 2024-03-03 04:36:18 +01:00
4ff51c8f6f chore: add TODOs and debug logs 2024-03-03 04:35:39 +01:00
434eed2217 chore(auth): do not authenticate against external sources on dummy login 2024-03-01 20:42:51 +01:00
f88e527fe4 chore(model): remigrate ExternalAuth -> ExternalUser for more general data lookup; redefine lastSync timestamp semantics contd 2024-03-01 12:03:38 +01:00
40fe8ecfc6 chore(model): remigrate ExternalAuth -> ExternalUser for more general data lookup; redefine lastSync timestamp semantics 2024-03-01 10:47:52 +01:00
13502d704e refactor(auth): add missing TODOs, remove debris 2024-02-29 22:16:11 +01:00
d1e1f25162 chore(login): use correct auth plugin identifiers for comparison in login template 2024-02-29 17:52:31 +01:00
ac5bca2fcd chore(ldap): use separate source-id for ldap instance identification 2024-02-28 15:50:47 +01:00
064645d1b3 refactor(ldap): move orphan instance 2024-02-28 12:00:06 +01:00
956c85a9f3 chore(migration): remove old ldap-primary-key index 2024-02-28 11:05:01 +01:00
David Mosbach
bee135ab48 chore(auth): connect azure user lookup 2024-02-22 18:56:03 +00:00
42ecc91c22 chore(test): update test database 2024-02-21 07:19:37 +01:00
a37d4b369a chore(application): rename conf constructors 2024-02-21 07:14:18 +01:00
039b1234c5 chore(sap): generalize ldap-cutoff over configured ldap sources 2024-02-21 07:13:51 +01:00
87b3214c84 chore(lms): fix password in fake user 2024-02-21 07:13:00 +01:00
ad937cda8c chore(users): remove ldap-specific columns in admin users page 2024-02-21 07:12:29 +01:00
899071e4d6 chore(users): remove eppn support 2024-02-21 07:11:59 +01:00
55bf8c0355 chore: add forgotten audPassword 2024-02-21 07:11:22 +01:00
b4a8ccf9cc chore(admin): tweak ldap view 2024-02-21 07:10:19 +01:00
76d3c57658 chore(messages): add and tweak auth messages 2024-02-21 07:09:18 +01:00
2490f8e69f chore(users): add password to user data for addNewUser 2024-02-21 07:08:56 +01:00
6cd0152636 refactor(jobs): use new user sync job name 2024-02-21 07:07:54 +01:00
19433fdc56 chore(profile): better auth info on profile page 2024-02-21 07:05:57 +01:00
012c75db21 chore(pwhash): reintroduce digest computation 2024-02-21 02:32:15 +01:00
71e2d6827e chore(model): rename userLastLogin->userLastAuthentication for less migration woes 2024-02-21 02:06:00 +01:00
41b14f1ece chore(model): replace auth-source model tables with AuthSourceIdent jsonified unique ids 2024-02-21 02:02:58 +01:00
a2e01e74af chore(notifications): reimplement authmode-update notification to support new login modes 2024-02-20 01:33:34 +01:00
8a353c357f chore(users): tweak assimilateUsers for new config 2024-02-20 00:38:46 +01:00
9bf7033eac chore(guess-user): remove eppn lookup 2024-02-20 00:13:55 +01:00
0a01490aa7 chore(auth): use ldap external auth in health reports 2024-02-20 00:09:31 +01:00
115452035d refactor(jobs): SynchroniseUserdb -> SynchroniseUsers 2024-02-20 00:05:56 +01:00
b8e7ee2b3d chore(users): remove old auth kind digesting 2024-02-19 23:49:17 +01:00
3d1908d71a chore(users): tweak addNewUser to conform to new model 2024-02-19 23:48:33 +01:00
ed54b666ec chore: add todos 2024-02-19 23:46:45 +01:00
a1d8dc2e7e chore(auth): migrate password hash back to User model 2024-02-19 02:24:31 +01:00
David Mosbach
956464659e feat(auth): link to sso test from dev login widget 2024-02-19 00:52:15 +00:00
9a5c487b2c chore(auth): switch back to AuthId UniWorX == UserId 2024-02-19 01:44:58 +01:00
bcfcbd5c9b chore(auth): fix redundant imports 2024-02-18 18:43:44 +01:00
96038a4f22 chore(auth): fix azure exception handler 2024-02-18 18:42:22 +01:00
5c4042e5f3 chore(oauth2): fix query function exports 2024-02-18 18:41:29 +01:00
c9f1bc4047 Merge branch 'oauth2' into 142-userdata-oauth-mode 2024-02-18 18:29:24 +01:00
bf13473954 chore(auth): rewrote authenticate (still WIP) 2024-02-18 05:06:23 +01:00
a0e7b2f96c chore(auth): work on authenticate 2024-02-16 03:25:36 +01:00
848890d3cd chore(auth): add more data to user upsert mode 2024-02-16 02:28:15 +01:00
f8bf02df2b chore(ldap): move and add more instances 2024-02-16 02:26:24 +01:00
1489c27121 Merge branch '140-admin-handler-fur-oauth-response-inspection' into 'oauth2'
Resolve "Admin-Handler für OAuth Response Inspection"

See merge request fradrive/fradrive!24
2024-02-15 16:22:12 +00:00
0c5f4cb430 refactor(settings): use better settings type names for user-auth 2024-02-14 02:02:42 +01:00
9597663881 chore(ldap): add more Ldap instances 2024-02-13 22:44:47 +01:00
7ed5e7a326 chore(model): use more specific (new)types for ldap model 2024-02-13 22:44:30 +01:00
1180ef6fd0 chore(ldap): add Ldap.Scope instances 2024-02-13 19:01:49 +01:00
2c3292cadf chore(model): add authentication source models 2024-02-13 18:22:00 +01:00
7803b753cb refactor(model): migrate auth models and model types to models/auth.model 2024-02-13 17:38:22 +01:00
David Mosbach
bbeebc641e chore(auth): new port offset calculation 2024-02-12 15:06:30 +00:00
42c97924ec chore: remove debris 2024-02-11 17:41:22 +01:00
29fc201294 chore(auth): authenticate against new InternalAuthHash in internal login AuthPlugin 2024-02-11 17:40:46 +01:00
938423b832 chore(auth): AuthTagLDAP -> AuthTagExternal, AuthTagPWHash -> AuthTagInternal 2024-02-11 17:39:42 +01:00
54f2430b3e chore(model)!: separate user authentication data from User table; add ExternalAuth and InternalAuth models 2024-02-11 17:36:57 +01:00
2e47df00b9 refactor(model): rename module Model.Types.Security -> Model.Types.Auth 2024-02-11 01:44:18 +01:00
223ae0f2f8 refactor(messages): rename campus error messages 2024-02-10 16:34:37 +01:00
cc8bd19f85 refactor(ldap): CampusUserError -> LdapUserError 2024-02-10 00:27:36 +01:00
David Mosbach
3f5a22c85d chore(auth): update oauth2 mock server 2024-02-09 17:38:35 +00:00
12fe58fc81 chore(model)!: move user authentication data to new ExternalUser model 2024-02-09 18:17:43 +01:00
David Mosbach
fafa25a7b5 chore(auth): auto start oauth2 mock server in develop 2024-02-03 21:10:24 +00:00
David Mosbach
d4cfce317d feat(auth): formatted output of user queries 2024-02-03 20:48:32 +00:00
ac045fdc70 chore(auth): oauth2MockServer->azureMockServer 2024-02-01 20:53:55 +01:00
a85a5be4cd chore(auth): mockPluginName->apAzureMock 2024-02-01 20:51:31 +01:00
1d7b46b4a4 chore(npm): remove oauth2-mock-server 2024-02-01 12:20:47 +01:00
David Mosbach
453034100b feat(auth): admin handler can query user data 2024-01-31 14:32:49 +00:00
9c608070ae chore(db-fill): add missing user fields contd 2024-01-30 22:08:55 +01:00
aa81de74a4 chore(db-fill): add missing user fields 2024-01-30 22:02:48 +01:00
d9ed893b52 chore(application): fix ldapPool setup 2024-01-30 21:54:46 +01:00
dfa774f655 chore(users): campusUser->ldapUser 2024-01-30 21:54:20 +01:00
608d8a3661 chore(users): add missing azure id field for UsersAdd 2024-01-30 21:53:58 +01:00
3c4e6b62fb chore: fix constructor names 2024-01-30 21:53:30 +01:00
f39de71c02 chore(jobs): upsertAzureUser on synchronise user job with azure config 2024-01-30 21:52:30 +01:00
24dbaf36bc chore(form): add uuidField 2024-01-30 21:51:25 +01:00
43bf25a5bd chore(azure): implement azureUser variant 2024-01-30 21:50:56 +01:00
f4b8417deb chore(messages): add admin azure message 2024-01-30 21:50:19 +01:00
c8350722a4 chore(ldap): migrate more campusUser usages 2024-01-30 14:01:54 +01:00
af09e02801 chore(lms): add missing user fields for fake user 2024-01-30 13:52:33 +01:00
8e2a98c12b chore(foundation): fix ldap auth and user lookup 2024-01-30 11:42:45 +01:00
1cdb20eb60 chore(ldap): fix user lookup types 2024-01-30 11:20:44 +01:00
David Mosbach
c8fa509ace feat(auth): tokens can be stored & refreshed 2024-01-30 05:06:06 +00:00
David Mosbach
5a023a9e32 chore(auth): added function for user queries to auth servers 2024-01-29 21:34:39 +00:00
David Mosbach
2763d2012a chore(auth): provide oauth2 test users yaml 2024-01-29 00:45:43 +00:00
264aaab24c chore: campus->ldap 2024-01-28 20:05:52 +01:00
c65dc04e8f chore: add missing AuthAzure case 2024-01-28 20:05:28 +01:00
a1ba004efa chore(messages): add message for Azure auth kind 2024-01-28 18:37:59 +01:00
514bca5257 chore: rename setting 2024-01-28 18:37:28 +01:00
9cbc35c263 chore(users): add azure id to AddUserData 2024-01-28 18:32:36 +01:00
84d7890ae4 chore(auth): oauth2User->azureUser 2024-01-28 18:32:14 +01:00
aa893062f1 chore(ldap): refactor ldapLogin type 2024-01-28 18:16:10 +01:00
d4a3459adf chore: user sources 2024-01-28 18:06:30 +01:00
David Mosbach
8acfc1d10c feat(auth): integrated oauth2 mock server 2024-01-28 12:53:00 +00:00
e9bbeffd7e chore(auth): campusLogin->ldapLogin 2024-01-28 12:45:59 +01:00
7e3e772055 chore(foundation): use multifunctional authenticate 2024-01-28 12:45:44 +01:00
471982d245 chore(application): reimplement ldapPool startup 2024-01-26 23:32:45 +01:00
3eec9ef8df refactor(jobs): ldap->userdb messages 2024-01-26 23:32:10 +01:00
ff5b31929e refactor(jobs): ldap->userdb 2024-01-26 23:31:13 +01:00
12bb8b7145 chore(foundation): loosen tight ldap<>failover coupling, move campusUser to ldapUser 2024-01-26 23:29:50 +01:00
2e005a90f2 chore(foundation): remove failover from ldap pool conf 2024-01-26 23:27:52 +01:00
843ac60aae chore(auth): oauth2->azure 2024-01-26 23:27:13 +01:00
a42ccb0faa chore(auth): campus->ldap 2024-01-26 23:26:53 +01:00
c929d42ebd chore(foundation): rename auth exceptions 2024-01-26 23:26:00 +01:00
4051d1e11b chore(settings): refactor userdb config structure 2024-01-26 23:24:40 +01:00
71af64dc28 chore(model): add AuthAzure 2024-01-26 23:22:58 +01:00
74f044919c chore(model): add azure primary key 2024-01-26 23:21:33 +01:00
9dc6ec461c chore(settings): simplify/flatten userdb config settings 2024-01-23 02:59:25 +01:00
1f31fe8cf2 chore(settings): add support for multiple modes for userdb 2024-01-23 02:16:06 +01:00
d56c9c3c31 Merge branch 'oauth2' into 142-userdata-oauth-mode 2024-01-22 10:36:43 +01:00
55ed01cb40 chore: improve settings, rename old ldap settings 2024-01-19 23:23:23 +01:00
Sarah Vaupel
9f299c854c chore(settings)!: rename userdb app settings 2024-01-19 14:53:00 +01:00
Sarah Vaupel
35902daff6 chore(settings): add default value for oauth2 scopes in yaml 2024-01-13 01:19:58 +01:00
Sarah Vaupel
31f657a15f chore(settings): fix oauth2 config json parsers 2024-01-13 01:14:54 +01:00
Sarah Vaupel
7946e046e2 chore(settings): update settings.yml 2024-01-13 00:42:25 +01:00
Sarah Vaupel
7ca12d064d refactor(settings): enhance field names 2024-01-13 00:40:57 +01:00
Sarah Vaupel
5e85eae825 refactor(settings): move ResourcePool, Ldap and OAuth2 settings to separate modules 2024-01-12 23:24:58 +01:00
Sarah Vaupel
3e9e90ed86 chore(settings): restructure Settings.hs; add OAuthConf to AppSettings 2024-01-12 17:14:42 +01:00
David Mosbach
a67697d159 chore(admin): added oauth2 handling widget 2023-12-18 02:58:14 +00:00
David Mosbach
ce8aa849f8 chore(admin): oauth2 admin form identifiers 2023-12-18 00:56:50 +00:00
5c4f742745 chore(admin): add basic admin route stub and navigation for response inspection 2023-12-13 16:36:52 +00:00
7b7b82cba3 Merge branch 'oauth2' into 140-admin-handler-fur-oauth-response-inspection 2023-12-13 14:52:32 +00:00
David Mosbach
cf89722c7f chore(auth): enabled ldap lookup for oauth2 creds 2023-12-04 00:32:01 +00:00
David Mosbach
44d082f8b9 feat(auth): added azure & mock server to login widget 2023-12-03 23:23:44 +00:00
David Mosbach
9b9370fed0 feat(auth): WIP authorization function 2023-12-03 15:06:39 +00:00
David Mosbach
2351388826 feat(auth): WIP support for OAuth2 2023-12-03 03:49:20 +00:00
aa41004c39 chore(release): 27.4.49 2023-11-09 10:21:10 +00:00
29df39f3b5 Merge branch 'fradrive/company' into test 2023-11-08 17:03:01 +00:00
de005691f1 chore(release): 27.4.48 2023-11-03 16:59:25 +00:00
050516c0bc Merge branch 'fradrive/company' into test 2023-11-03 16:58:31 +00:00
e63c8751eb Merge branch 'master' into test 2023-11-03 15:36:04 +00:00
2a4158303e chore(release): 27.4.47 2023-10-27 23:49:40 +00:00
1797d4eb9b Merge branch 'fradrive/company' into test 2023-10-27 16:39:18 +00:00
307cda543e chore(release): 27.4.46 2023-10-26 17:14:40 +00:00
de19073e11 Merge branch 'fradrive/company' into test 2023-10-26 17:14:08 +00:00
18af65da10 Merge branch 'master' into test 2023-10-26 08:12:52 +00:00
45048ce62d Merge branch 'fradrive/company' into test 2023-10-24 16:15:32 +00:00
bc4594bea2 fix(build): comment planned model changes 2023-10-23 08:02:03 +00:00
e4883c62d0 chore(test): ensure test branch uses different filenames and idents 2023-10-20 16:49:08 +00:00
6e5a58aa37 Merge branch 'fradrive/company' into test 2023-10-20 16:46:30 +00:00
d495a31ad8 chore(qualifications): thoughts on the prerequisite modelling 2023-09-25 06:48:49 +00:00
101 changed files with 8033 additions and 5540 deletions

272
.gitlab-ci/sanitize-docker.pl Executable file
View File

@ -0,0 +1,272 @@
#!/usr/bin/env perl
use strict;
use warnings;
use Data::Dumper;
print "Sanitize script for node removal from container.\n";
system("pwd");
{
my @l = (".","..");
for(1..8) {
push @l, (("../" x $_)."..")
}
for(@l) {
my $cmd = "ls -ld $_";
print "running: $cmd\n";
system $cmd;
}
}
my $tmpdir = "tmp-sanitize";
die "Has already run, abort" if -e $tmpdir;
mkdir $tmpdir;
chmodWrap(0755, $tmpdir);
chdir($tmpdir);
system("ln -s ../uniworx.tar.gz .");
system("tar xzvf uniworx.tar.gz");
chmodWrap(0755, '.'); # tar can change the rights of '.' if it contains an entry for '.' with other rights
my %truerights = ();
storeRightsMake7(".");
#print "=== Extended rights:\n";
#system("ls -l *");
#resetRights(".");
#print "=== Reset rights:\n";
#system("ls -l *");
sub chmodWrap {
my ($mode, $fn) = @_;
my $tries = 0;
die "file '$fn' does not exist; cannot change its permissions to $mode" unless -e $fn;
RIGHTS: {
chmod($mode, $fn);
my $ismode = (stat($fn))[2];
my $fm = $ismode % 512;
if($fm != $mode) {
if($tries++ > 20) {
die "Problem with file permissions, abort"
}
warn sprintf "File rights were meant to be set, but were not updated properly for file '%s', is %03o but was set to %03o; try again in 1 second";
sleep 1;
redo RIGHTS;
}
}
}
#
sub storeRightsMake7 {
my ($pwd) = @_;
my $dh = undef;
opendir($dh, $pwd) or die "Could not read dir '$pwd', because: $!";
while(my $fn = readdir($dh)) {
next if $fn=~m#^\.\.?$#;
#perl -le 'my $dh = undef;opendir($dh, ".");while(my $fn = readdir($dh)) { my $mode = (stat($fn))[2];my $fm = $mode % 512;my $fmo=sprintf("%03o",$fm);print "$fn -> $fmo" }'
my $fullname = "$pwd/$fn";
my $mode = (stat($fullname))[2];
my $fm = $mode % 512;
#my $fmo = sprintf("%03o",$fm);
$truerights{$fullname} = $fm;
chmodWrap(($fm | 0700), $fullname);
storeRightsMake7($fullname) if -d $fullname;
}
}
sub resetRights {
my ($pwd) = @_;
print "Resetting rights to:\n" if '.' eq $pwd;
print Data::Dumper::Dumper(\%truerights);
my $dh = undef;
opendir($dh, $pwd) or die "Could not read dir '$pwd', because: $!";
while(my $fn = readdir($dh)) {
next if $fn=~m#^\.\.?$#;
#perl -le 'my $dh = undef;opendir($dh, ".");while(my $fn = readdir($dh)) { my $mode = (stat($fn))[2];my $fm = $mode % 512;my $fmo=sprintf("%03o",$fm);print "$fn -> $fmo" }'
my $fullname = "$pwd/$fn";
printf(" set rights of '$fullname' back to %03o\n", $truerights{$fullname});
chmodWrap($truerights{$fullname}, $fullname);
resetRights($fullname) if -d $fullname;
}
}
sub renameWithRights {
my ($from, $to) = @_;
print " rename file '$from' to '$to'\n";
my %oldrights = %truerights;
%truerights = ();
while(my ($k,$v) = each %oldrights) {
$k =~ s#^\./\Q$from\E#./$to#;
$truerights{$k} = $v;
}
#my $rights = $truerights{$from};
#delete $truerights{$from};
rename($from, $to) or die "Could not rename '$from' to '$to', because $!";
my $waittimer = 20;
while(-e $from || not(-e $to) and $waittimer-- > 0) {
sleep 1
}
die "rename file from '$from' to '$to', but it is still there" if -e $from;
die "rename file from '$from' to '$to', but there is no file under the new name" unless -e $to;
#$truerights{$to} = $rights
}
print Data::Dumper::Dumper(\%truerights);
#exit 0;
# Checksummen:
# outerjson c27f -- toplevel $outerjson.json, by sha256sum $outerjson.json
# imageid d940 -- toplevel verzeichnis mit der layer darin; doc says: Each images ID is given by the SHA256 hash of its configuration JSON.
# we'll try as configuration "remove nodejs $oldhash"
# or we just use a random number ;)
# layertar fd3d -- doc says: Each images ID is given by the SHA256 hash of its configuration JSON.
#
##### FOUND
# outerjson c27f64c8de183296ef409baecc27ddac8cd4065aac760b1b512caf482ad782dd -- in manifest.json
# imageid d940253667b5ab47060e8bf537bd5b3e66a2447978f3c784a22b115a262fccbf -- in manifest.json
# imageid d940253667b5ab47060e8bf537bd5b3e66a2447978f3c784a22b115a262fccbf -- as toplevel dirname
# outerjson c27f64c8de183296ef409baecc27ddac8cd4065aac760b1b512caf482ad782dd -- as toplevel filename
# imageid d940253667b5ab47060e8bf537bd5b3e66a2447978f3c784a22b115a262fccbf -- in $layerdir/json
# layertar fd3d3cdf4ece09864ac933aa664eb5f397cf5ca28652125addd689726f8485cd -- in $outerjson.json
#
#
##### COMPUTE
# toplevel
# outerjson c27f64c8de183296ef409baecc27ddac8cd4065aac760b1b512caf482ad782dd $outerjson.json
# b21db3fcc85b23d91067a2a5834e114ca9eec0364742c8680546f040598d8cd9 manifest.json
# 238f234e3a1ddb27a034f4ee1e59735175741e5cc05673b5dd41d9a42bac2ebd uniworx.tar.gz
# in $layerdir/
# 028c1e8d9688b420f7316bb44ce0e26f4712dc21ef93c5af8000c102b1405ad4 json
# layertar fd3d3cdf4ece09864ac933aa664eb5f397cf5ca28652125addd689726f8485cd layer.tar
# d0ff5974b6aa52cf562bea5921840c032a860a91a3512f7fe8f768f6bbe005f6 VERSION
#
#
# sha256sum layer.tar fd3d3cdf4ece09864ac933aa664eb5f397cf5ca28652125addd689726f8485cd
my ($outerjson, $imageid) = ();
{
my $dirh = undef;
opendir($dirh, '.') or die "Could not read dir '.', because: $!";
while(my $fn = readdir($dirh)) {
next if $fn=~m#^\.#;
if($fn=~m#(.{16,})\.json#) { # it shall match on hash sums but not for example on manifest.json
$outerjson = $1;
next
}
if($fn=~m#^[0-9a-f]{64}$#) {
$imageid = $fn
}
}
}
die "Bad archive, could not found expected files and directories" unless defined($outerjson) and defined($imageid);
#system("pwd");
#print "will run: sha256sum $imageid/layer.tar\n";
my $oldLayerdir = qx(sha256sum $imageid/layer.tar);
#print "oldLayerdir is for now $oldLayerdir\n\n";
$oldLayerdir =~ m#^([0-9a-f]{64}).*$# or die "layer.tar not found or sha256sum not installed!";
$oldLayerdir = $1;
# tar --delete --file layer.tar nix/store/cdalbhzm3z4gz07wyg89maprdbjc4yah-nodejs-14.17.0
my $layerContent = qx(tar -tf $imageid/layer.tar);
my @rms = $layerContent=~m#^((?:\./)?nix/store/[a-z0-9]+-(?:nodejs|openjdk|ghc)-[^/]+/)$#gm;
print "rm <<$_>>\n" for @rms;
system("tar --delete --file $imageid/layer.tar '$_'") for @rms;
### Deconstruction finished, now lets put everything together again after fixing the checksums
my $newImageId = qx(echo 'remove nodejs $imageid' | sha256sum);
$newImageId =~ m#^([0-9a-f]{64}).*$# or die "sha256sum not installed!";
$newImageId = $1;
my $newLayerdir = qx(sha256sum $imageid/layer.tar);
$newLayerdir =~ m#^([0-9a-f]{64}).*$# or die "sha256sum not installed!";
$newLayerdir = $1;
# new outerjson is computed later, as we first have to change its content
sub cautionWaiter {
# some file operations give the impression that they are not instant.
# Hence, we wait here a bit to see if that fixes stuff
#sleep 5; # seems not to be the reason
}
sub replaceInFile {
my ($filename, $replacer) = @_;
return unless -e $filename;
my $fh = undef;
open($fh, '<', $filename) or die "Could not read $filename, because: $!";
my $content = join '', <$fh>;
close $fh;
keys %$replacer;
while(my ($k,$v) = each %$replacer) {
$content=~s#\Q$k\E#$v#g;
}
my $wh = undef;
open($wh, '>', $filename) or die "Could not write $filename, because: $!";
print $wh $content;
close $wh;
}
my %replacer = (
$oldLayerdir => $newLayerdir,
$imageid => $newImageId,
);
replaceInFile("$imageid/json", \%replacer);
replaceInFile("$outerjson.json", \%replacer);
cautionWaiter();
my $newOuterjson = qx(sha256sum '$outerjson.json');
$newOuterjson =~ m#^([0-9a-f]{64}).*$# or die "sha256sum not installed!";
$newOuterjson = $1;
cautionWaiter();
renameWithRights("$outerjson.json", "$newOuterjson.json");
$replacer{$outerjson} = $newOuterjson;
replaceInFile("manifest.json", \%replacer);
replaceInFile("repositories", \%replacer);
cautionWaiter();
renameWithRights($imageid, $newImageId);
cautionWaiter();
resetRights(".");
system("find");
unlink("uniworx.tar.gz");
system("tar czvf uniwox-rmnodejs.tar.gz *");
cautionWaiter();
print "Debug output, content of container:\n";
system("tar tzvf uniwox-rmnodejs.tar.gz");
cautionWaiter();
#unlink("../uniworx.tar.gz");
system("cp uniwox-rmnodejs.tar.gz ../uniworx-sanitized.tar.gz");

64
.ports/assign.hs Normal file
View File

@ -0,0 +1,64 @@
-- SPDX-FileCopyrightText: 2024 David Mosbach <david.mosbach@uniworx.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
{-# Language OverloadedStrings, LambdaCase, TypeApplications #-}
import Data.Text (Text)
import qualified Data.Text as T
import System.Directory
import System.Environment
import System.IO
main :: IO ()
main = getArgs >>= \case
["--assign", offsetFile] -> parseOffsets offsetFile >>= uncurry nextOffset
["--remove", offset] -> removeOffset offset
_ -> fail "unsupported args"
parseOffsets :: FilePath -> IO (Int,Int)
parseOffsets offsetFile = do
user <- T.pack <$> getEnv "USER"
let pred x = "//" `T.isPrefixOf` x || T.null (T.strip x)
tokenise = map (filter (not . pred) . T.lines) . T.split (=='#')
extract = map tail . filter (\u -> not (null u) && user == (T.strip $ head u))
((extract . tokenise . T.pack) <$> readFile offsetFile) >>= \case
[[min,max]] -> return (read $ T.unpack min, read $ T.unpack max)
x -> print x >> fail "malformed offset file"
nextOffset :: Int -> Int -> IO ()
nextOffset min max
| min > max = nextOffset max min
| otherwise = do
home <- getEnv "HOME"
offset <- findFile [home] ".port-offsets" >>= \case
Nothing -> writeFile (home ++ "/.port-offsets") (show min) >> return min
Just path -> do
used <- (map (read @Int) . filter (not . null) . lines) <$> readFile path
o <- next min max used
appendFile path ('\n' : show o)
return o
print offset
where
next :: Int -> Int -> [Int] -> IO Int
next min max used
| min > max = fail "all offsets currently in use"
| min `elem` used = next (min+1) max used
| otherwise = return min
removeOffset :: String -> IO ()
removeOffset offset = do
home <- getEnv "HOME"
findFile [home] ".port-offsets" >>= \case
Nothing -> fail "offset file does not exist"
Just path -> do
remaining <- (filter (/= offset) . lines) <$> readFile path
run <- getEnv "XDG_RUNTIME_DIR"
(tempPath, fh) <- openTempFile run ".port-offsets"
let out = unlines remaining
hPutStr fh $ out
case T.null (T.strip $ T.pack out) of
True -> removeFile path
False -> writeFile path $ out
removeFile tempPath

24
.ports/offsets Normal file
View File

@ -0,0 +1,24 @@
// SPDX-FileCopyrightText: 2024 David Mosbach <david.mosbach@uniworx.de>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
# gkleen
-1000
-950
# ishka
-949
-899
# jost
-898
-848
# mosbach
-847
-797
# savau
-796
-746

View File

@ -2,6 +2,879 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
* **model:** separate user authentication data from User table; add ExternalAuth and InternalAuth models
* **model:** move user authentication data to new ExternalUser model
* **settings:** rename userdb app settings
### Features
* **auth:** added azure & mock server to login widget ([44d082f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/44d082f8b95ad1b2d1ee0e9ce71d84dfbcd23df4))
* **auth:** admin handler can query user data ([4530341](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/453034100b38540a884ebfa4d46fdba04cf90b77))
* **auth:** formatted output of user queries ([d4cfce3](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d4cfce317d00714404ea3640cae8ad25182594b0))
* **auth:** implemented single sign out ([b947037](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b947037ea29bb721f3c5cade28ba606ad3e9e26f))
* **auth:** integrated oauth2 mock server ([8acfc1d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8acfc1d10c740766b55d5315fa6b413dcad50df5))
* **auth:** link to sso test from dev login widget ([9564646](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/956464659eccdb519ad9a18ca05b908486904b27))
* **auth:** oidc based sso for auth protected routes ([fbe0e37](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fbe0e37d281e19bcdbb926eb9a128c69186dd596))
* **auth:** tokens can be stored & refreshed ([c8fa509](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c8fa509ace7cc0746ac1df5678b49e393f39d397))
* **auth:** WIP authorization function ([9b9370f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9b9370fed0f55098163b55d88aea5fd55ffd736c))
* **auth:** WIP support for OAuth2 ([2351388](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/235138882650f54410154243cc6c61122e556d6d))
* **sso:** redirect to login when auto-sign-on is enabled and user is not authenticated ([2aa64f7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2aa64f7360ec9fa66a2be1868ddcc7aa8abea71d))
### Bug Fixes
* **acs:** fix overzealous avs error catching resulting in unnecessary error messages ([fa5fd98](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fa5fd98619895191156d19a77897342e247c531e))
* add missing do ([55319c8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/55319c8c5060a0d8763abb56c27d30e852c51f52))
* add missing translations ([d798dc4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d798dc48be5dc182ac3acb9efe46f556a7e95d17))
* **add-users:** fix and refactor confirm post param handling ([727d78c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/727d78cabc01e9f520b7140336bdacebc6188e2b))
* **add-users:** fix confirm secret field decoding ([57c9535](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/57c9535733b9ca2888a01d940a4a8888ca342c97))
* **add-users:** fix typo in message ([5e02c99](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5e02c99c44783672a66f2562bc92d14287b0ff48))
* added check in async table and removeddebug log output ([f807e2a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f807e2af78aa0d1d135990d764df9da89a0e61d0))
* added uw-enter-as-tab to CCommR subject field ([93a829b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/93a829b81b45639b0841bac72dc57416b50ef01c))
* **admin-tokens:** avoid option none ([af3ec98](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/af3ec98de512f72220d363b9dd0c06532ae1a960))
* **admin-workflows:** fix workflow definition descriptions forms ([f9d933b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f9d933bdacaf21618da9dc74e7bd6bea5e369aa7))
* **admin:** minor fixes and translations for admin problem page ([30fae33](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/30fae33dedb1501e570e9edca288fea3c84ac84a))
* **aform:** show info about required fields in all aforms ([63f6d01](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/63f6d016191fd1529ad7545b795bd4d174e6586a)), closes [#418](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/418)
* **allocation-list:** fix default sorting ([9eff3cf](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9eff3cfa10806f90d655fb32f72116e30020afab))
* **allocation-list:** fix sorting ([33d9bac](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/33d9bacc8aa3e412e88251a76a389d19fd148210))
* **allocation:** don't restart cloneCount when allocating successors ([e1c6fd4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e1c6fd43b807abd3126b7ae8b948f585416f883c))
* **allocation:** fix allocation-results notifications ([ed700a3](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ed700a34295525cb760d3afa975238171fbaeda5))
* **allocations:** better explain capped allocation bounds ([a890e34](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a890e346c8f76fb2fb9467910085d4d41a40b7d8))
* **allocations:** better handle participants without applications ([05d37fb](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/05d37fbc0ce4edd737b86b6c7646bcd16dbf1746))
* **allocations:** don't show all allocation information to lecturers ([ad6c503](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ad6c503ef56a97fc8210e06a1c4dd9e0c19ae949))
* **allocations:** fix allocation-course-accept-substitutes ([b4df980](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b4df98069982752e36e69571f5557a6179b44cff))
* **allocations:** fix behaviour of "active" dbTable-filter ([b694a09](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b694a093d56c034df69be4805c76a84155871165))
* **allocations:** fix result notifications ([bb6703d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/bb6703de47cfca156be4e763a3cf5c32ec27f389))
* **allocations:** notify for new course upon registration ([9e0b43a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9e0b43a60d26a05f6e1b9d4dae2b2f75dd52fff1))
* **allocations:** show assignment green ([9d62b3a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9d62b3a79e7566cb2d32f8e4147a2237146a0c3a))
* **allocations:** work around yesod weirdness wrt "none" ([4a731ec](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4a731eca4e69b5ee080f229a602e76f5ae165c64))
* allow deregistering from full courses ([d7e1e67](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d7e1e671abd7504556f977d26de0af6ea2d56444))
* **apc:** apc cannot distinguish ij from ji, partial fix only. Needs new font ([b4ba0a3](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b4ba0a30dc7c513bb9e3c567ca771d5d75de4343))
* apply margin-left to both ol und ul ([c0d319e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c0d319e0fde393f543da5ee585e1e89802188ecf))
* **arc:** actually invalidate ([ef4734e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ef4734ebb671d9ef19c284a4c5cc9412d6e62874))
* **arc:** reduce lock contention ([1be391f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1be391f5f5bf2588939fea92809dd629c0a69d99))
* **assign correctors:** also show names of unenlisted correctors ([de49a77](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/de49a777ebc31463893555720ffc2d07cb618ab5))
* **assign-submissions:** avoid division by zero ([640326c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/640326ca5de12c21f87fe728bde69c21d8444320))
* async table js util now knows current random css prefix ([cc90faf](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/cc90faf7320de85fe9ace1b4b9dfc36f4f53fd14))
* **async-table:** bind callback in updateTableFrom call ([cd3e72c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/cd3e72c0f1389a80786df1f4e2433a2152cf3d55))
* **async-table:** fix condition for uw-async-table class ([9a87730](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9a87730517f019987f8dbd93e7496c7b8c459758))
* **async-table:** update legacy call to datepicker ([d56e12d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d56e12d2070ea81f0f082df5b30914234cdda3c1))
* **async-table:** uw-async-table instead of .uw-async-table ([a5d9bfc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a5d9bfc1a29ca7b6bfd3c20ba157e563e9f21e36))
* **audit:** add missing submission edit ([537e66e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/537e66e4877027b858d6ecc55d12ee40e87928b7))
* **auth-caching:** submission-group ([896bd41](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/896bd41e3b415283cce16cb84a8219b8d4c1702c))
* **auth:** authorize exam offices by school ([946a42b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/946a42b7f01016652d03dd214fdf2bc7202ab8ab))
* **auth:** fix infinite auth loop for workflow files ([21cf6cf](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/21cf6cfa873b841c2f9f8ab9f69c08ea72fc2420))
* **authorisation:** inverted logic for empty ([65814c0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/65814c005e2637bb5f6347bf1f35133654538e7a))
* **authorisation:** keep showing allocations (ro) to lecturers ([c8e1d51](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c8e1d51e252e037daa72aaf058239091694af74a))
* **authorization:** have AllocationTime consider ParticipantState ([b69481e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b69481e88fb20890b4ece7a0023dcfdad21604d6))
* **authorship-statements:** resolve exam-part to exam properly ([3a2d031](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3a2d031bb5f5b4d6e5df06f8ec82957a1bc81a72))
* **auth:** prettier active directory errors in help messages ([b631ed7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b631ed7d0620748fd833c4cda4b421dc147d0906))
* **auth:** properly restrict various auth by school ([6f04a6b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6f04a6b693e99b573efcc94023dab0be4d6d83bb))
* **auth:** tutors may see sheet list ([e0c05f3](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e0c05f39d4c162dc793745325b69807a59df0c5e))
* **auth:** wrong caching for external-exam-staff ([9d1f1c6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9d1f1c691085ec65ad0f19cc51602a59ee133fc4))
* avoid subSelectForeign join issues ([576fccb](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/576fccb5222a5dbd19db69f142a39b4155b7486d))
* **avs:** attempt to fix avs background jobs ([bbaa42e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/bbaa42eefaaae88982b091973adb295cdc0e80ff))
* **avs:** avs background synchs and lms userlist result no longer block handler ([0beb0e4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0beb0e4011745ea51906e018c53548bb2f6d978e))
* **avs:** background avs synch yielding undefined due to wrong monad ([2e59d3c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2e59d3c2ea4d5017be9b4e578b7da12c4da0e2fa))
* **avs:** background synch was only triggerd by manual synchs ([48ef25a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/48ef25aa8ffbbd96c1578ae85b76f090d9042595))
* **avs:** chunk avs status query automatically ([352ee21](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/352ee215b4075c70dbf9229434e62c8e6d847ae4))
* **avs:** eliminate call to undefined in Esqueleto.Internals ([240c6f8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/240c6f81f81d1872317da01411fa67ec97e3b16d))
* **avs:** fix [#7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/7) by sequencing avs background jobs one after another ([6dc3d8d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6dc3d8d059e132d19c119c5f1de906342fdf6d2c))
* **avs:** fix tests (do not exit with failure on empty avs config) ([89aff47](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/89aff471528ad9002e309d50d706a412b7e67eb6))
* **avs:** import avs users without ldap entry ([850c52b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/850c52b496a28b6ecced6cb1275a6cf7705ec2cf))
* **avs:** incomplete config throws error ([1d3c278](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1d3c27868277c28350dfb087802bc1f7c6732aeb))
* **avs:** normalize internal personal numbers between LDAP and AVS ([b20008d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b20008d3bcb730ff76a76ce2928364e6ce9e7c35))
* **avs:** preserve unset pin passwords in update ([8c4f848](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8c4f848675e1125547d1fdfa05560affe4794118))
* **avs:** strip trailing whitespace from avs names upon import only ([d47e8c4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d47e8c4909f0f8a7a3f84b8beae4b5d4e223dcff))
* **avs:** update names from avs too ([e2a8fee](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e2a8feee3b186881fb2b323ed9fd9e0cc93787c8))
* better concurrency behaviour ([a0392dd](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a0392dd329c871ed855ee832ee97230d9c72d59e))
* better pathPieceJoined ([adcd5d5](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/adcd5d5aee3d541fbf65a532b81d86f236575b7b))
* better translation for "exam office" ([edbdceb](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/edbdceb74847f79cda0fd8087e2441a0eff3952e))
* **block:** negate condition to test ([9cf7f39](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9cf7f3965aa95f0b8f2a1574dbad90c0257edafd))
* broken dom ([02e8825](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/02e8825cbaad54b8a0b5a8e2bde25268488c5311))
* build ([7c86293](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7c8629300529d18554aac0cd66cf6bb13814337e))
* build ([071df90](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/071df906da6c41afa226f944a90c2f294eeba243))
* build ([9fd95d1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9fd95d181c498d460eaf30436ff110f7c1f9413e))
* build ([5c709f1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5c709f1bbb077d981fbd5d59e9c0f30cddbb468d))
* build ([cf33f0a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/cf33f0af84166b040de4b9a685a58d9884bc67f8))
* build ([68b8b45](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/68b8b458b1e02ec992df811cca2cda08a2f77d9a))
* build ([6322fd4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6322fd449bd83edf2e1909b95c6aa4c795d49a54))
* build ([b1641ad](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b1641ad57e036c33d12cc9002f2355231676f9d1))
* build ([43bb0ab](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/43bb0abe7218f6d43b52d9a64e62f0dc29b9972e))
* build ([23a21b9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/23a21b905c902bea5fe88abd84da600e757a194e))
* build ([fa61b46](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fa61b46d308753354623df17241b5312f324321e))
* build ([7147bb4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7147bb478db7ac039afd2004c9300304b1aa94ea))
* build ([5684213](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/56842134d3d68c22ee875cca18cd05f31087f83e))
* build ([f92e555](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f92e555de2555a030f1b52859f56181ac54ba78d))
* **build:** accepting linter suggestions ([d25dd64](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d25dd64eec0a75b8d0e53795341088835f34fd85))
* **build:** add missing file ([1fd24f6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1fd24f608dc9202fa98f52f7908f4be908a18efc))
* **build:** add some guards at calls to (%) for issue [#34](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/34) ([d8d75ed](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d8d75edafef3af6886de3048a566fb85419c1aab))
* **build:** bump version numbers for containers ([6678ddc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6678ddcf1aef920e2b04a491f086ac721cae07c9))
* **build:** empty avs config is ignored again ([1720e12](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1720e1229df80b0fe6cbecdef1cc0b5ce7d7c65a))
* **build:** fix botched merge in fill ([fb5cd55](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fb5cd5558c7db634497f2b7d97489fc023ce59d6))
* **build:** fix build ([49dc413](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/49dc4139cf900ae3b46c5946fb38ef6cad7d5cab))
* **build:** fix frontend vulnerability ([c2e3693](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c2e3693490c5ac870df7b701dc9499dfa69228b7))
* **build:** fix whitespace in routes ([a24e44e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a24e44efc9a20d3934d96640bb9e21b3b6d55b96))
* **build:** hlint did not like unnecessary monadic code ([acb52c5](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/acb52c51f429b35f91c9c8a491af6b353c9b5e75))
* **build:** linter complains ([ac57b1c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ac57b1cd32909c8314f3a3b15431d280911af643))
* **build:** major qualfication block quirks fixed ([ab48e40](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ab48e40ac7e5024b7847b3995e6ae16d1c401c60))
* **build:** merge ci/cd changes from gitlab ([30d5af0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/30d5af00bfe6cf946ecbc489726212178c2b794a))
* **build:** minor ([954a239](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/954a23936a35ea6c32247d7e191312e63888c12d))
* **build:** minor ([f9930f2](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f9930f2a00d1e0f0af9b7f2af7c387bcc09cef5a))
* **build:** minor errors firm handler ([06bb44c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/06bb44cf715375b5dd0141a46f8e10924ad6cd9c))
* **build:** minor move parenthesis to make linter happy ([02bf1d9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/02bf1d9a2ca433e55cf7d1e06f0ff300b53c7efb))
* **build:** no change, just retry merge pipeline ([9e156f4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9e156f407d00c85ec5212ca08b4efb446ddee8b0))
* **build:** package-lock.json changed somehow see issue [#18](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/18) ([3caf0d5](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3caf0d55f75d43b7a25a32b0383ab66022cd9fc3))
* **build:** prevent migration on non-existing table ([5bb49cd](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5bb49cd88941e510a50759efaad88690f841ca47))
* **build:** reactivate optimisations and llvm backend ([172181f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/172181f1c6d7673de5422374ba5738c3eb4f7983))
* **build:** reduce container size by removing LaTeX, Pandoc ([d5a2dd0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d5a2dd07fc7465a3745f48ad706f0994e5142751))
* **build:** redundant parenthesis ([50eda5f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/50eda5f65f7394fe519546609fe748490cb4dd72))
* **build:** refix test commits somehow ([34ada53](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/34ada53de0cc5804468791854e824b730fcc84de))
* **build:** remove impossible ([90b38ca](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/90b38ca5dc319f2d175978242b2fdd4477568a3c))
* **build:** remove obsolete import ([a5d5d8d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a5d5d8dbd668c6c35019ce14e8b5e59f5374bf80))
* **build:** remove redundant constraints ([ea82d75](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ea82d75a0934f8e13f26af5cb8a06c11d32dc0c5))
* **build:** remove redundant import ([a35341d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a35341d4b77bb564bd026073915b986eacd65bd8))
* **build:** remove tests for workflows ([bb696d0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/bb696d0332839f0f95c940c56db6249585ba65fa))
* **build:** remove traces of wflint for removed workflows ([3a089d9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3a089d957b7af5149fd132ba8e0c791d93f83ac4))
* **build:** revert nix flake config to obtain container ([99b6724](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/99b672483a2e0ead889693c4460aa1b2864c11ec))
* **build:** schools.model examDiscouragedModes default contained whitespace, which is not allowed ([9ee7ec8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9ee7ec8d7ab74832d10b6384490b9d20263f49b9))
* **build:** tests were overzealous ([ed3ca8c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ed3ca8c3d6949c22a3918f3b1832ea06e1300cf7))
* **build:** update frontend hash ([74c361d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/74c361d7dec8f93863d1b0e91f89cbd3f646777e))
* **build:** Update ParticipantInvite.hs ([f888da3](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f888da3ab0df45bb3c515ebb7cbb43569fdaa1fa))
* **build:** Update ParticipantInvite.hs ([fa4f9b2](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fa4f9b24475261afc1e534541c8878a85e6a1b10))
* **build:** Update Utils.hs ([87f0b2e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/87f0b2edab2bcf696b7b776e47272ef2204c0b75))
* **build:** user basic texlive package with required packages only ([cba748e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/cba748e94e69e1839432d33d81fa81d4159fd76f))
* **build:** v2 ([ac77aa1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ac77aa176a3c3977c4a802e5ed534fa2850528fe))
* **build:** weird build error, probably whitespace in routes ([11cc45a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/11cc45aacf0546a24498b01a896aa58fd28150a0))
* **build:** while the blank is necessary to prevent unnecessary migrations, it is not allowed either, see [#133](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/133) ([a4b2af7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a4b2af7f157444ead8c9df989741b266f7c2b4f2))
* bump changelog ([60a7bb2](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/60a7bb2b194e47e924f56d9f461f07e17cea3f5e))
* bump changelog & translate ([a75f3eb](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a75f3eb2f14d3a392294fc85b0fcc1efbd75dbd1))
* buttons know about ALL actions from other buttons ([11664dc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/11664dcd82c13eef1c395e2e590c4fb0c587aa65))
* **cache:** atomicity & workflow instance invalidations ([ef7fde9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ef7fde937ebf1bc31e3706fba1da166bb82133c5))
* **cache:** remove risky caching for submissions ([4ae59fc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4ae59fc1fa658e1462139ddddd6dc80308d85872))
* **campus-auth:** properly handle login failures ([ec42d83](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ec42d834ee627401849910c44ed18ee696c8fc76))
* **campus-login:** add i18n for ident placeholder ([692e533](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/692e533da0380337838c63e32370fde060905ac7)), closes [#417](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/417)
* **campus:** fix corner case with study features ([76098cc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/76098cc3c84e1e51cfadc381347aae483d62dbeb))
* changed DEBUG_MODE back ([cc63f63](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/cc63f636e928c9071bbc3208b77aa20d88800a17))
* changed enter to tab behavior in CCommR ([7aeb8e6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7aeb8e61f46dcc5c918df20450df2fdf1d10f217))
* changed keypress to keydown. ([9288e5c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9288e5c203a88368ec853ba7001290df87069fc6))
* **changelog:** add date ([52a88f8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/52a88f8fadcc6a115a569842197fa0b06367c368))
* **changelog:** try not to crash on unknown changelog items ([850c8d4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/850c8d4dae47489e0dbf0eb46276eaf0002bf123))
* **changelog:** update changelog ([fa5358a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fa5358a91da997d041be6110b7a0482886dfe52f))
* check if number of relevant user is >0 to prevent crash ([317b95b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/317b95be317ea038ad9fa398fc0c0c456b53495d))
* check space of occurrences after ignoring ([fabf56c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fabf56c1640c94f806d43aaca264100cbc39b840))
* **check-all:** fix column collection ([9935efe](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9935efe96689e8208630523424f1eba285b77db0))
* **cicd:** remove wflint step, update .gitlab-ci.yml file ([a07c66c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a07c66c5ab622b6c80e28e056f3e5b73da317198))
* **communication:** make communication form more intuitive ([7a2b972](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7a2b972f9f78817688b344ac269ba99694f0854a)), closes [#387](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/387)
* configure sessions to be strictly same-site ([a7e64bc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a7e64bce7b3961c05e568d365381097e9239a7fb))
* correct (switch) sheetHint and sheetSolution mail templates ([d6f0d28](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d6f0d28a1fe7f9b3b01a05d177c1e604e893fa8f))
* correct rebase-sourced error ([02589e4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/02589e4d00de233d847d6be71e44f9fc451fbfe9))
* **correction assignment:** correcting lecturer's names are shown now ([16c556b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/16c556b852501a5e6c88094556f5054c4d4f352b))
* **correction-upload:** better error messages wrt rating files ([8bb3bc5](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8bb3bc50a24664d7a9425e83822c19592cd35056))
* **correction:** comment column made wide in online correction form ([d83b1f6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d83b1f696f0ebdc631bd460e87c762d16fbc3ade)), closes [#373](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/373)
* **corrections-grade-r:** add get following post ([14f9ab6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/14f9ab6a31147290ca5e6d446baf641ddc317f40)), closes [#532](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/532)
* **corrections-grade:** fix inFix ([2c2dd8d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2c2dd8d3ff3334df675972bafc367349e13d7ef5))
* **corrections-overview:** behavioural fixes ([e10cfe9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e10cfe9c581e7b3b9ca93aced2293592a57ac78b))
* **corrections-r:** allow filtering by matriculation ([1b6b781](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1b6b781e82c39bc29c8984c587ac836f0da77a02))
* **corrections:** properly link corrector emails ([9385595](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/93855957e62b7764f001a83a77419fdeb465326b))
* correctly apply suggestion ([67d6fd7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/67d6fd7d438a31b50e6f4e6e921873ee11b32e9c))
* correctly calculate maximum user name length ([cd07a56](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/cd07a56a9fd3ee99b74e5304581574671e3689a0))
* correctly handle original minimizeRooms-flag ([d5bd504](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d5bd5042ad920b26df847845cc437c3f0616575c))
* correctly report NoUsers for ExamRoomRandom ([16cbc78](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/16cbc78878615a8d123de5d8fda11136685a824c))
* **corrector assignment:** sheet tabel mixed up columns sorted ([d07f53e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d07f53e1d8db10407d26d4fdd6e7c4c8a34b973d))
* **corrector handling:** show correctors by a consistent order ([9c5ed5f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9c5ed5f8424b36723e4ef483b85db305e7c6cfa9))
* **course and exam registration:** distinguish registrations buttons ([ad825b6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ad825b66b80dc6676c2f881f70630ac293162aec)), closes [#416](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/416)
* **course list:** show complete registration span ([754d6ca](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/754d6caa1ba056de70ba5fa868a1a4f3976876f9)), closes [#446](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/446)
* **course-application:** better display of priorities ([64f7715](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/64f771518ede9e1b11aaeeaaeb3a2e7d449a13ed))
* **course-applications-csv:** record rating time ([c2c6974](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c2c6974a7700e4d9bad92c4477f64ad2b1eed5a2))
* **course-deregister:** only delete relevant users exam results ([3997857](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/39978574feff5c1a761f84ab4e040dd53cc7f739))
* **course-deregistration:** fix check on exam registration ([0b8c30f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0b8c30f534eb07d75fce44b16d5af68f4ab41863))
* **course-edit:** additional permission checks wrt allocations ([fca5caa](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fca5caaa3137f3e8a11d76fbacf4d0ed0f1b78dd))
* **course-edit:** edit courses without being school-wide lecturer ([d7d1f27](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d7d1f273036a2dba5113c04319cdf075fbdb829c)), closes [#464](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/464)
* **course-edit:** expand rights of allocation admins ([7f2dd78](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7f2dd7808ebca01a2e2b857b67d8ebcbd12492b9))
* **course-edit:** improve instructions ([9d53730](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9d537307c23f5385effd359b26cfd695102dd955))
* **course-edit:** only show allocation error message when relevant ([00a6ca8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/00a6ca83bcc096075b65a70cf18860c1d7bf5a6b))
* **course-edit:** show old allocation ([fc53497](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fc53497aa330e50dc6c61a864cd417e0bb0c5b30)), closes [#450](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/450)
* **course-news:** fix permissions ([9e5fde9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9e5fde902724ad50a8b79519a798f53c6945e786))
* **course-news:** prevent display of edit-functions unless auth'ed ([89cc9ad](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/89cc9ad35e34d2938746b4ef5b86c8473417988b))
* **course-register:** swapped warning message ([32c0605](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/32c060575ce4d17430b7dc6432da1cb6b138dcb9))
* **course-show:** show display-email for correctors & tutors ([a2e3699](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a2e36995ea4699718ef88f7b9ecda8e8264a9fd0))
* **course-teaser-css:** class name fixes ([8a92985](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8a92985e16f569f6478c06772e6d89f5f4b78590))
* **course-teaser:** don't collapse unless chevron is clicked ([fca99be](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fca99bebe63b6e63c4597d7becb2e273ec40260d))
* **course-user:** handle allocations when deregistering single users ([ef5bb70](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ef5bb70b652a739db4eefc5e663f804414a43ce8))
* **course-users:** add missing dbt sorting ([1bc14c9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1bc14c9e19c5c165c735f9c3d00062fb1dcf6077))
* **course-users:** deregistration w/ allocation & w/o reason ([4f237e1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4f237e19682d489bdfc87ee6a1b18dac7eb99bfa))
* **course-users:** insertUnique and only count and audit true inserts ([1325ff2](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1325ff2a95d75c6fc1cb9f1f2eb4d1e464aa34ad))
* **course-visibility:** (more) correct visibility check for favourites ([796a806](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/796a8066aaac4d4c2789b65563672fff0d07dfe5))
* **course-visibility:** account for active auth tags everywhere ([c99433c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c99433c291bda25b45bf0c36ca8469324a71202e))
* **course-visibility:** allow access for admin-like roles ([7569195](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7569195c8b26c02eeea85aa95e7c3952041b4e98))
* **course-visibility:** allow deregistration from invisible courses ([29da6e2](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/29da6e2ac51b0dd2e02fb114f5b5db75a4f0da30))
* **course-visibility:** allow for caching Nothing results of getBy ([f129ce6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f129ce6b2b12e4cfe856f1de728873868c7a0bba))
* **course-visibility:** check for mayEdit on course list ([b1d0893](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b1d08939930c56fdc0efaf0e133a5feffa1cee64))
* **course-visibility:** correctly count courses on AllocationListR ([7530287](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/753028778833ea9972d37c973fe1f339512774e8))
* **course-visibility:** fix favourites ([1ac3c08](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1ac3c08d01056e0d4b8c7f740ebd78eb19bc73a7))
* **course-visibility:** rework routes ([7ce60a3](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7ce60a36f4c28ef6d4ff6f93015444dc62ff27d5))
* **course-visibility:** show icon to lecturers only ([cbb8e72](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/cbb8e7217d2ec256fff0b09eed1665c7f7d30c1b))
* **course-visibility:** visibility for admin-like users ([43f625b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/43f625ba0c009519f7e37b833e33a99f1ac97069))
* **course:** add links between users & applications ([edaca1b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/edaca1b394b2eb624f4c2bedd4fcc7ef91b5c4f6))
* **course:** better explanation for material access ([78c5bc5](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/78c5bc5258c9305deafac18b010dc6a41e5ea864))
* **course:** don't delete applications when deregistering ([b666408](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b6664089f75dcb3b2c89dbd2941c064e8aa86404)), closes [#648](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/648)
* **course:** fix [#28](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/28) by allowing course deletion with inactive participants only ([9dfd91b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9dfd91b2f864424cf940259890afb8bf8cb0fdcf))
* **course:** grant qualifications now issues and unblocks ([5d8d8cf](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5d8d8cf17e634ecb950a1c329c859fb93f94ef77))
* **courses:** better defaults for application/registration ([1c2c8fe](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1c2c8fe3d99176e079d0473dd45039b44128c491))
* **cron-exec:** consider lastExec before executing job ([43833db](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/43833db3e110cad1224ee9599992cec64f24b8a4))
* **cron:** consider scheduling precision in all time comparisons ([4ded04b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4ded04b80df94a4655af52b64cbcd156394286fd))
* **cron:** disallow jobs executing twice within scheduling precision ([bc74c9e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/bc74c9ef10d8d5e115679f9b1ac43ea69030dd8e))
* **cron:** time out sheet notifications ([d5a897c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d5a897c020cb0e6ef05a85a4f683210ce7c44069))
* **cron:** work around extraneous sheet notifications ([cbe211b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/cbe211bf2377a3c41644000f562c2dc353aac01c))
* csp-sandbox downloads ([50cbba1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/50cbba114a850469ed6893e697d0c329c8e894e0))
* **csv exam import:** ignore unchanged noshow and voided ([a346524](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a3465240731423e3ea5d7f85f8f8c73935166b76))
* **csv import:** csv import preview help text adjusted ([b7321df](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b7321dfbc54eb58e405ceebd9150683aa23095de))
* **csv import:** fix spelling and expand help text ([2c57a77](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2c57a77316e41bdac940747111009fa2f9b7d43d))
* **csv upload exams:** allow ambiguous harmless study fields ([7d2937c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7d2937c71df50e2fdd1346629d2fc1ca0016cf57))
* **csv-export:** mime confusion ([8bdaae0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8bdaae0881fe98c4c5f69f1332ac2ffb0ca83081))
* **csv-import:** fix incorrect map merge ([0d283fd](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0d283fd9e5ebfd2cabfc70eecd9d53fc1fc87e31))
* **csv-import:** major usability improvements ([2dc6641](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2dc6641e68f73f048ee9187e1f6ccd924870577f))
* **csv:** ignore empty lines ([211ff5e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/211ff5eacc83bb47e564dd88e11bc18ae7e0a6af))
* **csv:** less quoting in semicolon separated lists ([42f1eab](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/42f1eabb2c984a7d30ea8b90710c68aff8af9f97))
* **cvs:** export company in e-learning view ([2093cf5](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2093cf501827ab2305f26ab5cf742f2b0be4a7de))
* date formatting ([0af3b87](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0af3b87a474544231f8c277ed2fdb421aabfe86a))
* **datepicker:** close datepickers on focus loss ([3f9ca5e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3f9ca5e230f0d8b5f83b5fc35c18fca6afcae6f7))
* **datepicker:** close datepickers on focusout or click outside ([7fa0124](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7fa0124fe25a53caa07c478e157dace3c4f2a6ee))
* **datepicker:** close on focusout of elements in document only ([ee0edc7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ee0edc7d21393e217f88486be56a9d79f542a510))
* **datepicker:** fix for empty or browser-filled inputs ([3c24e5f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3c24e5f187a2d516948b9f11c8b46fd8dd111b33))
* **datepicker:** fix selecting date from manual input in internal format ([8bdcc92](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8bdcc9254ef681e0d6061cb69f8c48d392384351))
* **datepicker:** fixes [#456](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/456) ([613426b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/613426b27459a84fc7ea9c8e85a729ba4a43e625))
* **datepicker:** format time on copy paste as well ([99d9efa](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/99d9efa9465f68d55fdbc276207987b7289302bd))
* **datepicker:** handle output format when reformatting ([09622bd](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/09622bdb12c08957d750121fbd8b9d1f1631530d))
* **datepicker:** hide number input spinners in datepicker ([2073130](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2073130867ffa10c3f9469643355b8dfb67fa413))
* **datepicker:** increase datepicker z-index in modals ([593a6a7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/593a6a72d2750135dbe6348b53f48b18db6032b5))
* **datepicker:** insert datepicker after the form ([b590995](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b5909950930b1bff36d5834d5fc8f9e6ca65d0e6))
* **datepicker:** manually add scroll offset based on scroll target ([3ecf834](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3ecf834756bb6b12fe8c335370b9988a2d8e4ab0))
* **datepicker:** no manual positioning; update tail.datetime ([3cd71d6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3cd71d6b197428729d2bf7bede676df855eafef3))
* **datepicker:** partial focusout and click fix ([434c0da](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/434c0daa239dc6d502f55be39783edd75c96703e))
* **datepicker:** quickfix to fix datepicker position in modals ([3f9454a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3f9454a7ef4ff2303827b692c52151d07a96c67d))
* **datepicker:** removes idle cancel and submit buttons ([805676f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/805676f2b9141318e488cbb2509346dcd23715eb))
* **datepicker:** select time from preselected date on edit ([d3375bb](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d3375bb2c150611e891d883674831ede574ad346))
* **datepicker:** workaround for new Date(..) inconsistency ([d24ebf8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d24ebf81455aa049a1c621d3e3c06befd08c45a2))
* **datetime:** remove redundant constraints ([9258ba7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9258ba766577a4a573ab70f87bdaaa38ad674ea6))
* **db:** migration qualification block ([3d59527](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3d595271d979f29ed8bbc546f495e5ad1deae5ca))
* **db:** prevent superfluous migrations ([b73557a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b73557a1eee4315911c6369032447f8d1836d964))
* **dbtable-ui:** fix position of submit button for pagesize ([cf35118](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/cf351180dcaa39a3af69f26291af6dc058dce01a))
* **dbtable:** calculate height of header correctly ([5659f2d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5659f2df1e6ea473794075d85f2a43fc1037fce9)), closes [#634](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/634)
* **dbtable:** fix pagination bug ([b43f236](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b43f2364bbb4d9fdf8f3d4c2540246cf16a6be7a))
* **dbtable:** improve sorting for haskell+sql ([fd8255d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fd8255de8ca5f3e8f9faa9170a45297ea021c9d5))
* **deletion:** fix usage of deleteR from POST handler ([c87c9c1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c87c9c13d1e1ab1957d59467ae0e97a5dd8ee1a1))
* design tweaks ([18ae758](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/18ae75890a83d82f49343778d18185258e1985a4))
* design tweaks ([68eb448](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/68eb44884ebeec8a48ac962c53bf7c2d813cd3a9))
* **displayable:** fixed faulty display of db keys (SchoolId, TermId) ([c7312e8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c7312e8ec662eea25dd7f8d6ff36c28af890ec59))
* divide by zero ([674b949](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/674b94938812e93958e991367c261337fe64f2f6))
* do not add async-table class to empty tables ([b8e2911](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b8e2911e49c2880cd493f2df872f9c050cb5c570))
* do not apply target link height fix on targets in tables ([e7ff384](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e7ff3846f2b758e6fa1c10a5157eef1747750d38))
* **docker:** remove missing docker dependency ([1ac35e0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1ac35e0bf816ccdccaf984ee5fe28b329c9cb258))
* don't set user-last-authentication during ldap sync ([fdaad16](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fdaad16e713e69a7b47f80a690a97d2ff5eb9986))
* don't treat ExamBonusManual as override ([16abcd2](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/16abcd2265136b63e28dbde252f44c94417d0aff))
* **downloads:** do download links via redirect ([3ba41d8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3ba41d8f24b4358ad7a045eba0f630e1e2b67663))
* **eecorrectr:** encrypt eeid ([5d9ca45](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5d9ca454fa5353009c33678e21d9f49bd45f6cc3))
* **eecorrectr:** use default time ([3369155](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/33691556abd40041a1707e3f902cfcf5d513342d))
* **eexamlistr:** allow access for users with exam results ([885de44](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/885de4403c0172b3e9c3b59c277628106a7e925b))
* **email:** avoid sending to invalid email address ([71ccac0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/71ccac0dfe3e165f5ea674165f0cf2ed740b1c84))
* **email:** better wording for qualifcation expired notice ([412c56e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/412c56e78ceaef263e4ca8b8678bb0e8ea2efb9a))
* **email:** ensure sending to valid emails only ([3865afb](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3865afbceb69f8941c25c814abf855b4b035201a))
* **email:** instead of sender set reply-to only ([4c8f7e1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4c8f7e1267fe50196d664d733eb794ffaf55aa1c))
* **email:** invert invalid email error indicator ([731d0ce](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/731d0ce7c70533039d4ef82e8eaf366368c69403))
* **email:** reenable ldap logins with invalid email addresses (missing mail field problem) ([88a85bb](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/88a85bb5b63470a0fb117a67cbf8c597c96f6904))
* **email:** remove test for E#[@fraport](https://gitlab2.rz.ifi.lmu.de/fraport).de ([7c2226e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7c2226e138addc2154c58f98233d7c875d2ab0f9))
* **email:** rename settings parameter and switch to safe default ([5aa096f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5aa096f56acb37269b681abafef67b8a375f4d64))
* ensure termination for non-{'A'..'Z']-names ([873d5a0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/873d5a02adae8f33db349bd9de3c7bd49331d27f))
* **errors:** better handling of errors from separated approots ([833b674](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/833b674c31ef3d4bf3b9b1af13201f33c98ef82f))
* **exam add users:** correctly differentiate and fix messages ([a473599](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a47359997cd12a8a3053df5aa459c3f7d2c59cfd))
* exam auto-occurrence by matriculation ([3ef10d9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3ef10d98a174ce6c9fb5e7aaf2f2642073ea65c9))
* **exam grading keys:** Fix spacing ([24aacef](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/24aacef6af65d1c6c0cec53bd121abe1d889de2d))
* **exam import:** inactive registered features may be selected ([3c4172c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3c4172cbc2abb8b692241cc7fe73b62384c92a94))
* **exam participant download:** fix icon not being shown ([a075b16](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a075b1648eb9b49c37f8b6f228ae38e83baaf9fe))
* **exam registration:** icons added to exam register message ([ce61528](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ce615287180976d24f24abd16ec8bac79a4a881d))
* **exam-bonus:** avoid divide by zero if all sheets are bonus ([0fd7e86](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0fd7e86695f47047c9e4e1bb8efe9477103707ab)), closes [#671](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/671)
* **exam-bonus:** fix rounding ([854fa6b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/854fa6b968ed01d60dea0d9ba6fc93d37e5ec361)), closes [#672](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/672)
* **exam-correct:** add additional exam result td; table layout ([af32789](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/af3278912c960ad405a749ebd0bbd39c77f664a8))
* **exam-correct:** add XSRF token to post header ([2fd996b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2fd996be220228807548bbebab6f9531335879d6))
* **exam-correct:** add XSRF token to post header ([2b30461](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2b3046164f1c503dcd7fc558fce4fe8fc052c892))
* **exam-correct:** also persist local time on non-success ([41a9539](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/41a9539c27c32a5a867173bd9295380c1d497b25))
* **exam-correct:** also persist local time on non-success ([dcb79d4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/dcb79d4cb8c1cb96f0af61eeab57181cbe20063b))
* **exam-correct:** correctly htmlify user on failure ([ef34755](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ef3475539b3c144da67399ed29eb3af22ee51eb1))
* **exam-correct:** correctly htmlify user on failure ([595f46d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/595f46d860f3f893cdeb6618e361d533b5c90b5c))
* **exam-correct:** cut off at maxPoints for now (TODO) ([af8d77c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/af8d77c4a4d97cdc66b7f2e09568c1481768aa73))
* **exam-correct:** different values for examResult options ([aa794c0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/aa794c06e0f8819582d52f2089d26f8b781a718b))
* **exam-correct:** fix addRow rowInfo ([88768eb](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/88768eb1d184cc545da8a41566c893ea81a03c03))
* **exam-correct:** fix addRow rowInfo ([792da22](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/792da2220466f9e6a880052613c35ea5246452cd))
* **exam-correct:** fix attended values and submit on only exam-result ([df0aaca](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/df0aaca759aadeba718f02e946828ad55fdd07b7))
* **exam-correct:** fix attributes in template ([62bf73a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/62bf73ac1f348e60767e7a687893d6ac1a397a0c))
* **exam-correct:** fix attributes in template ([000f97c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/000f97c270577194c1e1d5c5adae1969db28b7ea))
* **exam-correct:** fix hlint ([630194c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/630194c4c0f0c9bdacf7d7a2c10118b407bc9b21))
* **exam-correct:** fix hlint ([c520918](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c520918eb350adcfc7fbda2b6edb6b8915cc8ea7))
* **exam-correct:** fix request bodies ([0b186a5](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0b186a5e1a76c2d593a596188a6432a44a796dc5))
* **exam-correct:** fix result info and response handling ([cd479e2](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/cd479e2f0cedbc7e076d3768b919da7da8f769ba))
* **exam-correct:** fix returning null if old and new results are equal ([968c6de](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/968c6defa676abf5705e9d3bdd62248f592e3298))
* **exam-correct:** fix returning null if old and new results are equal ([2e7bca6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2e7bca6333deea697e98d4bd84a1aec6ef0c1f77))
* **exam-correct:** fix usage for non-lecturer ([dd7fe84](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/dd7fe84ffdf89780cf1476e2697d2d5ed3e82222))
* **exam-correct:** id on td instead of select ([1d0be2d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1d0be2d6828f1221110af71bee709612fc09f4b4))
* **exam-correct:** reintroduce examResults ([f7136bc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f7136bca152ef455e6e6ce38c9cfaa39b213370d))
* **exam-correct:** send correct results ([2ca56fb](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2ca56fb8b1fc653e165611af9f7beb3653854f79))
* **exam-correct:** temporarily disable exam results (WIP) ([533e748](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/533e7482c9baab10c6b82781f57e610f5ca45129))
* **exam-csv:** audit registrations/deregistrations ([a278cc5](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a278cc5048a3c15c393cca86ea12b3ea095ae65c))
* **exam-form:** allow finished without start ([fbc3680](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fbc36806b13a7d674be3aa64a4c6491e8f051587))
* **exam-form:** sort occurrences and parts ([6d47549](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6d475497c0caee49ad34c5c3c6e7b1bf91ca0ba2))
* **exam-office:** better logic for isSynced ([cb9ff32](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/cb9ff32063046871edd7fe84729d5ea175e132f0))
* **exam-users:** don't crash when participant doesn't have bonus ([0fa910a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0fa910ae7c440b94a153fd48cf0585cd034fad4a))
* **exam-users:** make csv import much more lenient ([2ddb566](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2ddb56640fd0b5ac6bc7757e03b1819007cabd3a))
* **exam-users:** prevent exam results without registration via csv ([1c6ac4c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1c6ac4cb4a52ac7e69e615e0e3ff96432b173962))
* examAutoOccurence no longer user >100% of a room ([eaf245b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/eaf245beaaa1f739d6b857712f1e4ea5b53e7c82))
* **exam:** fix warning message ([60869fd](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/60869fd4a0fdb3efd2565c3b02aa45fa0f9d4eb5))
* **exams:** allow occurrences after exam end ([3d63b35](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3d63b355eb15daf858b554afe41932b117b00dd7))
* **exams:** better behaviour for optional statements wrt school default ([fe78377](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fe78377fae8af7766f9720628aebef599656ed2f))
* **exams:** change heading to rooms if no occurrence times are shown ([5cb9404](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5cb9404b7d2eaaf674a9f80ca323f052b2a1d3eb))
* **exams:** cleanup exam interface ([05e7b52](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/05e7b52f08354b9a83d5db1be9c084955a8cba97))
* **exams:** correctly treat school-mode optional as off by default ([ac86832](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ac86832b34a605e5d64d56ef08a871bf307347a8))
* **exams:** default exam mode to Nothing ([4b459ea](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4b459ea1430a4947364562f1a9881596325696ad))
* **exams:** don't show manual bonus as inconsistent ([fb54c84](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fb54c8445aa8b9762b6a8e2b4dd18ae379b80d2e))
* **exams:** error messages for foreign key constraint violations ([ca29a66](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ca29a66330a977a1f28bbdbe9a733aef10371427))
* **exams:** exam-auto-occurrence introduced spurious MappingSpecial ([a1d5479](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a1d547990df712f1866113dfeedc01d573d730c5))
* **exams:** fix caculation of maximum exercise points ([a9e74ca](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a9e74ca4af31e6f392fc79ae30d2a771800828d3))
* **exams:** fix form validation wrt non-empty statements ([0082135](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0082135c56b7fc0e5db3af6910f8365e12920c46))
* **exams:** Fix registration ([1684da0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1684da07f2352f76df7cef3bd7b33aa32a8dda97))
* **exams:** fixhance exam authship form section ([4109db6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4109db6f815fbb49c861177b3caecb98c2a963d8))
* **exams:** include bonus points in sum for exam participants ([2bc6894](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2bc68946e379e7d29a85148017ca9fd14b01ab18))
* **exams:** make examClosed a button ([530a8c6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/530a8c688e93ad25a5c4e3691cb64003e3a3676c))
* **exams:** prefill with school authship statement in optional mode ([0cd8f4c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0cd8f4c02f383f43b5e3ea059cd3acd38595ab56))
* **exams:** provide bonus information in return of examBonusGrade ([731231d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/731231d5eae5ab91361fec09686c2d1977ab9bae))
* **exams:** remove deprecated/unnecessary form validation wrt. authship statements ([bf059a1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/bf059a132094e53c3ef956582b5e13517e9c133d))
* **exams:** set use-custom correctly if forced ([8bb6140](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8bb61401a77f20fcb35aa05401bf16285aad1d93))
* **explained-selection-field:** support linebreak in titles ([627a2df](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/627a2df7adf41651e698d8cd9d632d066fc2f868))
* **failover:** don't always record as failed ([16643b6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/16643b62447315362e25922f817b92c50d754a43))
* fallback for determining user email ([6a1a256](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6a1a256cc2ee6928131422afea6a61017fd50272))
* **faqs:** mention mail to set password ([32097d1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/32097d18f9d5411e1bf8a3923ad8f04dcc7b4c83))
* **faqs:** wording ([02d284f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/02d284fb876af3256ef2bd82b92c40f3be36c446))
* **favourites:** always move current course up ([56d89d7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/56d89d7f5840033985f1b053920ebe8a6fdc3dc7))
* **favourites:** clear old favourites when changing max number ([92fb6f2](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/92fb6f2270d06a8a6520be10cb58e8fad3d7ecda))
* **fe-async-table:** Emulate no-js behaviour when handling pagesize ([28dcc8d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/28dcc8dc377c5a7c942daad7743f473208f5b99c))
* **fe-check-all:** use arrow fn to keep scope in event listeners ([09e681e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/09e681eeb1ec77a1f8c263978322026683aae031))
* **fe-deflist:** avoid horizontal scroll on pages with deflist ([16d422d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/16d422d9d82467ea6e6800bee5d1af06b7fe1d3b))
* **fe-i18n-spec:** fix tests ([339fa39](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/339fa398b48eefd7b208ede9036a35795185725e))
* **fe:** style notifications acceptably for now ([fc80f08](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fc80f087242201ddcf2509dbed1f6034dc5ea73e))
* **file-jobs:** improve log messages ([e099e13](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e099e13816d2ca79cbcc6a84fe970052980c0feb))
* **file-upload-form:** don't check case of file extensions ([6c49c50](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6c49c509ac6819bb2a46345b3f75c29ecae50e3b))
* **file-upload:** fix inverted logic for when upload is required ([3868e8f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3868e8feaeb0ab7aa9cbdac46c0f80bee68b891a))
* **file-upload:** size limitation was inverted ([de53c80](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/de53c80a1e715d0346ef2d28710b01dea16af905))
* **files:** allow clobbering files during form submission ([a60ad1a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a60ad1abae341283b1342018f57f7fc736c239a6))
* **files:** better configuration for file batch jobs ([3a90c88](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3a90c88b359f3e0cb0ed03df6e81b1532509ea48))
* **files:** count personalised sheet files as alive ([e54b985](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e54b985815fbbc637d8f4681ac55b3d46e2263a3))
* **files:** don't inject serializable ([2ca024b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2ca024b9351df800b57d3235c4a00776cd669952))
* **files:** fix download of non-injected files ([ce54adc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ce54adce6b67f3de95d65d74ff62b36cccdba47e))
* **fill:** correct term start day guessing ([538aa5b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/538aa5b3b9f0741e1dba80cd9e2ba70adfce1938))
* **fill:** minor testdata fixes ([59a7e1c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/59a7e1ceb285412bd7e61cb79b4c9e64a2ecdc81))
* filter submission by not having corrector ([3bded50](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3bded5071bf0e32a6d4df0dcc49f6039f283785e))
* **firm:** add sql indices for frequent filters to greatly enhance performance ([63e6d94](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/63e6d94df2fd1ce879cb59d14bc854f3c2556586))
* **firm:** firm messaging now works fine ([65cdc8d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/65cdc8ddfef19eb3a5578c536575f91ba9717a13))
* **firm:** foreign supervisor counts correct and sortable ([601ce7a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/601ce7abdf2a392d30f1ff799a2338968be795f1))
* **firm:** group multi select field supervisor ([fc0ca7b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fc0ca7b854a686cf395dadf81b7423e530fd26b8))
* **firm:** improve supervisor filter by caching ([88f24fe](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/88f24fe6f199290a83af2d204ba9aa2a838d11b8))
=======
* **admin:** minor fixes and translations for admin problem page ([30fae33](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/30fae33dedb1501e570e9edca288fea3c84ac84a))
* **avs:** background synch was only triggerd by manual synchs ([48ef25a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/48ef25aa8ffbbd96c1578ae85b76f090d9042595))
* **avs:** preserve unset pin passwords in update ([8c4f848](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8c4f848675e1125547d1fdfa05560affe4794118))
* **build:** minor errors firm handler ([06bb44c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/06bb44cf715375b5dd0141a46f8e10924ad6cd9c))
* **build:** redundant parenthesis ([50eda5f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/50eda5f65f7394fe519546609fe748490cb4dd72))
* **build:** while the blank is necessary to prevent unnecessary migrations, it is not allowed either, see [#133](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/133) ([a4b2af7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a4b2af7f157444ead8c9df989741b266f7c2b4f2))
* **cache:** remove risky caching for submissions ([4ae59fc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4ae59fc1fa658e1462139ddddd6dc80308d85872))
* **course:** fix [#147](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/147) abort addd participant aborts now ([d332c0c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d332c0c11afd8b1dfe1343659f0b1626c968bbde))
* **db:** prevent superfluous migrations ([b73557a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b73557a1eee4315911c6369032447f8d1836d964))
* **doc:** minor haddock problems ([d4f8a6c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d4f8a6c77b2a4a4540935f7f0beca0d0605508c8))
* **firm:** add sql indices for frequent filters to greatly enhance performance ([63e6d94](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/63e6d94df2fd1ce879cb59d14bc854f3c2556586))
* **firm:** firm messaging now works fine ([65cdc8d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/65cdc8ddfef19eb3a5578c536575f91ba9717a13))
* **firm:** group multi select field supervisor ([fc0ca7b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fc0ca7b854a686cf395dadf81b7423e530fd26b8))
* **firm:** improve supervisor filter by caching ([88f24fe](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/88f24fe6f199290a83af2d204ba9aa2a838d11b8))
* **firm:** improve supervisor filter yet once more ([c7b5a3c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c7b5a3c6cb70c314ecbfbe25969b4b6be1d43161))
* **firm:** restrict firm access to company supervisors only ([0a06efd](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0a06efd76c63180c996657c2c7d78efc5bddd83d))
* **firm:** sending messages works, but not test messages ([42ff02d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/42ff02d27e431a8855db7bf3046a1b74d297e6da))
* **firm:** set supervisor field not all fields required ([9878956](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9878956716b04c7ae88989cb9b059d3edcb923dc))
* **firm:** show default supervisors with no employees too ([0f9a7a8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0f9a7a8c53d216ca7a6d0a25462b19ab1fa00bb4))
* **firm:** supervisor changes led to inconsistent DB ([1d3345c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1d3345cbba1cb65ee49c6f62e145750545439642))
* **firm:** supervisor filter ([3acb847](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3acb847915010d10358ea02000c231dbba7cba26))
* **firm:** supervisor filter performance ([db77850](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/db77850c4f4cd1d68bfd38e02e0ae24584e1e556))
* fix .dual-heated.degenerate ([6058692](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/605869204fdf89d11b9dbd07edb02ab31a11cde5))
* fix [#571](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/571) ([aefb7e0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/aefb7e0b426fb2319e098add3231fc5274aeccc4))
* fix app frontend test ([49bafe1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/49bafe1276e13f0961e9d327b6506b97de39d079))
* fix build ([69f4a80](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/69f4a80dc18c58e5980251d3932b469004abe8c1))
* fix build ([d13ace4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d13ace4eddb581167544de5e5f788ab6d5836041))
* fix build ([1a66716](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1a66716e8afee4b1f34785e0743590ee0cab0830))
* fix build ([caf4092](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/caf4092d12ce1785bf153a372609f6d64b9c19cd))
* fix build & minor refactor ([bb9b4f0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/bb9b4f06ae135e9af8b3333b42e78ff38baab3d8))
* fix collision with keyword "none" ([203dbd3](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/203dbd3705717660cb2bef3a735eb10ce0085155))
* fix course duplicate message & name -> title for courses ([d87e8b7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d87e8b7142d879eb0a5d320332e967e1c19a1b33))
* fix creating new terms ([9676615](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9676615c55cbecce02dca95b01ad69c1a2455f1d))
* fix form-notification styling ([0226593](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0226593450502165933529e926293031cbd620ee))
* fix grid blowout on definition lists ([3cb3dcd](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3cb3dcdc9b5e20bc4a64ac787773fdd22edc3580))
* fix hlint ([e60aef4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e60aef4f8bbf4b81a66b4a8c4b6769744890f3f0))
* fix hlint ([9ecffc8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9ecffc8d8c843441365e871a50fa74cfa36abd5c))
* fix hlint ([37f0936](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/37f0936d91b59b66b8c261b0e25676d6ad1606c4))
* fix i18n widget files ([e517a8e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e517a8e4701f2df2f1a39e3ed16db0e5e3393ea1))
* fix merge ([d19cca6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d19cca6a400e41f8ac2733b474fbaa6cad2b48f6))
* fix merge ([38afa90](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/38afa901bab462b889ea036f142022afe4b32498))
* fix migration ([d2478a3](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d2478a3657a483dccc6852544097709bd3aa1f30))
* fix migration & tests ([e05ea8e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e05ea8ea8ccfa8a5bf0a97cf7d3273ba158938d2))
* fix rendering of weekdays ([94b87a2](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/94b87a2d0d0ca30477e0e63b2fcb4707ab1059c1))
* fix startup on unix-socket ([39f1295](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/39f12957f55256db74960b47be3797e881c525b8))
* fix tests ([a671937](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a671937868dd65c6a92ee58b9d47d296d502b0ea))
* fix tutorial registration group applying globally ([d2ba173](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d2ba173776a6caa84ae53e6f87245205e344fa40))
* fix webpack config ([50e4212](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/50e421222422d0d2a13c2284aaaa4cb10af922a6))
* fix webpack config ([5393a55](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5393a55482db21b5cb04ae3774d0f97e37789a3c))
* **form:** multiSelectField working with grouped options ([3aa8901](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3aa89019a8b4393da0eca715871a3793c1e3abb2))
* **frontend:** fix typo in navigate-away-prompt ([061349e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/061349efb0ce65079e01ca9420b1c4eaabf731c2))
* **frontend:** improve performance of table-related utils ([eff273b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/eff273bf090518d41ba46b8a52b91f20cf9d7074)), closes [#603](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/603)
* **generic-file-field:** allow .zip when doUnpack ([46e9908](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/46e99081d9bad67b42f09792ac12e8a5f7d72723))
* **generic-file-field:** better explain extension restrictions ([342c64a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/342c64a93acd557f69c5953876391157f6193174)), closes [#509](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/509)
* **guess-user:** fix ldap-lookup condition and refactor ([ad4ae71](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ad4ae713c8c87c13616582389fda05a6dba8d962))
* **haddock:** fix accidental haddock comments ([882ca7c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/882ca7c5828fbf42fa9d08a5cb545fc2fd8bbba5))
* **haddock:** hoogle.sh fails on a comment, turned into normal comment ([c6264f7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c6264f75b4034c76168f07c2731af1b77ae8b16c))
* **haddock:** merge haddock fix from master ([e6c4125](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e6c41250e8f9a8a75f6a1d76e7da36964adf1336))
* handle rare cases where a mappingDescription with start>end would be produced ([c99d96e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c99d96ecb8a43400eb10dfe192bf751cb00a9d25))
* have exam deregistration always delete stored grades ([24f428b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/24f428b13bb181bec99417b4e69fc538e35acbcf))
* **health:** correct file path ([6214448](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/621444829e8c51e114f219285fb0ebc7a43a1829))
* **health:** include compile time instead of version number ([8130eb6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8130eb6b7f6a14be6fa6bbab73eb47622736e28e))
* **health:** ldap check only admins ([f889ec6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f889ec674e35af24f8d33acf7102a2bee5dcf68b))
* **health:** monitor flush by check interval not flush interval ([03226ec](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/03226eca6aba91e2f10f5828a38f2a15d747dd0c))
* **health:** more generous healthchecks ([466203d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/466203d866ba92a2ee6fed8a01dcf1e610e6e896))
* **Help Widget, Corrector Assignment:** Modal Form closes in place; assign alerts ([89d5364](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/89d5364c937132a642d7b7960e90b73868fe56f4)), closes [#195](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/195)
* **hide-columns:** account for undefined element in isTableHider ([ee5a005](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ee5a0052a1b1ae1842ce968818f6fae2b2bd1150))
* **hide-columns:** bump storage manager minor version ([9053b87](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9053b878c98fcc8cccd5c1a6b241eb36c372f087))
* **hide-columns:** check for content div in isEmptyColumn ([615555e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/615555eb597bf4a87b83020275cce7f30495d0ac))
* **hide-columns:** correctly hide hiders of previously hidden columns ([364991c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/364991c42bbb82301dd71cee8823d061f92da1ad))
* **hide-columns:** fix crash if no row is present ([827cecd](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/827cecda8f4244861f05ec988c00929591c94bec))
* **hide-columns:** fix repositioning of table hiders onclick ([9d8ca38](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9d8ca38f2e7016b753d7951a12b4b10ac3089c9f))
* **hide-columns:** fix vertical positioning of hider and minor refactor ([3fbb4db](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3fbb4db962c6493a1da2c243850d65e4f735ab4b))
* **hide-columns:** improve positioning ([e371412](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e371412db48c600589800890477531706a6313bd))
* **hide-columns:** no hide-columns in tail.datetime ([03bcf56](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/03bcf56487c5bf363bf6d9f595b735208e3fa87f))
* **hide-columns:** remove debug text from template ([9e449dd](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9e449ddaed31aba904d7a6bbd367cc256180d237))
* hlint ([7e14fef](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7e14fef5c57acf436669a7cbe637d0d019810031))
* hlint ([58c933c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/58c933c62466aae823b7c61a2e80f9150a41fae8))
* hlint ([662943b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/662943b256e013c02317181f9a30f6b2f7bd606a))
* hlint ([5ea7816](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5ea781692671ed60e2851297b7eecd6ce666ec34))
* hlint ([908e6de](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/908e6def80d8ac2b65e6d5722607db7571c007ea))
* hlint ([4348efc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4348efca35c80720e6d49b55bcd6256ae52a0b55))
* hlint ([b0b92b4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b0b92b4b26310b0a41b45d6928fe469d47e5df5d))
* hlint ([c19f427](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c19f427cd76d09533fab8a24c8b54e1eefa9ed08))
* hlint & build ([036c74e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/036c74ef49d33efebaaecec16d2daa5900170f94))
* **holidays:** add proper memoization to yet unused function ([d2938e3](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d2938e3ae90def29736ccb884bfebfcd275cb4a9))
* **holidays:** minor improvement to memoization ([f411fde](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f411fde42d913d5a9ef357674ab77934c8e1ba42))
* **home:** fix build ([551c4cb](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/551c4cb23cb9243acca793b3430e8732754336bb))
* **home:** fix hlint and other minor bugs ([839251e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/839251ede34f99446e371c4119abca535cf0b834))
* **hoogle:** remove erroneous comment ([c011d88](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c011d887cece8338920355b540aa4b233e0b994f))
* hopefully improve workflow auth performance ([1d3fd8c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1d3fd8c8a7824d6c6d043f4114067238af4bdc6e))
* hopefully speed up aeson via ffi ([a00ba10](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a00ba10e9cf1ffa534908b9125730e88179052eb))
* **html-field:** introduce stored-markup ([e25e8a2](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e25e8a2f4ca65afc29acc8a3884df9acf77d4398))
* **html-field:** remove warning about html-input ([d0358b4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d0358b4a500c64c6a25558cf8f5179f0c385cd45))
* **html:** use non-breakable dash in menu and column translations ([56c3c8f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/56c3c8fe40bffec2c83f085ed5afae9af06b9d2f))
* i18n ([3dd6e21](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3dd6e21f8ec95ca9ebfe71e4a3dc9743ab7bdbc8))
* **i18n:** add missing translations ([773c6c5](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/773c6c5dc02ed5626f3a83e309a229538fd4b27f))
* **i18n:** custom language inference ([205d768](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/205d7688bf821b1c899b9f3b4d3759a9d89de3cb))
* **i18n:** fix typos ([8af256e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8af256e55dd2c23276c2894f734e80ee5b5712bb))
* **i18n:** get started on i18n-breadcrumbs ([268d9e0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/268d9e0b1cfc2a41079d42415fccc075ba909ee7))
* **i18n:** i18n for all widgets ([3fe278e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3fe278ec3087b7cb219a94c3b9fe28c0f3e204fa))
* **i18n:** i18n in various places ([155ed1d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/155ed1d557e8b12fc40ac7de4efc9987a4e560f8))
* **i18n:** missing translations ([14b1706](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/14b17068a02004266a13b9770172fc7a6697644f))
* **i18n:** missing translations ([d0ce45b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d0ce45ba318ba324845c513b45e776f0d03e7c4c))
* **i18n:** missing translations ([b6a2412](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b6a24127055757f8dfe33035cd098335be5d6df7))
* **i18n:** missing translations & changelog ([76663b0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/76663b057d58bb4cb531f80076954c443ce1dc92))
* **i18n:** missing workflow translations ([ed4ee13](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ed4ee1320bd2eaeb462a1e6b72b0f4ff24e447f3))
* **i18n:** prepare translation file for en-eu ([281c98f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/281c98fe916d1d44805605d5ffca3c6b3aecca36))
* **i18n:** rename i18nWidgetFiles to proper language code ([33ddbfb](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/33ddbfb7ccd648bf875152ba2c626c0220088bb2))
* **i18n:** s/Typ/Art/ ([0e43851](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0e438513368ea0b44726e7d7e1aeddac2a4183cc)), closes [#493](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/493)
* **i18n:** submissionDownloadAnonymous ([e6af788](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e6af7888825e66624b94f4af45ad4d698fc8d3ae))
* **implementation:** spaces ([53471d1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/53471d166273fd1a8ef3a4c49f8d39c8a329dfbd))
* improve async behaviour ([cc7a528](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/cc7a5289a4ef7965b3464bb826e6a1e32a5d2929))
* improve csv import explanation ([729a8e8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/729a8e8bcea420a856a2d577520e943968807ce1))
* improve exam occurrence ui ([83fa9c9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/83fa9c9c69a6e986c341e8283698f6c23ced2a90))
* improve explanation of multiUserField invitations ([954bb78](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/954bb78aae1316bbcc4e8145d63ae072c63beff8))
* improve hidecolumns behaviour ([9a4f30b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9a4f30b811fdf8c58ec5c50c185628eb3158931a))
* improve labeling of button to switch exam occurrence ([727b89b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/727b89bf4b80b1f5c83e3f6eeebe5c9da7226596))
* increase size of test instances again (oops) ([4e76fe7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4e76fe7e504515845d468fc3251a38c90aaaaf66))
* **info-lecturer:** Touch ups ([e1e26ab](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e1e26abbbffd0f30a9e53c1a73f5bff7b1cb4afc))
* **info-lecturer:** translate german headline ([069d15a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/069d15abcd4cb811a0aeb07ef1cb55ced97729c1))
* **info:** minor whitespace correction ([0ce4dd1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0ce4dd181c57788de71a400c0635088634155d7e))
* inherit authorization of CAddUserR in more places ([3391904](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3391904cff75cf9646d647ee15d907a8080d00ce))
* **interactive-fieldset:** fix behaviour for nested fieldsets ([65b429a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/65b429a320b6876a7d72d40935a7d49257ef77d7))
* **interval jobs:** avoid accumulation, reduce job size ([24491b4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/24491b446b870698564adb9718e868e082873539))
* invalidate nav caches ([e88b6d6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e88b6d6bab3ea4577af3cd9465e66aa7e48177a2))
* **job:** fix [#95](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/95) by implementing queued job deletion for admins ([5b9a554](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5b9a5545457dbe506d20f7362fb6e0d6bae4f7f4))
* **jobs:** adjust job handling to hopefully reduce load ([ed38f93](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ed38f93537b57b5b3e4563dc0259d805760071bc))
* **jobs:** better flushing, correct metrics, better etas ([e4416e7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e4416e7f0e2ea2cf9db0e61cf2d20c27260ccaf8))
* **jobs:** cleaner shutdown of job-pool-manager ([adc8d46](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/adc8d466ac0948dcddf601fac439bb4e8d3bf619))
* **jobs:** delimit resource allocation to within handler ([7038099](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7038099389fcca684a9e1a3f28f76629e0c194bd))
* **jobs:** flush only partially for reliability ([59c7c17](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/59c7c1766588052383754b16e575347fa960ad6a))
* **jobs:** implement job priorities ([e29f042](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e29f042229d397bb37b692621a10c66ff9489db1))
* **jobs:** improve job worker healthchecks & logging ([2a84edc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2a84edccb4cdfddc2bdc03ebdd2b934fd7f53884))
* **jobs:** more general no queue same ([b1143cb](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b1143cb12bea48d75a2453f92122edcfb4fe51f1))
* **jobs:** only write CronLastExec after job has executed ([67eda82](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/67eda82bbcaf768fadda9735455f3d260532ee09))
* **jobs:** prevent offloading instances from deleting cron last exec ([e61b561](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e61b5611b1568180aa3ccfc3e3b981eb9a13cd53))
* **jobs:** queue certain jobs at most once ([1be9716](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1be971677b25689a895734d9efa5898fcbf0ca08))
* **jobs:** reduce likelihood for multiple queueing of notifications ([970ca78](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/970ca784b0286a0f8341356e9769d9a80ab60903))
* **jobs:** use more read only/deferrable transactions ([db48bbb](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/db48bbb7765604aaab8f8d5c540793b1ceaff16a))
* **jobs:** wake more often during waitUntil ([6115b83](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6115b83bb32d767ef54f2a2ae210ad1c7415e69c))
* **jobs:** weaken crontab guarantees for performance ([212e316](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/212e316c7e256f7883e0b883942e98bf795d870b))
* **js:** fix i18n not loading ([a3ee6f6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a3ee6f6fa6cf90f3a4e1ef4bed6ff104412318c8))
* **ldap-failover:** improve concurrency & error handling ([da1bf86](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/da1bf86d5e72b59448037487cbc4bf5194f5aac7))
* **ldap:** allow ldap update for mangled user entries ([006ab63](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/006ab632a394ed8d566f7930e8f6fd5a6ba86814))
* **ldap:** allow punctuation in displaynames ([61cfdc8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/61cfdc8758dcc6ea8aabf52b9d0c0e1d61d6e522))
* **ldap:** fix type in department descriptor ([9697d8c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9697d8c7fa8bbe6db6418107acb21c387cd4672c))
* **ldap:** improve debug message ([9fafb0b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9fafb0b7c3a13209be8a8d37c0c99c46608c9a81))
* **ldap:** update phone numbers and company data from ldap ([991ee9c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/991ee9c704dea31e65bb347af582f8a81a72aca4))
* **legal:** move anchor targets to headings ([a5c98e0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a5c98e092d7e242010e7598e8317ca8c10c39849))
* **letter:** email wrapper for renewal letter reinstated in full again ([1c02b85](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1c02b85fa256302b01c18baa64c8d0b7f9ffb671))
* **letter:** renewal reminder and renewal idents switched ([064b984](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/064b984945288172c094485d8d3622d196333e0e))
* **letter:** update receiver postal address before sending ([7d5c4bf](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7d5c4bff2512154c087133e029713efa0657fa5a))
* **lint:** remove redundant parenthesis ([4956e6b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4956e6bf57458a5e76efc582f6127b84a055c4de))
* **lms:** accept success for no-status learners and print several more debug messages processing reports ([a7ed659](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a7ed659866de1d4a178bbe4e8f9cd8fbc629c724))
* **lms:** add safeguard to LmsUserlist dispatch running twice, thus ending LMS prematurely ([a8df40d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a8df40d9f8943f2e0c4e219074486dbbf0eaf0fe))
* **lms:** correct lms table column sorting key ([9ee4767](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9ee476736ccd361559edd3570dc27ba83ff8a334))
* **lms:** direct upload did not commit to DB ([e7cea4a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e7cea4aa6c5b05285e5c47d815067a4d45315024))
* **lms:** disable workaround for late lms success ([cb9e09d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/cb9e09d071d22f41a92ab8140d7aaa643c748373))
* **lms:** do not mark lms users with open status as ended ([a848126](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a84812640f02981875275c96e37338de4ab49996))
* **lms:** ensure lms uniqueness across all qualifications ([b85c8bd](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b85c8bd74f8db526fb1cbb43ff12a24b93c07eb3))
* **lms:** filter by status ([a74c3d8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a74c3d80cada4f9d224365727dab9676cc905f54))
* **lms:** filtering qualifications by supervisor works properly now ([15f7a75](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/15f7a7576ab48a362a479f43034510b4e80bb1b2))
* **lms:** improve sorting for firm all ([3865bda](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3865bda64d488c161b55e1f6eb48ca1b742dff98))
* **lms:** lms admin renew pin actions were ignored ([242dd0b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/242dd0b8d46566e67d9a9e75c6f35650cd6da27e))
* **lms:** LMS restart failing due to old LmsUser entry ([6761767](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6761767c6ca8cab62a22aa6f755e6231e07ab411))
* **lms:** lms-direct/deletion-days setting now represent #days to presever lms (used to be #days+1) ([d02e62e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d02e62ec20b8cdc9dd6144de558895885ad1e692))
* **lms:** mark as ended only if not seen for at least one day ([8165892](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8165892b2e4f945780bb8420cfc4eed50fdd294d))
* **lms:** mark expired learners as ended with status expired ([db9ffa1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/db9ffa18302bf56649466ed78b5040f3c46e5e09))
* **lms:** negate learner locking condition ([a452b03](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a452b032c43dbdfd086ffa4793c83ecc32c450f8))
* **lms:** negating unsigned word auditDuration bug squashed ([7b152b6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7b152b67edfc20f9d2a5dd3573c2bd52e710ff41))
* **lms:** prevent duplicated LmsIdents and Letter sending ([1731d22](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1731d22ba56045fd1c636e2897748faa735053e6))
* **lms:** report log did not match qualification ([390ff31](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/390ff317ea3bb4ef8918c9cda858f5f228e4a882))
* **lms:** reset e-learning more lenient ([8b0737e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8b0737e2aabc7153ae3a3df4f97f86ffc8592e7a))
* **lms:** send e-learning failed qualification only once ([c62a42d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c62a42d5c2175c4a2fbfcb47e54cbff273441b51))
* **lms:** simultaneous block/unblock lets unblock win in all situations ([ecd1a0f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ecd1a0fc210d1340bff5c79d8bb676a47654b509))
* **lms:** sorting and filtering lms status ([f48862e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f48862efbcb95e92203a200267e1bcc613af4af1))
* **lms:** sorting and filtering lms status works throughout now ([ae44703](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ae4470333e2b1b5c271b38092210c094822f4a19))
* **lms:** transmit renewed pins to lms ([be3fb39](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/be3fb39171c1eb5d015ae006286bed747055a7a6))
* **lms:** treat simultaneous blocks/unblocks correctly ([11752dc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/11752dc5ac96f36ebf9a4cad43fa4e4b55c1b21c))
* **lms:** trigger userlist job after upload ([cceb600](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/cceb60074fbb26d7ed2d10a1c37297fa6e52292a))
* **lpr:** fix [#96](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/96) by various minor improvements to PrintCenter ([80c632d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/80c632df1ca4871c10cdac1141d87f92a7646cf7))
* **mail:** add debug info why setting reply to instead of sender does not work ([3453fc3](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3453fc34598581f88dd5696fd28ddd726b389ed1))
* **mail:** better separation of sender/from/envelope-from ([0dbf4f8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0dbf4f8bde99431cafeec954dc164a73227154ad))
* **mail:** fix various minor email attachment problems ([90a5f07](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/90a5f07c0412c6820f935b483db8645bcefba160))
* **mail:** honor userCsvOptions and userDisplayEmail ([89adf7f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/89adf7f2dc1caa90fc71adbcf0dc04936b685bd3))
* **mail:** mail-reroute-to now changes envelope-recipients as expected ([86d947f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/86d947f7e80b96362be8d662318c0140055d4786))
* **mails:** prevent emails being resent to due archiving errors ([8cf39dc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8cf39dcbe68cefcc50691ae8a7194315d18420d6))
* **mail:** use only RFC822-timezones ([59b8bb9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/59b8bb982d33b14a01133819c375884a8a7f7ce9))
* make migration idempotent again ([9778404](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/977840446e5a9b5836c6f7370c6134b144b8902e))
* make sure it compiles again + add 2-letter name ([d60f935](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d60f93561f5ee84d460645a945db35ac6b55e97d))
* make sure line-break algorithm respects available lines ([e487cef](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e487ceff5858671eb0bcbd813e9de0d3b4c74f75))
* make sure to report NoUsers, regardless of rule ([9c928b0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9c928b0375c1aab0c46768101849ce8daeae9b81))
* make sure unfortunate combination doesn't only produce 0-9 ranges for matrikelnummer ([8e4cb09](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8e4cb0917db1098f5b19be0dfad4c6fafb900c49))
* **many occurrences throughout the project:** Fix typo: occurence -> occurrence everywhere ([96387cb](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/96387cbed5bda9b901706318e1931e6e718a0680))
* mappingDescription doesn't overlap for the first n rooms/with small names/matrikelnummer ([fc35fd2](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fc35fd26c1eb699d6eb8aa1b9febb48641c26d05))
* **mass-input:** defaultValue is safe ([03f36ae](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/03f36aea1f1cb1998a030d1e3cf360faedea58c7))
* **mass-input:** properly escape query selector ([9a3f401](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9a3f401b38e86e2f9e7fa722698a437d853b422e))
* **massinput:** properly render massInputList ([7c28448](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7c2844807f217b675b1730b128a3a418caee92d1))
* **memcached:** don't 500 upon hitting item size limit ([d79a539](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d79a539f71e8250f677ac4e0b42c9ffd4de50af5))
* **memcached:** navAccess & quick actions cache invalidations ([d05306a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d05306a39a5324b2b09503bfc09ca4b7f2ee38f8))
* merge ([a9636af](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a9636af13a86d405b03b538e5489ecb2d30c4294))
* **merge:** fix build ([0bd0260](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0bd0260a3e916b6541f43e036744e80b8d0f00bb))
* **metrics:** allow free access to metrics during development only ([085c841](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/085c841035b3f808ce6d8ae3f6e0e9e6452028df))
* **metrics:** larger range for worker_state_duration ([34a5265](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/34a52653d71140bcc664cbe864cad069441b5c6e))
* **metrics:** sort metrics ([e5ae152](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e5ae1521a0577df35abe13b6bcc602f3a38a6f9c))
* migrate so as not to resend allocation notifications ([132a510](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/132a510a2372f15db671c05c84b5a815fdc0e0a1))
* migration ([dd23559](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/dd235590b47a90d70753458ffc7ab61c771f3d9b))
* migration ([4383eb1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4383eb1359c7ddb6d2cf9cbcd8d92523522c7d8b))
* **migration:** don't consider changelog in requiresMigration ([ea95d74](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ea95d74cb5572688531ba0fdeed3983fb70ab236))
* **migration:** drop more tables in w.a. for inconsistent 21→22 ([d79dca6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d79dca6be9a00c5dc1671e4779418f2fdc54aa02))
* **migration:** fix [#133](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/133) by removing old outdated migrations irrelevant to FRADrive ([d4f0d69](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d4f0d69428a4f7fc887cb6854cb59e3dea83b9bc))
* **migration:** handle deleted courses & users ([35621df](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/35621df03ece390d9513871c9def990828a51ecf))
* **migration:** ignore superfluous migration entries gracefully ([1d48b62](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1d48b627f6b8cf1b03e2ef63850c36c429c9d3d6))
* **migration:** make index migration truly idempotent ([7a17535](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7a17535600ef9408af2da5e0b01bea4b6e2fb63b))
* **migration:** omit index for old versions of postgres ([cf412a4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/cf412a4b54f7159601187305ad34f188a7e3e9a0))
* **migration:** typo ([fb7c7ef](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fb7c7efebd84ac46c1d1463e6cdb53d277834ef3))
* **migration:** typos ([e508277](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e508277496f7aa4114ab9685166e91caab941226))
* minor corrections, also fix luatex dependencies ([5015dba](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5015dba5e3dc287a8b6042ef43ed6a3952e9d9d1))
* minor heat correction for correction overview ([5546849](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/55468496e0842ca014af6d6b93e87c1c0f46f222))
* missing translations ([dcfdb51](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/dcfdb5130d19e737147bfe9065a6ccb5edf49a77))
* **missing-files:** properly account for workflows ([c272618](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c272618aa6dd68a1acb5b959c6d905978b26eb07))
* **models:** correct erroneous default values ([282a7d4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/282a7d44b24b70df0bcfa65a31c2c8c48bdee021))
* more verbose watchdog notification failures ([48028c4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/48028c40532577f74430340ed924af7116b8bd96))
* **navbar:** restore border to language buttons ([a2e9a9c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a2e9a9c32ddb78a487b2d69f048922c405d14ee5))
* **new-submissions:** always check for existing sub ([c7d23e6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c7d23e64ff565ac6920c2f4dc2ab4da20a1fb70c))
* **news-allocations:** i18n ([5a23d87](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5a23d87380badfe1ee8e6b4c93730050cc42305f))
* next input area is now selected via a css query ([1aaf254](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1aaf254c3c3c07f95b63bdf760791859a80aa4a4))
* nix eval ([effddc7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/effddc7bfb3beed997ff342e707b49b88a31c395))
* non-dev build ([dfea399](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/dfea39907cd114e6d8aea29ae3835dd323ca4df8))
* non-exhaustive patterns ([5bff34e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5bff34ed0a1b2d4d160c506cbe7090209d28da66))
* nonmoving-gc still segfaults ([c404ce9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c404ce9b3529cf402a0f9d649ca3299df09ba089))
* **notification-form:** define rules for all notification-triggers ([0261b39](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0261b3979df73e1b24a920371682cea14ea6d1fe)), closes [#561](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/561)
* **notifications:** direct notifications now respect user triggers ([3e5f271](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3e5f271cacfcc5dbd95aa68a342f56db566f8dee))
* **notifications:** qualification renewals are more robust and not sent multiple times at once ([1cdd52e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1cdd52e96c727139d6cd630da5117fd3b4aa5a7f))
* **number-input-fields:** number inputs made HTML5 compatible ([6098215](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/609821595b547999a571b4c0d1aa5a10ed58aa9a)), closes [#412](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/412)
* occurence exception end times not shown correctly ([725468b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/725468bfd3e27787875e8d2bf21693b3145da77f))
* oops ([f6cbf99](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f6cbf99245ffdd19a2d6c9acc7c0b9a7f8df45ca))
* order of on in exam office auth ([f44f150](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f44f1507471a9310a9c88738ca5b3d8268afc136))
* ordinalPriorities ([d4ab6f6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d4ab6f64e24109892b5665d154d8811420452038))
* **pageaction:** fixes [#463](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/463) ([849c6c4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/849c6c49ca254e493538685f2ca82e052d4aa931))
* **pdf:** embed din5008 templates within binary ([b76c414](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b76c414220bd73ea8fb67b226a007cdcb0bbd4fc))
* **personalised-sheet-files:** don't delete files when "keep" ([6008cb0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6008cb040dea268e0a096f6c2fafa87f321d115f))
* **personalised-sheet-files:** more thorough check wrt sub-warnings ([0b0eaff](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0b0eaff20daf9f38e42678d7ab159a0e75ebec66))
* possible workaround? ([757e148](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/757e1480329d11521c1ef7afb78702a251fd5b89))
* prevent deleting sheet-referenced exam parts ([9859c2e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9859c2e99c1e0c7531ee38864a24ff279a8e6a7c)), closes [#681](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/681)
* **print-center:** fix syntax ([957bf4c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/957bf4c966cfd4bccd7da443d02de559f58dc703))
* **print:** apc ident aliases did not stop at first success ([b7d4f69](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b7d4f6913d8b1a70c1b7ef73782cf29861dc11a7))
* **print:** disable default filter for print acknowledged ([f0b20a1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f0b20a1b263a072a9811ff677f25e6518d314133))
* **print:** keep print jobs on user merge and lms id deletion ([a15862e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a15862ea72bc374af870ef3a23f86ae32c2c67a9))
* **profile:** bad email indicator ([6699f1d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6699f1d72f148ccd2c82bebb3f582cf61d711425))
* **profile:** email validation inverted ([799f1fe](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/799f1fe184276aea2eb3659e5d439cd76f4ca4d8))
* properly apply auth to corrections in sheet table ([d59f686](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d59f6860215ebbffde61062a501b5eeeabdb58ae)), closes [#700](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/700)
* **qualification:** new block/unblock mechanism working now ([5397c7b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5397c7be353fc1b1e8310f66b49a9b93ee890253))
* **qualification:** prevent qualification mixups ([88d4356](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/88d43560ae8de1480502914d9c95d6376a3c68cc))
* **qualifications:** counts for lms/quals correct now ([33a847b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/33a847baa3310e6e261409f2cda9d964cf5a821d))
* **qualifications:** fix [#78](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/78) block/unblock no longer deletes company association ([3cb66c6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3cb66c6211b9f15127d88f448557acb4a3a2dd5c))
* **qualifications:** latest block could ignore itself ([bb708ca](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/bb708ca540557b41d33996cfea9a390a457ed855))
* **rating files:** better descriptions & tests ([5f04593](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5f04593b30c14fffc58e513b79ce1c98e75328b1))
* **rating-files:** support integral points values ([62dd7b9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/62dd7b9f047e504db783b2ceef19307c21d5550b)), closes [#604](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/604)
* **ratings:** disallow ratings for graded sheets without point value ([c0b90c4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c0b90c4c4ab9e26143c0ac76ad7af0fb4ac5fb0e))
* **ratings:** disallow ratings for graded sheets without point value ([463b2b7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/463b2b78780ecf24aa3b48f0740c18fce45e8d3c))
* **ratings:** improve decoding error reporting ([c873150](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c87315006df39550a58d89bcb5491a0afe0e4481))
* **ratio:** more attempts to fix ratio bug ([b813442](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b813442012cab26edb6e04552eb77aaea4103e03))
* **reachability:** account for e-users being assigned a useless company department ([bb27324](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/bb27324ee8dff257da09c1575468048d793bec8e))
* **release:** ought to fix issue [#4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/4) faulty version numbers for demo container ([934026f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/934026fc79e93d2aeb2d46f5697288ee2374e06f))
* remove cached-db-runner ([ff82700](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ff8270042f74d8019e121aebf8636472e1e4d79e))
* remove link icon on table sorting links ([e7e7d2b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e7e7d2bc6b4a96ce3dc6735b4a869e0cfe2bf4fe))
* remove manually inserted error for testing ([8c17f33](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8c17f3354a6e7768ecf427e4c0a899cbff9c7e0a))
* remove merge artifacts ([99e39bc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/99e39bc27ab1e68aed565751ae395cb2a4c8cf2a))
* removed duplicated code from merge ([9fb9540](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9fb9540bf7fd16334eeaf09d22c05a1ce80b3737))
* restore behaviour of waiting asynchronously for job-management ([5ebcd89](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5ebcd89e11841fd777f9ab6fbe1c4c46b02313a7))
* restore storting for exam-office exams ([5698e9c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5698e9ca0bb19585b9a9d2d3c10f8b5f99ae5db9))
* restore workflowWorkflowList columns ([e55c6d7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e55c6d795fd724bdb732e22d13c96d6b67ea7da1))
* restrict guessUser to consistent queries ([bcd5326](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/bcd532612914f33fc3bb053d00a3319ce2748d29))
* revert wrong hlint suggestion ([ba2ed97](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ba2ed97731a5ca8d95a9118785c9cce163244267))
* **rights:** split applicant off participant ([9d709ca](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9d709ca400ab39957e0c5b6b7a4c466b4587dc83))
* **rooms:** honor roomHidden ([ed5d871](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ed5d871182954e2f0a9a5063f61277d925628c40))
* **route:** correct typo in route /lms/../userlist/upload ([89be36e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/89be36e35b82713e20b3665396d551ca1788fdd8))
* **routes:** change ex to sheet ([9d9ead9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9d9ead95d847a96927b3c8d66d1adff9f31af2f4))
* **routes:** remove redundant auth tag ([5ef36f1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5ef36f1d1c0bd92773bbb58af417bae8307a3610))
* **sap:** combine immediate next day licence chnages for SAP ([f4adfdf](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f4adfdf87270930d4ca6611f2a9956613fcace53))
* **sap:** combine immediate next day licence chnages for SAP ([cbb44f1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/cbb44f106ad59e0a53ca04963ade5544120b7e21))
* **sap:** combineBlocks yet another bug squashed ([3924d14](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3924d14abd868305b42c9d04913536b4999dc45b))
* **sap:** compileBlocks ([b4a88ab](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b4a88abcf85783c350ad2bf3a5e973d13d1eb1f6))
* **sap:** do not export e-accounts ([086e49e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/086e49e2ae126f6acb9be774b0351d37443c31d8))
* **sap:** yet another fix for finding date intervals ([fde97b0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fde97b048ab04ab59c9e3f2a2f74bb2c1e996b22))
* **school:** fix [#133](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/133) by adjusting default value ([2509358](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/25093588784381a19f34e5b091677b908420ddea))
* **schools:** fix schools form wrt. discouraged modes ([53a8f1b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/53a8f1ba122466312947cdbdb49749a61acab37c))
* **schools:** insert correct authorship statement definition for exam-unrelated sheets ([2272647](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/227264743e0e8d0acf76839300a034b4bb1bf2a6))
* **schools:** perform authorship statement inserts ([579371c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/579371cffd87c247805bf4ead8bc2c278269a5ee))
* **schools:** rename messages ([0e62073](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0e6207376043af8fe0929019e3c39f80bcfea9a6))
* **schools:** switch authorship modes to required in form ([8fb49dd](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8fb49dd602f4eb854b300b5b399206aa2fbca87b))
* **schools:** use StoredMarkup instead of Html for authorship statement ([67c3016](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/67c30165ae90603e8a97ad2661d2bacb92e2e53f))
* **serversession-backend-memcached:** don't throw on deleteSession ([bcd3e46](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/bcd3e467d6a9e4f3ba210d0303412b3657530eb7))
* **set-serializable:** logging limit ([60be62b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/60be62b63bf328407e4ba0f01221d87020e89f24))
* **settings:** disable lms jobs by default ([daa1fe1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/daa1fe1a37b075ce420607723e4255fd0ff7979f))
* **settings:** memcached host defaults to localhost, because why not? ([3d5e532](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3d5e532e2d963cacdd74c1b3a7237f37d1fd3751))
* **sheet corrector assigment:** minor bugfix ([749cd2f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/749cd2f7bcf990f5eee18d90099843151cb56716))
* **sheet list:** do not show icons for inaccessible items ([0bb9a0f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0bb9a0fa60d83e91c91bb97833126a23a6f03989)), closes [#421](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/421)
* **sheet list:** only show corrections after they are finished ([d4907cd](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d4907cd776467dcd863ea761f9c0c88408e7248a)), closes [#533](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/533)
* **sheet type info:** give better tooltips and name to sheet types ([9dbef1f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9dbef1fe0f2d69eef5f6ff830d5ac338b84aa0f7)), closes [#402](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/402)
* **sheet-inactive-notification:** improve wording ([8af6bde](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8af6bde8a6efd35ec2c534eabe92c5aa6c3a87b0)), closes [#514](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/514)
* **sheet-show:** move message ([1d8a2ce](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1d8a2cef60a688bd514d529f8e1230e462811f1e))
* **sheets:** fixhance sheet authship form section ([7192cb5](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7192cb527c7f66c320308a80de9906a6edc6e9ec))
* **sheets:** integrate corrector interface into SheetEdit ([acfd312](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/acfd3129ec561725dded5a24c7f293086c9087e5))
* shown ranges "include" special mappings ([7e1b75c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7e1b75c2e167c75ebc3a05f881ad7fb07c29af55))
* shutdown behaviour & tests ([19b8b06](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/19b8b0616f306b57390b7a6e26023e8d59aa1239))
* **smtp:** case-insensitive from-domain comparison for reply-to instead option ([859f5b8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/859f5b8494ce326fcdf13ed8fcca9355273fb42e))
* **smtp:** use full email with name in reply-to field ([8cdc2b5](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8cdc2b5267095ecb816398037bc830c996250456))
* sort occurrences in the right order ([732df50](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/732df5053033c3533f52850cc6220dd06a7e3500))
* **specific file submission:** swap labels ([7fadcf5](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7fadcf52b7c99c6259e36f1caf46b92e08b2aafa))
* spelling plugin had a suggestion; actually Hello World commit :p ([7b0fd61](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7b0fd61f7f8bf1e995209bec7b44231b5ba011a6))
* **sql:** fix transaction behaviour of setSerializable ([e5acdad](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e5acdad134a7c4d8457c1cf8755d69306a52dd9e)), closes [#535](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/535)
* **sql:** quiet warnings in setSerializable ([859ae5e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/859ae5eea103cce3dee84d6ba9d104f21d120f43))
* **standard-version:** properly reset staging area before release ([5aa906e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5aa906e7eb3798d590a38ae3e18ce4c18741d9b4))
* **status:** module imports fixed ([c59ecf5](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c59ecf5019af67c849f32962b8ce9485a7adab1c))
* **status:** nix files inaccessible on build server ([1bb500a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1bb500ab027993a97848d357a3382c8975f113eb))
* **status:** route status exempt from approot normalisation, might not fix the issue yet ([074a33d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/074a33dc51bd7e9ed72434223ceb06d70f1044c5))
* **storage-key:** fix types ([a0d067f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a0d067fabfa91027831e86fc5c52a676e63ea9f7))
* **storage-key:** fix types ([a23a473](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a23a4735c2a4a8a372b6479d79223099d59e3bea))
* **storage-manager:** correctly use encryption key in decrypt call ([2667aac](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2667aac1a382c52d9dc6c465ee8ccb173e893a28))
* **storage-manager:** correctly use encryption key in decrypt call ([9e9726e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9e9726e173dbfa5a0a4dff0402a19af6ca8dfa51))
* **storage-manager:** post salt and timestamp only when fetching key ([6340509](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/63405093c4fc2fd30b919da7b26ad7f984c0ba9d))
* **storage-manager:** post salt and timestamp only when fetching key ([301c88f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/301c88f2ea9dd0b430e982f11adcf6fd29d74096))
* **storage-manager:** remove and clear SessionStorage ([e42452e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e42452e4da3eb6182f792f0711a94395ee430eee))
* **storage-manager:** remove and clear SessionStorage ([38b0a8e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/38b0a8eebc2e0774fb80e11e18736492e6ae2755))
* **storage-manager:** save salt and timestamp ([0282918](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0282918c2e3ce50b471dcff80a6438ba1f9fd197))
* **storage-manager:** save salt and timestamp ([8bee033](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8bee033efaee308731dd62486fee7aba2ae3ddc0))
* **study-features:** account for existing StudyFeatures ([b6cada4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b6cada43f218f3c688360c114f020e9037a87a21))
* **study-features:** also apply caching to table columns ([564c0b9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/564c0b975ae65881cb3a168855b36e4b1614a6cb))
* **style:** breadcrumb bar width ([7340fc1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7340fc1fa60c241b2b5a054386a3484e724e9681))
* **style:** padding of language buttons ([e704b23](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e704b23a53f0af2a1e474c181ebd079063e5e469))
* submission download token generation broke viewing ([e1b6084](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e1b60844cb77b1fd41900d0a3c4829ba21b6b3fe))
* submission user notification recipients for pseudonym subs ([a7b7bdb](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a7b7bdbea754873e11fea8d2af42bf3aacaff3f2))
* **submission-create:** ensure number of buddies is acceptable ([ec24a04](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ec24a04c9b1dfaefe8f95548206278a2d6ac9774))
* **submission-create:** sanity check submittors in form ([3bf37a4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3bf37a4c1ab89379794c57acd5c30516072c721a))
* **submission-form:** fix display of all courseParticipants ([b67819d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b67819d061fb3409fa9978a31cb94b700e9e86fb))
* **submission-groups:** prevent deleting group before insert ([f87cf7a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f87cf7a378ec2641ed3a9a13182b39b170f61c1f))
* **submission-groups:** wrong sql query for finding buddies ([0679626](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/06796269d4da73f9c31c978fc2e05e32fbe04b2f))
* **submission-multi-archive:** fix cleanup & improve ([27731ac](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/27731ac077ae6eee31eeb5cd39d24b0a0ea8f490))
* **submission-users:** properly delete old invitations ([91c926b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/91c926b1c5eb92a4d8bbf3077020a0121165f3eb))
* **submission:** allow non-group-subs when user isn't in sub-group ([9a35c85](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9a35c8542c358fbb8e88e6661e981631ba4c1fbb))
* **submission:** allow not modifying submissionUsers ([030fd7a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/030fd7abf1bdcd700d45bfa14cc192ac98dee24e))
* **submission:** ignore extension case within zips ([f8442cf](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f8442cfea9f5ab0c4b8b2b6959153034f390839e))
* **submission:** race condition allowed creating multiple subs ([02fc0d4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/02fc0d476f07c786cb4ef23600baaed36530fa24))
* **submissions:** allow user to resolve themself for auth'stmt' ([5bbb86a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5bbb86aa7750dd907f49cb3ba5daf2cee8485bae))
* **submissions:** cascade delete to authorship statements ([fcce16d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fcce16d838e5cba3187a82a5762b831d7df54cd0))
* **submissions:** don't leak info from corrected versions of files ([66f5e96](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/66f5e96eca4cbcb6cb092092b1b1b069ce30f159))
* **submissions:** fix ambiguity with multiple past co-submissions ([6e4f469](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6e4f4690237625280d24ce6cdfb6f3e57bc16d38))
* **submissions:** fix distribution without consideration for deficit ([5035dff](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5035dff9021260cd45dabfc175bb535bdc19dc71)), closes [#713](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/713)
* **submissions:** fix users being deleted for other submissions ([2462c68](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2462c68f49d9952eeb4c9f5413c53d307f10975a))
* **submissions:** hide correction-only files ([575fadc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/575fadcd8cbc4d899ed0ab5d58e3fa8aa64df111))
* **submissions:** improve submission process ([7219131](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/72191315b6daed78cd0f31b02627e1d27db620f3)), closes [#675](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/675)
* **submissions:** maintain anonymity ([0184a5f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0184a5fe3b1af635318fa0fa317e3497f24fbc90))
* **submissions:** more precise feedback ([d151b6f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d151b6fc14e5b32d9f07923149923d5ab7ea4880))
* **submissions:** off-by-one when isLecturer ([01e61f9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/01e61f9bfda777c09c5b64c4bf4368e590328545))
* **submissions:** only notify submittors if rating changes doneness ([4f1162c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4f1162c363d15d9577302d064e4dd352111fd628))
* **submissions:** only notify submittors if rating is done ([8e0c379](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8e0c379c71ca1226590b333f9a740c6cc0aa98be))
* **submissions:** submitting produces an success alert now ([bf20d6f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/bf20d6f4e84353d7d83626377ccf41204832ac2c)), closes [#286](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/286)
* **submissions:** take care when to display corrections ([a6390ec](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a6390eccbd164ee5e821d3ecb0fab794a417425a))
* **supervisors:** reroute to non-avs supervisors too ([1cc6240](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1cc62403541404e4ab4c448841ff5fa0da508ce8))
* suppress exceptions relating to expired sessions ([d47d6aa](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d47d6aa6ccaa3007aae64f555ceec519dd03f029))
* syntax ([7afd569](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7afd569eaa363487f1775489ab1fe667aee84fe7))
* **system-message:** lastChanged & unhide logic error ([36abb3e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/36abb3ee2675e4da5e9baeccd1057db6d20303fb))
* **termidentifier:** rational not working use derived day instances instead ([ecdb22a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ecdb22aa613757ee6f0197a2d4191dcb3d603da1))
* **test:** build ([443b871](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/443b87168074b60d0b7806450dfbf512ad5b2135))
* **test:** fixed compiler errors (oops) ([bc42f30](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/bc42f3072fd37ee6f37c70a0b3999d9ac793b240))
* **test:** isNullResultJustified reported false positives ([292f5cf](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/292f5cf91b56953189ee72e42b822d66761ff3bb))
* **test:** LmsStatus is no longer a semigroup ([bf8cd4f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/bf8cd4fa899bccd4a37906a4d899aca6ca25d726))
* **test:** resepct uniqueness for ldap primary keys ([d1badf1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d1badf16fc9c45ee547ec1e3fe677e16645683f0))
* **test:** resepect uniqueness for ldap, 2nd attempt ([d06448a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d06448a4a8c194368c077d5ca7bf8d0feca86d60))
* tests ([4803026](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4803026a2c091128a7370c12f0c06de9bd7b9180))
* tests ([3c322af](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3c322af49e2021b2963fef9fbe303fa70ec77e18))
* tests ([65e0688](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/65e06882d2491da5e30b1401db6ecc81efcac58b))
* tests ([ca81f3b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ca81f3b0f2913431cbaf399c33ed30a21979ce69))
* tests ([018d26f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/018d26f4a1a1cf411324aeac56ce4d4203670942))
* tests ([5541619](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5541619372f4a4e46ccc403004e869afdfaed7b0))
* tests ([b4b4a96](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b4b4a96aaeb468881c8fe510a9a871e4931eeb1a))
* tests ([4854d83](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4854d830fb14ecff54d32aa93a95eaaf67c0499f))
* tests ([96b3ba4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/96b3ba4392ff76e48e032a08fff36808e006d0f2))
* tests ([daa1f83](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/daa1f837c78e19ab5dd0f9a79363ba9227707083))
* tests & hlint ([4e9b618](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4e9b618b61a2e35577dc9414d4c8c923b01b783f))
* **tests:** explicit post parameter name for dummy login ([2ccd50f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2ccd50fa85b05751de6b36185bf52a1386458992))
* **tests:** fix build ([b0f2304](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b0f2304273506947922637fa29132135cf11931f))
* **tests:** generate sensible WorkflowPayloadLabels ([8a888d3](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8a888d3945f0fd0d67ef83bae621744c943b99de))
* **tests:** i18n changes ([9ba0e27](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9ba0e27ba2208ef835538f6fecc092aab85411ea))
* **tests:** remove invalid claim of commutativity ([d2f0361](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d2f0361e49114e6dc6c55e64b677b8c842e93bee))
* **test:** test for applyMetas handles duplicate keys in correct order now ([0366f8c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0366f8cccaa7cf644d25d99c5a27c0d73a5714b0))
* **time:** midnight timezone conversion bug eliminated ([dfa07a9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/dfa07a95eb29f1fceec258a466e1e7c779ff6e5c))
* **tokens:** introduce clock leniency and remove start for downloads ([8939a8b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8939a8b90a39a26614da18dd3985aee253cd191f))
* **tooltips:** add dark variants of theme independent colors ([e5c7aa0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e5c7aa03dbe59060a2303d378229b2de3a2ee3c6))
* **tooltips:** fixes font-color when used in tableheaders ([f4bb70e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f4bb70e19161afffb396da182df5aeda0191285e))
* translation ([80960f4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/80960f42c578c201f78e226653431e9dd965cfce))
* **translation:** fix typos in translations; add bug to known bugs ([ac3f7bb](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ac3f7bb8b48d64b0db05f292211b6c7df955649b))
* **tutorial:** fix [#94](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/94) tutorial renaming (de) and template naming ([1ce8f75](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1ce8f75c2d192051929b1a74b17f4e6494961901))
* **tutorials:** improve creation interface ([bc248d0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/bc248d0fc21d6b8361d66e3bd6fa17b368da71b5))
* **tutorial:** template moving works now ([b982e59](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b982e59b630fbdb3fe8f37c979de8e8726b78ea9))
* tweak debouncing & canceling ([6b51cc5](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6b51cc5e53ab7686f2394cd80bc4ee4fc426c8d5))
* **txt:** delete old txt file ([0c639b9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0c639b9c53591e8c68d9c288ffddd72117ca3711))
* **types:** move term identifier start/end information to type definition; simplifies fill ([aeafe31](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/aeafe3118bb6111cc6f5d4ff012b0fc6c70fe23b))
* typo ([26c3a60](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/26c3a60592c02570ceeed42cc977ad223baa16ae))
* typo ([f155a4b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f155a4bf08d169309c05e3efbb47a246f3010816))
* typo ([f931c67](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f931c67a9ecf37bd9a6c9814ee61de7cb054dcc5))
* typo ([a1b03e8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a1b03e819fc41da7ca83be8d27955a29a4e73904))
* typo ([52670bc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/52670bc90526490b3b11b639cc3e4bd27bb4a184))
* typo ([c06a472](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c06a4723591cc3d716b2d6b39f2757e17387ae47))
* typo ([4c58699](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4c58699d1f2efa0695909de8be4ec660578696ae))
* typo ([ad5494e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ad5494ef03604250fbec63d4e48f0d6bbd66f87f))
* typo ([23f4eb3](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/23f4eb3f2b8154e412d280ab22eb881941d7e736))
* typo ([a6e40f1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a6e40f1be8dff7fc9e2711988aba0e431b3eb6dd))
* typo ([fb1e42d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fb1e42dc6994e79692f38b93a14eeaaaf9d53578))
* typo ([fc5ffb7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fc5ffb7c5231302c478999cb944821d0cbf5bbf7))
* typo course-assistant ([c7ce167](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c7ce1679de799285ec7a9a0a62c0a202b9078eb3))
* typos ([97f62b9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/97f62b92c1cc8a68682f6df3b104bd20e0e035b7))
* typos ([b9c284c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b9c284cde268f0865cc32d2c973206b227042700))
* ui improvements for (external-)exams ([b3ce3dd](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b3ce3dd93a576dd3b5c6a8ecb1b278556067806a))
* unbreak arc ([8ecb460](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8ecb460f39f48557b5935b1cd18709ba197d3490))
* uniworxdb ([e5608d2](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e5608d2d5bf5349f33d83dbd2f54af84442fd93d))
* update imprint & add instructions for help ([eec9a39](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/eec9a3974fc4cde5cc70ab650d018667ce044a92))
* **uploadcache:** set default to localhost ([eeb22de](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/eeb22dec9737e014c20b852438da4373219884b0))
* use extraUsers instead of extraCapacity for unrestricted pseudo-capacity ([2be9d76](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2be9d76af2b3e9fd52284c639a4c3f6dc1c51779))
* use recent locales & tzdata ([bedb47e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/bedb47e9d47e98621efb56db0cc24615cb7a8de4))
* user with a pre-assigned room count towards the capacity limit ([4fc0535](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4fc05351fa8048752f2ec3260dcaac64f962c9a3))
* **user-deregister:** remove tutorial participation ([cfcb28d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/cfcb28d1d491f1cbc77b0c4d4d522c44c5f6b807))
* **user:** add new user failed due to AuthNoLogin not treated in notification template ([8456f18](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8456f18bf688ad475f8f5d78c83df9f393c11ae3))
* **user:** add new user failed due to AuthNoLogin not treated in notification template ([a1516d9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a1516d9116451fb9a24761cdaee8b22dcab58680))
* **user:** check reachability by post or email did not account for department ([ed147db](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ed147dbd20b89d32281dbcaed3a1ba7cb00c347b))
* **user:** display name may omit hyphenated given name parts ([ddb1a15](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ddb1a15c183f25b81153baaec51033e32436e98b))
* **users-add:** upsert tutorial only if users not empty ([e65d388](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e65d38898e78dffa3dedf89292bf28e39a2ce3cf))
* **users:** allow prefer postal setting for users with fraport department ([a9d56c5](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a9d56c51dcc727f8637b09a0e849372e75032f5e))
* **users:** assimilate merges possibly incomplete user fields ([52afd13](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/52afd13b6dc2b870ab8dbba956874e8950e07973))
* **users:** fallback email to name ([7bf018c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7bf018c2a4cc8cd2d087da1d1c6cfff755bd3003))
* **users:** fix [#112](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/112) and also add some convenience ([35096ac](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/35096ace01a2bc2a2d666794bb1ff92f52b3edec))
* **users:** fix [#112](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/112) working now ([88bf21c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/88bf21c9c5de3755ea6591c97dc1f99a928914d5))
* **users:** fix broken email fallback ([f4e9f2c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f4e9f2c973f4c3eccda0a7997d25696fbe286e5c))
* **users:** prevent accidental user hijacking ([014d479](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/014d479df8f36515915bc7991bb97bad24dcbef9))
* **users:** synchronise sex ([25912e0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/25912e0616e1b771b0d76fa7ffb85ed7f5f676c0))
* **util-registry:** fix initAll and tests ([2620fb2](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2620fb2f9569368fdaafe3924c02f2f5a2671818))
* **util-registry:** start setup instances and not all active instances ([ddf94bf](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ddf94bf5650addbf6de40894ecda9ba6ef36e143))
* valid binary ci instance ([8cfdd28](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8cfdd286517e0a9ca99dd31b9d220560adc6c93d))
* **volatile-cluster-config:** fix pathpiece instance ([dcd5ddd](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/dcd5dddec82da359a2100360cfeb6845ed320821))
* **watchdog:** improve status&watchdog notification ([2d4ccd6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2d4ccd693394ac75aef291cdadf5bdbc72e173ac))
* weight random token impersonation towards active users ([a314f64](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a314f64a70d9e7e427383c8d656d9bdceed5f9f3))
* weird sql casting ([eb9c676](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/eb9c6760b9a62d263f0c30531a643d43c7318b3f))
* work around conduit-bug releasing fh to early ([3ff2cf1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3ff2cf1fec1bf582fe1d5e1f6ee08dcc85d6bc00))
* work around regression in esqueleto ([25cf946](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/25cf94657067dfc60e9acdb370274873ae0b6a6e))
* **workflow-types:** fix Int64 workaround; update test defs ([ce9648e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ce9648e47a870e3a051591631c2eb3a26c6b4b3c))
* **workflow-types:** minor import fix ([b19c1b3](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b19c1b31b7b079f3abe6aff93e3b5603050c6131))
* **workflow-workflow-list:** restore default sorting ([454a917](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/454a91702bdbbed7e473ef94a603bcea2e716406))
* **workflow:** add missing optional ([8608e83](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8608e83ef80497b7ddcc451759a98a07b435fc08))
* **workflow:** fix false instance with atrocious instances ([8812f24](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8812f24d9060314d60c3ae495ea7daccdabff30d))
* **workflow:** fix node and graph FromJSON instances ([263fee1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/263fee19f25a4782bf426347e385eedd1742c8da))
* **workflow:** fix types ([ce1acec](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ce1acec444762d254ccf07b37c280ffb02934669))
* **workflow:** fix types ([4334253](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4334253122f4208fb4dc61f5cbf4436f122b14e2))
* **workflows:** add missing import/reexport ([5e92a6e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5e92a6e04aeb9a54ab361f83c7168e57ae5e9c33))
* **workflows:** cleanup ([0a3eaa2](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0a3eaa29946ba00ba0c9597d124f4ca5cc25620d))
* **workflows:** disabled warning for top workflows/instances ([17ed2fa](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/17ed2fad2230944c629c6a0c8d8181f6fec8983f))
* **workflows:** don't cache instance-list empty for correctness ([cb1e715](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/cb1e715e9b2da2f5ac0bd03b636de0f961307efd))
* **workflows:** integrate in new master ([99f3fca](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/99f3fca6d08f098b996931c8c4736eefbc9db77c))
* **workflows:** navigation order ([c5eea64](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c5eea64b270369ef69e1aa368ec87f0a8846e1fd))
* **workflows:** prefer payload label from target state ([2619b08](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2619b08ad1be2921d2cdd568f9419852c374df10))
* **workflows:** properly offer previous payload files ([aa0404a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/aa0404a0075acbcd4c6f94984acdbb4d68f08d0a))
* **workflows:** refer by id in model ([94f78a0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/94f78a07d9376670122a2adce01cf7180a64d33d))
* **workflows:** ui improvements ([c7f4fa0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c7f4fa0e412d2b920a3819ffed5b79b8aeea2842))
* **workflows:** workflow-definition edit translations ([5c5cbad](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5c5cbaddf8b33f455ff18789806a3e0f9ac447ed))
* **xss-sanitize:** use forked version ([fb50d5b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fb50d5b9d002b979d59b9f11c64048d335090082))
* zip handling & tests ([350ee79](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/350ee79af3c8fcc480970166a559596873beab2a))
* bump esqueleto & redo StudySubTerms ([0e027b1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0e027b129eada45ce90f6b32223aab3fde8cf9cd))
* bump to lts-15.0 ([cfaea9c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/cfaea9c08bcc52b2db33f24efe4967ef9bfbe9d2))
* bump versions ([67e3b38](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/67e3b38834ae079ec601633a60799359853f9b5e))
* remove applications and allocations ([66b4cf8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/66b4cf8542b1e8c16346861ceed2833d9a56f35f))
* split foundation & llvm ([c68a01d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c68a01d7ae26bfa61306e143d663d28f641d0998))
* **sub-study-fields:** reformulate as superStudyField ([b7d6f3c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b7d6f3c9e991790cda7920ced039a3d8b4ffa8ac)), closes [#531](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/531)
=======
* **form:** multiSelectField working with grouped options ([3aa8901](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3aa89019a8b4393da0eca715871a3793c1e3abb2))
* **health:** fix [#151](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/151) by offering route /health/interface/* ([c71814d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c71814d1ef1efc16c278136dfd6ebd86bd1d20db))
* **health:** fix [#153](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/153) and offer interface health route matching ([ce3852e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ce3852e3d365e62b32d181d58b7cbcc749e49373))
* **health:** negative interface routes working as intended now ([3303c4e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3303c4eebf928e527d2f9c1eb6e2495c10b94b13))
* **lms:** LMS restart failing due to old LmsUser entry ([6761767](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6761767c6ca8cab62a22aa6f755e6231e07ab411))
* **lms:** previouly failed notifications will be sent again ([263894b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/263894b05899ce55635d790f5334729fbc655ecc))
* **migration:** fix [#133](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/133) by removing old outdated migrations irrelevant to FRADrive ([d4f0d69](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d4f0d69428a4f7fc887cb6854cb59e3dea83b9bc))
* **migration:** ignore superfluous migration entries gracefully ([1d48b62](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1d48b627f6b8cf1b03e2ef63850c36c429c9d3d6))
* **print:** keep print jobs on user merge and lms id deletion ([a15862e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a15862ea72bc374af870ef3a23f86ae32c2c67a9))
* **school:** fix [#133](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/133) by adjusting default value ([2509358](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/25093588784381a19f34e5b091677b908420ddea))
* **sql:** remove potential bug in relation to missing parenthesis after not_ ([42695cf](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/42695cf5ef9f21691dc027f1ec97d57eec72f03e))
* **users:** fix [#121](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/121) by providing last login column, which was the last part missing ([decc5af](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/decc5af6829998e2d0db79382bbd9a7bad7b5b09))
* **model:** move user authentication data to new ExternalUser model ([12fe58f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/12fe58fc81eada015103f6eff4a486fd6f03cbec))
* **model:** separate user authentication data from User table; add ExternalAuth and InternalAuth models ([54f2430](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/54f2430b3e79d3b7c396ac4cf1d4d0da860e3d02))
* **settings:** rename userdb app settings ([9f299c8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9f299c854c9d2d2f1b1127c85a31b787f85fa210))
=======
## [27.4.79](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.78...v27.4.79) (2024-09-10)

View File

@ -24,9 +24,9 @@ mail-from:
email: "_env:MAILFROM_EMAIL:uniworx@localhost"
mail-object-domain: "_env:MAILOBJECT_DOMAIN:localhost"
mail-use-replyto-instead-sender: "_env:MAIL_USES_REPLYTO:true"
mail-reroute-to:
name: "_env:MAIL_REROUTE_TO_NAME:"
email: "_env:MAIL_REROUTE_TO_EMAIL:"
mail-reroute-to:
name: "_env:MAIL_REROUTE_TO_NAME:"
email: "_env:MAIL_REROUTE_TO_EMAIL:"
#mail-verp:
# separator: "_env:VERP_SEPARATOR:+"
# prefix: "_env:VERP_PREFIX:bounce"
@ -45,7 +45,7 @@ legal-external:
imprint: "https://www.fraport.com/de/tools/impressum.html"
data-protection: "https://www.fraport.com/de/konzern/datenschutz.html"
terms-of-use: "https://www.fraport.com/de/tools/disclaimer.html"
payments: "https://www.fraport.com/de/geschaeftsfelder/service/geschaeftspartner/richtlinien-und-zahlungsbedingungen.html"
payments: "https://www.fraport.com/de/geschaeftsfelder/service/geschaeftspartner/richtlinien-und-zahlungsbedingungen.html"
job-workers: "_env:JOB_WORKERS:10"
job-flush-interval: "_env:JOB_FLUSH:30"
@ -66,7 +66,7 @@ keep-unreferenced-files: 86400
health-check-interval:
matching-cluster-config: "_env:HEALTHCHECK_INTERVAL_MATCHING_CLUSTER_CONFIG:600"
http-reachable: "_env:HEALTHCHECK_INTERVAL_HTTP_REACHABLE:600"
ldap-admins: "_env:HEALTHCHECK_INTERVAL_LDAP_ADMINS:600"
ldap-admins: "_env:HEALTHCHECK_INTERVAL_LDAP_ADMINS:600" # TODO: either generalize over every external auth sources, or otherwise reimplement for different semantics
smtp-connect: "_env:HEALTHCHECK_INTERVAL_SMTP_CONNECT:600"
widget-memcached: "_env:HEALTHCHECK_INTERVAL_WIDGET_MEMCACHED:600"
active-job-executors: "_env:HEALTHCHECK_INTERVAL_ACTIVE_JOB_EXECUTORS:60"
@ -77,7 +77,7 @@ health-check-http: "_env:HEALTHCHECK_HTTP:true" # Can we assume, that we can rea
health-check-active-job-executors-timeout: "_env:HEALTHCHECK_ACTIVE_JOB_EXECUTORS_TIMEOUT:5"
health-check-active-widget-memcached-timeout: "_env:HEALTHCHECK_ACTIVE_WIDGET_MEMCACHED_TIMEOUT:2"
health-check-smtp-connect-timeout: "_env:HEALTHCHECK_SMTP_CONNECT_TIMEOUT:5"
health-check-ldap-admins-timeout: "_env:HEALTHCHECK_LDAP_ADMINS_TIMEOUT:60"
health-check-ldap-admins-timeout: "_env:HEALTHCHECK_LDAP_ADMINS_TIMEOUT:60" # TODO: either generalize over every external auth sources, or otherwise reimplement for different semantics
health-check-http-reachable-timeout: "_env:HEALTHCHECK_HTTP_REACHABLE_TIMEOUT:2"
health-check-matching-cluster-config-timeout: "_env:HEALTHCHECK_MATCHING_CLUSTER_CONFIG_TIMEOUT:2"
@ -126,24 +126,47 @@ database:
database: "_env:PGDATABASE:uniworx"
poolsize: "_env:PGPOOLSIZE:990"
auto-db-migrate: '_env:AUTO_DB_MIGRATE:true'
auto-db-migrate: "_env:AUTO_DB_MIGRATE:true"
ldap:
- host: "_env:LDAPHOST:"
tls: "_env:LDAPTLS:"
port: "_env:LDAPPORT:389"
user: "_env:LDAPUSER:"
pass: "_env:LDAPPASS:"
baseDN: "_env:LDAPBASE:"
scope: "_env:LDAPSCOPE:WholeSubtree"
timeout: "_env:LDAPTIMEOUT:5"
search-timeout: "_env:LDAPSEARCHTIME:5"
pool:
stripes: "_env:LDAPSTRIPES:1"
timeout: "_env:LDAPTIMEOUT:20"
limit: "_env:LDAPLIMIT:10"
# External sources used for user authentication and userdata lookups
user-auth:
# mode: single-source
protocol: "_env:USERAUTH_MODE:azureadv2"
config:
client-id: "_env:AZURECLIENTID:00000000-0000-0000-0000-000000000000"
client-secret: "_env:AZURECLIENTSECRET:''"
tenant-id: "_env:AZURETENANTID:00000000-0000-0000-0000-000000000000"
scopes: "_env:AZURESCOPES:[ID,Profile]"
# protocol: "ldap"
# config:
# host: "_env:LDAPHOST:"
# tls: "_env:LDAPTLS:"
# port: "_env:LDAPPORT:389"
# user: "_env:LDAPUSER:"
# pass: "_env:LDAPPASS:"
# baseDN: "_env:LDAPBASE:"
# scope: "_env:LDAPSCOPE:WholeSubtree"
# timeout: "_env:LDAPTIMEOUT:5"
# search-timeout: "_env:LDAPSEARCHTIME:5"
ldap-re-test-failover: 60
single-sign-on: "_env:OIDC_SSO:false"
# Automatically redirect to SSO route when not signed on
# Note: This will force authentication, thus the site will be inaccessible without external credentials. Only use this option when it is ensured that every user that should be able to access the site has valid external credentials!
auto-sign-on: "_env:AUTO_SIGN_ON:false"
# TODO: generalize for arbitrary auth protocols
# TODO: maybe use separate pools for external databases?
ldap-pool:
stripes: "_env:LDAPSTRIPES:1"
timeout: "_env:LDAPTIMEOUT:20"
limit: "_env:LDAPLIMIT:10"
# TODO: reintroduce and move into failover settings once failover mode has been reimplemented
# user-retest-failover: 60
# TODO; maybe implement syncWithin and syncInterval per auth source
user-sync-within: "_env:USER_SYNC_WITHIN:1209600" # 14 Tage in Sekunden
user-sync-interval: "_env:USER_SYNC_INTERVAL:3600" # jede Stunde
lms-direct:
upload-header: "_env:LMSUPLOADHEADER:true"
@ -166,7 +189,7 @@ avs:
lpr:
host: "_env:LPRHOST:fravm017173.fra.fraport.de"
port: "_env:LPRPORT:515"
queue: "_env:LPRQUEUE:fradrive"
queue: "_env:LPRQUEUE:fradrive"
smtp:
host: "_env:SMTPHOST:"
@ -189,7 +212,7 @@ widget-memcached:
timeout: "_env:WIDGET_MEMCACHED_TIMEOUT:20"
base-url: "_env:WIDGET_MEMCACHED_ROOT:"
expiration: "_env:WIDGET_MEMCACHED_EXPIRATION:3600"
session-memcached:
host: "_env:SESSION_MEMCACHED_HOST:localhost"
port: "_env:SESSION_MEMCACHED_PORT:11211"

View File

@ -301,7 +301,7 @@ export class ExamCorrect {
users: [user],
status: STATUS.LOADING,
};
if (results && results != {}) rowInfo.results = results;
if (results && Object.keys(results).length > 0) rowInfo.results = results;
if (result !== undefined) rowInfo.result = result;
this._addRow(rowInfo);

View File

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2022-25 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>,Steffen Jost <s.jost@fraport.de>
# SPDX-FileCopyrightText: 2022-2025 Sarah Vaupel <sarah.vaupel@uniworx.de>,Gregor Kleen <gregor.kleen@ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>,Steffen Jost <s.jost@fraport.de>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
@ -150,6 +150,8 @@ InterfaceName: Schnittstelle
InterfaceLastSynch: Zuletzt
InterfaceSubtype: Betreffend
InterfaceWrite: Schreibend
AdminUserPassword: Passwort
InterfaceSuccess: Rückmeldung
InterfaceInfo: Nachricht
InterfaceFreshness: Maximale Zugriffsfrist
@ -161,4 +163,4 @@ IWTActDelete: Entfernen
InterfaceWarningAdded: Schnittstellenwarnungszeit hinzugefügt oder geändert
InterfaceWarningDeleted n@Int: #{pluralDEeN n "Schnittstellenwarnungszeit"} gelöscht
InterfaceWarningDisabledEntirely: Alle Fehler ignorieren
InterfaceWarningDisabledInterval: Keine Zugriffsfrist
InterfaceWarningDisabledInterval: Keine Zugriffsfrist

View File

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2022-25 Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>,Steffen Jost <s.jost@fraport.de>
# SPDX-FileCopyrightText: 2022-2025 Sarah Vaupel <sarah.vaupel@uniworx.de>,Winnie Ros <winnie.ros@campus.lmu.de>,Steffen Jost <s.jost@fraport.de>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
@ -150,6 +150,8 @@ InterfaceName: Interface
InterfaceLastSynch: Last
InterfaceSubtype: Affecting
InterfaceWrite: Write
AdminUserPassword: Password
InterfaceSuccess: Returned
InterfaceInfo: Message
InterfaceFreshness: Maximum usage period
@ -161,4 +163,4 @@ IWTActDelete: Delete
InterfaceWarningAdded: Interface warning time added/changed
InterfaceWarningDeleted n: #{pluralENsN n "interface warning time"} deleted
InterfaceWarningDisabledEntirely: Ignore all errors
InterfaceWarningDisabledInterval: No maximum usage period
InterfaceWarningDisabledInterval: No maximum usage period

View File

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>
# SPDX-FileCopyrightText: 2022-2024 David Mosbach <david.mosbach@uniworx.de>, Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>, Sarah Vaupel <sarah.vaupel@ifi.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>, Winnie Ros <winnie.ros@campus.lmu.de>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
@ -72,8 +72,8 @@ UnauthorizedTutorialTutorControl: Ausbilder:innen dürfen diesen Kurs nicht edit
UnauthorizedCourseTutor: Sie sind nicht Ausbilder:in für diese Kursart.
UnauthorizedTutor: Sie sind nicht Ausbilder:in.
UnauthorizedTutorialRegisterGroup: Sie sind bereits in einem Kurs mit derselben Registrierungs-Gruppe eingetragen.
UnauthorizedLDAP: Angegebener Nutzer/Angegebene Nutzerin meldet sich nicht mit Fraport Login an.
UnauthorizedPWHash: Angegebener Nutzer/Angegebene Nutzerin meldet sich nicht mit FRADrive-Kennung an.
UnauthorizedExternal: Angegebene:r Benuzter:in meldet sich nicht über einen aktuell unterstützten externen Login an.
UnauthorizedInternal: Angegebene:r Benutzer:in meldet sich nicht mit FRADrive-Kennung an.
UnauthorizedExternalExamListNotEmpty: Liste von externen Prüfungen ist nicht leer
UnauthorizedExternalExamLecturer: Sie sind nicht als Prüfer:in für diese externe Prüfung eingetragen
UnauthorizedSubmissionSubmissionGroup: Sie sind nicht Mitglied in einer der registrierten Abgabegruppen, die an dieser Abgabe beteiligt sind
@ -102,15 +102,15 @@ LDAPLoginTitle: Fraport Login für interne und externe Nutzer
PWHashLoginTitle: Spezieller Funktionsnutzer Login
PWHashLoginNote: Verwenden Sie dieses Formular nur, wenn Sie explizit dazu aufgefordert wurden. Alle anderen sollten das andere Login Formular verwenden!
DummyLoginTitle: Development-Login
InternalLdapError: Interner Fehler beim Fraport Büko-Login
CampusUserInvalidIdent: Konnte anhand des Fraport Büko-Logins keine eindeutige Identifikation ermitteln
CampusUserInvalidEmail: Konnte anhand des Fraport Büko-Logins keine E-Mail-Addresse ermitteln
CampusUserInvalidDisplayName: Konnte anhand des Fraport Büko-Logins keinen vollen Namen ermitteln
CampusUserInvalidGivenName: Konnte anhand des Fraport Büko-Logins keinen Vornamen ermitteln
CampusUserInvalidSurname: Konnte anhand des Fraport Büko-Logins keinen Nachname ermitteln
CampusUserInvalidTitle: Konnte anhand des Fraport Büko-Logins keinen akademischen Titel ermitteln
CampusUserInvalidFeaturesOfStudy parseErr@Text: Konnte anhand des Fraport Büko-Logins keine Studiengänge ermitteln
CampusUserInvalidAssociatedSchools parseErr@Text: Konnte anhand des Fraport Büko-Logins keine Bereiche ermitteln
InternalLoginError: Interner Fehler beim Login
DecodeUserInvalidIdent: Konnte anhand des Fraport Büko-Logins keine eindeutige Identifikation ermitteln
DecodeUserInvalidEmail: Konnte anhand des Fraport Büko-Logins keine E-Mail-Addresse ermitteln
DecodeUserInvalidDisplayName: Konnte anhand des Fraport Büko-Logins keinen vollen Namen ermitteln
DecodeUserInvalidGivenName: Konnte anhand des Fraport Büko-Logins keinen Vornamen ermitteln
DecodeUserInvalidSurname: Konnte anhand des Fraport Büko-Logins keinen Nachname ermitteln
DecodeUserInvalidTitle: Konnte anhand des Fraport Büko-Logins keinen akademischen Titel ermitteln
DecodeUserInvalidFeaturesOfStudy parseErr@Text: Konnte anhand des Fraport Büko-Logins keine Studiengänge ermitteln
DecodeUserInvalidAssociatedSchools parseErr@Text: Konnte anhand des Fraport Büko-Logins keine Bereiche ermitteln
InvalidCredentialsADNoSuchObject: Benutzereintrag existiert nicht
InvalidCredentialsADLogonFailure: Ungültiges Passwort
InvalidCredentialsADAccountRestriction: Beschränkungen des Fraport Accounts verhindern Login
@ -139,3 +139,6 @@ FormHoneypotNamePlaceholder: Name
FormHoneypotComment: Kommentar
FormHoneypotCommentPlaceholder: Kommentar
FormHoneypotFilled: Bitte füllen Sie keines der verstecken Felder aus
Logout: Abmeldung
SingleSignOut: Abmeldung bei Azure

View File

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>
# SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, David Mosbach <david.mosbach@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>, Sarah Vaupel <sarah.vaupel@ifi.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>, Winnie Ros <winnie.ros@campus.lmu.de>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
@ -72,8 +72,8 @@ UnauthorizedTutorialTutorControl: Instructors may not edit this course.
UnauthorizedCourseTutor: You are no instructor for this course.
UnauthorizedTutor: You are no instructor.
UnauthorizedTutorialRegisterGroup: You are already registered for a course with the same registration group.
UnauthorizedLDAP: Specified user does not log in with their Fraport password.
UnauthorizedPWHash: Specified user does not log in with an FRADrive-account.
UnauthorizedExternal: Specified user does not log in with any currently supported external login.
UnauthorizedInternal: Specified user does not log in with a FRADrive-account.
UnauthorizedExternalExamListNotEmpty: List of external exams is not empty
UnauthorizedExternalExamLecturer: You are not an associated person for this external exam
UnauthorizedSubmissionSubmissionGroup: You are not member in any of the submission groups for this submission
@ -103,15 +103,15 @@ LDAPLoginTitle: Fraport login for intern and extern users
PWHashLoginTitle: Special function user login
PWHashLoginNote: Only use this login form if you have received special instructions to do so. All others should use the other login field.
DummyLoginTitle: Development login
InternalLdapError: Internal error during Fraport Büko login
CampusUserInvalidIdent: Could not determine unique identification during Fraport Büko login
CampusUserInvalidEmail: Could not determine email address during Fraport Büko login
CampusUserInvalidDisplayName: Could not determine display name during Fraport Büko login
CampusUserInvalidGivenName: Could not determine given name during Fraport Büko login
CampusUserInvalidSurname: Could not determine surname during Fraport Büko login
CampusUserInvalidTitle: Could not determine title during Fraport Büko login
CampusUserInvalidFeaturesOfStudy parseErr: Could not determine features of study during Fraport Büko login
CampusUserInvalidAssociatedSchools parseErr: Could not determine associated departments during Fraport Büko login
InternalLoginError: Internal error during login
DecodeUserInvalidIdent: Could not determine unique identification during Fraport Büko login
DecodeUserInvalidEmail: Could not determine email address during Fraport Büko login
DecodeUserInvalidDisplayName: Could not determine display name during Fraport Büko login
DecodeUserInvalidGivenName: Could not determine given name during Fraport Büko login
DecodeUserInvalidSurname: Could not determine surname during Fraport Büko login
DecodeUserInvalidTitle: Could not determine title during Fraport Büko login
DecodeUserInvalidFeaturesOfStudy parseErr: Could not determine features of study during Fraport Büko login
DecodeUserInvalidAssociatedSchools parseErr: Could not determine associated departments during Fraport Büko login
InvalidCredentialsADNoSuchObject: User entry does not exist
InvalidCredentialsADLogonFailure: Invalid password
InvalidCredentialsADAccountRestriction: Restrictions on your Fraport account prevent a login
@ -140,3 +140,6 @@ FormHoneypotNamePlaceholder !ident-ok: Name
FormHoneypotComment: Comment
FormHoneypotCommentPlaceholder: Comment
FormHoneypotFilled: Please do not fill in any of the hidden fields
Logout: Logout
SingleSignOut: Azure logout

View File

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>
# SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>, Winnie Ros <winnie.ros@campus.lmu.de>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
@ -75,11 +75,10 @@ NotPassed: Nicht bestanden
#userAuthModeUpdate.hs + templates
MailSubjectUserAuthModeUpdate: Ihr FRADrive-Login
UserAuthModeChangedToLDAP: Sie können sich nun mit Ihrer Fraport AG Kennung (Büko) in FRADrive einloggen.
UserAuthModeChangedToPWHash: Sie können sich nun mit einer FRADrive-internen Kennung einloggen.
UserAuthModeChangedToNoLogin: Ihr Login auf der FRADrive Webseite wurde deaktiviert, aber ihr FRADrive Konto besteht weiterhin. Gültigkeit und Verlängerungen Ihrer Qualifikationen sind dadurch nicht beeinträchtigt. Wenden Sie sich an die Fahrschuladmins, wenn der Login auf der FRADrive Webseite benötigt werden sollte.
AuthPWHashTip: Sie müssen nun das mit "FRADrive-Login" beschriftete Login-Formular verwenden. Stellen Sie bitte sicher, dass Sie ein Passwort gesetzt haben, bevor Sie versuchen sich anzumelden.
PasswordResetEmailIncoming: Einen Link um ihr Passwort zu setzen bzw. zu ändern bekommen Sie, aus Sicherheitsgründen, in einer separaten E-Mail.
UserAuthPasswordEnabled: Sie können sich nun mit einer FRADrive-internen Kennung einloggen.
UserAuthPasswordDisabled: Sie können sich nun nicht mehr mit Ihrer FRADrive-internen Kennung einloggen.
AuthExternalLoginTip: Sollten Sie Zugriff zu einem von FRADrive unterstützten externen Account (Azure-Login über Fraport-Kennung, Fraport-BüKo-Login) besitzen, so können Sie sich mit Ihren externen Login-Daten in FRADrive einloggen.
PasswordResetEmailIncoming: Einen Link um ihr Passwort zu setzen bzw. zu ändern bekommen Sie aus Sicherheitsgründen in einer separaten E-Mail.
MailFradrive !ident-ok: FRADrive
MailBodyFradrive: ist die Führerscheinverwaltungsapp der Fraport AG.

View File

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>
# SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>, Winnie Ros <winnie.ros@campus.lmu.de>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
@ -75,10 +75,9 @@ NotPassed: Failed
#userAuthModeUpdate.hs + templates
MailSubjectUserAuthModeUpdate: Your FRADrive login
UserAuthModeChangedToLDAP: You can now log in to FRADrive using your Fraport AG account (Büko)
UserAuthModeChangedToPWHash: You can now log in using your FRADrive-internal account
UserAuthModeChangedToNoLogin: Your login for the FRADrive website has been deactivated, but you FRADrive account persists. This has no effect on you qualifications. Please contact the driving school admins, if you need new login credentials for the FRADrive website.
AuthPWHashTip: You now need to use the login form labeled "FRADrive login". Please ensure that you have already set a password when you try to log in.
UserAuthPasswordEnabled: You can now log in using your FRADrive-internal account credentials.
UserAuthPasswordDisabled: You can no longer log in using your FRADrive-internal account credentials.
AuthExternalLoginTip: If you have access to an external account supported by FRADrive (Azure login via Fraport identification, Fraport-BüKo login), you can login in FRADrive using your external credentials.
PasswordResetEmailIncoming: For security reasons you will receive a link to the page on which you can set and later change your password in a separate email.
MailFradrive: FRADrive
MailBodyFradrive: is the apron driver's licence management app of Fraport AG.

View File

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>
# SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>, Winnie Ros <winnie.ros@campus.lmu.de>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
@ -45,8 +45,8 @@ AuthTagUserSubmissions: Abgaben erfolgen durch Kursartteilnehmer:innen
AuthTagCorrectorSubmissions: Abgaben erfolgen durch Korrektor:innen
AuthTagCorrectionAnonymous: Korrektur ist anonymisiert
AuthTagSelf: Nutzer:in greift nur auf eigene Daten zu
AuthTagIsLDAP: Nutzer:in meldet sich mit Fraport AG Kennung an
AuthTagIsPWHash: Nutzer:in meldet sich mit FRADrive spezifischer Kennung an
AuthTagIsExternal: Nutzer:in meldet sich mit extern verwalteten Logindaten an
AuthTagIsInternal: Nutzer:in meldet sich mit FRADrive-internen Logindaten an
AuthTagAuthentication: Nutzer:in ist angemeldet, falls erforderlich
AuthTagRead: Zugriff ist nur lesend
AuthTagWrite: Zugriff ist i.A. schreibend

View File

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>
# SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>, Sarah Vaupel <sarah.vaupel@ifi.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>, Winnie Ros <winnie.ros@campus.lmu.de>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
@ -45,8 +45,8 @@ AuthTagUserSubmissions: Submissions are made by course category participants
AuthTagCorrectorSubmissions: Submissions are registered by correctors
AuthTagCorrectionAnonymous: Correction is anonymised
AuthTagSelf: User is only accessing their only data
AuthTagIsLDAP: User logs in using their Fraport AG account
AuthTagIsPWHash: User logs in using their FRADrive specific account
AuthTagIsExternal: User logs in using externally managed credentials
AuthTagIsInternal: User logs in using FRADrive-internal credentials
AuthTagAuthentication: User is authenticated
AuthTagRead: Access is read only
AuthTagWrite: Access might write

View File

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Steffen Jost <jost@cip.ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>
# SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>, Steffen Jost <jost@cip.ifi.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>, Winnie Ros <winnie.ros@campus.lmu.de>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
@ -40,8 +40,8 @@ UsersCourseSchool: Bereich
ActionNoUsersSelected: Keine Benutzer:innen ausgewählt
SynchroniseAvsUserQueued n@Int: AVS-Synchronisation von #{n} #{pluralDE n "Benutzer:in" "Benutzer:innen"} zwingend angestoßen, die Ausführung wird mehrere Minuten benötigen!
SynchroniseAvsAllUsersQueued n@Int64: AVS-Synchronisation von allen #{n} #{pluralDE n "Benutzer:in" "Benutzer:innen"} angestoßen, welche heute noch nicht synchronisiert wurden, die Ausführung wird eine Weile brauchen!
SynchroniseLdapUserQueued n@Int: LDAP-Synchronisation von #{n} #{pluralDE n "Benutzer:in" "Benutzer:innen"} angestoßen, die Ausführung wird mehrere Minuten benötigen!
SynchroniseLdapAllUsersQueued: LDAP-Synchronisation von allen Benutzer:innen angestoßen, die Ausführung kann eine Weile brauchen!
SynchroniseUserdbUserQueued n@Int: Benutzerdatenbank-Synchronisation von #{n} #{pluralDE n "Benutzer:in" "Benutzer:innen"} angestoßen, die Ausführung wird mehrere Minuten benötigen!
SynchroniseUserdbAllUsersQueued: Benutzerdatenbank-Synchronisation von allen Benutzer:innen angestoßen, die Ausführung kann eine Weile brauchen!
UserListTitle: Komprehensive Benutzerliste
UserRecipientsTitle name@Text: Benachrichtigungsempfänger für #{name}
AccessRightsSaved: Berechtigungen erfolgreich verändert
@ -51,6 +51,7 @@ AuthLDAPInvalidLookup: Bestehender Nutzer/Bestehende Nutzerin konnte nicht einde
AuthLDAPAlreadyConfigured: Nutzer:in meldet sich bereits per Fraport AG Kennung in FRADrive an
AuthLDAPConfigured: Nutzer:in meldet sich nun per Fraport AG Kennung in FRADrive an
AuthLDAP !ident-ok: Fraport AG Kennung
AuthAzure: Azure-Account
AuthNoLogin: Kein Login erlaubt.
PasswordResetQueued: Link zum Passwort-Zurücksetzen versandt
UserAssimilateUser: Benutzer:in
@ -105,9 +106,6 @@ AllUsersLdapSync: Alle LDAP-Synchronisieren
AllUsersAvsSync: Alle AVS-Synchronisieren
ThisUserLdapSync: LDAP Synchronisation
ThisUserAvsSync: AVS Synchronisation
AuthKindLDAP: Fraport AG Kennung
AuthKindPWHash: FRADrive Kennung
AuthKindNoLogin: Kein Login möglich
Name !ident-ok: Name
UsersChangeSupervisorsSuccess usr@Int spr@Int: #{tshow spr} Ansprechpartner für #{tshow usr} Benutzer gesetzt.
UsersChangeSupervisorsWarning usr@Int spr@Int bad@Int: Nur _{MsgUsersChangeSupervisorsSuccess usr spr} #{tshow bad} Ansprechpartner #{pluralDE bad "wurde" "wurden"} nicht gefunden!
@ -118,4 +116,10 @@ UserCompanyReasonTooltip: Optionale Notiz für besondere Fälle. Kann ggf. autma
UserSupervisorReason: Begründung Ansprechpartner
UserSupervisorReasonTooltip: Optionale Notiz für besondere Fälle. Kann ggf. autmatische Entfernung bei AVS Firmenwechsel verhindern.
UserSupervisorCompany: Ansprechpartner wegen Firma
AdminUserAllNotifications: Alle Benachrichtigungen and diesen Benutzer
AdminUserAllNotifications: Alle Benachrichtigungen and diesen Benutzer
AdminUserAuthentication: Authentification
AdminUserAuthLastSync: Zuletzt synchronisiert
AuthKindLDAP: Fraport-AG-Kennung (LDAP)
AuthKindAzure: Azure-Login
AuthKindPWHash: Interne FRADrive-Kennung
AuthKindNoLogin: Kein Login möglich

View File

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2022 Steffen Jost <jost@cip.ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>
# SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Steffen Jost <jost@cip.ifi.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>, Winnie Ros <winnie.ros@campus.lmu.de>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
@ -40,8 +40,8 @@ UsersCourseSchool: Department
ActionNoUsersSelected: No users selected
SynchroniseAvsUserQueued n: Triggered forced AVS synchronisation of #{n} #{pluralEN n "user" "users"}, which may take several minutes to complete.
SynchroniseAvsAllUsersQueued n: Triggered AVS synchronisation of all #{n} #{pluralEN n "user" "users"} that were not already synchronised today, which may take quite a while to complete.
SynchroniseLdapUserQueued n: Triggered LDAP synchronisation of #{n} #{pluralEN n "user" "users"}, which may take several minutes to complete.
SynchroniseLdapAllUsersQueued: Triggered LDAP synchronisation of all users, which may take quite a while to complete.
SynchroniseUserdbUserQueued n: Triggered user database synchronisation of #{n} #{pluralEN n "user" "users"}, which may take several minutes to complete.
SynchroniseUserdbAllUsersQueued: Triggered user database synchronisation of all users, which may take quite a while to complete.
UserListTitle: Comprehensive list of users
UserRecipientsTitle name: Notificationrecipients for #{name}
AccessRightsSaved: Successfully updated permissions
@ -51,6 +51,7 @@ AuthLDAPInvalidLookup: Existing user could not be uniquely matched with a LDAP e
AuthLDAPAlreadyConfigured: User already logs in using their Fraport AG account
AuthLDAPConfigured: User now logs in using their Fraport AG account
AuthLDAP: Fraport AG account
AuthAzure: Azure account
AuthNoLogin: No login allowed.
PasswordResetQueued: Sent link to reset password
UserAssimilateUser: User
@ -105,9 +106,6 @@ AllUsersLdapSync: Synchronise all with LDAP
AllUsersAvsSync: Synchronise all with AVS
ThisUserLdapSync: Synchronise user with LDAP
ThisUserAvsSync: Synchronise user with AVS
AuthKindLDAP: Fraport AG account
AuthKindPWHash: FRADrive account
AuthKindNoLogin: No login
Name: Name
UsersChangeSupervisorsSuccess usr spr: #{pluralENsN spr "supervisor"} for #{pluralENsN usr "user"} set.
UsersChangeSupervisorsWarning usr spr bad: Only _{MsgUsersChangeSupervisorsSuccess usr spr} #{pluralENsN bad "supervisors"} could not be identified!
@ -118,4 +116,11 @@ UserCompanyReasonTooltip: Optional note for special cases. In some case this may
UserSupervisorReason: Reason for supervision
UserSupervisorReasonTooltip: Optional note for special cases. In some case this may prevent automatic removel upon AVS user company changes.
UserSupervisorCompany: Supervisor for company
AdminUserAllNotifications: All notification sent to this user
AdminUserAllNotifications: All notification sent to this user
AdminUserAuthentication: Authentifizierung
AdminUserAuthLastSync: Last synchronised
AuthKindLDAP: Fraport AG account (LDAP)
AuthKindAzure: Azure account
AuthKindPWHash: Internal FRADrive login
AuthKindNoLogin: No login

View File

@ -78,6 +78,7 @@ BreadcrumbAdminFeaturesHeading: Studiengänge
BreadcrumbAdminTest: Admin-Demo
BreadcrumbAdminErrMsg: Fehlermeldung entschlüsseln
BreadcrumbAdminTokens: Tokens ausstellen
BreadcrumbAdminLdap !ident-ok: LDAP
BreadcrumbSchoolList: Bereiche
BreadcrumbSchoolNew: Neuen Bereich anlegen
BreadcrumbExamOfficeExams: Prüfungen

View File

@ -78,6 +78,7 @@ BreadcrumbAdminFeaturesHeading: Features of study
BreadcrumbAdminTest: Admin-demo
BreadcrumbAdminErrMsg: Decrypt error message
BreadcrumbAdminTokens: Issue tokens
BreadcrumbAdminLdap: LDAP
BreadcrumbSchoolList: Departments
BreadcrumbSchoolNew: Create new department
BreadcrumbExamOfficeExams: Exams

View File

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2022-25 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@cip.ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>,Steffen Jost <s.jost@fraport.de>
# SPDX-FileCopyrightText: 2022-25-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, David Mosbach <david.mosbach@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@cip.ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>,Steffen Jost <s.jost@fraport.de>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
@ -25,6 +25,7 @@ MenuInstance: Instanz-Identifikation
MenuHealth: Instanz-Zustand
MenuHealthInterface: Schnittstellen Zustand
MenuHelp: Hilfe
MenuAccount: Konto
MenuProfile: Anpassen
MenuLogin !ident-ok: Login
MenuLogout !ident-ok: Logout
@ -148,7 +149,7 @@ MenuSap: SAP Schnittstelle
MenuAvs: AVS Schnittstelle
MenuAvsSynchError: AVS Problemübersicht
MenuLdap: LDAP Schnittstelle
MenuExternalUser: Externe Benutzer
MenuApc: Druck
MenuPrintSend: Manueller Briefversand
MenuPrintDownload: Brief herunterladen

View File

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2022-25 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>,Steffen Jost <s.jost@fraport.de>
# SPDX-FileCopyrightText: 2022-25-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, David Mosbach <david.mosbach@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>,Steffen Jost <s.jost@fraport.de>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
@ -25,6 +25,7 @@ MenuInstance: Instance identification
MenuHealth: Instance health
MenuHealthInterface: Interface health
MenuHelp: Support
MenuAccount: Account
MenuProfile: Settings
MenuLogin: Login
MenuLogout: Logout
@ -148,7 +149,7 @@ MenuSap: SAP Interface
MenuAvs: AVS Interface
MenuAvsSynchError: AVS Problem Overview
MenuLdap: LDAP Interface
MenuExternalUser: External users
MenuApc: Print
MenuPrintSend: Send Letter
MenuPrintDownload: Download Letter

View File

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2023-25 Steffen Jost <jost@tcs.ifi.lmu.de>,Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>,Steffen Jost <s.jost@fraport.de>
# SPDX-FileCopyrightText: 2023-25-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Steffen Jost <jost@tcs.ifi.lmu.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>, Sarah Vaupel <sarah.vaupel@ifi.lmu.de>, Winnie Ros <winnie.ros@campus.lmu.de>,Steffen Jost <s.jost@fraport.de>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
@ -164,4 +164,6 @@ SheetGradingPassAlways': Automatisch bestanden, sobald korrigiert
SheetTypeNormal !ident-ok: Normal
SheetTypeBonus !ident-ok: Bonus
InvalidFormAction: Keine Aktion ausgeführt wegen ungültigen Formulardaten
InvalidFormAction: Keine Aktion ausgeführt wegen ungültigen Formulardaten
InvalidUuid: Invalide UUID!

View File

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2023-25 Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>
# SPDX-FileCopyrightText: 2023-25-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Sarah Vaupel <sarah.vaupel@ifi.lmu.de>, Winnie Ros <winnie.ros@campus.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
@ -164,4 +164,6 @@ SheetGradingPassAlways': Automatically passed when corrected
SheetTypeNormal: Normal
SheetTypeBonus: Bonus
InvalidFormAction: No action taken due to invalid form data
InvalidFormAction: No action taken due to invalid form data
InvalidUuid: Invalid UUID!

View File

@ -42,11 +42,11 @@ Qualification
-- - PinReset==1 mit bestehendem Passwort kann problemlos erneut gesendet werden
-- - Flag "interner Mitarbeiter" wird von Know-How ignoriert / nicht ausgewertet (legacy)
-- QualificationPrecondition -- NOTE: this can only be enforced through a background job adding or removing qualifications
-- qualification QualificationId OnDeleteCascade OnUpdateCascade -- AND: not unique, ie. qualification can have multiple required preconditions
-- required [QualificationId] -- OR : alternatives, any one will suffice -- we don't want array, since we have recursive CTEs
-- continuous Bool -- expiring precondition blocks qualification
-- deriving Generic Show
-- -- QualificationPrecondition -- NOTE: this can only be enforced through a background job adding or removing qualifications
-- -- qualification QualificationId OnDeleteCascade OnUpdateCascade -- AND: not unique, ie. qualification can have multiple required preconditions
-- -- required [QualificationId] -- OR : alternatives, any one will suffice -- we don't want array, since we have recursive CTEs
---- continuous Bool -- expiring precondition blocks qualification
-- -- deriving Generic Show
-- Maybe an alternative for online qualification validity checking, transitivity through recursive CTEs? (already available in our version)
QualificationRequirement
@ -57,7 +57,7 @@ QualificationRequirement
UniqueQualificationRequirement qualification requirement
deriving Generic Show
-- TODO: connect Qualification with Exams!
-- TODO: connect Qualifications with Exams!?
QualificationEdit
user UserId
@ -84,6 +84,7 @@ QualificationUserBlock
from UTCTime
reason Text
blocker UserId Maybe
-- precondition Bool default=false -- if true, this was due to a precondition
deriving Eq Ord Read Show Generic
-- LMS Interface Tables, need regular processing by background jobs, per QualificationId:

View File

@ -1,8 +1,8 @@
-- SPDX-FileCopyrightText: 2022-24 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>, Sarah Vaupel <sarah.vaupel@ifi.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>, Steffen Jost <s.jost@fraport.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
-- The files in /models determine t he database scheme.
-- The files in /models determine the database scheme.
-- The organisational split into several files has no operational effects.
-- White-space and case matters: Each SQL table is named in 1st column of this file
-- Indendent lower-case lines describe the SQL-columns of the table with name, type and options
@ -11,21 +11,22 @@
-- Indendent upper-case lines usually impose Uniqueness constraints for rows by some columns.
-- Each table will also have an column storing a unique numeric row key, unless there is a row Primary columnname
--
User json -- Each Uni2work user has a corresponding row in this table; created upon first login.
surname UserSurname -- Display user names always through 'nameWidget displayName surname'
displayName UserDisplayName
displayEmail UserEmail -- Case-insensitive eMail address, used for sending; leave empty for using auto-update CompanyEmail via UserCompany
email UserEmail -- Case-insensitive eMail address, used for identification and fallback for sending. Defaults to "AVSNO:dddddddd" if unknown
ident UserIdent -- Case-insensitive user-identifier. Defaults to "AVSID:dddddddd" if unknown
authentication AuthenticationMode -- 'AuthLDAP' or ('AuthPWHash'+password-hash)
lastAuthentication UTCTime Maybe -- last login date
created UTCTime default=now()
passwordHash Text Maybe -- If specified, allows the user to login with credentials independently of external authentication
lastAuthentication UTCTime Maybe -- When did the user last authenticate?
lastLdapSynchronisation UTCTime Maybe
ldapPrimaryKey UserEduPersonPrincipalName Maybe -- Fraport Personnel Number or Email-Prefix for @fraport.de work here
tokensIssuedAfter UTCTime Maybe -- do not accept bearer tokens issued before this time (accept all tokens if null)
matrikelnummer UserMatriculation Maybe -- usually a number; AVS Personalnummer; nicht Fraport Personalnummer!
surname UserSurname -- Display user names always through 'nameWidget displayName surname'
firstName Text -- For export in tables, pre-split firstName from displayName
title Text Maybe -- For upcoming name customisation
displayName UserDisplayName
displayEmail UserEmail -- Case-insensitive eMail address, used for sending; leave empty for using auto-update CompanyEmail via UserCompany
email UserEmail -- Case-insensitive eMail address, used for identification and fallback for sending. Defaults to "AVSNO:dddddddd" if unknown
maxFavourites Int default=12 -- max number of non-manual entries in favourites bar (pruned only if below a set importance threshold)
maxFavouriteTerms Int default=2 -- max number of term-sections in favourites bar
theme Theme default='ThemeDefault' -- Color-theme of the frontend; user-defined
@ -50,11 +51,20 @@ User json -- Each Uni2work user has a corresponding row in this table; create
prefersPostal Bool default=false -- user prefers letters by post instead of email
examOfficeGetSynced Bool default=true -- whether synced status should be displayed for exam results by default
examOfficeGetLabels Bool default=true -- whether labels should be displayed for exam results by default
UniqueAuthentication ident -- Column 'ident' can be used as a row-key in this table
lastSync UTCTime Maybe -- When was the User data last synchronised with external sources?
UniqueAuthentication ident -- Column 'ident' can be used as a row-key in this table
UniqueEmail email -- Column 'email' can be used as a row-key in this table
UniqueLdapPrimaryKey ldapPrimaryKey !force -- Column 'ldapPrimaryKey' is either empty or contains a unique value
deriving Show Eq Ord Generic -- Haskell-specific settings for runtime-value representing a row in memory
-- | User data fetched from external user sources, used for authentication and data queries
ExternalUser
user UserIdent
source AuthSourceIdent -- Identifier of the external source in the config
data Value "default='{}'::jsonb" -- Raw user data from external source -- TODO: maybe make Maybe, iff the source only ever responds with "success"?
lastSync UTCTime -- When was the external source last queried?
UniqueExternalUser user source -- At most one entry of this user per source
deriving Show Eq Ord Generic
UserFunction -- Administratively assigned functions (lecturer, admin, evaluation, ...)
user UserId
school SchoolId

8181
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "fradrive",
"version": "27.4.59",
"version": "27.4.79",
"description": "",
"keywords": [],
"author": "",

View File

@ -3,13 +3,14 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
name: uniworx
version: 27.4.79
version: 28.1.1
dependencies:
- base
- yesod
- yesod-core
- yesod-persistent
- yesod-auth
- yesod-auth-oauth2
- yesod-static
- yesod-form
- yesod-persistent
@ -24,7 +25,6 @@ dependencies:
- template-haskell
- shakespeare
- monad-control
- wai-extra
- yaml
- http-conduit
- directory
@ -34,7 +34,6 @@ dependencies:
- conduit
- monad-logger
- fast-logger
- wai-logger
- foreign-store
- file-embed
- unordered-containers
@ -43,6 +42,10 @@ dependencies:
- time
- case-insensitive
- wai
- wai-cors
- wai-extra
- wai-logger
- wai-middleware-prometheus
- cryptonite
- cryptonite-conduit
- saltine
@ -147,7 +150,6 @@ dependencies:
- cookie
- prometheus-client
- prometheus-metrics-ghc
- wai-middleware-prometheus
- extended-reals
- rfc5051
- unidecode

View File

@ -1,6 +1,6 @@
#!/usr/bin/env bash
# SPDX-FileCopyrightText: 2023 Sarah Vaupel <sarah.vaupel@uniworx.de>
# SPDX-FileCopyrightText: 2023-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
@ -14,7 +14,12 @@ case "$(git rev-parse --abbrev-ref HEAD)" in
standard-version -a -t t
;;
*)
echo "Current branch not supported for release!"
exit 1
if echo $@ | grep -xqe '--dev';
then
standard-version -a -t d
else
echo "Current branch not supported for release!"
exit 1
fi
;;
esac

8
routes
View File

@ -30,8 +30,8 @@
-- !capacity -- course this route is associated with has at least one unit of participant capacity
-- !empty -- course this route is associated with has no participants whatsoever
--
-- !is-ldap -- user has authentication mode set to LDAP
-- !is-pw-hash -- user has authentication mode set to PWHash
-- !is-external -- user can login using external sources
-- !is-internal -- user can login using internal credentials
--
-- !materials -- only if course allows all materials to be free (no meaning outside of courses)
-- !time -- access depends on time somehow
@ -47,6 +47,9 @@
/auth AuthR Auth getAuth !free
/logout SOutR GET !free
/logout/ssout SSOutR GET !free -- single sign-out (OIDC)
/metrics MetricsR GET !free -- verify if this can be free
/err ErrorR GET !free
@ -71,6 +74,7 @@
/admin/crontab/jobs AdminJobsR GET POST
/admin/avs AdminAvsR GET POST
/admin/avs/#CryptoUUIDUser AdminAvsUserR GET POST
/admin/external-user AdminExternalUserR GET POST
/admin/ldap AdminLdapR GET POST
/admin/problems AdminProblemsR GET POST
/admin/problems/no-contact ProblemUnreachableR GET POST

View File

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2022-2023 Gregor Kleen <gregor@kleen.consulting>, Sarah Vaupel <sarah.vaupel@uniworx.de>, Steffen Jost <jost@tcs.ifi.lmu.de>
# SPDX-FileCopyrightText: 2022-2024 Gregor Kleen <gregor@kleen.consulting>, Sarah Vaupel <sarah.vaupel@uniworx.de>, Steffen Jost <jost@tcs.ifi.lmu.de>, David Mosbach <david.mosbach@uniworx.de>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
@ -9,6 +9,13 @@ let
haskellPackages = pkgs.haskellPackages;
oauth2Flake = (builtins.getFlake "git+https://gitlab.uniworx.de/mosbach/oauth2-mock-server/?rev=7b995e6cffa963a24eb5d0373b2d29089533284f&ref=main").packages.x86_64-linux;
oauth2MockServer = oauth2Flake.default;
mkOauth2DB = oauth2Flake.mkOauth2DB;
killOauth2DB = oauth2Flake.killOauth2DB;
postgresSchema = pkgs.writeText "schema.sql" ''
CREATE USER uniworx WITH SUPERUSER;
CREATE DATABASE uniworx_test;
@ -21,6 +28,17 @@ let
local all all trust
'';
oauth2Schema = pkgs.writeText "oauth2_schema.sql" ''
CREATE USER oauth2mock WITH SUPERUSER;
CREATE DATABASE test_users;
GRANT ALL ON DATABASE test_users TO oauth2mock;
'';
oauth2Hba = pkgs.writeText "oauth2_hba_file" ''
local all all trust
'';
develop = pkgs.writeScriptBin "develop" ''
#!${pkgs.zsh}/bin/zsh -e
@ -44,6 +62,9 @@ let
type cleanup_cache_memcached &>/dev/null && cleanup_cache_memcached
type cleanup_minio &>/dev/null && cleanup_minio
type cleanup_maildev &>/dev/null && cleanup_maildev
[[ -z "$OAUTH2_PGDIR" ]] || source ${killOauth2DB}/bin/killOauth2DB
[[ -z "$OAUTH2_PGHOST" ]] || pkill oauth2-mock-ser
[[ -z "$PORT_OFFSET" ]] || runghc .ports/assign.hs --remove $PORT_OFFSET
[ -f "''${basePath}/.develop.env" ] && rm -vf "''${basePath}/.develop.env"
set +x
@ -51,7 +72,17 @@ let
trap cleanup EXIT
export PORT_OFFSET=$(((16#$(sha256sum <<<"$(hostname -f):''${basePath}" | head -c 16)) % 1000))
export PORT_OFFSET=$(runghc .ports/assign.hs --assign .ports/offsets)
# export PORT_OFFSET=$(((16#$(sha256sum <<<"$(hostname -f):''${basePath}" | head -c 16)) % 1000))
if [[ -z "$OAUTH2_PGHOST" ]]; then
set -xe
export OAUTH2_SERVER_PORT=$((9443 + $PORT_OFFSET))
export OAUTH2_DB_PORT=$((9444 + $PORT_OFFSET))
source ${mkOauth2DB}/bin/mkOauth2DB
${oauth2MockServer}/bin/oauth2-mock-server&
set +xe
fi
if [[ -z "$PGHOST" ]]; then
set -xe

View File

@ -1,4 +1,4 @@
-- SPDX-FileCopyrightText: 2022-2024 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Sarah Vaupel <vaupel.sarah@campus.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>,-2024 Gregor Kleen <gregor.kleen@ifi.lmu.de>, Sarah Vaupel <sarah.vaupel@ifi.lmu.de>, Sarah Vaupel <vaupel.sarah@campus.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>, David Mosbach <david.mosbach@uniworx.de>,Steffen Jost <s.jost@fraport.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
@ -6,10 +6,8 @@
module Application
( getAppSettings, getAppDevSettings
, appMain
, develMain
, appMain, develMain
, makeFoundation
, makeMiddleware
-- * for DevelMain
, foundationStoreNum
, getApplicationRepl
@ -20,96 +18,97 @@ module Application
, addPWEntry
) where
import Control.Monad.Logger (liftLoc, LoggingT(..), MonadLoggerIO(..))
import Import hiding (cancel, respond)
import Handler.Utils (runAppLoggingT)
import Handler.Utils.Memcached (manageMemcachedLocalInvalidations)
import Jobs
import Middleware
import Utils.Avs
import qualified Utils.Pool as Custom
import Utils.Postgresql
import Control.Concurrent.STM.Delay
import Control.Monad.Logger (liftLoc, LoggingT(..), MonadLoggerIO(..))
import Control.Monad.Trans.Cont (runContT, callCC)
import Control.Monad.Trans.Resource
import qualified Data.Acid.Memory as Acid
import qualified Data.Aeson as Aeson
import qualified Data.ByteString.Lazy as LBS
import qualified Data.IntervalMap.Strict as IntervalMap
import qualified Data.Map as Map
import Data.Ratio ((%))
import qualified Data.Set as Set
import Data.Streaming.Network (bindPortTCP)
import qualified Data.Text.Encoding as Text
import qualified Data.UUID as UUID
import qualified Data.UUID.V4 as UUID
import qualified Database.Memcached.Binary.IO as Memcached
import Database.Persist.Postgresql ( openSimpleConn, pgConnStr, pgPoolIdleTimeout
, pgPoolSize
)
import Database.Persist.SqlBackend.Internal ( connClose )
import qualified Database.PostgreSQL.Simple as PG
import Import hiding (cancel, respond)
import Language.Haskell.TH.Syntax (qLocation)
import Network.Wai (Middleware)
import qualified Network.Wai as Wai
import Network.Wai.Handler.Warp (Settings, defaultSettings,
defaultShouldDisplayException,
runSettings, runSettingsSocket, setHost,
setBeforeMainLoop,
setOnException, setPort, getPort)
import Network.Connection (settingDisableCertificateValidation)
import Data.Streaming.Network (bindPortTCP)
import Network.Wai.Middleware.RequestLogger (Destination (Logger),
IPAddrSource (..),
OutputFormat (..), destination,
mkRequestLogger, outputFormat)
import System.Log.FastLogger ( defaultBufSize, newStderrLoggerSet, newStdoutLoggerSet, newFileLoggerSet
, toLogStr, rmLoggerSet
)
import Handler.Utils (runAppLoggingT)
import Foreign.Store
import Web.Cookie
import Network.HTTP.Types.Header (hSetCookie)
import GHC.RTS.Flags (getRTSFlags)
import qualified Data.UUID as UUID
import qualified Data.UUID.V4 as UUID
import Language.Haskell.TH.Syntax (qLocation)
import System.Directory
import Jobs
import qualified Data.Text.Encoding as Text
import Yesod.Auth.Util.PasswordStore
import qualified Data.ByteString.Lazy as LBS
import qualified Ldap.Client as Ldap (Host(Plain,Tls))
import Network.Connection (settingDisableCertificateValidation)
import Network.HaskellNet.SSL hiding (Settings)
import Network.HaskellNet.SMTP.SSL as SMTP hiding (Settings)
import Network.HTTP.Client.TLS (mkManagerSettings)
import qualified Network.Minio as Minio
import Network.Socket (socketPort, Socket, PortNumber)
import qualified Network.Socket as Socket (close)
import Network.Wai.Handler.Warp ( Settings
, defaultSettings
, defaultShouldDisplayException
, runSettings, runSettingsSocket
, getPort, setPort
, setHost, setBeforeMainLoop, setOnException
)
import qualified Prometheus
import qualified System.Clock as Clock
import System.Directory
import System.Environment (lookupEnv)
import System.Exit
import System.Log.FastLogger ( defaultBufSize
, newStderrLoggerSet, newStdoutLoggerSet, newFileLoggerSet
, toLogStr, rmLoggerSet
)
import System.Log.FastLogger.Date
import System.Posix.Process (getProcessID)
import System.Posix.Signals (SignalInfo(..), installHandler, sigTERM, sigINT)
import qualified System.Posix.Signals as Signals (Handler(..))
import qualified System.Systemd.Daemon as Systemd
import UnliftIO.Concurrent
import UnliftIO.Pool
import Control.Monad.Trans.Resource
import qualified Web.ServerSession.Backend.Acid as Acid
import Web.ServerSession.Core (StorageException(..))
import System.Log.FastLogger.Date
import Yesod.Auth.OAuth2.AzureAD (oauth2AzureADScoped)
import Yesod.Auth.Util.PasswordStore
import qualified Yesod.Core.Types as Yesod (Logger(..))
import qualified Data.HashMap.Strict as HashMap
import qualified Data.Aeson as Aeson
import System.Exit
import qualified Database.Memcached.Binary.IO as Memcached
import qualified System.Systemd.Daemon as Systemd
import System.Environment (lookupEnv)
import System.Posix.Process (getProcessID)
import System.Posix.Signals (SignalInfo(..), installHandler, sigTERM, sigINT)
import qualified System.Posix.Signals as Signals (Handler(..))
import Network.Socket (socketPort, Socket, PortNumber)
import qualified Network.Socket as Socket (close)
import Control.Concurrent.STM.Delay
import Control.Monad.Trans.Cont (runContT, callCC)
import Data.Ratio ((%))
import qualified Data.Set as Set
import qualified Data.Map as Map
import Handler.Utils.Routes (classifyHandler)
import qualified Data.Acid.Memory as Acid
import qualified Web.ServerSession.Backend.Acid as Acid
import qualified Ldap.Client as Ldap (Host(Plain, Tls))
import qualified Network.Minio as Minio
import Web.ServerSession.Core (StorageException(..))
#ifdef DEVELOPMENT
import Data.Maybe (fromJust)
import Auth.OAuth2 (azureMockServer)
#endif
import GHC.RTS.Flags (getRTSFlags)
@ -159,11 +158,11 @@ import Handler.PrintCenter
import Handler.ApiDocs
import Handler.Swagger
import Handler.Firm
import Handler.SingleSignOut
import ServantApi () -- YesodSubDispatch instances
import Servant.API
import Servant.Client
import Network.HTTP.Client.TLS (mkManagerSettings)
-- This line actually creates our YesodDispatch instance. It is the second half
@ -222,7 +221,7 @@ makeFoundation appSettings''@AppSettings{..} = do
-- from there, and then create the real foundation.
let
mkFoundation :: _ -> (forall m. MonadIO m => Custom.Pool' m DBConnLabel DBConnUseState SqlBackend) -> _
mkFoundation appSettings' appConnPool appSmtpPool appLdapPool appCryptoIDKey appSessionStore appSecretBoxKey appWidgetMemcached appJSONWebKeySet appClusterID appMemcached appUploadCache appVerpSecret appAuthKey appPersonalisedSheetFilesSeedKey appVolatileClusterSettingsCache appAvsQuery = UniWorX{..}
mkFoundation appSettings' appConnPool appSmtpPool appLdapPool appCryptoIDKey appSessionStore appSecretBoxKey appWidgetMemcached appJSONWebKeySet appClusterID appMemcached appUploadCache appVerpSecret appAuthKey appAuthPlugins appPersonalisedSheetFilesSeedKey appVolatileClusterSettingsCache appAvsQuery = UniWorX{..}
tempFoundation = mkFoundation
(error "appSettings' forced in tempFoundation")
(error "connPool forced in tempFoundation")
@ -238,10 +237,12 @@ makeFoundation appSettings''@AppSettings{..} = do
(error "MinioConn forced in tempFoundation")
(error "VerpSecret forced in tempFoundation")
(error "AuthKey forced in tempFoundation")
(error "AuthPlugins forced in tempFoundation")
(error "PersonalisedSheetFilesSeedKey forced in tempFoundation")
(error "VolatileClusterSettingsCache forced in tempFoundation")
(error "AvsQuery forced in tempFoundation")
runAppLoggingT tempFoundation $ do
$logInfoS "InstanceID" $ UUID.toText appInstanceID
$logInfoS "Configuration" $ tshowCrop appSettings''
@ -276,13 +277,32 @@ makeFoundation appSettings''@AppSettings{..} = do
sqlPool = Custom.hoistPool (liftIO . flip runLoggingT logFunc) sqlPool'
void . Prometheus.register . poolMetrics PoolDatabaseConnections $ sqlPool @IO
ldapPool <- traverse mkFailoverLabeled <=< forOf (traverse . traverse) appLdapConf $ \conf@LdapConf{..} -> do
let ldapLabel = case ldapHost of
Ldap.Plain str -> pack str <> ":" <> tshow ldapPort
Ldap.Tls str _ -> pack str <> ":" <> tshow ldapPort
$logDebugS "setup" $ "LDAP-Pool " <> ldapLabel
(ldapLabel,) . (conf,) <$> createLdapPool ldapHost ldapPort (poolStripes ldapPool) (poolTimeout ldapPool) ldapTimeout (poolLimit ldapPool)
forM_ ldapPool $ registerFailoverMetrics "ldap"
-- ldapPool <- traverse mkFailoverLabeled <=< forOf (traverse . traverse) appUserDbConf $ \conf -> if
-- | UserDbSingleSource{..} <- conf
-- , UserDbLdap LdapConf{..} <- userdbSingleSource
-- , Just ResourcePoolConf{..} <- userdbPoolConf
-- -> do
-- let ldapLabel = case ldapHost of
-- Ldap.Plain str -> pack str <> ":" <> tshow ldapPort
-- Ldap.Tls str _ -> pack str <> ":" <> tshow ldapPort
-- $logDebugS "setup" $ "LDAP-Pool " <> ldapLabel
-- (ldapLabel,) . (conf,) <$> createLdapPool ldapHost ldapPort poolStripes poolTimeout ldapTimeout poolLimit
-- | otherwise
-- -> return mempty
-- forM_ ldapPool $ registerFailoverMetrics "ldap"
-- TODO: reintroduce failover once UserDbFailover is implemented (see above)
ldapPool <- fmap join . forM appLdapPoolConf $ \ResourcePoolConf{..} -> if
| UserAuthConfSingleSource{..} <- appUserAuthConf
, AuthSourceConfLdap conf@LdapConf{..} <- userAuthConfSingleSource
-> do -- set up a singleton ldap pool with no failover
let ldapLabel = case ldapConfHost of
Ldap.Plain str -> pack str <> ":" <> tshow ldapConfPort
Ldap.Tls str _ -> pack str <> ":" <> tshow ldapConfPort
$logDebugS "setup" $ "LDAP-Pool " <> ldapLabel
Just . (conf,) <$> createLdapPool ldapConfHost ldapConfPort poolStripes poolTimeout ldapConfTimeout poolLimit
| otherwise -- No LDAP pool to be initialized
-> return Nothing
-- Perform database migration using our application's logging settings.
flip runReaderT tempFoundation $
@ -303,6 +323,34 @@ makeFoundation appSettings''@AppSettings{..} = do
appAuthKey <- clusterSetting (Proxy :: Proxy 'ClusterAuthKey) `customRunSqlPool` sqlPool
appPersonalisedSheetFilesSeedKey <- clusterSetting (Proxy :: Proxy 'ClusterPersonalisedSheetFilesSeedKey) `customRunSqlPool` sqlPool
-- TODO: use scopes from Settings
#ifdef DEVELOPMENT
oauth2Plugins <- liftIO $ sequence
[ (azureMockServer . fromJust) <$> lookupEnv "OAUTH2_SERVER_PORT"
, return $ oauth2AzureADScoped ["openid", "profile", "offline_access"] "42" "shhh"
]
#else
let -- Auth Plugins
-- loadPlugin p prefix = do -- Loads given YesodAuthPlugin
-- mID <- fmap Text.pack <$> appUserAuthConf ^? _UserAuthConfSingleSource . _AuthSourceConfAzureAdV2 . _azureConfClientId
-- mSecret <- fmap Text.pack <$> appUserAuthConf ^? _UserAuthConfSingleSource . _AuthSourceConfAzureAdV2 . _azureConfClientSecret
-- let mArgs = (,) <$> mID <*> mSecret
-- guard $ isJust mArgs
-- return . uncurry p $ fromJust mArgs
-- tenantID = case appUserAuthConf of
-- UserAuthConfSingleSource (AuthSourceConfAzureAdV2 AzureConf{..})
-- -> tshow azureConfTenantId
-- _other
-- -> error "Tenant ID missing!"
oauth2Plugins
| UserAuthConfSingleSource (AuthSourceConfAzureAdV2 AzureConf{..}) <- appUserAuthConf
= singleton $ oauth2AzureADScoped (Set.toList azureConfScopes) (tshow azureConfClientId) azureConfClientSecret
| otherwise
= mempty
#endif
let appAuthPlugins = oauth2Plugins
let appVolatileClusterSettingsCacheTime' = Clock.fromNanoSecs ns
where (MkFixed ns :: Nano) = realToFrac appVolatileClusterSettingsCacheTime
appVolatileClusterSettingsCache <- newTVarIO $ mkVolatileClusterSettingsCache appVolatileClusterSettingsCacheTime'
@ -356,7 +404,8 @@ makeFoundation appSettings''@AppSettings{..} = do
$logDebugS "Runtime configuration" $ tshowCrop appSettings'
let foundation = mkFoundation appSettings' sqlPool smtpPool ldapPool appCryptoIDKey appSessionStore appSecretBoxKey appWidgetMemcached appJSONWebKeySet appClusterID appMemcached appUploadCache appVerpSecret appAuthKey appPersonalisedSheetFilesSeedKey appVolatileClusterSettingsCache appAvsQuery
-- TODO: reimplement user db failover
let foundation = mkFoundation appSettings' sqlPool smtpPool ldapPool appCryptoIDKey appSessionStore appSecretBoxKey appWidgetMemcached appJSONWebKeySet appClusterID appMemcached appUploadCache appVerpSecret appAuthKey appAuthPlugins appPersonalisedSheetFilesSeedKey appVolatileClusterSettingsCache appAvsQuery
-- Return the foundation
$logInfoS "setup" "*** DONE ***"
@ -455,66 +504,6 @@ createMemcached MemcachedConf{memcachedConnectInfo} = snd <$> allocate (Memcache
makeApplication :: MonadIO m => UniWorX -> m Application
makeApplication foundation = liftIO $ makeMiddleware foundation <*> toWaiAppPlain foundation
makeMiddleware :: MonadIO m => UniWorX -> m Middleware
makeMiddleware app = do
logWare <- makeLogWare
return $ observeHTTPRequestLatency classifyHandler . logWare . normalizeCookies . defaultMiddlewaresNoLogging
where
makeLogWare = do
logWareMap <- liftIO $ newTVarIO HashMap.empty
let
mkLogWare ls@LogSettings{..} = do
logger <- readTVarIO . snd $ appLogger app
logWare <- mkRequestLogger def
{ outputFormat = bool
(Apache . bool FromSocket FromHeader $ app ^. _appIpFromHeader)
(Detailed True)
logDetailed
, destination = Logger $ loggerSet logger
}
atomically . modifyTVar' logWareMap $ HashMap.insert ls logWare
return logWare
void. liftIO $
mkLogWare =<< readTVarIO (appLogSettings app)
return $ \wai req fin -> do
lookupRes <- atomically $ do
ls <- readTVar $ appLogSettings app
existing <- HashMap.lookup ls <$> readTVar logWareMap
return $ maybe (Left ls) Right existing
logWare <- either mkLogWare return lookupRes
logWare wai req fin
normalizeCookies :: Wai.Middleware
normalizeCookies waiApp req respond = waiApp req $ \res -> do
resHdrs' <- go $ Wai.responseHeaders res
respond $ Wai.mapResponseHeaders (const resHdrs') res
where parseSetCookie' :: ByteString -> IO (Maybe SetCookie)
parseSetCookie' = fmap (either (\(_ :: SomeException) -> Nothing) Just) . try . evaluate . force . parseSetCookie
go [] = return []
go (hdr@(hdrName, hdrValue) : hdrs)
| hdrName == hSetCookie = do
mcookieHdr <- parseSetCookie' hdrValue
case mcookieHdr of
Nothing -> (hdr :) <$> go hdrs
Just cookieHdr -> do
let cookieHdrMatches hdrValue' = maybeT (return False) $ do
cookieHdr' <- MaybeT $ parseSetCookie' hdrValue'
-- See https://tools.ietf.org/html/rfc6265
guard $ setCookiePath cookieHdr' == setCookiePath cookieHdr
guard $ setCookieName cookieHdr' == setCookieName cookieHdr
guard $ setCookieDomain cookieHdr' == setCookieDomain cookieHdr
return True
others <- filterM (\(hdrName', hdrValue') -> and2M (pure $ hdrName' == hSetCookie) (cookieHdrMatches hdrValue')) hdrs
if | null others -> (hdr :) <$> go hdrs
| otherwise -> go hdrs
| otherwise = (hdr :) <$> go hdrs
-- | Warp settings for the given foundation value.
warpSettings :: UniWorX -> Settings
warpSettings foundation = defaultSettings
@ -595,6 +584,8 @@ appMain = runResourceT $ do
foundation <- makeFoundation settings
runAppLoggingT foundation $ do
$logDebugS "AppSettings" $ tshow settings
$logInfoS "setup" "Job-Handling"
handleJobs foundation
@ -711,7 +702,7 @@ shutdownApp app = do
liftIO $ do
Custom.purgePool $ appConnPool app
for_ (appSmtpPool app) destroyAllResources
for_ (appLdapPool app) . mapFailover $ views _2 destroyAllResources
for_ (appLdapPool app) $ views _2 destroyAllResources
for_ (appWidgetMemcached app) Memcached.close
for_ (appMemcached app) $ views _memcachedConn Memcached.close
release . fst $ appLogger app
@ -736,7 +727,7 @@ db' = handler' . runDB
addPWEntry :: User
-> Text {-^ Password -}
-> IO ()
addPWEntry User{ userAuthentication = _, ..} (Text.encodeUtf8 -> pw) = db' $ do
addPWEntry User{ userPasswordHash = _, ..} (Text.encodeUtf8 -> pw) = db' $ do
PWHashConf{..} <- getsYesod $ view _appAuthPWHash
(AuthPWHash . Text.decodeUtf8 -> userAuthentication) <- liftIO $ makePasswordWith pwHashAlgorithm pw pwHashStrength
(Just . Text.decodeUtf8 -> userPasswordHash) <- liftIO $ makePasswordWith pwHashAlgorithm pw pwHashStrength
void $ insert User{..}

View File

@ -1,4 +1,4 @@
-- SPDX-FileCopyrightText: 2022 Felix Hamann <felix.hamann@campus.lmu.de>,Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@cip.ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Felix Hamann <felix.hamann@campus.lmu.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>, Sarah Vaupel <sarah.vaupel@ifi.lmu.de>, Steffen Jost <jost@cip.ifi.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
@ -7,11 +7,11 @@
module Auth.LDAP
( apLdap
, ADError(..), ADInvalidCredentials(..)
, campusLogin
, CampusUserException(..)
, campusUser, campusUser', campusUser''
, campusUserReTest, campusUserReTest'
, campusUserMatr, campusUserMatr'
, ldapLogin
, LdapUserException(..)
, ldapUser, ldapUser', ldapUser''
--, ldapUserReTest, ldapUserReTest'
, ldapUserMatr, ldapUserMatr'
, CampusMessage(..)
, ldapPrimaryKey
, ldapUserPrincipalName, ldapUserEmail, ldapUserDisplayName
@ -20,32 +20,36 @@ module Auth.LDAP
, ldapUserMobile, ldapUserTelephone
, ldapUserFraportPersonalnummer, ldapUserFraportAbteilung
, ldapUserTitle
, ldapSearch
) where
import Import.NoFoundation
import qualified Data.CaseInsensitive as CI
import Utils.Metrics
import Utils.Form
import Auth.LDAP.AD
import qualified Ldap.Client as Ldap
import Utils.Form
import Utils.Metrics
import qualified Data.CaseInsensitive as CI
import qualified Data.Text.Encoding as Text
import qualified Yesod.Auth.Message as Msg
import Auth.LDAP.AD
-- allow Ldap.Attr usage as key for Data.Map
deriving newtype instance Ord Ldap.Attr
-- | Plugin name of the LDAP yesod auth plugin
apLdap :: Text
apLdap = "LDAP"
-- TODO: rename
data CampusLogin = CampusLogin
{ campusIdent :: CI Text
, campusPassword :: Text
} deriving (Generic)
-- TODO: rename
data CampusMessage = MsgCampusIdentPlaceholder
| MsgCampusIdent
| MsgCampusPassword
@ -53,8 +57,12 @@ data CampusMessage = MsgCampusIdentPlaceholder
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic)
findUser :: LdapConf -> Ldap -> Text -> [Ldap.Attr] -> IO [Ldap.SearchEntry]
findUser conf@LdapConf{..} ldap ident retAttrs = fromMaybe [] <$> findM (assertM (not . null) . lift . flip (Ldap.search ldap ldapBase $ userSearchSettings conf) retAttrs) userFilters
findUser :: LdapConf
-> Ldap
-> Text -- ^ needle
-> [Ldap.Attr]
-> IO [Ldap.SearchEntry]
findUser conf@LdapConf{..} ldap ident retAttrs = fromMaybe [] <$> findM (assertM (not . null) . lift . flip (Ldap.search ldap ldapConfBase $ userSearchSettings conf) retAttrs) userFilters
where
userFilters =
[ ldapUserPrincipalName Ldap.:= Text.encodeUtf8 ident
@ -69,21 +77,37 @@ findUser conf@LdapConf{..} ldap ident retAttrs = fromMaybe [] <$> findM (assertM
[ ldapUserFraportPersonalnummer Ldap.:= Text.encodeUtf8 ident
]
findUserMatr :: LdapConf -> Ldap -> Text -> [Ldap.Attr] -> IO [Ldap.SearchEntry]
findUserMatr conf@LdapConf{..} ldap userMatr retAttrs = fromMaybe [] <$> findM (assertM (not . null) . lift . flip (Ldap.search ldap ldapBase $ userSearchSettings conf) retAttrs) userFilters
findUserMatr :: LdapConf
-> Ldap
-> Text -- ^ matriculation needle
-> [Ldap.Attr]
-> IO [Ldap.SearchEntry]
findUserMatr conf@LdapConf{..} ldap userMatr retAttrs = fromMaybe [] <$> findM (assertM (not . null) . lift . flip (Ldap.search ldap ldapConfBase $ userSearchSettings conf) retAttrs) userFilters
where
userFilters =
[ ldapUserFraportPersonalnummer Ldap.:= Text.encodeUtf8 userMatr
]
userSearchSettings :: LdapConf -> Ldap.Mod Ldap.Search
userSearchSettings :: LdapConf
-> Ldap.Mod Ldap.Search
userSearchSettings LdapConf{..} = mconcat
[ Ldap.scope ldapScope
[ Ldap.scope ldapConfScope
, Ldap.size 2
, Ldap.time ldapSearchTimeout
, Ldap.time ldapConfSearchTimeout
, Ldap.derefAliases Ldap.DerefAlways
]
ldapSearch :: forall m.
( MonadUnliftIO m
, MonadCatch m
)
=> (LdapConf, LdapPool)
-> Text -- ^ needle
-> m [Ldap.SearchEntry]
ldapSearch (conf@LdapConf{..}, ldapPool) needle = either (throwM . LdapUserLdapError) return <=< withLdap ldapPool $ \ldap -> liftIO $ do
Ldap.bind ldap ldapConfDn ldapConfPassword
findUser conf ldap needle []
ldapPrimaryKey, ldapUserPrincipalName, ldapUserDisplayName, ldapUserFirstName, ldapUserSurname, ldapAffiliation, ldapUserTitle, ldapUserTelephone, ldapUserMobile, ldapUserFraportPersonalnummer, ldapUserFraportAbteilung :: Ldap.Attr
ldapPrimaryKey = Ldap.Attr "cn" -- should always be identical to "sAMAccountName"
ldapUserPrincipalName = Ldap.Attr "userPrincipalName"
@ -104,30 +128,35 @@ ldapUserEmail = Ldap.Attr "mail" :|
]
data CampusUserException = CampusUserLdapError LdapPoolError
| CampusUserNoResult
| CampusUserAmbiguous
-- TODO: deprecate in favour of FetchUserDataException
data LdapUserException = LdapUserLdapError LdapPoolError
| LdapUserNoResult
| LdapUserAmbiguous
deriving (Show, Eq, Generic)
instance Exception CampusUserException
instance Exception LdapUserException
makePrisms ''CampusUserException
makePrisms ''LdapUserException
campusUserWith :: ( MonadUnliftIO m
, MonadCatch m
)
=> ( Lens (LdapConf, LdapPool) (LdapConf, Ldap) LdapPool Ldap
-> Failover (LdapConf, LdapPool)
-> FailoverMode
-> ((LdapConf, Ldap) -> m (Either CampusUserException (Ldap.AttrList [])))
-> m (Either LdapPoolError (Either CampusUserException (Ldap.AttrList [])))
)
-> Failover (LdapConf, LdapPool)
-> FailoverMode
-> Creds site
-> m (Either CampusUserException (Ldap.AttrList []))
campusUserWith withLdap' pool mode Creds{..} = either (throwM . CampusUserLdapError) return <=< withLdap' _2 pool mode $ \(conf@LdapConf{..}, ldap) -> liftIO . runExceptT $ do
lift $ Ldap.bind ldap ldapDn ldapPassword
ldapUserWith :: ( MonadUnliftIO m
, MonadCatch m
--, MonadLogger m
)
-- ( Lens (LdapConf, LdapPool) (LdapConf, Ldap) LdapPool Ldap
-- -> (LdapConf, LdapPool)
-- -> ((LdapConf, Ldap) -> m (Either CampusUserException (Ldap.AttrList [])))
-- -> m (Either LdapPoolError (Either CampusUserException (Ldap.AttrList [])))
-- )
=> ( LdapPool
-> (Ldap -> m (Either LdapUserException (Ldap.AttrList [])))
-> m (Either LdapPoolError (Either LdapUserException (Ldap.AttrList [])))
)
-> (LdapConf, LdapPool)
-> Creds site
-> m (Either LdapUserException (Ldap.AttrList []))
ldapUserWith withLdap' (conf@LdapConf{..}, pool) Creds{..} = either (throwM . LdapUserLdapError) return <=< withLdap' pool $ \ldap -> liftIO . runExceptT $ do
lift $ Ldap.bind ldap ldapConfDn ldapConfPassword
results <- case lookup "DN" credsExtra of
Just userDN -> do
let userFilter = Ldap.Present ldapUserPrincipalName
@ -135,43 +164,91 @@ campusUserWith withLdap' pool mode Creds{..} = either (throwM . CampusUserLdapEr
Nothing -> do
lift $ findUser conf ldap credsIdent []
case results of
[] -> throwE CampusUserNoResult
[] -> throwE LdapUserNoResult
[Ldap.SearchEntry _ attrs] -> return attrs
_otherwise -> throwE CampusUserAmbiguous
campusUserReTest :: (MonadUnliftIO m, MonadMask m, MonadLogger m) => Failover (LdapConf, LdapPool) -> (Nano -> Bool) -> FailoverMode -> Creds site -> m (Ldap.AttrList [])
campusUserReTest pool doTest mode creds = throwLeft =<< campusUserWith (\l -> flip (withLdapFailoverReTest l) doTest) pool mode creds
campusUserReTest' :: (MonadMask m, MonadLogger m, MonadUnliftIO m) => Failover (LdapConf, LdapPool) -> (Nano -> Bool) -> FailoverMode -> User -> m (Maybe (Ldap.AttrList []))
campusUserReTest' pool doTest mode User{userIdent,userLdapPrimaryKey}
= runMaybeT . catchIfMaybeT (is _CampusUserNoResult) $ campusUserReTest pool doTest mode (Creds apLdap upsertIdent [])
where upsertIdent = fromMaybe (CI.original userIdent) userLdapPrimaryKey
_otherwise -> throwE LdapUserAmbiguous
campusUser :: (MonadMask m, MonadUnliftIO m, MonadLogger m) => Failover (LdapConf, LdapPool) -> FailoverMode -> Creds site -> m (Ldap.AttrList [])
campusUser pool mode creds = throwLeft =<< campusUserWith withLdapFailover pool mode creds
-- TODO: reintroduce once failover has been reimplemented
-- ldapUserReTest :: ( MonadUnliftIO m
-- , MonadMask m
-- , MonadLogger m
-- )
-- => Failover (LdapConf, LdapPool)
-- -> (Nano -> Bool)
-- -> FailoverMode
-- -> Creds site
-- -> m (Ldap.AttrList [])
-- ldapUserReTest pool doTest mode creds = throwLeft =<< ldapUserWith (\l -> flip (withLdapFailoverReTest l) doTest) pool mode creds
--
-- ldapUserReTest' :: ( MonadMask m
-- , MonadLogger m
-- , MonadUnliftIO m
-- )
-- => Failover (LdapConf, LdapPool)
-- -> (Nano -> Bool)
-- -> FailoverMode
-- -> User
-- -> m (Maybe (Ldap.AttrList []))
-- ldapUserReTest' pool doTest mode User{userIdent,userLdapPrimaryKey}
-- = runMaybeT . catchIfMaybeT (is _CampusUserNoResult) $ ldapUserReTest pool doTest mode (Creds apLdap upsertIdent [])
-- where upsertIdent = fromMaybe (CI.original userIdent) userLdapPrimaryKey
campusUser' :: (MonadMask m, MonadUnliftIO m, MonadLogger m) => Failover (LdapConf, LdapPool) -> FailoverMode -> User -> m (Maybe (Ldap.AttrList []))
campusUser' pool mode User{userIdent}
= campusUser'' pool mode $ CI.original userIdent
campusUser'' :: (MonadMask m, MonadUnliftIO m, MonadLogger m) => Failover (LdapConf, LdapPool) -> FailoverMode -> Text -> m (Maybe (Ldap.AttrList []))
campusUser'' pool mode ident
= runMaybeT . catchIfMaybeT (is _CampusUserNoResult) $ campusUser pool mode (Creds apLdap ident [])
-- TODO: deprecate in favour of fetchUserData
ldapUser :: ( MonadMask m
, MonadUnliftIO m
--, MonadLogger m
)
=> (LdapConf, LdapPool)
-> Creds site
-> m (Ldap.AttrList [])
ldapUser pool creds = throwLeft =<< ldapUserWith withLdap pool creds
campusUserMatr :: (MonadUnliftIO m, MonadMask m, MonadLogger m) => Failover (LdapConf, LdapPool) -> FailoverMode -> UserMatriculation -> m (Ldap.AttrList [])
campusUserMatr pool mode userMatr = either (throwM . CampusUserLdapError) return <=< withLdapFailover _2 pool mode $ \(conf@LdapConf{..}, ldap) -> liftIO $ do
Ldap.bind ldap ldapDn ldapPassword
ldapUser' :: ( MonadMask m
, MonadUnliftIO m
--, MonadLogger m
)
=> (LdapConf, LdapPool)
-> User
-> m (Maybe (Ldap.AttrList []))
ldapUser' pool User{userIdent}
= ldapUser'' pool $ CI.original userIdent
ldapUser'' :: ( MonadMask m
, MonadUnliftIO m
--, MonadLogger m
)
=> (LdapConf, LdapPool)
-> Text
-> m (Maybe (Ldap.AttrList []))
ldapUser'' pool ident
= runMaybeT . catchIfMaybeT (is _LdapUserNoResult) $ ldapUser pool (Creds apLdap ident [])
ldapUserMatr :: ( MonadUnliftIO m
, MonadMask m
--, MonadLogger m
)
=> (LdapConf, LdapPool)
-> UserMatriculation
-> m (Ldap.AttrList [])
ldapUserMatr (conf@LdapConf{..}, pool) userMatr = either (throwM . LdapUserLdapError) return <=< withLdap pool $ \ldap -> liftIO $ do
Ldap.bind ldap ldapConfDn ldapConfPassword
results <- findUserMatr conf ldap userMatr []
case results of
[] -> throwM CampusUserNoResult
[] -> throwM LdapUserNoResult
[Ldap.SearchEntry _ attrs] -> return attrs
_otherwise -> throwM CampusUserAmbiguous
campusUserMatr' :: (MonadMask m, MonadUnliftIO m, MonadLogger m) => Failover (LdapConf, LdapPool) -> FailoverMode -> UserMatriculation -> m (Maybe (Ldap.AttrList []))
campusUserMatr' pool mode
= runMaybeT . catchIfMaybeT (is _CampusUserNoResult) . campusUserMatr pool mode
_otherwise -> throwM LdapUserAmbiguous
ldapUserMatr' :: ( MonadMask m
, MonadUnliftIO m
--, MonadLogger m
)
=> (LdapConf, LdapPool)
-> UserMatriculation
-> m (Maybe (Ldap.AttrList []))
ldapUserMatr' pool = runMaybeT . catchIfMaybeT (is _LdapUserNoResult) . ldapUserMatr pool
newtype ADInvalidCredentials = ADInvalidCredentials ADError
@ -186,25 +263,28 @@ campusForm :: ( RenderMessage (HandlerSite m) FormMessage
, RenderMessage (HandlerSite m) (ValueRequired (HandlerSite m))
, RenderMessage (HandlerSite m) CampusMessage
, MonadHandler m
) => WForm m (FormResult CampusLogin)
)
=> WForm m (FormResult CampusLogin)
campusForm = do
MsgRenderer mr <- getMsgRenderer
aFormToWForm $ CampusLogin
<$> areq ciField (fslpI MsgCampusIdent (mr MsgCampusIdentPlaceholder) & addAttr "autofocus" "" & addAttr "autocomplete" "username") Nothing
<*> areq passwordField (fslpI MsgCampusPassword (mr MsgCampusPasswordPlaceholder) & addAttr "autocomplete" "current-password") Nothing
apLdap :: Text
apLdap = "LDAP"
campusLogin :: forall site.
( YesodAuth site
, RenderMessage site CampusMessage
, RenderAFormSite site
, RenderMessage site (ValueRequired site)
, RenderMessage site ADInvalidCredentials
, Button site ButtonSubmit
) => Failover (LdapConf, LdapPool) -> FailoverMode -> AuthPlugin site
campusLogin pool mode = AuthPlugin{..}
-- TODO: reintroduce Failover
ldapLogin :: forall site.
( YesodAuth site
, RenderMessage site CampusMessage
, RenderAFormSite site
, RenderMessage site (ValueRequired site)
, RenderMessage site ADInvalidCredentials
, Button site ButtonSubmit
)
=> LdapConf
-> LdapPool
-> AuthPlugin site
ldapLogin conf@LdapConf{..} pool = AuthPlugin{..}
where
apName :: Text
apName = apLdap
@ -215,8 +295,8 @@ campusLogin pool mode = AuthPlugin{..}
tp <- getRouteToParent
resp <- formResultMaybe loginRes $ \CampusLogin{ campusIdent = CI.original -> campusIdent, ..} -> Just <$> do
ldapResult <- withLdapFailover _2 pool mode $ \(conf@LdapConf{..}, ldap) -> liftIO $ do
Ldap.bind ldap ldapDn ldapPassword
ldapResult <- withLdap pool $ \ldap -> liftIO $ do
Ldap.bind ldap ldapConfDn ldapConfPassword
searchResults <- findUser conf ldap campusIdent [ldapUserPrincipalName]
case searchResults of
[Ldap.SearchEntry (Ldap.Dn userDN) userAttrs]

248
src/Auth/OAuth2.hs Normal file
View File

@ -0,0 +1,248 @@
-- SPDX-FileCopyrightText: 2023-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, David Mosbach <david.mosbach@uniworx.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# OPTIONS_GHC -fno-warn-redundant-constraints #-}
module Auth.OAuth2
( apAzure
, azurePrimaryKey, azureUserPrincipalName, azureUserDisplayName, azureUserGivenName, azureUserSurname, azureUserMail, azureUserTelephone, azureUserMobile, azureUserPreferredLanguage
-- , azureUser, azureUser'
, AzureUserException(..), _AzureUserError, _AzureUserNoResult, _AzureUserAmbiguous
, apAzureMock
, azureMockServer
, queryOAuth2User
, refreshOAuth2Token
, singleSignOut
) where
-- import qualified Data.CaseInsensitive as CI
import Data.Maybe (fromJust)
import Data.Text
import Import.NoFoundation hiding (pack, unpack)
import Network.HTTP.Simple (httpJSONEither, getResponseBody, JSONException)
import System.Environment (lookupEnv)
import Yesod.Auth.OAuth2
import Yesod.Auth.OAuth2.Prelude hiding (encodeUtf8)
-- | Plugin name of the OAuth2 yesod plugin for Azure ADv2
apAzure :: Text
apAzure = "AzureADv2"
-- TODO: deprecate in favour of FetchUserDataException
data AzureUserException = AzureUserError
| AzureUserNoResult
| AzureUserAmbiguous
deriving (Show, Eq, Generic)
instance Exception AzureUserException
makePrisms ''AzureUserException
azurePrimaryKey, azureUserPrincipalName, azureUserDisplayName, azureUserGivenName, azureUserSurname, azureUserMail, azureUserTelephone, azureUserMobile, azureUserPreferredLanguage :: Text
azurePrimaryKey = "id"
azureUserPrincipalName = "userPrincipalName"
azureUserDisplayName = "displayName"
azureUserGivenName = "givenName"
azureUserSurname = "surname"
azureUserMail = "mail"
azureUserTelephone = "businessPhones"
azureUserMobile = "mobilePhone"
azureUserPreferredLanguage = "preferredLanguage"
-- | User lookup in Microsoft Graph with given credentials
-- TODO: deprecate in favour of fetchUserData
-- azureUser :: ( MonadMask m
-- , MonadHandler m
-- -- , HandlerSite m ~ site
-- -- , BackendCompatible SqlBackend (YesodPersistBackend site)
-- -- , BaseBackend (YesodPersistBackend site) ~ SqlBackend
-- -- , YesodPersist site
-- -- , PersistUniqueWrite (YesodPersistBackend site)
-- )
-- => AzureConf
-- -> Creds site
-- -> m [(Text, [ByteString])] -- (Either AzureUserException [(Text, [ByteString])])
-- azureUser AzureConf{..} Creds{..} = fmap throwLeft . runExceptT $ do
-- now <- liftIO getCurrentTime
-- results <- queryOAuth2User @[(Text, [ByteString])] credsIdent
-- case results of
-- Right [res] -> do
-- -- void . liftHandler . runDB $ upsert ExternalUser
-- -- { externalUserUser = error "no userid" -- TODO: use azureUserPrimaryKey once UserIdent is referenced instead of UserId
-- -- , externalUserSource = AuthSourceIdAzure azureConfClientId
-- -- , externalUserData = toJSON res
-- -- , externalUserLastSync = now
-- -- }
-- -- [ ExternalUserData =. toJSON res
-- -- , ExternalUserLastSync =. now
-- -- ]
-- return res
-- Right _multiple -> throwE AzureUserAmbiguous
-- Left _ -> throwE AzureUserNoResult
-- | User lookup in Microsoft Graph with given user
-- azureUser' :: ( MonadMask m
-- , MonadHandler m
-- , HandlerSite m ~ site
-- , BaseBackend (YesodPersistBackend site) ~ SqlBackend
-- , YesodPersist site
-- , PersistUniqueWrite (YesodPersistBackend site)
-- )
-- => AzureConf
-- -> User
-- -> ReaderT (YesodPersistBackend site) m (Maybe [(Text, [ByteString])]) -- (Either AzureUserException [(Text, [ByteString])])
-- azureUser' conf User{userIdent}
-- = runMaybeT . catchIfMaybeT (is _AzureUserNoResult) $ azureUser conf (Creds apAzure (CI.original userIdent) [])
-----------------------------------------------
---- OAuth2 + OIDC development auth plugin ----
-----------------------------------------------
apAzureMock :: Text
apAzureMock = "uniworx_dev"
newtype UserID = UserID Text
instance FromJSON UserID where
parseJSON = withObject "UserID" $ \o ->
UserID <$> o .: "id"
azureMockServer :: YesodAuth m => String -> AuthPlugin m
azureMockServer port =
let oa = OAuth2
{ oauthClientId = "42"
, oauthClientSecret = Just "shhh"
, oauthOAuthorizeEndpoint = fromString (mockServerURL <> "/auth")
`withQuery` [ scopeParam " " ["openid", "profile", "email", "offline_access"] -- TODO read scopes from config
, ("response_type", "code id_token")
, ("nonce", "Foo") -- TODO generate meaningful value
]
, oauthAccessTokenEndpoint = fromString $ mockServerURL <> "/token"
, oauthCallback = Nothing
}
mockServerURL = "http://localhost:" <> fromString port
profileSrc = fromString $ mockServerURL <> "/users/me"
in authOAuth2 apAzureMock oa $ \manager token -> do
(UserID userID, userResponse) <- authGetProfile apAzureMock manager token profileSrc
return Creds
{ credsPlugin = apAzureMock
, credsIdent = userID
, credsExtra = setExtra token userResponse
}
----------------------
---- User Queries ----
----------------------
data UserDataException = UserDataJSONException JSONException
| UserDataInternalException Text
deriving Show
instance Exception UserDataException
queryOAuth2User :: forall j m.
( FromJSON j
, MonadHandler m
, HasAppSettings (HandlerSite m)
, MonadThrow m
)
=> Text -- ^ User identifier (arbitrary needle)
-> m (Either UserDataException j)
queryOAuth2User userID = runExceptT $ do
(queryUrl, tokenUrl) <- mkBaseUrls
req <- parseRequest $ "GET " ++ queryUrl ++ unpack userID
mTokens <- lookupSessionJson SessionOAuth2Token
unless (isJust mTokens) . throwE $ UserDataInternalException "Tried to load session Oauth2 tokens, but there are none"
# ifdef DEVELOPMENT
let secure = False
# else
let secure = True
# endif
newTokens <- refreshOAuth2Token @m (fromJust mTokens) tokenUrl secure
setSessionJson SessionOAuth2Token (Just $ accessToken newTokens, refreshToken newTokens)
eResult <- lift $ getResponseBody <$> httpJSONEither @m @j (req
{ secure = secure
, requestHeaders = [("Authorization", encodeUtf8 . ("Bearer " <>) . atoken $ accessToken newTokens)]
})
case eResult of
Left x -> throwE $ UserDataJSONException x
Right x -> return x
mkBaseUrls :: (MonadHandler m, HasAppSettings (HandlerSite m)) => m (String, String)
mkBaseUrls = do
# ifndef DEVELOPMENT
tenantID <- fmap (maybe (throwM $ UserDataInternalException "Could not determine tenant ID from current app configuration") show) . getsYesod . preview $ _appUserAuthConf . _userAuthConfSingleSource . _AuthSourceConfAzureAdV2 . _azureConfTenantId
return ( "https://graph.microsoft.com/v1.0/users/"
, "https://login.microsoftonline.com/" ++ tenantID ++ "/oauth2/v2.0" )
# else
port :: String <- liftIO $ maybe (throwM $ UserDataInternalException "Development environment variable OAUTH2_SERVER_PORT is unset") id <$> lookupEnv "OAUTH2_SERVER_PORT"
let base = "http://localhost:" ++ port
return ( base ++ "/users/query?id="
, base ++ "/token" )
# endif
refreshOAuth2Token :: forall m.
( MonadHandler m
, MonadThrow m
)
=> (Maybe AccessToken, Maybe RefreshToken)
-> String
-> Bool
-> ExceptT UserDataException m OAuth2Token
refreshOAuth2Token (_, rToken) url secure
| isJust rToken = do
req <- parseRequest $ "POST " ++ url
let
body =
[ ("grant_type", "refresh_token")
, ("refresh_token", encodeUtf8 . rtoken $ fromJust rToken)
]
body' <- if secure then do
clientID <- liftIO $ fromJust <$> lookupEnv "CLIENT_ID"
clientSecret <- liftIO $ fromJust <$> lookupEnv "CLIENT_SECRET"
return $ body ++ [("client_id", fromString clientID), ("client_secret", fromString clientSecret), scopeParam " " ["openid","profile"," offline_access"]] -- TODO read from config
else return $ scopeParam " " ["openid","profile","offline_access"] : body -- TODO read from config
$logDebugS "\27[31mAdmin Handler\27[0m" $ tshow (requestBody $ urlEncodedBody body' req{ secure = secure })
eResult <- lift $ getResponseBody <$> httpJSONEither @m @OAuth2Token (urlEncodedBody body' req{ secure = secure })
case eResult of
Left x -> throwE $ UserDataJSONException x
Right x -> return x
| otherwise = throwE $ UserDataInternalException "Could not refresh access token. Refresh token is missing."
instance Show RequestBody where
show (RequestBodyLBS x) = show x
show _ = error ":("
-----------------------
---- Single Sign-Out ----
-----------------------
singleSignOut :: forall a m. (MonadHandler m)
=> Maybe Text -- ^ redirect uri
-> m a
singleSignOut mRedirect = do
# ifdef DEVELOPMENT
port <- liftIO $ fromJust <$> lookupEnv "OAUTH2_SERVER_PORT"
let base = "http://localhost:" <> pack port <> "/logout"
# else
let base = "" -- TODO find out fraport oidc end_session_endpoint
# endif
endpoint = case mRedirect of
Just r -> base <> "?post_logout_redirect_uri=" <> r
Nothing -> base
$logDebugS "\n\27[31mSSO\27[0m" endpoint
redirect endpoint

View File

@ -1,4 +1,4 @@
-- SPDX-FileCopyrightText: 2022 Felix Hamann <felix.hamann@campus.lmu.de>,Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Felix Hamann <felix.hamann@campus.lmu.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>, Sarah Vaupel <sarah.vaupel@ifi.lmu.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
@ -68,12 +68,13 @@ hashLogin pwHashAlgo = AuthPlugin{..}
tp <- getRouteToParent
resp <- formResultMaybe loginRes $ \HashLogin{..} -> Just <$> do
user <- liftHandler . runDB . getBy $ UniqueAuthentication hashIdent
user :: Maybe (Entity User) <- liftHandler . runDB . getBy $ UniqueAuthentication hashIdent
case user of
Just (Entity _ User{ userAuthentication = AuthPWHash{..}, userIdent = CI.original -> userIdent })
| verifyPasswordWith pwHashAlgo (2^) (encodeUtf8 hashPassword) (encodeUtf8 authPWHash) -> do -- (2^) is magic.
Just (Entity _ User{userIdent,userPasswordHash})
| Just pwHash <- userPasswordHash
, verifyPasswordWith pwHashAlgo (2^) (encodeUtf8 hashPassword) (encodeUtf8 pwHash) -> do -- (2^) is magic.
observeLoginOutcome apName LoginSuccessful
setCredsRedirect $ Creds apName userIdent []
setCredsRedirect $ Creds apName (CI.original userIdent) []
other -> do
$logDebugS apName $ tshow other
observeLoginOutcome apName LoginInvalidCredentials

View File

@ -1,4 +1,4 @@
-- SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Wolfgang Witt <Wolfgang.Witt@campus.lmu.de>
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>, Sarah Vaupel <sarah.vaupel@ifi.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>, Wolfgang Witt <Wolfgang.Witt@campus.lmu.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
@ -1523,7 +1523,7 @@ tagAccessPredicate AuthSelf = APDB $ \_ _ mAuthId route _ -> exceptT return retu
| uid == referencedUser -> return Authorized
Nothing -> return AuthenticationRequired
_other -> unauthorizedI MsgUnauthorizedSelf
tagAccessPredicate AuthIsLDAP = APDB $ \_ _ _ route _ -> exceptT return return $ do
tagAccessPredicate AuthIsExternal = APDB $ \_ _ _ route _ -> exceptT return return $ do
referencedUser <- case route of
AdminUserR cID -> return cID
AdminUserDeleteR cID -> return cID
@ -1531,13 +1531,17 @@ tagAccessPredicate AuthIsLDAP = APDB $ \_ _ _ route _ -> exceptT return return $
UserNotificationR cID -> return cID
UserPasswordR cID -> return cID
CourseR _ _ _ (CUserR cID) -> return cID
_other -> throwError =<< $unsupportedAuthPredicate AuthIsLDAP route
_other -> throwError =<< $unsupportedAuthPredicate AuthIsExternal route
referencedUser' <- catchIfMExceptT (const $ unauthorizedI MsgUnauthorizedSelf) (const True :: CryptoIDError -> Bool) $ decrypt referencedUser
maybeTMExceptT (unauthorizedI MsgUnauthorizedLDAP) $ do
User{..} <- MaybeT $ get referencedUser'
guard $ userAuthentication == AuthLDAP
availableSources <- getsYesod (view _appUserAuthConf) >>= \case
UserAuthConfSingleSource{..} -> return . singleton $ case userAuthConfSingleSource of
AuthSourceConfAzureAdV2 AzureConf{..} -> AuthSourceIdAzure azureConfTenantId
AuthSourceConfLdap LdapConf{..} -> AuthSourceIdLdap ldapConfSourceId
maybeTMExceptT (unauthorizedI MsgUnauthorizedExternal) $ do
Entity _ User{userIdent} <- MaybeT $ getEntity referencedUser'
guardM . lift $ exists [ ExternalUserUser ==. userIdent, ExternalUserSource <-. availableSources ]
return Authorized
tagAccessPredicate AuthIsPWHash = APDB $ \_ _ _ route _ -> exceptT return return $ do
tagAccessPredicate AuthIsInternal = APDB $ \_ _ _ route _ -> exceptT return return $ do
referencedUser <- case route of
AdminUserR cID -> return cID
AdminUserDeleteR cID -> return cID
@ -1545,11 +1549,11 @@ tagAccessPredicate AuthIsPWHash = APDB $ \_ _ _ route _ -> exceptT return return
UserNotificationR cID -> return cID
UserPasswordR cID -> return cID
CourseR _ _ _ (CUserR cID) -> return cID
_other -> throwError =<< $unsupportedAuthPredicate AuthIsPWHash route
_other -> throwError =<< $unsupportedAuthPredicate AuthIsInternal route
referencedUser' <- catchIfMExceptT (const $ unauthorizedI MsgUnauthorizedSelf) (const True :: CryptoIDError -> Bool) $ decrypt referencedUser
maybeTMExceptT (unauthorizedI MsgUnauthorizedPWHash) $ do
maybeTMExceptT (unauthorizedI MsgUnauthorizedInternal) $ do
User{..} <- MaybeT $ get referencedUser'
guard $ is _AuthPWHash userAuthentication
guard $ is _Just userPasswordHash
return Authorized
tagAccessPredicate AuthAuthentication = APDB $ \_ _ mAuthId route _ -> case route of
MessageR cID -> maybeT (unauthorizedI MsgUnauthorizedSystemMessageAuth) $ do

View File

@ -410,8 +410,6 @@ embedRenderMessage ''UniWorX ''ExamRequiredEquipmentPreset id
embedRenderMessage ''UniWorX ''ChangelogItemKind id
embedRenderMessage ''UniWorX ''RoomReference' $ dropSuffix "'"
embedRenderMessage ''UniWorX ''AuthenticationMode id
embedRenderMessage ''UniWorX ''RatingValidityException id
embedRenderMessage ''UniWorX ''UrlFieldMessage id

View File

@ -1,4 +1,4 @@
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Wolfgang Witt <Wolfgang.Witt@campus.lmu.de>
-- SPDX-FileCopyrightText: 2022-2025 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>, Sarah Vaupel <sarah.vaupel@ifi.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>, Wolfgang Witt <Wolfgang.Witt@campus.lmu.de>, David Mosbach <david.mosbach@uniworx.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
@ -11,11 +11,14 @@ module Foundation.Instances
, unsafeHandler
) where
import qualified Prelude as P
import Import.NoFoundation
import qualified Data.Text as Text
import Data.List (inits)
import Yesod.Auth.OAuth2
import qualified Yesod.Core.Unsafe as Unsafe
import qualified Yesod.Auth.Message as Auth
@ -23,6 +26,7 @@ import Utils.Form
import Auth.LDAP
import Auth.PWHash
import Auth.Dummy
import Auth.OAuth2
import qualified Foundation.Yesod.Session as UniWorX
import qualified Foundation.Yesod.Middleware as UniWorX
@ -41,6 +45,8 @@ import Foundation.DB
import Network.Wai.Parse (lbsBackEnd)
import System.Environment (lookupEnv)
import UnliftIO.Pool (withResource)
import qualified Control.Monad.State.Class as State
@ -119,7 +125,7 @@ instance YesodPersistRunner UniWorX where
getDBRunner :: HasCallStack => HandlerFor UniWorX (DBRunner UniWorX, HandlerFor UniWorX ())
getDBRunner = UniWorX.getDBRunner' callStack
instance YesodAuth UniWorX where
type AuthId UniWorX = UserId
@ -128,21 +134,30 @@ instance YesodAuth UniWorX where
-- Where to send a user after logout
logoutDest _ = NewsR
-- Override the above two destinations when a Referer: header is present
redirectToReferer _ = True
redirectToReferer _ = False
loginHandler = do
plugins <- getsYesod authPlugins
AppSettings{..} <- getsYesod appSettings'
when appSingleSignOn $ do
let plugin = P.head $ P.filter ((`elem` [apAzureMock, apAzure]) . apName) plugins
pieces = case oauth2Url (apName plugin) of
PluginR _ p -> p
_ -> error "Unexpected OAuth2 AuthRoute"
void $ apDispatch plugin "GET" pieces
toParent <- getRouteToParent
liftHandler . defaultLayout $ do
plugins <- getsYesod authPlugins
$logDebugS "Auth" $ "Enabled plugins: " <> Text.intercalate ", " (map apName plugins)
mPort <- liftIO $ lookupEnv "OAUTH2_SERVER_PORT"
setTitleI MsgLoginTitle
$(widgetFile "login")
authenticate = UniWorX.authenticate
authPlugins UniWorX{ appSettings' = AppSettings{..}, appLdapPool } = catMaybes
[ flip campusLogin campusUserFailoverMode <$> appLdapPool
authPlugins UniWorX{ appSettings' = AppSettings{..}, appLdapPool, appAuthPlugins } = appAuthPlugins ++ catMaybes
[ uncurry ldapLogin <$> appLdapPool
, Just . hashLogin $ pwHashAlgorithm appAuthPWHash
, dummyLogin <$ guard appAuthDummyLogin
]
@ -157,6 +172,11 @@ instance YesodAuth UniWorX where
addMessage Success . toHtml $ mr Auth.NowLoggedIn
-- onLogout = do
-- AppSettings{..} <- getsYesod appSettings'
-- when appSingleSignOn $ singleSignOut @UniWorX Nothing
onErrorHtml dest msg = do
addMessage Error $ toHtml msg
redirect dest

View File

@ -75,6 +75,8 @@ breadcrumb :: ( BearerAuthSite UniWorX
=> Route UniWorX
-> m Breadcrumb
breadcrumb (AuthR _) = i18nCrumb MsgMenuLogin $ Just NewsR
breadcrumb SOutR = i18nCrumb MsgLogout Nothing
breadcrumb SSOutR = i18nCrumb MsgSingleSignOut Nothing
breadcrumb (StaticR _) = i18nCrumb MsgBreadcrumbStatic Nothing
breadcrumb (WellKnownR _) = i18nCrumb MsgBreadcrumbWellKnown Nothing
breadcrumb MetricsR = i18nCrumb MsgBreadcrumbMetrics Nothing
@ -116,9 +118,10 @@ breadcrumb AdminErrMsgR = i18nCrumb MsgMenuAdminErrMsg $ Just
breadcrumb AdminTokensR = i18nCrumb MsgMenuAdminTokens $ Just AdminR
breadcrumb AdminCrontabR = i18nCrumb MsgBreadcrumbAdminCrontab $ Just AdminR
breadcrumb AdminJobsR = i18nCrumb MsgBreadcrumbAdminJobs $ Just AdminCrontabR
breadcrumb AdminLdapR = i18nCrumb MsgBreadcrumbAdminLdap $ Just AdminR
breadcrumb AdminAvsR = i18nCrumb MsgMenuAvs $ Just AdminR
breadcrumb AdminAvsUserR{} = i18nCrumb MsgAvsPersonInfo $ Just AdminAvsR
breadcrumb AdminLdapR = i18nCrumb MsgMenuLdap $ Just AdminR
breadcrumb AdminExternalUserR = i18nCrumb MsgMenuExternalUser $ Just AdminR
breadcrumb AdminProblemsR = i18nCrumb MsgProblemsHeading $ Just AdminR
breadcrumb ProblemUnreachableR = i18nCrumb MsgProblemsUnreachableHeading $ Just AdminProblemsR
breadcrumb ProblemWithoutAvsId = i18nCrumb MsgProblemsNoAvsIdHeading $ Just AdminProblemsR
@ -562,42 +565,37 @@ defaultLinks :: ( MonadHandler m
, BearerAuthSite UniWorX
) => m [Nav]
defaultLinks = fmap catMaybes . mapM runMaybeT $ -- Define the menu items of the header.
[ return NavHeader
[ return NavHeaderContainer
{ navHeaderRole = NavHeaderSecondary
, navIcon = IconMenuLogout
, navLink = NavLink
{ navLabel = MsgMenuLogout
, navRoute = AuthR LogoutR
, navAccess' = NavAccessHandler $ is _Just <$> maybeAuthId
, navType = NavTypeLink { navModal = False }
, navQuick' = mempty
, navForceActive = False
}
}
, return NavHeader
{ navHeaderRole = NavHeaderSecondary
, navIcon = IconMenuLogin
, navLink = NavLink
{ navLabel = MsgMenuLogin
, navRoute = AuthR LoginR
, navAccess' = NavAccessHandler $ is _Nothing <$> maybeAuthId
, navType = NavTypeLink { navModal = True }
, navQuick' = mempty
, navForceActive = False
}
}
, return NavHeader
{ navHeaderRole = NavHeaderSecondary
, navIcon = IconMenuProfile
, navLink = NavLink
{ navLabel = MsgMenuProfile
, navRoute = ProfileR
, navAccess' = NavAccessHandler $ is _Just <$> maybeAuthId
, navType = NavTypeLink { navModal = False }
, navQuick' = mempty
, navForceActive = False
}
}
, navLabel = SomeMessage MsgMenuAccount
, navIcon = IconMenuAccount
, navChildren =
[ NavLink
{ navLabel = MsgMenuLogout
, navRoute = SSOutR -- AuthR LogoutR
, navAccess' = NavAccessHandler $ is _Just <$> maybeAuthId
, navType = NavTypeLink { navModal = False }
, navQuick' = mempty
, navForceActive = False
}
, NavLink
{ navLabel = MsgMenuLogin
, navRoute = AuthR LoginR
, navAccess' = NavAccessHandler $ is _Nothing <$> maybeAuthId
, navType = NavTypeLink { navModal = False }
, navQuick' = mempty
, navForceActive = False
}
, NavLink
{ navLabel = MsgMenuProfile
, navRoute = ProfileR
, navAccess' = NavAccessHandler $ is _Just <$> maybeAuthId
, navType = NavTypeLink { navModal = False }
, navQuick' = mempty
, navForceActive = False
}
]
}
, do
mCurrentRoute <- getCurrentRoute
@ -876,8 +874,8 @@ defaultLinks = fmap catMaybes . mapM runMaybeT $ -- Define the menu items of the
, navForceActive = False
}
, NavLink
{ navLabel = MsgMenuLdap
, navRoute = AdminLdapR
{ navLabel = MsgMenuExternalUser
, navRoute = AdminExternalUserR
, navAccess' = NavAccessTrue
, navType = NavTypeLink { navModal = False }
, navQuick' = mempty
@ -1264,8 +1262,8 @@ pageActions (AdminUserR cID) = return
, navRoute = UserPasswordR cID
, navAccess' = NavAccessDB $ do
uid <- decrypt cID
User{userAuthentication} <- get404 uid
return $ is _AuthPWHash userAuthentication
User{userPasswordHash} <- get404 uid
return $ is _Just userPasswordHash
, navType = NavTypeLink { navModal = True }
, navQuick' = mempty
, navForceActive = False

View File

@ -9,7 +9,7 @@ module Foundation.Routes
( module Foundation.Routes.Definitions
, module Foundation.Routes
) where
import Import.NoFoundation
import Foundation.Type

View File

@ -157,6 +157,10 @@ siteLayout' overrideHeading widget = do
isAuth <- isJust <$> maybeAuthId
when (appAutoSignOn && not isAuth) $ do
$logDebugS "AutoSignOn" "AutoSignOn is enabled in AppSettings and user is not authenticated"
redirect $ AuthR LoginR
now <- liftIO getCurrentTime
muid <- maybeAuthPair

View File

@ -67,7 +67,7 @@ data UniWorX = UniWorX
, appStatic :: EmbeddedStatic -- ^ Settings for static file serving.
, appConnPool :: forall m. MonadIO m => Custom.Pool' m DBConnLabel DBConnUseState SqlBackend -- ^ Database connection pool.
, appSmtpPool :: Maybe SMTPPool
, appLdapPool :: Maybe (Failover (LdapConf, LdapPool))
, appLdapPool :: Maybe (LdapConf, LdapPool) -- TODO: reintroduce Failover
, appWidgetMemcached :: Maybe Memcached.Connection -- ^ Actually a proper pool
, appHttpManager :: Manager
, appLogger :: (ReleaseKey, TVar Logger)
@ -84,6 +84,7 @@ data UniWorX = UniWorX
, appUploadCache :: Maybe MinioConn
, appVerpSecret :: VerpSecret
, appAuthKey :: Auth.Key
, appAuthPlugins :: [AuthPlugin UniWorX]
, appPersonalisedSheetFilesSeedKey :: PersonalisedSheetFilesSeedKey
, appVolatileClusterSettingsCache :: TVar VolatileClusterSettingsCache
, appStartTime :: UTCTime -- for Status Page

View File

@ -1,23 +1,44 @@
-- SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>
-- SPDX-FileCopyrightText: 2023-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>, David Mosbach <david.mosbach@uniworx.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
module Foundation.Types
( UpsertCampusUserMode(..)
, _UpsertCampusUserLoginLdap, _UpsertCampusUserLoginDummy, _UpsertCampusUserLoginOther, _UpsertCampusUserLdapSync, _UpsertCampusUserGuessUser
, _upsertCampusUserIdent
( UpsertUserMode(..)
, _UpsertUserLogin, _UpsertUserLoginDummy, _UpsertUserLoginOther, _UpsertUserSync, _UpsertUserGuessUser
, _upsertUserSource, _upsertUserIdent
, UpsertUserData(..)
, _UpsertUserDataAzure, _UpsertUserDataLdap
, _upsertUserAzureTenantId, _upsertUserAzureData, _upsertUserLdapHost, _upsertUserLdapData
) where
import Import.NoFoundation
import qualified Ldap.Client as Ldap
data UpsertCampusUserMode
= UpsertCampusUserLoginLdap
| UpsertCampusUserLoginDummy { upsertCampusUserIdent :: UserIdent }
| UpsertCampusUserLoginOther { upsertCampusUserIdent :: UserIdent } -- erlaubt keinen späteren Login
| UpsertCampusUserLdapSync { upsertCampusUserIdent :: UserIdent }
| UpsertCampusUserGuessUser
deriving (Eq, Ord, Read, Show, Generic)
makeLenses_ ''UpsertCampusUserMode
makePrisms ''UpsertCampusUserMode
-- TODO: rename?
data UpsertUserMode
= UpsertUserLogin { upsertUserSource :: Text } -- TODO: use type synonym?
| UpsertUserLoginDummy { upsertUserIdent :: UserIdent }
| UpsertUserLoginOther { upsertUserIdent :: UserIdent } -- does not allow further login
| UpsertUserSync { upsertUserIdent :: UserIdent }
| UpsertUserGuessUser
deriving (Show)
makeLenses_ ''UpsertUserMode
makePrisms ''UpsertUserMode
data UpsertUserData
= UpsertUserDataAzure
{ upsertUserAzureTenantId :: UUID
, upsertUserAzureData :: [(Text, [ByteString])] -- TODO: use type synonym?
}
| UpsertUserDataLdap
{ upsertUserLdapHost :: Text
, upsertUserLdapData :: Ldap.AttrList []
}
deriving (Show)
makeLenses_ ''UpsertUserData
makePrisms ''UpsertUserData

View File

@ -1,69 +1,66 @@
-- SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Steffen Jost <jost@cip.ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>, Steffen Jost <jost@cip.ifi.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>, David Mosbach <david.mosbach@uniworx.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
module Foundation.Yesod.Auth
( authenticate
, ldapLookupAndUpsert
, upsertCampusUser
, userLookupAndUpsert
, upsertUser, maybeUpsertUser
, decodeUserTest
, CampusUserConversionException(..)
, campusUserFailoverMode, updateUserLanguage
, DecodeUserException(..)
, updateUserLanguage
) where
import Import.NoFoundation hiding (authenticate)
import Auth.Dummy (apDummy)
import Auth.LDAP
import Auth.OAuth2
import Auth.PWHash (apHash)
import Foundation.Type
import Foundation.Types
import Foundation.I18n
import Handler.Utils.Profile
-- import Handler.Utils.Profile
import Handler.Utils.LdapSystemFunctions
import Handler.Utils.Memcached
import Foundation.Authorization (AuthorizationCacheKey(..))
import Yesod.Auth.Message
import Auth.LDAP
import Auth.PWHash (apHash)
import Auth.Dummy (apDummy)
import Yesod.Auth.OAuth2 (getAccessToken, getRefreshToken)
import qualified Data.CaseInsensitive as CI
import qualified Control.Monad.Catch as C (Handler(..))
import qualified Ldap.Client as Ldap
import qualified Data.ByteString as ByteString
import qualified Data.CaseInsensitive as CI
import qualified Data.Map as Map
import qualified Data.List.NonEmpty as NonEmpty (toList)
import qualified Data.Set as Set
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
import qualified Data.ByteString as ByteString
import qualified Data.Set as Set
import qualified Data.Map as Map
-- import qualified Data.Conduit.Combinators as C
-- import qualified Data.List as List ((\\))
-- import qualified Data.UUID as UUID
-- import Data.ByteArray (convert)
-- import Crypto.Hash (SHAKE128)
-- import qualified Data.Binary as Binary
-- import qualified Database.Esqueleto.Legacy as E
-- import qualified Database.Esqueleto.Utils as E
-- import Crypto.Hash.Conduit (sinkHash)
import qualified Ldap.Client as Ldap
authenticate :: ( MonadHandler m, HandlerSite m ~ UniWorX
, YesodPersist UniWorX, BackendCompatible SqlBackend (YesodPersistBackend UniWorX)
, YesodAuth UniWorX, UserId ~ AuthId UniWorX
)
=> Creds UniWorX -> m (AuthenticationResult UniWorX)
=> Creds UniWorX
-> m (AuthenticationResult UniWorX)
authenticate creds@Creds{..} = liftHandler . runDB . withReaderT projectBackend $ do
$logDebugS "Auth Debug" $ "\a\27[31m" <> tshow creds <> "\27[0m"
setSessionJson SessionOAuth2Token (getAccessToken creds, getRefreshToken creds)
now <- liftIO getCurrentTime
let
uAuth = UniqueAuthentication $ CI.mk credsIdent
upsertMode = creds ^? _upsertCampusUserMode
upsertMode = creds ^? _upsertUserMode
isDummy = is (_Just . _UpsertCampusUserLoginDummy) upsertMode
isOther = is (_Just . _UpsertCampusUserLoginOther) upsertMode
isDummy = is (_Just . _UpsertUserLoginDummy) upsertMode
isOther = is (_Just . _UpsertUserLoginOther) upsertMode
excRecovery res
| isDummy || isOther
@ -77,21 +74,21 @@ authenticate creds@Creds{..} = liftHandler . runDB . withReaderT projectBackend
= return res
excHandlers =
[ C.Handler $ \case
CampusUserNoResult -> do
$logWarnS "LDAP" $ "User lookup failed after successful login for " <> credsIdent
[ C.Handler $ \(fExc :: FetchUserDataException) -> case fExc of
FetchUserDataNoResult -> do
$logWarnS "FetchUserException" $ "User lookup failed after successful login for " <> credsIdent
excRecovery . UserError $ IdentifierNotFound credsIdent
CampusUserAmbiguous -> do
$logWarnS "LDAP" $ "Multiple LDAP results for " <> credsIdent
FetchUserDataAmbiguous -> do
$logWarnS "FetchUserException" $ "Multiple User results for " <> credsIdent
excRecovery . UserError $ IdentifierNotFound credsIdent
err -> do
$logErrorS "LDAP" $ tshow err
$logErrorS "FetchUserException" $ tshow err
mr <- getMessageRender
excRecovery . ServerError $ mr MsgInternalLdapError
, C.Handler $ \(cExc :: CampusUserConversionException) -> do
$logErrorS "LDAP" $ tshow cExc
excRecovery . ServerError $ mr MsgInternalLoginError
, C.Handler $ \(dExc :: DecodeUserException) -> do
$logErrorS "Auth" $ tshow dExc
mr <- getMessageRender
excRecovery . ServerError $ mr cExc
excRecovery . ServerError $ mr dExc
]
acceptExisting :: SqlPersistT (HandlerFor UniWorX) (AuthenticationResult UniWorX)
@ -107,115 +104,178 @@ authenticate creds@Creds{..} = liftHandler . runDB . withReaderT projectBackend
| not isDummy -> res <$ update uid [ UserLastAuthentication =. Just now ]
_other -> return res
$logDebugS "auth" $ tshow Creds{..}
ldapPool' <- getsYesod $ view _appLdapPool
$logDebugS "Auth" $ tshow Creds{..}
flip catches excHandlers $ case ldapPool' of
Just ldapPool
| Just upsertMode' <- upsertMode -> do
ldapData <- campusUser ldapPool campusUserFailoverMode Creds{..}
$logDebugS "LDAP" $ "Successful LDAP lookup: " <> tshow ldapData
Authenticated . entityKey <$> upsertCampusUser upsertMode' ldapData
_other
flip catches excHandlers $ if
| not isDummy, not isOther
, Just upsertMode' <- upsertMode -> fetchUserData Creds{..} >>= \case
Just userData -> do
$logDebugS "Auth" $ "Successful user data lookup: " <> tshow userData
Authenticated . entityKey <$> upsertUser upsertMode' userData
Nothing
-> throwM FetchUserDataNoResult
| otherwise
-> acceptExisting
data CampusUserConversionException
= CampusUserInvalidIdent
| CampusUserInvalidEmail
| CampusUserInvalidDisplayName
| CampusUserInvalidGivenName
| CampusUserInvalidSurname
| CampusUserInvalidTitle
-- | CampusUserInvalidMatriculation
| CampusUserInvalidFeaturesOfStudy Text
| CampusUserInvalidAssociatedSchools Text
data DecodeUserException
= DecodeUserInvalidIdent
| DecodeUserInvalidEmail
| DecodeUserInvalidDisplayName
| DecodeUserInvalidGivenName
| DecodeUserInvalidSurname
| DecodeUserInvalidTitle
| DecodeUserInvalidFeaturesOfStudy Text
| DecodeUserInvalidAssociatedSchools Text
deriving (Eq, Ord, Read, Show, Generic)
deriving anyclass (Exception)
_upsertCampusUserMode :: Traversal' (Creds UniWorX) UpsertCampusUserMode
_upsertCampusUserMode mMode cs@Creds{..}
| credsPlugin == apDummy = setMode <$> mMode (UpsertCampusUserLoginDummy $ CI.mk credsIdent)
| credsPlugin == apLdap = setMode <$> mMode UpsertCampusUserLoginLdap
| otherwise = setMode <$> mMode (UpsertCampusUserLoginOther $ CI.mk credsIdent)
_upsertUserMode :: Traversal' (Creds UniWorX) UpsertUserMode
_upsertUserMode mMode cs@Creds{..}
| credsPlugin == apDummy = setMode <$> mMode (UpsertUserLoginDummy $ CI.mk credsIdent)
| credsPlugin `elem` loginAPs
= setMode <$> mMode (UpsertUserLogin credsPlugin)
| otherwise = setMode <$> mMode (UpsertUserLoginOther $ CI.mk credsIdent)
where
setMode UpsertCampusUserLoginLdap
= cs{ credsPlugin = apLdap }
setMode (UpsertCampusUserLoginDummy ident)
= cs{ credsPlugin = apDummy
, credsIdent = CI.original ident
}
setMode (UpsertCampusUserLoginOther ident)
= cs{ credsPlugin = bool defaultOther credsPlugin (credsPlugin /= apDummy && credsPlugin /= apLdap)
, credsIdent = CI.original ident
}
setMode UpsertUserLogin{..} | upsertUserSource `elem` loginAPs
= cs { credsPlugin = upsertUserSource }
setMode UpsertUserLoginDummy{..}
= cs { credsPlugin = apDummy
, credsIdent = CI.original upsertUserIdent
}
setMode UpsertUserLoginOther{..}
= cs { credsPlugin = bool defaultOther credsPlugin (credsPlugin `notElem` [apDummy, apLdap, apAzure])
, credsIdent = CI.original upsertUserIdent
}
setMode _ = cs
loginAPs = [ apAzure, apLdap ]
defaultOther = apHash
ldapLookupAndUpsert :: forall m. (MonadHandler m, HandlerSite m ~ UniWorX, MonadMask m, MonadUnliftIO m) => Text -> SqlPersistT m (Entity User)
ldapLookupAndUpsert ident =
getsYesod (view _appLdapPool) >>= \case
Nothing -> throwM $ CampusUserLdapError $ LdapHostNotResolved "No LDAP configuration in Foundation."
Just ldapPool ->
campusUser'' ldapPool campusUserFailoverMode ident >>= \case
Nothing -> throwM CampusUserNoResult
Just ldapResponse -> upsertCampusUser UpsertCampusUserGuessUser ldapResponse
{- THIS FUNCION JUST DECODES, BUT IT DOES NOT QUERY LDAP!
upsertCampusUserByCn :: forall m.
( MonadHandler m, HandlerSite m ~ UniWorX
, MonadThrow m
)
=> Text -> SqlPersistT m (Entity User)
upsertCampusUserByCn persNo = upsertCampusUser UpsertCampusUserGuessUser [(ldapPrimaryKey,[Text.encodeUtf8 persNo])]
-}
userLookupAndUpsert :: forall m.
( MonadHandler m
, HandlerSite m ~ UniWorX
, MonadMask m
, MonadUnliftIO m
)
=> Text
-> UpsertUserMode
-> SqlPersistT m (Maybe (Entity User))
userLookupAndUpsert credsIdent mode =
fetchUserData Creds{credsPlugin=mempty,credsExtra=mempty,..} >>= maybeUpsertUser mode
-- | Upsert User DB according to given LDAP data (does not query LDAP itself)
upsertCampusUser :: forall m.
( MonadHandler m, HandlerSite m ~ UniWorX
, MonadCatch m
)
=> UpsertCampusUserMode -> Ldap.AttrList [] -> SqlPersistT m (Entity User)
upsertCampusUser upsertMode ldapData = do
data FetchUserDataException
= FetchUserDataNoResult
| FetchUserDataAmbiguous
| FetchUserDataException
deriving (Eq, Ord, Read, Show, Generic)
deriving anyclass (Exception)
-- | Fetch user data with given credentials from external source(s)
fetchUserData :: forall m site.
( MonadHandler m
, HandlerSite m ~ UniWorX
, MonadCatch m
, MonadMask m
, MonadUnliftIO m
)
=> Creds site
-> SqlPersistT m (Maybe (NonEmpty UpsertUserData))
fetchUserData Creds{..} = do
userAuthConf <- getsYesod $ view _appUserAuthConf
now <- liftIO getCurrentTime
results :: Maybe (NonEmpty UpsertUserData) <- case userAuthConf of
UserAuthConfSingleSource{..} -> fmap (:| []) <$> case userAuthConfSingleSource of
AuthSourceConfAzureAdV2 AzureConf{ azureConfClientId = upsertUserAzureTenantId } -> do
queryOAuth2User @[(Text, [ByteString])] credsIdent >>= \case
Right upsertUserAzureData -> return $ Just UpsertUserDataAzure{..}
Left _ -> return Nothing
AuthSourceConfLdap LdapConf{..} -> getsYesod (view _appLdapPool) >>= \case
Just ldapPool -> fmap (UpsertUserDataLdap ldapConfSourceId) <$> ldapUser'' ldapPool credsIdent
Nothing -> throwM FetchUserDataException
-- insert ExternalUser entries for each fetched dataset
whenIsJust results $ \ress -> forM_ ress $ \res -> do
let externalUserLastSync = now
(externalUserData, externalUserSource) = case res of
UpsertUserDataAzure{..} -> (toJSON upsertUserAzureData, AuthSourceIdAzure upsertUserAzureTenantId)
UpsertUserDataLdap{..} -> (toJSON upsertUserLdapData, AuthSourceIdLdap upsertUserLdapHost)
externalUserUser <- if
| UpsertUserDataAzure{..} <- res
, azureData <- Map.fromListWith (++) $ upsertUserAzureData <&> second (filter (not . ByteString.null))
, [Text.decodeUtf8' -> Right azureUserPrincipalName'] <- azureData !!! azureUserPrincipalName
-> return $ CI.mk azureUserPrincipalName'
| UpsertUserDataLdap{..} <- res
, ldapData <- Map.fromListWith (++) $ upsertUserLdapData <&> second (filter (not . ByteString.null))
, [Text.decodeUtf8' -> Right ldapPrimaryKey'] <- ldapData !!! ldapPrimaryKey
-> return $ CI.mk ldapPrimaryKey'
| otherwise
-> throwM DecodeUserInvalidIdent
void $ upsert ExternalUser{..} [ExternalUserData =. externalUserData, ExternalUserLastSync =. externalUserLastSync]
return results
-- | Upsert User and related auth in DB according to given external source data (does not query source itself)
maybeUpsertUser :: forall m.
( MonadHandler m
, HandlerSite m ~ UniWorX
, MonadCatch m
)
=> UpsertUserMode
-> Maybe (NonEmpty UpsertUserData)
-> SqlPersistT m (Maybe (Entity User))
maybeUpsertUser _upsertMode Nothing = return Nothing
maybeUpsertUser _upsertMode (Just upsertData) = do
now <- liftIO getCurrentTime
userDefaultConf <- getsYesod $ view _appUserDefaults
(newUser,userUpdate) <- decodeUser now userDefaultConf upsertMode ldapData
--TODO: newUser should be associated with a company and company supervisor through Handler.Utils.Company.upsertUserCompany, but this is called by upsertAvsUser already - conflict?
oldUsers <- for (userLdapPrimaryKey newUser) $ \pKey -> selectKeysList [ UserLdapPrimaryKey ==. Just pKey ] []
oldUsers <- selectKeysList [ UserIdent ==. userIdent newUser ] []
user@(Entity userId userRec) <- case oldUsers of
Just [oldUserId] -> updateGetEntity oldUserId userUpdate
_other -> upsertBy (UniqueAuthentication (newUser ^. _userIdent)) newUser userUpdate
unless (validDisplayName (newUser ^. _userTitle)
unless (validDisplayName (newUser ^. _userTitle)
(newUser ^. _userFirstName)
(newUser ^. _userSurname)
(newUser ^. _userSurname)
(userRec ^. _userDisplayName)) $
update userId [ UserDisplayName =. (newUser ^. _userDisplayName) ] -- update invalid display names only
when (validEmail' (userRec ^. _userEmail)) $ do -- RECALL: userRec already contains basic updates
update userId [ UserDisplayName =. (newUser ^. _userDisplayName) ]
when (validEmail' (userRec ^. _userEmail)) $ do
let emUps = [ UserDisplayEmail =. (newUser ^. _userEmail) | not (validEmail' (userRec ^. _userDisplayEmail)) ]
++ [ UserAuthentication =. AuthLDAP | is _AuthNoLogin (userRec ^. _userAuthentication) ]
update userId emUps -- update already checks whether list is empty
unless (null emUps) $ update userId emUps
-- Attempt to update ident, too:
unless (validEmail' (userRec ^. _userIdent)) $
void $ maybeCatchAll (update userId [ UserIdent =. (newUser ^. _userEmail) ] >> return (Just ()))
let
userSystemFunctions = determineSystemFunctions . Set.fromList $ map CI.mk userSystemFunctions'
userSystemFunctions' = do
(k, v) <- ldapData
guard $ k == ldapAffiliation
v' <- v
Right str <- return $ Text.decodeUtf8' v'
assertM' (not . Text.null) $ Text.strip str
userSystemFunctions' = concat $ upsertData <&> \case
UpsertUserDataAzure{..} -> do
(_k, v) <- upsertUserAzureData
v' <- v
Right str <- return $ Text.decodeUtf8' v'
assertM' (not . Text.null) $ Text.strip str
UpsertUserDataLdap{..} -> do
(k, v) <- upsertUserLdapData
guard $ k == ldapAffiliation
v' <- v
Right str <- return $ Text.decodeUtf8' v'
assertM' (not . Text.null) $ Text.strip str
iforM_ userSystemFunctions $ \func preset -> do
memcachedByInvalidate (AuthCacheSystemFunctionList func) $ Proxy @(Set UserId)
if | preset -> void $ upsert (UserSystemFunction userId func False False) []
| otherwise -> deleteWhere [UserSystemFunctionUser ==. userId, UserSystemFunctionFunction ==. func, UserSystemFunctionIsOptOut ==. False, UserSystemFunctionManual ==. False]
return user
return $ Just user
decodeUserTest :: (MonadHandler m, HandlerSite m ~ UniWorX, MonadCatch m)
=> Maybe UserIdent -> Ldap.AttrList [] -> m (Either CampusUserConversionException (User, [Update User]))
@ -227,10 +287,10 @@ decodeUserTest mbIdent ldapData = do
decodeUser :: (MonadThrow m) => UTCTime -> UserDefaultConf -> UpsertCampusUserMode -> Ldap.AttrList [] -> m (User,_)
decodeUser now UserDefaultConf{..} upsertMode ldapData = do
decodeUser now UserDefaultConf{..} upsertMode ldapData = do
let
userTelephone = decodeLdap ldapUserTelephone <&> canonicalPhone
userMobile = decodeLdap ldapUserMobile <&> canonicalPhone
userTelephone = decodeLdap ldapUserTelephone
userMobile = decodeLdap ldapUserMobile
userCompanyPersonalNumber = decodeLdap ldapUserFraportPersonalnummer
userCompanyDepartment = decodeLdap ldapUserFraportAbteilung
@ -250,12 +310,14 @@ decodeUser now UserDefaultConf{..} upsertMode ldapData = do
-- (maybeThrow CampusUserInvalidDisplayName . checkDisplayName userTitle userFirstName userSurname)
userIdent <- if
| [bs] <- ldapMap !!! ldapUserPrincipalName
, Right userIdent' <- CI.mk <$> Text.decodeUtf8' bs
, hasn't _upsertCampusUserIdent upsertMode || has (_upsertCampusUserIdent . only userIdent') upsertMode
-> return userIdent'
| Just userIdent' <- upsertMode ^? _upsertCampusUserIdent
-> return userIdent'
| Just azureData <- mbAzureData
, [Text.decodeUtf8' -> Right azureUserPrincipalName'] <- azureData !!! azureUserPrincipalName
, Just azureUserPrincipalName'' <- assertM' (not . Text.null) $ Text.strip azureUserPrincipalName'
-> return $ CI.mk azureUserPrincipalName''
| Just ldapData <- mbLdapData
, [Text.decodeUtf8' -> Right ldapPrimaryKey'] <- ldapData !!! ldapPrimaryKey
, Just ldapPrimaryKey'' <- assertM' (not . Text.null) $ Text.strip ldapPrimaryKey'
-> return $ CI.mk ldapPrimaryKey''
| otherwise
-> throwM CampusUserInvalidIdent
@ -266,7 +328,7 @@ decodeUser now UserDefaultConf{..} upsertMode ldapData = do
-- -> return $ CI.mk userEmail
| otherwise
-> throwM CampusUserInvalidEmail
userLdapPrimaryKey <- if
| [bs] <- ldapMap !!! ldapPrimaryKey
, Right userLdapPrimaryKey'' <- Text.decodeUtf8' bs
@ -277,41 +339,42 @@ decodeUser now UserDefaultConf{..} upsertMode ldapData = do
let
newUser = User
{ userMaxFavourites = userDefaultMaxFavourites
, userMaxFavouriteTerms = userDefaultMaxFavouriteTerms
, userTheme = userDefaultTheme
, userDateTimeFormat = userDefaultDateTimeFormat
, userDateFormat = userDefaultDateFormat
, userTimeFormat = userDefaultTimeFormat
, userDownloadFiles = userDefaultDownloadFiles
, userWarningDays = userDefaultWarningDays
, userShowSex = userDefaultShowSex
, userSex = Nothing
, userBirthday = Nothing
, userExamOfficeGetSynced = userDefaultExamOfficeGetSynced
, userExamOfficeGetLabels = userDefaultExamOfficeGetLabels
, userNotificationSettings = def
, userLanguages = Nothing
, userCsvOptions = def
, userTokensIssuedAfter = Nothing
, userCreated = now
, userLastLdapSynchronisation = Just now
, userDisplayName = userDisplayName
, userDisplayEmail = userEmail
, userMatrikelnummer = Nothing -- not known from LDAP, must be derived from REST interface to AVS TODO
, userPostAddress = Nothing -- not known from LDAP, must be derived from REST interface to AVS TODO
, userPostLastUpdate = Nothing
, userPinPassword = Nothing -- must be derived via AVS
, userPrefersPostal = userDefaultPrefersPostal
{ userMaxFavourites = userDefaultMaxFavourites
, userMaxFavouriteTerms = userDefaultMaxFavouriteTerms
, userTheme = userDefaultTheme
, userDateTimeFormat = userDefaultDateTimeFormat
, userDateFormat = userDefaultDateFormat
, userTimeFormat = userDefaultTimeFormat
, userDownloadFiles = userDefaultDownloadFiles
, userWarningDays = userDefaultWarningDays
, userShowSex = userDefaultShowSex
, userSex = Nothing
, userBirthday = Nothing
, userTitle = Nothing
, userExamOfficeGetSynced = userDefaultExamOfficeGetSynced
, userExamOfficeGetLabels = userDefaultExamOfficeGetLabels
, userNotificationSettings = def
, userCsvOptions = def
, userTokensIssuedAfter = Nothing
, userDisplayEmail = userEmail
, userMatrikelnummer = Nothing -- TODO: not known from Azure/LDAP, must be derived from REST interface to AVS
, userPostAddress = Nothing -- TODO: not known from Azure/LDAP, must be derived from REST interface to AVS
, userPostLastUpdate = Nothing
, userPinPassword = Nothing -- must be derived via AVS
, userPrefersPostal = userDefaultPrefersPostal
, userPasswordHash = Nothing
, userLastAuthentication = Nothing
, userCreated = now
, userLastSync = Just now
, ..
}
userUpdate =
userUpdate =
[ UserLastAuthentication =. Just now | isLogin ] ++
[ UserEmail =. userEmail | validEmail' userEmail ] ++
[
-- UserDisplayName =. userDisplayName -- not updated here, since users are allowed to change their DisplayName; see line 191
-- UserDisplayName =. userDisplayName -- not updated here, since users are allowed to change their DisplayName; see line 272
UserFirstName =. userFirstName
, UserSurname =. userSurname
, UserSurname =. userSurname
, UserLastLdapSynchronisation =. Just now
, UserLdapPrimaryKey =. userLdapPrimaryKey
, UserMobile =. userMobile
@ -322,15 +385,21 @@ decodeUser now UserDefaultConf{..} upsertMode ldapData = do
return (newUser, userUpdate)
where
ldapMap :: Map.Map Ldap.Attr [Ldap.AttrValue] -- Recall: Ldap.AttrValue == ByteString
ldapMap = Map.fromListWith (++) $ ldapData <&> second (filter (not . ByteString.null))
mbAzureData :: Maybe (Map Text [ByteString])
mbAzureData = fmap (Map.fromListWith (++) . map (second (filter (not . ByteString.null)))) . concat $ preview _upsertUserAzureData <$> NonEmpty.toList upsertData
mbLdapData :: Maybe (Map Ldap.Attr [Ldap.AttrValue]) -- Recall: Ldap.AttrValue == ByteString
mbLdapData = fmap (Map.fromListWith (++) . map (second (filter (not . ByteString.null)))) . concat $ preview _upsertUserLdapData <$> NonEmpty.toList upsertData
-- just returns Nothing on error, pure
decodeLdap :: Ldap.Attr -> Maybe Text
decodeLdap attr = listToMaybe . rights $ Text.decodeUtf8' <$> ldapMap !!! attr
decodeAzure :: Map Text [ByteString] -> Text -> Maybe Text
decodeAzure azureData k = listToMaybe . rights $ Text.decodeUtf8' <$> azureData !!! k
decodeLdap :: Map Ldap.Attr [Ldap.AttrValue] -> Ldap.Attr -> Maybe Text
decodeLdap ldapData attr = listToMaybe . rights $ Text.decodeUtf8' <$> ldapData !!! attr
decodeLdap' :: Ldap.Attr -> Text
decodeLdap' = fromMaybe "" . decodeLdap
-- decodeAzure' :: Map Text [ByteString] -> Text -> Text
-- decodeAzure' azureData = fromMaybe "" . decodeAzure azureData
-- decodeLdap' :: Map Ldap.Attr [Ldap.AttrValue] -> Ldap.Attr -> Text
-- decodeLdap' ldapData = fromMaybe "" . decodeLdap ldapData
-- accept the first successful decoding or empty; only throw an error if all decodings fail
-- decodeLdap' :: (Exception e) => Ldap.Attr -> e -> m (Maybe Text)
-- decodeLdap' attr err
@ -342,11 +411,11 @@ decodeUser now UserDefaultConf{..} upsertMode ldapData = do
-- only accepts the first successful decoding, ignoring all others, but failing if there is none
-- decodeLdap1 :: (MonadThrow m, Exception e) => Ldap.Attr -> e -> m Text
decodeLdap1 attr err
| (h:_) <- rights vs = return h
| otherwise = throwM err
where
vs = Text.decodeUtf8' <$> (ldapMap !!! attr)
-- decodeLdap1 ldapData attr err
-- | (h:_) <- rights vs = return h
-- | otherwise = throwM err
-- where
-- vs = Text.decodeUtf8' <$> (ldapData !!! attr)
-- accept and merge one or more successful decodings, ignoring all others
-- decodeLdapN attr err
@ -356,6 +425,17 @@ decodeUser now UserDefaultConf{..} upsertMode ldapData = do
-- where
-- vs = Text.decodeUtf8' <$> (ldapMap !!! attr)
decodeUserTest :: ( MonadHandler m
, HandlerSite m ~ UniWorX
, MonadCatch m
)
=> NonEmpty UpsertUserData
-> m (Either DecodeUserException (User, [Update User]))
decodeUserTest decodeData = do
now <- liftIO getCurrentTime
userDefaultConf <- getsYesod $ view _appUserDefaults
try $ decodeUser now userDefaultConf decodeData
associateUserSchoolsByTerms :: MonadIO m => UserId -> SqlPersistT m ()
associateUserSchoolsByTerms uid = do
@ -370,11 +450,14 @@ associateUserSchoolsByTerms uid = do
, userSchoolIsOptOut = False
}
updateUserLanguage :: ( MonadHandler m, HandlerSite m ~ UniWorX
updateUserLanguage :: ( MonadHandler m
, HandlerSite m ~ UniWorX
, YesodAuth UniWorX
, UserId ~ AuthId UniWorX
)
=> Maybe Lang -> SqlPersistT m (Maybe Lang)
=> Maybe Lang
-> SqlPersistT m (Maybe Lang)
updateUserLanguage (Just lang) = do
unless (lang `elem` appLanguages) $
invalidArgs ["Unsupported language"]
@ -405,7 +488,4 @@ updateUserLanguage Nothing = runMaybeT $ do
setRegisteredCookie CookieLang lang
return lang
campusUserFailoverMode :: FailoverMode
campusUserFailoverMode = FailoverUnlimited
embedRenderMessage ''UniWorX ''CampusUserConversionException id
embedRenderMessage ''UniWorX ''DecodeUserException id

View File

@ -1,4 +1,4 @@
-- SPDX-FileCopyrightText: 2022-2025 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>
-- SPDX-FileCopyrightText: 2022-202025 Sarah Vaupel <sarah.vaupel@uniworx.de>,Gregor Kleen <gregor.kleen@ifi.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
@ -38,7 +38,11 @@ import Handler.Admin.ErrorMessage as Handler.Admin
import Handler.Admin.Tokens as Handler.Admin
import Handler.Admin.Crontab as Handler.Admin
import Handler.Admin.Avs as Handler.Admin
import Handler.Admin.Ldap as Handler.Admin
import Handler.Admin.ExternalUser as Handler.Admin
-- avoids repetition of local definitions
single :: (k,a) -> Map k a
single = uncurry Map.singleton
-- Types and Template Haskell

View File

@ -0,0 +1,74 @@
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, David Mosbach <david.mosbach@uniworx.de>, Steffen Jost <jost@tcs.ifi.lmu.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
module Handler.Admin.ExternalUser
( getAdminExternalUserR
, postAdminExternalUserR
) where
import Import
import Foundation.Yesod.Auth (userLookupAndUpsert) -- decodeUserTest
import Auth.OAuth2 (queryOAuth2User)
import Auth.LDAP
import Handler.Utils
import Data.Aeson.Encode.Pretty (encodePretty)
import qualified Data.Text.Lazy as Lazy
import qualified Data.Text.Lazy.Encoding as Lazy
getAdminExternalUserR, postAdminExternalUserR :: Handler Html
getAdminExternalUserR = postAdminExternalUserR
postAdminExternalUserR = do
((presult, pwidget), penctype) <- runFormPost $ identifyForm ("adminExternalUserLookup"::Text) $ \html ->
flip (renderAForm FormStandard) html $ areq textField (fslI MsgAdminUserIdent) Nothing
let
-- presentUtf8 v = Text.intercalate ", " (either tshow id . Text.decodeUtf8' <$> v)
-- presentLatin1 v = Text.intercalate ", " ( Text.decodeLatin1 <$> v)
procFormPerson :: Text -> Handler (Maybe [(AuthSourceIdent,Lazy.Text)]) -- (Maybe [(AuthSourceIdent, [(Text,(Int,Text,Text))])])
procFormPerson needle = getsYesod (view _appUserAuthConf) >>= \case
UserAuthConfSingleSource{..} -> case userAuthConfSingleSource of
AuthSourceConfAzureAdV2 AzureConf{..} -> do
-- only singleton results supported right now, i.e. lookups by email, userPrincipalName (aka fraport ident), or id
queryOAuth2User @Value needle >>= \case
Left _ -> addMessage Error (text2Html "Encountered UserDataException while Azure user query!") >> return Nothing
Right azureResponse -> return . Just . singleton . (AuthSourceIdAzure azureConfTenantId,) . Lazy.decodeUtf8 $ encodePretty azureResponse
-- Right azureData -> return . Just . singleton . (AuthSourceIdAzure azureConfTenantId,) $ azureData <&> \(k,vs) -> (k, (length vs, presentUtf8 vs, presentLatin1 vs))
AuthSourceConfLdap LdapConf{..} -> do
getsYesod (view _appLdapPool) >>= \case
Nothing -> addMessage Error (text2Html "LDAP Pool configuration missing!") >> return Nothing
Just pool -> do
ldapData <- ldapSearch pool needle
-- decodedErr <- decodeUserTest $ NonEmpty.singleton UpsertUserDataLdap{ upsertUserLdapHost = ldapConfSourceId, upsertUserLdapData = concat ldapData }
-- whenIsLeft decodedErr $ addMessageI Error
return . Just . singleton . (AuthSourceIdLdap ldapConfSourceId,) . Lazy.decodeUtf8 $ encodePretty ldapData
-- return . Just $ ldapData <&> \(Ldap.SearchEntry _dn attrs) -> (AuthSourceIdLdap{..}, (\(k,v) -> (tshow k, (length v, presentUtf8 v, presentLatin1 v))) <$> attrs)
mbData <- formResultMaybe presult procFormPerson
((uresult, uwidget), uenctype) <- runFormPost $ identifyForm ("adminExternalUserUpsert"::Text) $ \html ->
flip (renderAForm FormStandard) html $ areq textField (fslI MsgAdminUserIdent) Nothing
let procFormUpsert :: Text -> Handler (Maybe (Entity User))
procFormUpsert lid = runDB (userLookupAndUpsert lid UpsertUserGuessUser)
mbUpsert <- formResultMaybe uresult procFormUpsert
actionUrl <- fromMaybe AdminExternalUserR <$> getCurrentRoute
siteLayoutMsg MsgMenuExternalUser $ do
setTitleI MsgMenuExternalUser
let personForm = wrapForm pwidget def
{ formAction = Just $ SomeRoute actionUrl
, formEncoding = penctype
}
upsertForm = wrapForm uwidget def
{ formAction = Just $ SomeRoute actionUrl
, formEncoding = uenctype
}
$(widgetFile "admin/external-user")

View File

@ -1,69 +0,0 @@
-- SPDX-FileCopyrightText: 2022 Steffen Jost <jost@tcs.ifi.lmu.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
module Handler.Admin.Ldap
( getAdminLdapR
, postAdminLdapR
) where
import Import
-- import qualified Control.Monad.State.Class as State
-- import Data.Aeson (encode)
import qualified Data.CaseInsensitive as CI
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
-- import qualified Data.Set as Set
import Foundation.Yesod.Auth (decodeUserTest,ldapLookupAndUpsert,campusUserFailoverMode,CampusUserConversionException())
import Handler.Utils
import qualified Ldap.Client as Ldap
import Auth.LDAP
getAdminLdapR, postAdminLdapR :: Handler Html
getAdminLdapR = postAdminLdapR
postAdminLdapR = do
((presult, pwidget), penctype) <- runFormPost $ identifyForm ("adminLdapLookup"::Text) $ \html ->
flip (renderAForm FormStandard) html $ areq textField (fslI MsgAdminUserIdent) Nothing
let procFormPerson :: Text -> Handler (Maybe (Ldap.AttrList []))
procFormPerson lid = do
ldapPool' <- getsYesod $ view _appLdapPool
case ldapPool' of
Nothing -> addMessage Error (text2Html "LDAP Configuration missing.") >> return Nothing
Just ldapPool -> do
addMessage Info $ text2Html "Input for LDAP test received."
ldapData <- campusUser'' ldapPool campusUserFailoverMode lid
decodedErr <- decodeUserTest (pure $ CI.mk lid) $ concat ldapData
whenIsLeft decodedErr $ addMessageI Error
return ldapData
mbLdapData <- formResultMaybe presult procFormPerson
((uresult, uwidget), uenctype) <- runFormPost $ identifyForm ("adminLdapUpsert"::Text) $ \html ->
flip (renderAForm FormStandard) html $ areq textField (fslI MsgAdminUserIdent) Nothing
let procFormUpsert :: Text -> Handler (Maybe (Either CampusUserConversionException (Entity User)))
procFormUpsert lid = pure <$> runDB (try $ ldapLookupAndUpsert lid)
mbLdapUpsert <- formResultMaybe uresult procFormUpsert
actionUrl <- fromMaybe AdminLdapR <$> getCurrentRoute
siteLayoutMsg MsgMenuLdap $ do
setTitleI MsgMenuLdap
let personForm = wrapForm pwidget def
{ formAction = Just $ SomeRoute actionUrl
, formEncoding = penctype
}
upsertForm = wrapForm uwidget def
{ formAction = Just $ SomeRoute actionUrl
, formEncoding = uenctype
}
presentUtf8 lv = Text.intercalate ", " (either tshow id . Text.decodeUtf8' <$> lv)
presentLatin1 lv = Text.intercalate ", " ( Text.decodeLatin1 <$> lv)
-- TODO: use i18nWidgetFile instead if this is to become permanent
$(widgetFile "ldap")

View File

@ -1,4 +1,4 @@
-- SPDX-FileCopyrightText: 2022-2925 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>
-- SPDX-FileCopyrightText: 2022-2025 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
@ -331,8 +331,8 @@ postAdminTestR = do
<dl .deflist>
<dt .deflist__dt> appJobCronInterval
<dd .deflist__dd>#{tshow appJobCronInterval}
<dt .deflist__dt> appSynchroniseLdapUsersWithin
<dd .deflist__dd>#{tshow appSynchroniseLdapUsersWithin}
<dt .deflist__dt> appUserSyncWithin
<dd .deflist__dd>#{tshow appUserSyncWithin}
<dt .deflist__dt> appSynchroniseAvsUsersWithin
<dd .deflist__dd>#{tshow appSynchroniseAvsUsersWithin}
|]

View File

@ -266,7 +266,6 @@ data UserTableCsv = UserTableCsv
, csvUserSex :: Maybe Sex
, csvUserBirthday :: Maybe Day
, csvUserMatriculation :: Maybe UserMatriculation
, csvUserEPPN :: Maybe UserEduPersonPrincipalName
, csvUserEmail :: UserEmail
, csvUserQualifications :: [QualificationName]
, csvUserSubmissionGroup :: Maybe SubmissionGroupName
@ -286,7 +285,6 @@ instance Csv.ToNamedRecord UserTableCsv where
, "sex" Csv..= csvUserSex
, "birthday" Csv..= csvUserBirthday
, "matriculation" Csv..= csvUserMatriculation
, "eduPersonPrincipalName" Csv..= csvUserEPPN
, "email" Csv..= csvUserEmail
, "qualifications" Csv..= CsvSemicolonList csvUserQualifications
, "submission-group" Csv..= csvUserSubmissionGroup
@ -348,7 +346,6 @@ data UserTableJson = UserTableJson
, jsonUserName :: UserDisplayName
, jsonUserSex :: Maybe (Maybe Sex)
, jsonUserMatriculation :: Maybe UserMatriculation
, jsonUserEPPN :: Maybe UserEduPersonPrincipalName
, jsonUserEmail :: UserEmail
, jsonUserQualifications :: Set QualificationName
, jsonUserSubmissionGroup :: Maybe SubmissionGroupName
@ -385,7 +382,6 @@ instance ToJSON UserTableJson where
, pure $ "name" JSON..= jsonUserName
, ("sex" JSON..=) <$> jsonUserSex
, ("matriculation" JSON..=) <$> jsonUserMatriculation
, ("eduPersonPrincipalName" JSON..=) <$> jsonUserEPPN
, pure $ "email" JSON..= jsonUserEmail
, ("qualifications" JSON..=) <$> assertM' (not . onull) jsonUserQualifications
, ("submission-group" JSON..=) <$> jsonUserSubmissionGroup
@ -650,7 +646,6 @@ makeCourseUserTable cid acts restrict colChoices psValidator csvColumns = do
<*> view (hasUser . _userSex)
<*> view (hasUser . _userBirthday)
<*> view (hasUser . _userMatrikelnummer)
<*> view (hasUser . _userLdapPrimaryKey)
<*> view (hasUser . _userEmail)
<*> (over traverse (qualificationName . entityVal) <$> view _userQualifications)
<*> preview (_userSubmissionGroup . _entityVal . _submissionGroupName)
@ -682,7 +677,6 @@ makeCourseUserTable cid acts restrict colChoices psValidator csvColumns = do
<*> view (hasUser . _userDisplayName)
<*> views (hasUser . _userSex) (guardOn showSex)
<*> view (hasUser . _userMatrikelnummer)
<*> view (hasUser . _userLdapPrimaryKey)
<*> view (hasUser . _userEmail)
<*> view (_userQualifications . folded . to (Set.singleton . qualificationName . entityVal))
<*> preview (_userSubmissionGroup . _entityVal . _submissionGroupName)

View File

@ -190,7 +190,6 @@ data ExamUserTableCsv = ExamUserTableCsv
, csvEUserFirstName :: Maybe Text
, csvEUserName :: Maybe Text
, csvEUserMatriculation :: Maybe Text
, csvEUserEPPN :: Maybe UserEduPersonPrincipalName
, csvEUserStudyFeatures :: UserTableStudyFeatures
, csvEUserOccurrence :: Maybe (CI Text)
, csvEUserExercisePoints :: Maybe (Maybe Points)
@ -211,7 +210,6 @@ instance ToNamedRecord ExamUserTableCsv where
, "first-name" Csv..= csvEUserFirstName
, "name" Csv..= csvEUserName
, "matriculation" Csv..= csvEUserMatriculation
, "eduPersonPrincipalName" Csv..= csvEUserEPPN
, "study-features" Csv..= csvEUserStudyFeatures
, "occurrence" Csv..= csvEUserOccurrence
] ++ catMaybes
@ -237,7 +235,6 @@ instance FromNamedRecord ExamUserTableCsv where
<*> csv .:?? "first-name"
<*> csv .:?? "name"
<*> csv .:?? "matriculation"
<*> csv .:?? "eduPersonPrincipalName"
<*> pure mempty
<*> csv .:?? "occurrence"
<*> fmap Just (csv .:?? "exercise-points")
@ -280,7 +277,7 @@ examUserTableCsvHeader :: ( MonoFoldable mono
=> SheetGradeSummary -> Bool -> mono -> Csv.Header
examUserTableCsvHeader allBoni doBonus pNames = Csv.header $
[ "surname", "first-name", "name"
, "matriculation", "eduPersonPrincipalName"
, "matriculation"
, "study-features"
, "course-note"
, "occurrence"
@ -626,7 +623,6 @@ postEUsersR tid ssh csh examn = do
<*> view (resultUser . _entityVal . _userFirstName . to Just)
<*> view (resultUser . _entityVal . _userDisplayName . to Just)
<*> view (resultUser . _entityVal . _userMatrikelnummer)
<*> view (resultUser . _entityVal . _userLdapPrimaryKey)
<*> view resultStudyFeatures
<*> preview (resultExamOccurrence . _entityVal . _examOccurrenceName)
<*> fmap (bool (const Nothing) Just showPoints) (preview $ resultUser . _entityKey . to (examBonusAchieved ?? bonus) . _achievedPoints . _Wrapped)
@ -950,7 +946,6 @@ postEUsersR tid ssh csh examn = do
guessUser' ExamUserTableCsv{..} = do
let criteria = PredDNF . maybe Set.empty Set.singleton . fromNullable . Set.fromList . fmap PLVariable $ catMaybes
[ GuessUserMatrikelnummer <$> csvEUserMatriculation
, GuessUserEduPersonPrincipalName <$> csvEUserEPPN
, GuessUserDisplayName <$> csvEUserName
, GuessUserSurname <$> csvEUserSurname
, GuessUserFirstName <$> csvEUserFirstName

View File

@ -70,7 +70,7 @@ fakeQualificationUsers (Entity qid Qualification{qualificationRefreshWithin}) (u
let pw = "123.456"
PWHashConf{..} <- getsYesod $ view _appAuthPWHash
pwHash <- liftIO $ PWStore.makePasswordWith pwHashAlgorithm pw pwHashStrength
return $ AuthPWHash $ TEnc.decodeUtf8 pwHash
return $ TEnc.decodeUtf8 pwHash
theSupervisor <- selectKeysList [UserSurname ==. "Jost", UserFirstName ==. "Steffen"] [Asc UserCreated, LimitTo 1]
let addSupervisor = case theSupervisor of
[s] -> \suid k -> case k of
@ -86,15 +86,14 @@ fakeQualificationUsers (Entity qid Qualification{qualificationRefreshWithin}) (u
fakeUser :: ([Text], UserSurname, (Maybe Languages, DateTimeFormat, DateTimeFormat, DateTimeFormat), Bool, Int) -> User
fakeUser (firstNames, userSurname, (userLanguages, userDateTimeFormat, userDateFormat, userTimeFormat), userPrefersPostal, _isSupervised) =
let userIdent = CI.mk $ Text.intercalate "." (take 1 firstNames ++ (Text.take 1 <$> drop 1 firstNames) ++ [userSurname]) <> "@example.com"
userPasswordHash = Just pwSimple
userLastAuthentication = Nothing
userEmail = userIdent
userDisplayEmail = userIdent
userDisplayName = Text.unwords $ firstNames <> [userSurname]
userMatrikelnummer = Just "TESTUSER"
userAuthentication = pwSimple
userLastAuthentication = Nothing
userCreated = now
userLastLdapSynchronisation = Nothing
userLdapPrimaryKey = Nothing
userLastSync = Just now
userTokensIssuedAfter = Nothing
userFirstName = Text.unwords firstNames
userTitle = Nothing

View File

@ -616,6 +616,8 @@ makeProfileData :: Entity User -> DB Widget
makeProfileData usrEnt@(Entity uid usrVal@User{..}) = do
now <- liftIO getCurrentTime
avsId <- entityVal <<$>> getBy (UniqueUserAvsUser uid)
externalUsers <- (\(Entity _ ExternalUser{..}) -> (externalUserUser, externalUserSource, externalUserLastSync)) <<$>> selectList [ ExternalUserUser ==. userIdent ] []
let usrAutomatic :: CU_UserAvs_User -> Widget
usrAutomatic = updateAutomatic . mayUpdate usrVal avsId . mkCheckUpdate
addressLinkdIcon <- messageTooltip <$> messageIconI Info IconLink MsgAddressIsLinkedTip

View File

@ -1,4 +1,4 @@
-- SPDX-FileCopyrightText: 2022 Steffen Jost <jost@tcs.ifi.lmu.de>
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Steffen Jost <jost@tcs.ifi.lmu.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
@ -17,11 +17,9 @@ import Handler.Utils.Csv
import Handler.Utils.Profile
import qualified Data.Text as Text (intercalate)
-- import qualified Data.CaseInsensitive as CI
import qualified Data.Csv as Csv
import Database.Esqueleto.Experimental ((:&)(..))
import qualified Database.Esqueleto.Experimental as E -- needs TypeApplications Lang-Pragma
-- import qualified Database.Esqueleto.Legacy as E
import qualified Database.Esqueleto.PostgreSQL as E
import qualified Database.Esqueleto.Utils as E
@ -95,12 +93,20 @@ compileBlocks dStart dEnd = go (dStart, True)
-- | Deliver all employess with a successful LDAP synch within the last 3 months
getQualificationSAPDirectR :: Handler TypedContent
getQualificationSAPDirectR = do
now <- liftIO getCurrentTime
now <- liftIO getCurrentTime
fdate <- formatTime' "%Y%m%d_%H-%M" now
let ldap_cutoff = addDiffDaysRollOver (fromMonths $ -3) now
userAuthConf <- getsYesod $ view _appUserAuthConf
let
ldapSources = case userAuthConf of
UserAuthConfSingleSource (AuthSourceConfLdap LdapConf{..})
-> singleton $ AuthSourceIdLdap ldapConfSourceId
_other -> mempty
ldapCutoff = addDiffDaysRollOver (fromMonths $ -3) now
qualUsers <- runDBRead $ E.select $ do
(qual :& qualUser :& user :& qualBlock) <-
E.from $ E.table @Qualification
(qual :& qualUser :& user :& qualBlock) <-
E.from $ E.table @Qualification
`E.innerJoin` E.table @QualificationUser
`E.on` (\(qual :& qualUser) -> qual E.^. QualificationId E.==. qualUser E.^. QualificationUserQualification)
`E.innerJoin` E.table @User
@ -111,9 +117,12 @@ getQualificationSAPDirectR = do
E.&&. E.val now E.>~. qualBlock E.?. QualificationUserBlockFrom
)
E.where_ $ E.isJust (qual E.^. QualificationSapId)
E.&&. E.isJust (user E.^. UserCompanyPersonalNumber)
E.&&. E.isJust (user E.^. UserLastLdapSynchronisation)
E.&&. (E.justVal ldap_cutoff E.<=. user E.^. UserLastLdapSynchronisation)
E.&&. E.isJust (user E.^. UserCompanyPersonalNumber)
E.where_ . E.exists $ do
externalUser <- E.from $ E.table @ExternalUser
E.where_ $ externalUser E.^. ExternalUserUser E.==. user E.^. UserIdent
E.&&. externalUser E.^. ExternalUserSource `E.in_` E.valList ldapSources
E.&&. externalUser E.^. ExternalUserLastSync E.>=. E.val ldapCutoff
E.groupBy ( user E.^. UserCompanyPersonalNumber
, qualUser E.^. QualificationUserFirstHeld
, qualUser E.^. QualificationUserValidUntil

View File

@ -0,0 +1,31 @@
-- SPDX-FileCopyrightText: 2024 David Mosbach <david.mosbach@uniworx.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
module Handler.SingleSignOut
( getSOutR
, getSSOutR
) where
import Import
import Auth.OAuth2 (singleSignOut)
import qualified Network.Wai as W
getSOutR :: Handler Html
getSOutR = do
$logDebugS "\27[31mSOut\27[0m" "Redirect to LogoutR"
redirect $ AuthR LogoutR
getSSOutR :: Handler Html
getSSOutR = do
app <- getYesod
let redir = intercalate "/" . fst . renderRoute $ SOutR
root = case approot of
ApprootRequest f -> f app W.defaultRequest
_ -> error "approt implementation changed"
url = decodeUtf8 . urlEncode True . encodeUtf8 $ root <> "/" <> redir
AppSettings{..} <- getsYesod appSettings'
$logDebugS "\27[31mSSOut\27[0m" "Redirect to auth server"
if appSingleSignOn then singleSignOut (Just url) else redirect (AuthR LogoutR)

View File

@ -1,4 +1,4 @@
-- SPDX-FileCopyrightText: 2022-2023 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Sarah Vaupel <vaupel.sarah@campus.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Sarah Vaupel <vaupel.sarah@campus.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
@ -12,7 +12,6 @@ module Handler.Users
import Import
import Jobs
-- import Data.Text
import Handler.Utils
import Handler.Utils.Users
import Handler.Utils.Invitations
@ -42,9 +41,7 @@ import qualified Data.ByteString.Base64 as Base64
import Data.Aeson hiding (Result(..))
-- import Handler.Users.Add as Handler.Users
-- import qualified Data.Conduit.List as C
import qualified Data.Conduit.List as C
import qualified Data.HashSet as HashSet
@ -130,10 +127,10 @@ postUsersR = do
icnReroute = text2widget " " <> toWgt (icon IconReroute)
pure $ mconcat supervisors
, sortable (Just "last-login") (i18nCell MsgLastLogin) $ \DBRow{ dbrOutput = Entity _ User{..} } -> maybe mempty dateTimeCell userLastAuthentication
, sortable (Just "auth-ldap") (i18nCell MsgAuthMode) $ \DBRow{ dbrOutput = Entity _ User{..} } -> i18nCell userAuthentication
-- , sortable (Just "auth-ldap") (i18nCell MsgAuthMode) $ \DBRow{ dbrOutput = Entity _ User{..} } -> i18nCell userAuthentication -- TODO: reintroduce via ExternalUser
-- , sortable (Just "ldap-sync") (i18nCell MsgLdapSynced) $ \DBRow{ dbrOutput = Entity _ User{..} } -> maybe mempty dateTimeCell userLastLdapSynchronisation -- TODO: reintroduce via ExternalUser
, colUserEmail
, colUserLetterEmailPin
, sortable (Just "ldap-sync") (i18nCell MsgLdapSynced) $ \DBRow{ dbrOutput = Entity _ User{..} } -> maybe mempty dateTimeCell userLastLdapSynchronisation
, flip foldMap universeF $ \function ->
sortable (Just $ SortingKey $ CI.mk $ toPathPiece function) (i18nCell function) $ \DBRow{ dbrOutput = Entity uid _ } -> flip (set' cellContents) mempty $ do
schools <- liftHandler . runDBRead . E.select . E.from $ \(school `E.InnerJoin` userFunction) -> do
@ -240,15 +237,15 @@ postUsersR = do
, ( "company-department"
, SortColumn $ \user -> user E.^. UserCompanyDepartment
)
, ( "auth-ldap"
, SortColumn $ \user -> user E.^. UserAuthentication E.!=. E.val AuthLDAP
)
-- , ( "auth-ldap"
-- , SortColumn $ \user -> user E.^. UserAuthentication E.!=. E.val AuthLDAP
-- ) -- TODO: reintroduce via ExternalUser
, ( "last-login"
, SortColumn $ \user -> user E.^. UserLastAuthentication
)
, ( "ldap-sync"
, SortColumn $ \user -> user E.^. UserLastLdapSynchronisation
)
-- , ( "ldap-sync"
-- , SortColumn $ \user -> user E.^. UserLastLdapSynchronisation
-- ) -- TODO: reintroduce via ExternalUser
, ( "user-company"
, SortColumn $ \user -> E.subSelect $ E.from $ \(usrComp `E.InnerJoin` comp) -> do
E.on $ usrComp E.^. UserCompanyCompany E.==. comp E.^. CompanyId
@ -291,24 +288,24 @@ postUsersR = do
| Set.null criteria -> E.true -- TODO: why can this be eFalse and work still?
| otherwise -> E.any (\c -> user E.^. UserCompanyDepartment `E.hasInfix` E.val c) criteria
)
, ( "auth-ldap", FilterColumn $ \user (criterion :: Last Bool) -> if
| Just crit <- getLast criterion
-> (user E.^. UserAuthentication E.==. E.val AuthLDAP) E.==. E.val crit
| otherwise
-> E.true
)
-- , ( "auth-ldap", FilterColumn $ \user (criterion :: Last Bool) -> if
-- | Just crit <- getLast criterion
-- -> (user E.^. UserAuthentication E.==. E.val AuthLDAP) E.==. E.val crit
-- | otherwise
-- -> E.true
-- ) -- TODO: reintroduce via ExternalUser
, ( "school", FilterColumn $ \user criterion -> if
| Set.null criterion -> E.val True :: E.SqlExpr (E.Value Bool)
| otherwise -> let schools = E.valList (Set.toList criterion) in
E.exists . E.from $ \ufunc -> E.where_ $ ufunc E.^. UserFunctionUser E.==. user E.^. UserId
E.&&. ufunc E.^. UserFunctionFunction `E.in_` schools
)
, ( "ldap-sync", FilterColumn $ \user criteria -> if
| Just criteria' <- fromNullable criteria
-> let minTime = minimum (criteria' :: NonNull (Set UTCTime))
in E.maybe E.true (E.<=. E.val minTime) $ user E.^. UserLastLdapSynchronisation
| otherwise -> E.val True :: E.SqlExpr (E.Value Bool)
)
-- , ( "ldap-sync", FilterColumn $ \user criteria -> if
-- | Just criteria' <- fromNullable criteria
-- -> let minTime = minimum (criteria' :: NonNull (Set UTCTime))
-- in E.maybe E.true (E.<=. E.val minTime) $ user E.^. UserLastLdapSynchronisation
-- | otherwise -> E.val True :: E.SqlExpr (E.Value Bool)
-- ) -- TODO: reintroduce via ExternalUser
, ( "avs-sync", FilterColumn . E.mkExistsFilter $ \user criterion ->
E.from $ \usrAvs -> do
let minTime = (E.val criterion :: E.SqlExpr (E.Value UTCTime))
@ -357,8 +354,8 @@ postUsersR = do
, prismAForm (singletonFilter "user-supervisor") mPrev $ aopt textField (fslI MsgTableSupervisor)
, prismAForm (singletonFilter "school") mPrev $ aopt (lift `hoistField` selectFieldList schoolOptions) (fslI MsgCourseSchool)
, prismAForm (singletonFilter "is-supervisor" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgUserIsSupervisor)
, prismAForm (singletonFilter "auth-ldap" . maybePrism _PathPiece) mPrev $ aopt (lift `hoistField` selectFieldList [(MsgAuthPWHash "", False), (MsgAuthLDAP, True)]) (fslI MsgAuthMode)
, prismAForm (singletonFilter "ldap-sync" . maybePrism _PathPiece) mPrev $ aopt utcTimeField (fslI MsgLdapSyncedBefore)
-- , prismAForm (singletonFilter "auth-ldap" . maybePrism _PathPiece) mPrev $ aopt (lift `hoistField` selectFieldList [(MsgAuthPWHash "", False), (MsgAuthLDAP, True)]) (fslI MsgAuthMode) -- TODO: reintroduce via ExternalUser
-- , prismAForm (singletonFilter "ldap-sync" . maybePrism _PathPiece) mPrev $ aopt utcTimeField (fslI MsgLdapSyncedBefore) -- TODO: reintroduce via ExternalUser
, prismAForm (singletonFilter "avs-sync" . maybePrism _PathPiece) mPrev $ aopt utcTimeField (fslI MsgLastAvsSyncedBefore)
]
, dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout }
@ -388,7 +385,7 @@ postUsersR = do
addMessageI Info MsgActionNoUsersSelected
(UserLdapSyncData, userSet) -> do
forM_ userSet $ \uid -> void . queueJob $ JobSynchroniseLdapUser uid
addMessageI Success . MsgSynchroniseLdapUserQueued $ Set.size userSet
addMessageI Success . MsgSynchroniseUserdbUserQueued $ Set.size userSet
redirectKeepGetParams UsersR
(UserAvsSyncData, userSet) -> do
n <- runDB $ queueAvsUpdateByUID userSet Nothing
@ -427,9 +424,9 @@ postUsersR = do
formResult allUsersRes $ \case
AllUsersLdapSync -> do
-- runDBJobs . runConduit $ selectSource [] [] .| C.mapM_ (queueDBJob . JobSynchroniseLdapUser . entityKey) -- to slow to execute directly
queueJob' JobSynchroniseLdapAll
addMessageI Success MsgSynchroniseLdapAllUsersQueued
-- runDBJobs . runConduit $ selectSource [] [] .| C.mapM_ (queueDBJob . JobSynchroniseUser . entityKey) -- to slow to execute directly
queueJob' JobSynchroniseUserdbAll
addMessageI Success MsgSynchroniseUserdbAllUsersQueued
redirect UsersR
AllUsersAvsSync -> do
now <- liftIO getCurrentTime
@ -583,7 +580,7 @@ postAdminUserR uuid = do
return (result, $(widgetFile "widgets/user-rights-form/user-rights-form"))
userAuthenticationForm :: Form ButtonAuthMode
userAuthenticationForm = buttonForm' $ if
| userAuthentication == AuthLDAP -> [BtnAuthPWHash]
| is _Nothing userPasswordHash -> [BtnAuthPWHash]
| otherwise -> [BtnAuthLDAP, BtnPasswordReset]
systemFunctionsForm' = funcForm systemFuncForm (fslI MsgUserSystemFunctions) False
where systemFuncForm func = apopt checkBoxField (fslI func) . Just $ systemFunctions func
@ -609,33 +606,41 @@ postAdminUserR uuid = do
redirect $ AdminUserR uuid
userAuthenticationAction = \case
BtnAuthLDAP -> do
let
campusHandler :: MonadPlus m => Auth.CampusUserException -> m a
campusHandler _ = mzero
campusResult <- runMaybeT . handle campusHandler $ do
Just pool <- getsYesod $ view _appLdapPool
void . lift . Auth.campusUser pool FailoverUnlimited $ Creds Auth.apLdap (CI.original userIdent) []
case campusResult of
Nothing -> addMessageI Error MsgAuthLDAPInvalidLookup
_other
| is _AuthLDAP userAuthentication
-> addMessageI Info MsgAuthLDAPAlreadyConfigured
Just () -> do
runDBJobs $ do
update uid [ UserAuthentication =. AuthLDAP ]
queueDBJob . JobQueueNotification $ NotificationUserAuthModeUpdate uid
BtnAuthLDAP -> do -- TODO: Reformulate messages and constructors to "remove pw hash" or "external login only"
-- let
-- ldapHandler :: MonadPlus m => Auth.LdapUserException -> m a
-- ldapHandler _ = mzero
-- ldapResult <- runMaybeT . handle ldapHandler $ do
-- Just pool <- getsYesod $ view _appLdapPool
-- void . lift . Auth.ldapUser pool $ Creds Auth.apLdap (CI.original userIdent) []
-- case ldapResult of
-- Nothing -> addMessageI Error MsgAuthLDAPInvalidLookup
-- _other
-- | is _AuthLDAP userAuthentication
-- -> addMessageI Info MsgAuthLDAPAlreadyConfigured
-- Just () -> do
-- runDBJobs $ do
-- update uid [ UserAuthentication =. AuthLDAP ]
-- queueDBJob . JobQueueNotification $ NotificationUserAuthModeUpdate uid
-- addMessageI Success MsgAuthLDAPConfigured
-- TODO: check current auth sources and warn if user cannot login using any source
case userPasswordHash of
Nothing -> addMessageI Error MsgAuthLDAPAlreadyConfigured
Just _ -> do
runDBJobs $ do
update uid [ UserPasswordHash =. Nothing ]
queueDBJob . JobQueueNotification $ NotificationUserAuthModeUpdate uid
addMessageI Success MsgAuthLDAPConfigured
redirect $ AdminUserR uuid
BtnAuthPWHash -> do
if
| is _AuthPWHash userAuthentication
| is _Just userPasswordHash
-> addMessageI Info MsgAuthPWHashAlreadyConfigured
| otherwise
-> do
runDBJobs $ do
update uid [ UserAuthentication =. AuthPWHash "" ]
update uid [ UserPasswordHash =. Just "" ]
queueDBJob . JobQueueNotification $ NotificationUserAuthModeUpdate uid
queueDBJob $ JobSendPasswordReset uid
@ -795,18 +800,18 @@ postUserPasswordR cID = do
isAdmin <- hasWriteAccessTo $ AdminUserR cID
requireCurrent <- maybeT (return True) $ asum
[ False <$ guard (isn't _AuthPWHash userAuthentication)
[ False <$ guard (isn't _Just userPasswordHash)
, False <$ guard isAdmin
, do
authMode <- Base64.decodeLenient . encodeUtf8 <$> MaybeT maybeCurrentBearerRestrictions
unless (authMode `constEq` computeUserAuthenticationDigest userAuthentication) . lift $
unless (authMode `constEq` computeUserAuthenticationDigest userPasswordHash) . lift $
invalidArgsI [MsgUnauthorizedPasswordResetToken]
return False
]
((passResult, passFormWidget), passEnctype) <- runFormPost . formEmbedBearerPost . renderAForm FormStandard . wFormToAForm $ do
currentResult <- if
| AuthPWHash (encodeUtf8 -> pwHash) <- userAuthentication
| Just (encodeUtf8 -> pwHash) <- userPasswordHash
, requireCurrent
-> wreq
(checkMap (bool (Left MsgCurrentPasswordInvalid) (Right ()) . flip (PWStore.verifyPasswordWith pwHashAlgorithm (2^)) pwHash . encodeUtf8) (const "") passwordField)
@ -823,7 +828,7 @@ postUserPasswordR cID = do
formResultModal passResult (bool ProfileR (UserPasswordR cID) isAdmin) $ \newPass -> do
newHash <- fmap decodeUtf8 . liftIO $ PWStore.makePasswordWith pwHashAlgorithm newPass pwHashStrength
liftHandler . runDB $ update tUid [ UserAuthentication =. AuthPWHash newHash ]
liftHandler . runDB $ update tUid [ UserPasswordHash =. Just newHash ]
tell . pure =<< messageI Success MsgPasswordChangedSuccess
siteLayout [whamlet|_{MsgUserPasswordHeadingFor} ^{userEmailWidget usr}|] $

View File

@ -1,4 +1,4 @@
-- SPDX-FileCopyrightText: 2022-2023 Gregor Kleen <gregor@kleen.consulting>, Sarah Vaupel <sarah.vaupel@ifi.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>, Winnie Ros <winnie.ros@campus.lmu.de>
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor@kleen.consulting>, Sarah Vaupel <sarah.vaupel@ifi.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>, Winnie Ros <winnie.ros@campus.lmu.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
@ -35,20 +35,18 @@ adminUserForm template = renderAForm FormStandard
<*> aopt (textField & cfStrip) (fslI MsgAdminUserPinPassword) (audPinPassword <$> template)
<*> areq (emailField & cfCI) (fslI MsgAdminUserEmail) (audEmail <$> template)
<*> areq (textField & cfStrip & cfCI) (fslI MsgAdminUserIdent) (audIdent <$> template)
<*> areq (selectField optionsFinite) (fslI MsgAdminUserAuth & setTooltip MsgAdminUserAuthTooltip) (audAuth <$> template <|> Just AuthKindLDAP)
<*> aopt passwordField (fslI MsgAdminUserPassword) (audPassword <$> template)
-- | Like `addNewUser`, but starts background jobs and tries to notify users, if applicable (i.e. /= AuthNoLogin )
-- | Like `addNewUser`, but starts background jobs and tries to notify users
addNewUserNotify :: AddUserData -> Handler (Maybe UserId)
addNewUserNotify aud = do
mbUid <- addNewUser aud
case mbUid of
Nothing -> return Nothing
Just uid -> runDBJobs $ do
queueDBJob $ JobSynchroniseLdapUser uid
let authKind = audAuth aud
when (authKind /= AuthKindNoLogin) $
queueDBJob $ JobSynchroniseUser uid
when (is _Just $ audPassword aud) $ do
queueDBJob . JobQueueNotification $ NotificationUserAuthModeUpdate uid
when (authKind == AuthKindPWHash) $
queueDBJob $ JobSendPasswordReset uid
return $ Just uid

View File

@ -1,4 +1,4 @@
-- SPDX-FileCopyrightText: 2022-2025 Steffen Jost <s.jost@fraport.de>
-- SPDX-FileCopyrightText: 2022-2025 Sarah Vaupel <sarah.vaupel@uniworx.de>, Steffen Jost <s.jost@fraport.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
@ -46,8 +46,7 @@ import qualified Data.Text as Text
import qualified Control.Monad.Catch as Catch
-- import Auth.LDAP (ldapUserPrincipalName)
import Foundation.Yesod.Auth (ldapLookupAndUpsert) -- , CampusUserConversionException())
import Foundation.Yesod.Auth (userLookupAndUpsert)
import Jobs.Queue
@ -1030,3 +1029,249 @@ getDifferingLicences (AvsResponseGetLicences licences) = do
set to 1: vorfeld-set && nicht in rollfeld-set || rollfeld-unset && nicht in vorfeld-unset
set to 2: rollfeld-set && nicht in vorfeld-unset && (in vorfeld-set || AVS_Licence>0 == vorORrollfeld)
-}
-- | Find or upsert User by AvsCardId (with dot), Fraport PersonalNumber, Fraport Email-Address or by prefixed AvsId or prefixed AvsNo; fail-safe, may or may not update existing users, may insert new users
-- If an existing User with internal number is found, an AVS query is executed
guessAvsUser :: Text -> Handler (Maybe UserId)
guessAvsUser (Text.splitAt 6 -> ("AVSID:", avsidTxt)) = ifMaybeM (readMay avsidTxt) Nothing $ \avsidNr ->
let avsid = AvsPersonId avsidNr
maybeAvsUpsert = maybeCatchAll $ upsertAvsUserById avsid
extractUid (Entity _ UserAvs{userAvsUser=uid}) = return $ Just uid
in maybeM maybeAvsUpsert extractUid $ runDB $ getBy $ UniqueUserAvsId avsid
guessAvsUser (Text.splitAt 6 -> ("AVSNO:", avsnoTxt)) = ifMaybeM (readMay avsnoTxt) Nothing $ \avsno ->
runDB (selectList [UserAvsNoPerson ==. avsno] []) <&> \case
[Entity _ UserAvs{userAvsUser=uid}] -> Just uid
_ -> Nothing
guessAvsUser someid = do
let maybeUpsertAvsUserByCard = maybeCatchAll . upsertAvsUserByCard
case discernAvsCardPersonalNo someid of
Just cid@(Left _cardNo) -> maybeUpsertAvsUserByCard cid
-- NOTE: card validity might be outdated, so we must always check with avs
-- maybeM (maybeUpsertAvsUserByCard cid) extractUid $ runDB $ do
-- let extractUid (Entity _ UserAvs{userAvsUser=uid}) = return $ Just uid
-- extractUidCard UserAvsCard{userAvsCardPersonId=avid} = getBy $ UniqueUserAvsId avid
-- cards <- selectList [UserAvsCardCardNo ==. cardNo] []
-- case [c | cent <- cards, let c = entityVal cent, avsDataValid (userAvsCardCard c)] of
-- [justOneCard] -> maybeM (return Nothing) extractUidCard (return $ Just justOneCard)
-- _ -> return Nothing
Just cid@(Right _wholeNumber) ->
maybeUpsertAvsUserByCard cid >>= \case
Nothing ->
runDB (selectList [UserCompanyPersonalNumber ==. Just someid] []) >>= \case
[Entity uid _] -> return $ Just uid
_ -> return Nothing
uid -> return uid
Nothing -> try (runDB $ userLookupAndUpsert someid UpsertUserGuessUser) >>= \case
Right (Just Entity{entityKey=uid, entityVal=User{userCompanyPersonalNumber=Just persNo}}) ->
maybeM (return $ Just uid) (return . Just) (maybeUpsertAvsUserByCard (Right $ mkAvsInternalPersonalNo persNo))
Right (Just Entity{entityKey=uid}) -> return $ Just uid
other -> do -- attempt to recover by trying other ids
whenIsLeft other (\(err::SomeException) -> $logInfoS "AVS" $ "upsertAvsUser external error " <> tshow err) -- this line primarily forces exception type to catch-all
runDB . runMaybeT $
let someIdent = stripCI someid
in MaybeT (getKeyBy $ UniqueEmail someIdent)
<|> MaybeT (getKeyBy $ UniqueAuthentication someIdent)
-- | Always update AVS Data, accepts AvsCardId (with dot), Fraport PersonalNumber or Fraport Email-Address
upsertAvsUser :: Text -> Handler (Maybe UserId) -- TODO: change to Entity
upsertAvsUser (discernAvsCardPersonalNo -> Just someid) = maybeCatchAll $ upsertAvsUserByCard someid -- Note: Right case is any number; it could be AvsCardNumber or AvsInternalPersonalNumber; we cannot know, but the latter is much more likely and useful to users!
upsertAvsUser otherId = -- attempt LDAP lookup to find by eMail
try (runDB $ userLookupAndUpsert otherId UpsertUserGuessUser) >>= \case
Right (Just Entity{entityVal=User{userCompanyPersonalNumber=Just persNo}}) -> maybeCatchAll $ upsertAvsUserByCard (Right $ mkAvsInternalPersonalNo persNo)
other -> do -- attempt to recover by trying other ids
whenIsLeft other (\(err::SomeException) -> $logInfoS "AVS" $ "upsertAvsUser LDAP error " <> tshow err) -- this line primarily forces exception type to catch-all
apid <- runDB . runMaybeT $ do
let someIdent = stripCI otherId
uid <- MaybeT (getKeyBy $ UniqueEmail someIdent)
<|> MaybeT (getKeyBy $ UniqueAuthentication someIdent)
MaybeT $ view (_entityVal . _userAvsPersonId) <<$>> getBy (UniqueUserAvsUser uid)
ifMaybeM apid Nothing upsertAvsUserById
-- | Given CardNo or internal Number, retrieve UserId. Create non-existing users, if possible. Always update.
-- Throws errors if the avsInterface in unavailable or the user is non-unique within external AVS DB.
upsertAvsUserByCard :: Either AvsFullCardNo AvsInternalPersonalNo -> Handler (Maybe UserId) -- Idee: Eingabe ohne Punkt is AvsInternalPersonalNo mit Punkt is Ausweisnummer?!
upsertAvsUserByCard persNo = do
let qry = case persNo of
Left AvsFullCardNo{..} -> def{ avsPersonQueryCardNo = Just avsFullCardNo, avsPersonQueryVersionNo = Just avsFullCardVersion }
Right fpn -> def{ avsPersonQueryInternalPersonalNo = Just fpn }
AvsQuery{..} <- maybeThrowM AvsInterfaceUnavailable $ getsYesod $ view _appAvsQuery
AvsResponsePerson adps <- throwLeftM $ avsQueryPerson qry
case Set.elems adps of
[] -> throwM AvsPersonSearchEmpty
(_:_:_) -> throwM AvsPersonSearchAmbiguous
[AvsDataPerson{avsPersonPersonID=api}] -> upsertAvsUserById api -- always trigger an update
-- do
-- mbuid <- runDB $ getBy $ UniqueUserAvsId api
-- case mbuid of
-- (Just (Entity _ UserAvs{userAvsUser=uau})) -> return $ Just uau
-- Nothing -> upsertAvsUserById api
-- | Retrieve and _always_ update user by AvsPersonId. Non-existing users are created. Ignore AVS Licence status! Updates Company, Address, PinPassword
-- Throws errors if the avsInterface in unavailable or the user is non-unique within external AVS DB (should never happen).
upsertAvsUserById :: AvsPersonId -> Handler (Maybe UserId)
upsertAvsUserById api = do
mbapd <- lookupAvsUser api
now <- liftIO getCurrentTime
mbuid <- runDB $ do
mbuid <- getBy (UniqueUserAvsId api)
case (mbuid, mbapd) of
(Nothing, Just AvsDataPerson{..}) -- FRADriver User does not exist yet, but found in AVS and has Internal Personal Number
| Just (avsInternalPersonalNo -> persNo) <- canonical avsPersonInternalPersonalNo -> do
$logInfoS "AVS" $ "Creating new user with avsInternalPersonalNo " <> tshow persNo
candidates <- selectKeysList [UserCompanyPersonalNumber ==. Just persNo] []
case candidates of
[uid] -> $logInfoS "AVS" "Matching user found, linking." >> insertUniqueEntity (UserAvs api uid avsPersonPersonNo now Nothing)
(_:_) -> throwM $ AvsUserAmbiguous api
[] -> do
upsRes :: Either SomeException (Maybe (Entity User))
<- try $ userLookupAndUpsert persNo UpsertUserGuessUser -- TODO: do azure lookup and upsert if appropriate
$logInfoS "AVS" $ "No matching existing user found. Attempted LDAP upsert returned: " <> tshow upsRes
case upsRes of
Right (Just Entity{entityKey=uid}) -> insertUniqueEntity $ UserAvs api uid avsPersonPersonNo now Nothing -- pin/addr are updated in next step anyway
Right Nothing -> do
$logWarnS "AVS" $ "AVS user with avsInternalPersonalNo " <> tshow persNo <> " not found in external databases"
return mbuid -- == Nothing -- user could not be created somehow
Left err -> do
$logWarnS "AVS" $ "AVS user with avsInternalPersonalNo " <> tshow persNo <> " not found in external databases: " <> tshow err
return mbuid -- == Nothing -- user could not be created somehow
(Just Entity{ entityKey = uaid }, _) -> do
update uaid [ UserAvsLastSynch =. now, UserAvsLastSynchError =. Nothing ] -- mark as updated early, to prevent failed users to clog the synch
return mbuid
_other -> return mbuid
$logInfoS "AVS" $ "upsert prestep result: " <> tshow mbuid <> " --- " <> tshow mbapd
case (mbuid, mbapd) of
( _ , Nothing ) -> throwM $ AvsUserUnknownByAvs api -- User not found in AVS at all, i.e. no valid card exists yet
(Nothing, Just AvsDataPerson{avsPersonFirstName= Text.strip -> avsFirstName, avsPersonLastName= Text.strip -> avsSurname, ..}) -> do -- No LDAP User, but found in AVS; create new user
let (mbCompany, mbCoFirmAddr, _) = guessLicenceAddress avsPersonPersonCards
userFirmAddr= plaintextToStoredMarkup <$> mbCoFirmAddr
pinCard = Set.lookupMax avsPersonPersonCards
userPin = personCard2pin <$> pinCard
fakeIdent = CI.mk $ "AVSID:" <> tshow api
fakeNo = CI.mk $ "AVSNO:" <> tshow avsPersonPersonNo
newUsr = AddUserData
{ audTitle = Nothing
, audFirstName = avsFirstName
, audSurname = avsSurname
, audDisplayName = avsFirstName <> Text.cons ' ' avsSurname
, audDisplayEmail = "" -- Email is unknown in this version of the avs query, to be updated later (FUTURE TODO)
, audMatriculation = Just $ tshow avsPersonPersonNo
, audSex = Nothing
, audBirthday = Nothing
, audMobile = Nothing
, audTelephone = Nothing
, audFPersonalNumber = avsInternalPersonalNo <$> canonical avsPersonInternalPersonalNo
, audFDepartment = Nothing
, audPostAddress = userFirmAddr
, audPrefersPostal = True
, audPinPassword = userPin
, audEmail = fakeNo -- Email is unknown in this version of the avs query, to be updated later (FUTURE TODO)
, audIdent = fakeIdent -- use AvsPersonId instead
, audPassword = Nothing
--, audAuth = maybe AuthKindNoLogin (const AuthKindAzure) avsPersonInternalPersonalNo -- FUTURE TODO: if email is known, use AuthKinfPWHash for email invite, if no internal personnel number is known
}
mbUid <- addNewUser newUsr -- triggers JobSynchroniseUserdbUser, JobSendPasswordReset and NotificationUserAutoModeUpdate -- TODO: check if these are failsafe
whenIsJust mbUid $ \uid -> runDB $ do
insert_ $ UserAvs avsPersonPersonID uid avsPersonPersonNo now Nothing
forM_ avsPersonPersonCards $ -- save all cards for later comparisons whether an update occurred
-- let cs :: Set AvsDataPersonCard = Set.fromList $ catMaybes [pinCard, addrCard]
-- forM_ cs $ -- only save used cards for the postal address update detection
\avsCard -> insert_ $ UserAvsCard avsPersonPersonID (getFullCardNo avsCard) avsCard now
upsertUserCompany uid mbCompany userFirmAddr
return mbUid
(Just (Entity _ UserAvs{userAvsUser=uid})
, Just AvsDataPerson{avsPersonPersonCards, avsPersonInternalPersonalNo, avsPersonPersonNo, avsPersonFirstName= Text.strip -> avsFirstName, avsPersonLastName= Text.strip -> avsSurname}) -> do -- known user, update address and pinPassword
let (mbCompany, mbCoFirmAddr, _) = guessLicenceAddress avsPersonPersonCards
userFirmAddr = plaintextToStoredMarkup <$> mbCoFirmAddr
pinCard = Set.lookupMax avsPersonPersonCards
userPin = personCard2pin <$> pinCard
runDB $ do
update uid [ UserFirstName =. avsFirstName -- update in case of name changes via AVS; might be changed again through LDAP
, UserSurname =. avsSurname
, UserDisplayName =. avsFirstName <> Text.cons ' ' avsSurname
, UserMatrikelnummer =. Just (tshow avsPersonPersonNo) -- TODO: Deactivate this update after Q2/2023; this is only needed since UserMatrikelnummer was used for AVSNO later
, UserCompanyPersonalNumber =. avsInternalPersonalNo <$> canonical avsPersonInternalPersonalNo
]
oldCards <- selectList [UserAvsCardPersonId ==. api] []
let oldAddrs = Set.fromList $ mapMaybe (snd3 . getCompanyAddress . userAvsCardCard . entityVal) oldCards
unless (maybe True (`Set.member` oldAddrs) mbCoFirmAddr) $ do -- update postal address, unless the exact address had been seen before
encRecipient :: CryptoUUIDUser <- encrypt uid
$logInfoS "AVS" $ "Postal address updated for" <> tshow encRecipient
updateWhere [UserId ==. uid] [UserPostAddress =. userFirmAddr, UserPostLastUpdate =. Just now]
whenIsJust pinCard $ \pCard -> -- update pin, but only if it was unset or set to the value of an old card
unlessM (exists [UserAvsCardCardNo ==. getFullCardNo pCard]) $ do
let oldPins = Just . personCard2pin . userAvsCardCard . entityVal <$> oldCards
updateWhere [UserId ==. uid, UserPinPassword !=. userPin, UserPinPassword <-. oldPins] -- check for old pin ensures that unset/manually set passwords remain unchanged
[UserPinPassword =. userPin]
insert_ $ UserAvsCard api (getFullCardNo pCard) pCard now
upsertUserCompany uid mbCompany userFirmAddr
forM_ avsPersonPersonCards $ \aCard -> do
let fcn = getFullCardNo aCard
-- probably not efficient, but fixes the problem that AvsCardNo is not unique as assumed before and may get reused
deleteWhere [UserAvsCardCardNo ==. fcn]
insert_ $ UserAvsCard
{ userAvsCardPersonId = api
, userAvsCardCardNo = fcn
, userAvsCardCard = aCard
, userAvsCardLastSynch = now
}
return $ Just uid
lookupAvsUser :: ( MonadThrow m, MonadHandler m, HandlerSite m ~ UniWorX ) =>
AvsPersonId -> m (Maybe AvsDataPerson)
lookupAvsUser api = Map.lookup api <$> lookupAvsUsers (Set.singleton api)
-- | retrieves complete avs user records for given AvsPersonIds.
-- Note that this requires several AVS-API queries, since
-- - avsQueryPerson does not support querying an AvsPersonId directly
-- - avsQueryStatus only provides limited information
-- avsQuery is used to obtain all card numbers, which are then queried separately an merged
-- May throw Servant.ClientError or AvsExceptions
-- Does not write to our own DB!
lookupAvsUsers :: ( MonadThrow m, MonadHandler m, HandlerSite m ~ UniWorX ) =>
Set AvsPersonId -> m (Map AvsPersonId AvsDataPerson)
lookupAvsUsers apis = do
AvsQuery{..} <- maybeThrowM AvsInterfaceUnavailable $ getsYesod $ view _appAvsQuery
AvsResponseStatus statuses <- throwLeftM . avsQueryStatus $ AvsQueryStatus apis
let forFoldlM = $(permuteFun [3,2,1]) foldlM
forFoldlM statuses mempty $ \acc1 AvsStatusPerson{avsStatusPersonCardStatus=cards} ->
forFoldlM cards acc1 $ \acc2 AvsDataPersonCard{avsDataCardNo, avsDataVersionNo} -> do
AvsResponsePerson adps <- throwLeftM . avsQueryPerson $ def{avsPersonQueryCardNo = Just avsDataCardNo, avsPersonQueryVersionNo = Just avsDataVersionNo}
return $ mergeByPersonId adps acc2
-- | Like `Handler.Utils.getReceivers`, but calls upsertAvsUserById on each user to ensure that postal address is up-to-date
updateReceivers :: UserId -> Handler (Entity User, [Entity User], Bool)
updateReceivers uid = do
-- First perform AVS update for receiver
runDB (getBy (UniqueUserAvsUser uid)) >>= \case
Just Entity{entityVal=UserAvs{userAvsPersonId = apid}} -> void . maybeCatchAll $ upsertAvsUserById apid
Nothing -> return ()
-- Retrieve updated user and supervisors now
(underling :: Entity User, avsSupers :: [(E.Value UserId, E.Value (Maybe AvsPersonId))]) <- runDB $ (,)
<$> getJustEntity uid
<*> (E.select $ do
(usrSuper :& usrAvs) <-
E.from $ E.table @UserSupervisor
`E.leftJoin` E.table @UserAvs
`E.on` (\(usrSuper :& userAvs) -> usrSuper E.^. UserSupervisorSupervisor E.=?. userAvs E.?. UserAvsUser)
E.where_ $ (usrSuper E.^. UserSupervisorUser E.==. E.val uid)
E.&&. (usrSuper E.^. UserSupervisorRerouteNotifications)
pure (usrSuper E.^. UserSupervisorSupervisor, usrAvs E.?. UserAvsPersonId)
)
let (superVs, avsIds) = unzip avsSupers
receiverIDs :: [UserId] = E.unValue <$> superVs
toUpdate = Set.fromList $ mapMaybe E.unValue avsIds
directResult = return (underling, pure underling, True) -- already contains updated address
forM_ toUpdate (void . maybeCatchAll . upsertAvsUserById) -- attempt to update postaddress from AVS
if null receiverIDs
then directResult
else do
receivers <- runDB $ selectList [UserId <-. receiverIDs] [] -- due to possible address updates, we must runDB once more and cannot join above
if null receivers
then directResult
else return (underling, receivers, uid `elem` (entityKey <$> receivers))

View File

@ -112,7 +112,7 @@ csvFilenameLmsReport = makeLmsFilename "report"
makeLmsFilename :: MonadHandler m => Text -> QualificationShorthand -> m Text
makeLmsFilename ftag (citext2lower -> qsh) = do
ymth <- getYMTH
return $ "fradrive_" <> qsh <> "_" <> ftag <> "_" <> ymth <> ".csv"
return $ "fradrive_" <> "test" <> "_" <> qsh <> "_" <> ftag <> "_" <> ymth <> ".csv"
-- | Return current datetime in YYYYMMDDHH format
getYMTH :: MonadHandler m => m Text
@ -188,8 +188,8 @@ maxLmsUserIdentRetries = 27
-- eopt = Elo.genOptions -- { genCapitals = False, genSpecials = False, genDigitis = True }
randomLMSIdent :: MonadIO m => Maybe Char -> m LmsIdent
randomLMSIdent Nothing = LmsIdent . Text.cons 'j' <$> randomText [] (pred lengthIdent) -- idents must not contain '_' nor '-'
randomLMSIdent (Just c) = LmsIdent . Text.cons c <$> randomText [] (pred lengthIdent)
randomLMSIdent Nothing = LmsIdent . Text.cons 't' . Text.cons 'j' <$> randomText [] (pred $ pred lengthIdent) -- idents must not contain '_' nor '-'
randomLMSIdent (Just c) = LmsIdent . Text.cons 't' . Text.cons c <$> randomText [] (pred $ pred lengthIdent)
randomLMSIdentBut :: MonadIO m => Maybe Char -> Set LmsIdent -> m (Maybe LmsIdent)
randomLMSIdentBut prefix banList = untilJustMaxM maxLmsUserIdentRetries getIdentOk

View File

@ -1,4 +1,4 @@
-- SPDX-FileCopyrightText: 2022-26 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Sarah Vaupel <vaupel.sarah@campus.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>
-- SPDX-FileCopyrightText: 2022-2025 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Sarah Vaupel <vaupel.sarah@campus.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
@ -29,8 +29,7 @@ module Handler.Utils.Users
) where
import Import
import Auth.LDAP (campusUserMatr')
import Foundation.Yesod.Auth (upsertCampusUser)
import Foundation.Yesod.Auth (userLookupAndUpsert)
import Crypto.Hash (hashlazy)
@ -42,7 +41,6 @@ import qualified Data.Aeson as JSON
import qualified Data.Aeson.Types as JSON
import qualified Data.Set as Set
-- import qualified Data.List as List
import qualified Data.CaseInsensitive as CI
import Database.Esqueleto.Experimental ((:&)(..))
@ -227,7 +225,7 @@ getSupervisees forceLogin = do
return $ Set.insert uid $ Set.fromAscList svs
computeUserAuthenticationDigest :: AuthenticationMode -> Digest SHA3_256
computeUserAuthenticationDigest :: Maybe Text -> Digest SHA3_256
computeUserAuthenticationDigest = hashlazy . JSON.encode
-- guessUserByCompanyPersonalNumber :: Text -> Text -> DB (Maybe UserId)
@ -245,8 +243,6 @@ guessUserByEmail eml = firstJustM $
data GuessUserInfo
= GuessUserMatrikelnummer
{ guessUserMatrikelnummer :: UserMatriculation }
| GuessUserEduPersonPrincipalName
{ guessUserEduPersonPrincipalName :: UserEduPersonPrincipalName }
| GuessUserDisplayName
{ guessUserDisplayName :: UserDisplayName }
| GuessUserSurname
@ -298,12 +294,11 @@ guessUser (((Set.toList . toNullable) <$>) . Set.toList . dnfTerms -> criteria)
toSql user pl = bool id E.not__ (is _PLNegated pl) $ case pl ^. _plVar of
GuessUserMatrikelnummer userMatriculation' -> user E.^. UserMatrikelnummer E.==. E.val (Just userMatriculation')
GuessUserEduPersonPrincipalName userEPPN' -> user E.^. UserLdapPrimaryKey E.==. E.val (Just userEPPN')
GuessUserDisplayName userDisplayName' -> user E.^. UserDisplayName `containsAsSet` userDisplayName'
GuessUserSurname userSurname' -> user E.^. UserSurname `containsAsSet` userSurname'
GuessUserFirstName userFirstName' -> user E.^. UserFirstName `containsAsSet` userFirstName'
go didLdap = do
go didUpsert = do
let retrieveUsers = E.select . EL.from $ \user -> do
E.where_ . E.or $ map (E.and . map (toSql user)) criteria
when (is _Just mQueryLimit) $ (E.limit . fromJust) mQueryLimit
@ -345,11 +340,7 @@ guessUser (((Set.toList . toNullable) <$>) . Set.toList . dnfTerms -> criteria)
| EQ <- x `closeness` x' = x : takeClosest (x':xs)
| otherwise = [x]
doLdap userMatr = do
ldapPool' <- getsYesod $ view _appLdapPool
fmap join . for ldapPool' $ \ldapPool -> do
ldapData <- campusUserMatr' ldapPool FailoverUnlimited userMatr
for ldapData $ upsertCampusUser UpsertCampusUserGuessUser
doUpsert = flip userLookupAndUpsert UpsertUserGuessUser
let
getTermMatr :: [PredLiteral GuessUserInfo] -> Maybe UserMatriculation
@ -365,25 +356,25 @@ guessUser (((Set.toList . toNullable) <$>) . Set.toList . dnfTerms -> criteria)
| otherwise = Nothing
getTermMatrAux acc (_:xs) = getTermMatrAux acc xs
convertLdapResults :: [Entity User] -> Maybe (Either (NonEmpty (Entity User)) (Entity User))
convertLdapResults [] = Nothing
convertLdapResults [x] = Just $ Right x
convertLdapResults xs = Just $ Left $ NonEmpty.fromList xs
convertUpsertResults :: [Entity User] -> Maybe (Either (NonEmpty (Entity User)) (Entity User))
convertUpsertResults [] = Nothing
convertUpsertResults [x] = Just $ Right x
convertUpsertResults xs = Just $ Left $ NonEmpty.fromList xs
if
| [x] <- users'
, Just True == matchesMatriculation x || didLdap
, Just True == matchesMatriculation x || didUpsert
-> return $ Just $ Right x
| x : x' : _ <- users'
, Just True == matchesMatriculation x || didLdap
, Just True == matchesMatriculation x || didUpsert
, GT <- x `closeness` x'
-> return $ Just $ Right x
| xs@(x:_:_) <- takeClosest users'
, Just True == matchesMatriculation x || didLdap
, Just True == matchesMatriculation x || didUpsert
-> return $ Just $ Left $ NonEmpty.fromList xs
| not didLdap
| not didUpsert
, userMatrs <- ((Set.toList . Set.fromList) (mapMaybe getTermMatr criteria))
-> mapM doLdap userMatrs >>= maybe (go True) (return . Just) . convertLdapResults . catMaybes
-> mapM doUpsert userMatrs >>= maybe (go True) (return . Just) . convertUpsertResults . catMaybes
| otherwise
-> return Nothing
@ -1076,8 +1067,7 @@ assimilateUser newUserId oldUserId = mapReaderT execWriterT $ do
mergeMaybe = mergeBy (\oldV newV -> isNothing newV && isJust oldV)
update newUserId $ catMaybes -- NOTE: persist does shortcircuit null updates as expected
[ mergeMaybe UserLdapPrimaryKey
, mergeBy (<) UserAuthentication
[ mergeMaybe UserPasswordHash
, mergeBy (>) UserLastAuthentication
, mergeBy (<) UserCreated
, toMaybe (not (validEmail' (newUser ^. _userEmail )) && validEmail' (oldUser ^. _userEmail))

View File

@ -254,6 +254,7 @@ import Data.Encoding.UTF8 as Import (UTF8(UTF8))
import GHC.TypeLits as Import (KnownSymbol)
import Data.Word as Import (Word16)
import Data.Word.Word24 as Import
import Data.Kind as Import (Type, Constraint)

View File

@ -61,7 +61,7 @@ import Jobs.Handler.SendCourseCommunication
import Jobs.Handler.Invitation
import Jobs.Handler.SendPasswordReset
import Jobs.Handler.TransactionLog
import Jobs.Handler.SynchroniseLdap
import Jobs.Handler.SynchroniseUser
import Jobs.Handler.SynchroniseAvs
import Jobs.Handler.PruneInvitations
import Jobs.Handler.ChangeUserDisplayEmail
@ -483,7 +483,7 @@ handleJobs' wNum = C.mapM_ $ \jctl -> hoist delimitInternalState . withJobWorker
, Exc.Handler $ \case
MailNotAvailable -> return $ Right ()
e -> return . Left $ SomeException e
, Exc.Handler $ \SynchroniseLdapNoLdap -> return $ Right ()
, Exc.Handler $ \SynchroniseUserNoSource -> return $ Right ()
#endif
, Exc.Handler $ \(e :: SomeException) -> return $ Left e
] . fmap Right

View File

@ -1,4 +1,4 @@
-- SPDX-FileCopyrightText: 2022-2023 Sarah Vaupel <sarah.vaupel@uniworx.de>, David Mosbach <david.mosbach@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Sarah Vaupel <vaupel.sarah@campus.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, David Mosbach <david.mosbach@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Sarah Vaupel <vaupel.sarah@campus.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
@ -253,15 +253,14 @@ determineCrontab = execWriterT $ do
return (nextEpoch, nextInterval, nextIntervalTime, numIntervals)
if
| is _Just appLdapConf
, Just syncWithin <- appSynchroniseLdapUsersWithin
, Just cInterval <- appJobCronInterval
| Just syncWithin <- appUserSyncWithin
, Just cInterval <- appJobCronInterval
-> do
nextIntervals <- getNextIntervals syncWithin appSynchroniseLdapUsersInterval cInterval
nextIntervals <- getNextIntervals syncWithin appUserSyncInterval cInterval
forM_ nextIntervals $ \(nextEpoch, nextInterval, nextIntervalTime, numIntervals) -> do
tell $ HashMap.singleton
(JobCtlQueue JobSynchroniseLdap
(JobCtlQueue JobSynchroniseUsers
{ jEpoch = fromInteger nextEpoch
, jNumIterations = fromInteger numIntervals
, jIteration = fromInteger nextInterval
@ -269,8 +268,8 @@ determineCrontab = execWriterT $ do
Cron
{ cronInitial = CronTimestamp $ utcToLocalTimeTZ appTZ $ toTimeOfDay 23 30 0 $ utctDay nextIntervalTime
, cronRepeat = CronRepeatNever
, cronRateLimit = appSynchroniseLdapUsersInterval
, cronNotAfter = Right . CronTimestamp . utcToLocalTimeTZ appTZ $ addUTCTime appSynchroniseLdapUsersInterval nextIntervalTime
, cronRateLimit = appUserSyncInterval
, cronNotAfter = Right . CronTimestamp . utcToLocalTimeTZ appTZ $ addUTCTime appUserSyncInterval nextIntervalTime
}
| otherwise
-> return ()

View File

@ -1,4 +1,4 @@
-- SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later

View File

@ -1,4 +1,4 @@
-- SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>, Winnie Ros <winnie.ros@campus.lmu.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
@ -36,7 +36,7 @@ dispatchJobSendPasswordReset jRecipient = JobHandlerException . userMailT jRecip
resetBearer' <- bearerToken (HashSet.singleton $ Right jRecipient) Nothing (HashMap.singleton BearerTokenRouteEval . HashSet.singleton $ UserPasswordR cID) Nothing (Just $ Just tomorrowEndOfDay) Nothing
let resetBearer = resetBearer'
& bearerRestrict (UserPasswordR cID) (decodeUtf8 . Base64.encode . BA.convert $ computeUserAuthenticationDigest userAuthentication)
& bearerRestrict (UserPasswordR cID) (decodeUtf8 . Base64.encode . BA.convert $ computeUserAuthenticationDigest userPasswordHash)
encodedBearer <- encodeBearer resetBearer
resetUrl <- toTextUrl (UserPasswordR cID, [(toPathPiece GetBearer, toPathPiece encodedBearer)])

View File

@ -1,69 +0,0 @@
-- SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
module Jobs.Handler.SynchroniseLdap
( dispatchJobSynchroniseLdap
, dispatchJobSynchroniseLdapUser
, dispatchJobSynchroniseLdapAll
, SynchroniseLdapException(..)
) where
import Import
import qualified Data.CaseInsensitive as CI
import qualified Data.Conduit.List as C
import Auth.LDAP
import Foundation.Yesod.Auth (CampusUserConversionException, upsertCampusUser)
import Jobs.Queue
data SynchroniseLdapException
= SynchroniseLdapNoLdap
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic)
instance Exception SynchroniseLdapException
dispatchJobSynchroniseLdap :: Natural -> Natural -> Natural -> JobHandler UniWorX
dispatchJobSynchroniseLdap numIterations epoch iteration
= JobHandlerAtomic . runConduit $
readUsers .| filterIteration .| sinkDBJobs
where
readUsers :: ConduitT () UserId (YesodJobDB UniWorX) ()
readUsers = selectKeys [] []
filterIteration :: ConduitT UserId Job (YesodJobDB UniWorX) ()
filterIteration = C.mapMaybeM $ \userId -> runMaybeT $ do
let
userIteration, currentIteration :: Integer
userIteration = toInteger (hash epoch `hashWithSalt` userId) `mod` toInteger numIterations
currentIteration = toInteger iteration `mod` toInteger numIterations
$logDebugS "SynchroniseLdap" [st|User ##{tshow (fromSqlKey userId)}: LDAP sync on #{tshow userIteration}/#{tshow numIterations}, now #{tshow currentIteration}|]
guard $ userIteration == currentIteration
return $ JobSynchroniseLdapUser userId
dispatchJobSynchroniseLdapUser :: UserId -> JobHandler UniWorX
dispatchJobSynchroniseLdapUser jUser = JobHandlerException $ do
UniWorX{..} <- getYesod
case appLdapPool of
Just ldapPool ->
runDB . void . runMaybeT . handleExc $ do
user@User{userIdent,userLdapPrimaryKey} <- MaybeT $ get jUser
let upsertIdent = maybe userIdent CI.mk userLdapPrimaryKey
$logInfoS "SynchroniseLdap" [st|Synchronising #{upsertIdent}|]
reTestAfter <- getsYesod $ view _appLdapReTestFailover
ldapAttrs <- MaybeT $ campusUserReTest' ldapPool ((>= reTestAfter) . realToFrac) FailoverUnlimited user
void . lift $ upsertCampusUser (UpsertCampusUserLdapSync upsertIdent) ldapAttrs
Nothing ->
throwM SynchroniseLdapNoLdap
where
handleExc :: MaybeT DB a -> MaybeT DB a
handleExc
= catchMPlus (Proxy @CampusUserException)
. catchMPlus (Proxy @CampusUserConversionException)
dispatchJobSynchroniseLdapAll :: JobHandler UniWorX
dispatchJobSynchroniseLdapAll = JobHandlerAtomic . runConduit $ selectSource [] [] .| C.mapM_ (queueDBJob . JobSynchroniseLdapUser . entityKey)

View File

@ -0,0 +1,48 @@
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
module Jobs.Handler.SynchroniseUser
( dispatchJobSynchroniseUsers, dispatchJobSynchroniseUser
, SynchroniseUserException(..)
) where
import Import
import Foundation.Yesod.Auth (userLookupAndUpsert)
import qualified Data.CaseInsensitive as CI
import qualified Data.Conduit.List as C
import Jobs.Queue
data SynchroniseUserException
= SynchroniseUserNoSource
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic)
instance Exception SynchroniseUserException
dispatchJobSynchroniseUsers :: Natural -> Natural -> Natural -> JobHandler UniWorX
dispatchJobSynchroniseUsers numIterations epoch iteration
= JobHandlerAtomic . runConduit $
readUsers .| filterIteration .| sinkDBJobs
where
readUsers :: ConduitT () UserId (YesodJobDB UniWorX) ()
readUsers = selectKeys [] []
filterIteration :: ConduitT UserId Job (YesodJobDB UniWorX) ()
filterIteration = C.mapMaybeM $ \userId -> runMaybeT $ do
let
userIteration, currentIteration :: Integer
userIteration = toInteger (hash epoch `hashWithSalt` userId) `mod` toInteger numIterations
currentIteration = toInteger iteration `mod` toInteger numIterations
$logDebugS "SynchroniseUsers" [st|User ##{tshow (fromSqlKey userId)}: sync on #{tshow userIteration}/#{tshow numIterations}, now #{tshow currentIteration}|]
guard $ userIteration == currentIteration
return $ JobSynchroniseUser userId
dispatchJobSynchroniseUser :: UserId -> JobHandler UniWorX
dispatchJobSynchroniseUser jUser = JobHandlerException . runDB $ do
User{userIdent = upsertUserIdent} <- getJust jUser
$logInfoS "SynchroniseUser" [st|Synchronising #{upsertUserIdent} with external sources|]
void $ userLookupAndUpsert (CI.original upsertUserIdent) UpsertUserSync{..}

View File

@ -1,4 +1,4 @@
-- SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
@ -107,23 +107,31 @@ dispatchHealthCheckHTTPReachable = fmap HealthHTTPReachable . yesodTimeout (^. _
getsYesod $ (== clusterId) . appClusterID
-- TODO: generalize health check
dispatchHealthCheckLDAPAdmins :: Handler HealthReport
dispatchHealthCheckLDAPAdmins = fmap HealthLDAPAdmins . yesodTimeout (^. _appHealthCheckLDAPAdminsTimeout) (Just 0) $ do
ldapPool' <- getsYesod appLdapPool
reTestAfter <- getsYesod $ view _appLdapReTestFailover
userAuthConf <- getsYesod $ view _appUserAuthConf
case ldapPool' of
Just ldapPool -> do
let currentLdapSources = case userAuthConf of
UserAuthConfSingleSource (AuthSourceConfLdap LdapConf{..})
-> singleton $ AuthSourceIdLdap ldapConfSourceId
_other -> mempty
ldapAdminUsers' <- fmap (map E.unValue) . runDB . E.select . E.from $ \(user `E.InnerJoin` userFunction) -> E.distinctOnOrderBy [E.asc $ user E.^. UserId] $ do
E.on $ user E.^. UserId E.==. userFunction E.^. UserFunctionUser
E.where_ $ userFunction E.^. UserFunctionFunction E.==. E.val SchoolAdmin
E.where_ $ user E.^. UserAuthentication E.==. E.val AuthLDAP
E.where_ . E.exists . E.from $ \externalUser -> E.where_ $
externalUser E.^. ExternalUserUser E.==. user E.^. UserIdent
E.&&. externalUser E.^. ExternalUserSource `E.in_` E.valList currentLdapSources
return $ user E.^. UserIdent
for (assertM' (not . null) ldapAdminUsers') $ \ldapAdminUsers -> do
let numAdmins = genericLength ldapAdminUsers
Sum numResolved <- fmap fold . forM ldapAdminUsers $ \(CI.original -> adminIdent) ->
let hCampusExc :: CampusUserException -> Handler (Sum Integer)
hCampusExc err = mempty <$ $logErrorS "healthCheckLDAPAdmins" (adminIdent <> ": " <> tshow err)
in handle hCampusExc $ Sum 1 <$ campusUserReTest ldapPool ((>= reTestAfter) . realToFrac) FailoverUnlimited (Creds apLdap adminIdent [])
let hLdapExc :: LdapUserException -> Handler (Sum Integer)
hLdapExc err = mempty <$ $logErrorS "healthCheckLDAPAdmins" (adminIdent <> ": " <> tshow err)
in handle hLdapExc $ Sum 1 <$ ldapUser ldapPool (Creds apLdap adminIdent [])
--in handle hLdapExc $ Sum 1 <$ ldapUserReTest ldapPool (const True) FailoverUnlimited (Creds apLdap adminIdent [])
if
| numAdmins >= 1 -> return $ numResolved % numAdmins
| otherwise -> return 0

View File

@ -1,4 +1,4 @@
-- SPDX-FileCopyrightText: 2022-2024 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
@ -91,12 +91,12 @@ data Job
| JobTruncateTransactionLog
| JobPruneInvitations
| JobDeleteTransactionLogIPs
| JobSynchroniseLdap { jNumIterations
| JobSynchroniseUsers { jNumIterations
, jEpoch
, jIteration :: Natural
}
| JobSynchroniseLdapUser { jUser :: UserId }
| JobSynchroniseLdapAll
| JobSynchroniseUser { jUser :: UserId }
| JobSynchroniseUserAll
| JobSynchroniseAvs { jNumIterations
, jEpoch
, jIteration :: Natural
@ -326,9 +326,9 @@ jobNoQueueSame = \case
JobTruncateTransactionLog{} -> Just JobNoQueueSame
JobPruneInvitations{} -> Just JobNoQueueSame
JobDeleteTransactionLogIPs{} -> Just JobNoQueueSame
JobSynchroniseLdap{} -> Just JobNoQueueSame
JobSynchroniseLdapUser{} -> Just JobNoQueueSame
JobSynchroniseLdapAll{} -> Just JobNoQueueSameTag
JobSynchroniseUsers{} -> Just JobNoQueueSame
JobSynchroniseUser{} -> Just JobNoQueueSame
JobSynchroniseUserAll{} -> Just JobNoQueueSameTag
JobSynchroniseAvs{} -> Just JobNoQueueSame
-- JobSynchroniseAvsUser{} -> Just JobNoQueueSame
-- JobSynchroniseAvsId{} -> Just JobNoQueueSame

View File

@ -1,4 +1,4 @@
-- SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
@ -9,7 +9,53 @@ module Ldap.Client.Instances
) where
import ClassyPrelude
import Data.Aeson.TH
import Data.Data (Data)
import Database.Persist.TH (derivePersistField)
import Utils.PathPiece (derivePathPiece)
import Ldap.Client
import Network.HTTP.Types.Method.Instances () -- for FromJSON instance for ByteString
deriving instance Ord Attr
deriving instance Ord Dn
deriving instance Ord Password
deriving instance Ord ResultCode
deriving instance Ord Scope
deriving instance Read Attr
deriving instance Read Dn
deriving instance Read Password
deriving instance Read Scope
deriving instance Data Attr
deriving instance Data Dn
deriving instance Data Password
deriving instance Data Scope
deriving instance Generic Attr
deriving instance Generic Dn
deriving instance Generic Password
deriving instance Generic Scope
deriving anyclass instance NFData Attr
deriving anyclass instance NFData Dn
deriving anyclass instance NFData Password
deriving instance NFData Scope
derivePathPiece ''Dn id "--"
derivePathPiece ''Scope id "--"
derivePersistField "Dn"
derivePersistField "Password"
derivePersistField "Scope"
deriveJSON defaultOptions ''Attr
deriveJSON defaultOptions ''Dn
deriveJSON defaultOptions ''Scope
deriveJSON defaultOptions ''SearchEntry

View File

@ -1,4 +1,4 @@
-- SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later

100
src/Middleware.hs Normal file
View File

@ -0,0 +1,100 @@
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
module Middleware
( makeMiddleware
) where
import Import
import Handler.Utils.Routes (classifyHandler)
import qualified Data.HashMap.Strict as HashMap
import Network.HTTP.Types.Header (hSetCookie)
import Network.Wai (Middleware)
import qualified Network.Wai as Wai
import Network.Wai.Middleware.Cors (CorsResourcePolicy(..), cors)
import Network.Wai.Middleware.RequestLogger ( Destination(Logger)
, IPAddrSource(..)
, OutputFormat(..)
, mkRequestLogger, outputFormat, destination
)
import Web.Cookie
makeMiddleware :: MonadIO m => UniWorX -> m Middleware
makeMiddleware app = do
logWare <- makeLogWare app
return $ observeHTTPRequestLatency classifyHandler . logWare . normalizeCookiesWare . corsWare . defaultMiddlewaresNoLogging
makeLogWare :: MonadIO m => UniWorX -> m Middleware
makeLogWare app = do
logWareMap <- liftIO $ newTVarIO HashMap.empty
let
mkLogWare ls@LogSettings{..} = do
logger <- readTVarIO . snd $ appLogger app
logWare <- mkRequestLogger def
{ outputFormat = bool
(Apache . bool FromSocket FromHeader $ app ^. _appIpFromHeader)
(Detailed True)
logDetailed
, destination = Logger $ loggerSet logger
}
atomically . modifyTVar' logWareMap $ HashMap.insert ls logWare
return logWare
void. liftIO $
mkLogWare =<< readTVarIO (appLogSettings app)
return $ \wai req fin -> do
lookupRes <- atomically $ do
ls <- readTVar $ appLogSettings app
existing <- HashMap.lookup ls <$> readTVar logWareMap
return $ maybe (Left ls) Right existing
logWare <- either mkLogWare return lookupRes
logWare wai req fin
normalizeCookiesWare :: Middleware
normalizeCookiesWare waiApp req res = waiApp req $ \res' -> do
resHdrs' <- go $ Wai.responseHeaders res'
res $ Wai.mapResponseHeaders (const resHdrs') res'
where parseSetCookie' :: ByteString -> IO (Maybe SetCookie)
parseSetCookie' = fmap (either (\(_ :: SomeException) -> Nothing) Just) . try . evaluate . force . parseSetCookie
go [] = return []
go (hdr@(hdrName, hdrValue) : hdrs)
| hdrName == hSetCookie = do
mcookieHdr <- parseSetCookie' hdrValue
case mcookieHdr of
Nothing -> (hdr :) <$> go hdrs
Just cookieHdr -> do
let cookieHdrMatches hdrValue' = maybeT (return False) $ do
cookieHdr' <- MaybeT $ parseSetCookie' hdrValue'
-- See https://tools.ietf.org/html/rfc6265
guard $ setCookiePath cookieHdr' == setCookiePath cookieHdr
guard $ setCookieName cookieHdr' == setCookieName cookieHdr
guard $ setCookieDomain cookieHdr' == setCookieDomain cookieHdr
return True
others <- filterM (\(hdrName', hdrValue') -> and2M (pure $ hdrName' == hSetCookie) (cookieHdrMatches hdrValue')) hdrs
if | null others -> (hdr :) <$> go hdrs
| otherwise -> go hdrs
| otherwise = (hdr :) <$> go hdrs
corsWare :: Middleware
corsWare = cors . const $ Just CorsResourcePolicy
{ corsOrigins = Nothing
, corsMethods = [ "GET", "HEAD", "POST" ]
, corsRequestHeaders = []
, corsExposedHeaders = Nothing
, corsMaxAge = Just 600
, corsVaryOrigin = True
, corsRequireOrigin = False
, corsIgnoreFailures = False
}

View File

@ -51,7 +51,8 @@ data ManualMigration
| Migration20230703LmsUserStatus
| Migration20240212InitInterfaceHealth -- create table interface_health and fill with default values
| Migration20240224UniquenessCompanyAvsNr
| Migration20240930RoomOccurrences -- rooms become a part of occurrences
| Migration20240312OAuth2
| Migration20240930RoomOccurrences
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic)
deriving anyclass (Universe, Finite)
@ -85,7 +86,7 @@ migrateManual = do
, ("user_matrikelnummer", "CREATE INDEX user_matrikelnummer ON \"user\" (matrikelnummer)" )
, ("submission_sheet", "CREATE INDEX submission_sheet ON submission (sheet)" )
, ("submission_edit_submission", "CREATE INDEX submission_edit_submission ON submission_edit (submission)" )
, ("user_ldap_primary_key", "CREATE INDEX user_ldap_primary_key ON \"user\" (ldap_primary_key)" )
-- , ("user_ldap_primary_key", "CREATE INDEX user_ldap_primary_key ON \"user\" (ldap_primary_key)" ) -- TODO: reintroduce
, ("file_content_entry_chunk_hash", "CREATE INDEX file_content_entry_chunk_hash ON \"file_content_entry\" (chunk_hash)" )
, ("sent_mail_bounce_secret", "CREATE INDEX sent_mail_bounce_secret ON \"sent_mail\" (bounce_secret) WHERE bounce_secret IS NOT NULL")
, ("sent_mail_recipient", "CREATE INDEX sent_mail_recipient ON \"sent_mail\" (recipient) WHERE recipient IS NOT NULL")
@ -210,6 +211,23 @@ customMigrations = mapF $ \case
ALTER TABLE "company" DROP CONSTRAINT IF EXISTS "unique_company_shorthand";
|]
Migration20240312OAuth2 -> whenM (andM [ columnNotExists "user" "password_hash", columnExists "user" "authentication", columnExists "user" "last_ldap_synchronisation", columnNotExists "user" "last_sync", columnExists "user" "ldap_primary_key" ]) $ do
[executeQQ|
ALTER TABLE "user" ADD COLUMN "password_hash" VARCHAR NULL;
|]
let getPWHashes = [queryQQ| SELECT "id", "authentication"->'pw-hash' FROM "user" WHERE "authentication"->'pw-hash' IS NOT NULL; |]
migratePWHash [ fromPersistValue -> Right (uid :: UserId), fromPersistValue -> Right (pwHash :: Text) ] = [executeQQ| UPDATE "user" SET "password_hash" = #{pwHash} WHERE "id" = #{uid}; |]
migratePWHash _ = return ()
in runConduit $ getPWHashes .| C.mapM_ migratePWHash
[executeQQ|
ALTER TABLE "user" DROP COLUMN "authentication";
|]
[executeQQ|
ALTER TABLE "user" RENAME COLUMN "last_ldap_synchronisation" TO "last_sync";
ALTER TABLE "user" DROP COLUMN "ldap_primary_key";
|]
Migration20240930RoomOccurrences -> do
whenM (tableColumnExists "tutorial" "room")
[executeQQ|

View File

@ -6,6 +6,7 @@ module Model.Types
( module Types
) where
import Model.Types.Auth as Types
import Model.Types.Common as Types
import Model.Types.Course as Types
import Model.Types.DateTime as Types
@ -13,7 +14,6 @@ import Model.Types.Exam as Types
import Model.Types.ExamOffice as Types
import Model.Types.Health as Types
import Model.Types.Mail as Types
import Model.Types.Security as Types
import Model.Types.Sheet as Types
import Model.Types.Submission as Types
import Model.Types.Misc as Types

View File

@ -1,75 +1,103 @@
-- SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Sarah Vaupel <vaupel.sarah@campus.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>, Sarah Vaupel <sarah.vaupel@ifi.lmu.de>, Sarah Vaupel <vaupel.sarah@campus.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-|
Module: Model.Types.Security
Module: Model.Types.Auth
Description: Types for authentication and authorisation
-}
module Model.Types.Security
( module Model.Types.Security
module Model.Types.Auth
( module Model.Types.Auth
) where
import ClassyPrelude.Yesod hiding (derivePersistFieldJSON, Proxy(..))
import Utils
import Data.Aeson
import Data.Aeson.TH
import Model.Types.TH.JSON
import Data.Universe
import Data.Universe.Instances.Reverse ()
import Data.Proxy
import Data.Data (Data)
import Model.Types.TH.PathPiece
import Utils
import Utils.Lens.TH
import Control.Lens
import qualified Data.Set as Set
import qualified Data.Text as Text
import qualified Data.HashMap.Strict as HashMap
import Data.Aeson
import Data.Aeson.TH
import qualified Data.Aeson.Types as Aeson
import Data.CaseInsensitive (CI)
import qualified Data.Binary as Binary
import Data.Binary (Binary)
import Data.Binary.Instances.UnorderedContainers ()
import qualified Data.CaseInsensitive as CI
import Data.CaseInsensitive (CI)
import Data.CaseInsensitive.Instances ()
import Data.Set.Instances ()
import Data.Data (Data)
import qualified Data.HashMap.Strict as HashMap
import Data.NonNull.Instances ()
import Data.Proxy
import qualified Data.Set as Set
import Data.Set.Instances ()
import qualified Data.Text as Text
import Data.Universe
import Data.Universe.Instances.Reverse ()
import Data.Universe.Instances.Reverse.MonoTraversable ()
import Data.UUID (UUID)
import Model.Types.TH.PathPiece
import Database.Persist.Sql
import Servant.Docs (ToSample(..), samples)
import Utils.Lens.TH
import Data.Binary (Binary)
import qualified Data.Binary as Binary
import Data.Binary.Instances.UnorderedContainers ()
data AuthenticationMode = AuthLDAP
| AuthPWHash { authPWHash :: Text }
| AuthNoLogin
deriving (Eq, Ord, Read, Show, Generic)
----------------------------------
----- Authentication Sources -----
----------------------------------
instance Hashable AuthenticationMode
instance NFData AuthenticationMode
type AzureScopes = Set Text
-- Note: Ldap.Host also stores TLS settings, which we will generate ad-hoc based on AuthSourceLdapTls field instead. We therefore use Text to store the hostname only
-- newtype LdapHost = LdapHost { ldapHost :: Text }
-- deriving (Eq, Ord, Read, Show, Generic, Data)
-- deriving newtype (NFData, PathPiece, PersistField, PersistFieldSql)
-- instance E.SqlString LdapHost
-- makeLenses_ ''LdapHost
-- Note: Ldap.PortNumber comes from Network.Socket, which does not export the constructor of the newtype. Hence, no Data and Generic instances can be derived. But PortNumber is a member of Num, so we will use Word16 instead (Word16 is also used for storing the port number inside PortNumber)
-- newtype LdapPort = LdapPort { ldapPort :: Word16 }
-- deriving (Eq, Ord, Read, Show, Generic, Data)
-- deriving newtype (NFData, PathPiece, PersistField, PersistFieldSql)
-- instance E.SqlString LdapPort
-- makeLenses_ ''LdapPort
type UserEduPersonPrincipalName = Text
-- | Subset of the configuration settings of an authentication source that uniquely identify a given source
-- | Used for uniquely storing ExternalUser entries per user and source
data AuthSourceIdent
= AuthSourceIdAzure
{ authSourceIdAzureClientId :: UUID -- FIXME: use tenant id instead
}
| AuthSourceIdLdap
{ authSourceIdLdapHost :: Text -- normally either just the hostname, or hostname and port
}
deriving (Eq, Ord, Read, Show, Data, Generic)
deriving anyclass (NFData)
deriveJSON defaultOptions
{ constructorTagModifier = camelToPathPiece' 1
, fieldLabelModifier = camelToPathPiece' 1
, sumEncoding = UntaggedValue
} ''AuthenticationMode
{ fieldLabelModifier = camelToPathPiece' 3
, constructorTagModifier = camelToPathPiece' 3
, sumEncoding = UntaggedValue
} ''AuthSourceIdent
derivePersistFieldJSON ''AuthenticationMode
derivePersistFieldJSON ''AuthSourceIdent
makeLenses_ ''AuthSourceIdent
makePrisms ''AuthSourceIdent
-------------------
----- AuthTag -----
-------------------
data AuthTag -- sortiert nach gewünschter Reihenfolge auf /authpreds, d.h. Prädikate sind sortier nach Relevanz für Benutzer
= AuthAdmin
@ -105,8 +133,8 @@ data AuthTag -- sortiert nach gewünschter Reihenfolge auf /authpreds, d.h. Prä
| AuthRegisterGroup
| AuthEmpty
| AuthSelf
| AuthIsLDAP
| AuthIsPWHash
| AuthIsExternal -- TODO: maybe distinguish between AuthenticationProtocols
| AuthIsInternal
| AuthAuthentication
| AuthNoEscalation
| AuthRead
@ -179,6 +207,11 @@ _ReducedActiveAuthTags = iso toReducedActiveAuthTags fromReducedActiveAuthTags
fromReducedActiveAuthTags (ReducedActiveAuthTags hm) = AuthTagActive $ \n -> fromMaybe (authTagIsActive def n) $ HashMap.lookup n hm
-------------------
----- PredDNF -----
-------------------
-- TODO: Use external PredDNF instead: https://github.com/savau/haskell-nf
data PredLiteral a = PLVariable { plVar :: a } | PLNegated { plVar :: a }
deriving (Eq, Ord, Read, Show, Data, Generic)
deriving anyclass (Hashable, Binary, NFData)
@ -220,7 +253,6 @@ parsePredDNF start = fmap (PredDNF . Set.mapMonotonic impureNonNull) . ofoldM pa
| otherwise
= Left t
$(return [])
instance ToJSON a => ToJSON (PredDNF a) where

View File

@ -73,7 +73,7 @@ import qualified Data.Foldable
import Data.Aeson (genericToJSON, genericParseJSON)
import Model.Types.Security
import Model.Types.Auth
{-# ANN module ("HLint: ignore Use newtype instead of data" :: String) #-}

View File

@ -1,4 +1,4 @@
-- SPDX-FileCopyrightText: 2022-2024 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>
-- SPDX-FileCopyrightText: 2022-2025 Sarah Vaupel <sarah.vaupel@uniworx.de>,-2024 Gregor Kleen <gregor.kleen@ifi.lmu.de>, Sarah Vaupel <sarah.vaupel@ifi.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
@ -8,9 +8,6 @@ import Import.NoModel
import Model.Types.TH.PathPiece
type UserEduPersonPrincipalName = Text
data SystemFunction
= SystemExamOffice
| SystemFaculty

View File

@ -1,4 +1,4 @@
-- SPDX-FileCopyrightText: 2022-2025 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>
-- SPDX-FileCopyrightText: 2022-2025 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>,David Mosbach <david.mosbach@uniworx.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
@ -13,10 +13,13 @@
module Settings
( module Settings
, module Settings.Cluster
, module Settings.Mime
, module Settings.Cookies
, module Settings.Ldap
, module Settings.Log
, module Settings.Locale
, module Settings.Mime
, module Settings.OAuth2
, module Settings.ResourcePool
) where
import Import.NoModel
@ -41,12 +44,8 @@ import Language.Haskell.TH.Syntax (Exp, Q)
import qualified Yesod.Auth.Util.PasswordStore as PWStore
import qualified Data.Scientific as Scientific
import Data.Word (Word16)
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
import qualified Ldap.Client as Ldap
import qualified Network.HaskellNet.Auth as HaskellNet (UserName, Password, AuthType(..))
import qualified Network.Socket as HaskellNet
@ -56,11 +55,15 @@ import Network.Mail.Mime.Instances ()
import qualified Database.Memcached.Binary.Types as Memcached
import Model
import Settings.Cluster
import Settings.Mime
import Settings.Cookies
import Settings.Ldap
import Settings.Log
import Settings.Locale
import Settings.Mime
import Settings.OAuth2
import Settings.ResourcePool
import qualified System.FilePath as FilePath
@ -73,8 +76,6 @@ import qualified Web.ServerSession.Core as ServerSession
import Text.Show (showParen, showString)
import qualified Data.List.PointedList as P
import qualified Network.Minio as Minio
import Data.Conduit.Algorithms.FastCDC
@ -84,176 +85,6 @@ import Utils.Lens.TH
import qualified Data.Set as Set
-- | Runtime settings to configure this application. These settings can be
-- loaded from various sources: defaults, environment variables, config files,
-- theoretically even a database.
data AppSettings = AppSettings
{ appStaticDir :: FilePath
-- ^ Directory from which to serve static files.
, appBundlerEntrypoints :: FilePath
, appWellKnownDir :: FilePath
, appWellKnownLinkFile :: FilePath
, appDatabaseConf :: PostgresConf
-- ^ Configuration settings for accessing the database.
, appAutoDbMigrate :: Bool
, appLdapConf :: Maybe (PointedList LdapConf)
-- ^ Configuration settings for CSV export/import to LMS (= Learn Management System)
, appLmsConf :: LmsConf
-- ^ Configuration settings for accessing the LDAP-directory
, appAvsConf :: Maybe AvsConf
-- ^ Configuration settings for accessing AVS Server (= Ausweis Verwaltungs System)
, appAvsLicenceSynchConf :: AvsLicenceSynchConf
-- ^ Configuration settings for automatically synching driving licences with AVS
, appLprConf :: LprConf
-- ^ Configuration settings for accessing a printer queue via lpr for letter mailing
, appSmtpConf :: Maybe SmtpConf
-- ^ Configuration settings for accessing a SMTP Mailserver
, appWidgetMemcachedConf :: Maybe WidgetMemcachedConf
-- ^ Configuration settings for accessing a Memcached instance for use with `addStaticContent`
, appRoot :: ApprootScope -> Maybe Text
-- ^ Base for all generated URLs. If @Nothing@, determined
-- from the request headers.
, appHost :: HostPreference
-- ^ Host/interface the server should bind to.
, appPort :: Int
-- ^ Port to listen on
, appIpFromHeader :: Bool
-- ^ Get the IP address from the header when logging. Useful when sitting
-- behind a reverse proxy.
, appServerSessionConfig :: ServerSessionSettings
, appServerSessionAcidFallback :: Bool
, appSessionMemcachedConf :: Maybe MemcachedConf
, appSessionTokenStart
, appSessionTokenExpiration :: Maybe NominalDiffTime
, appSessionTokenEncoding :: JwtEncoding
, appSessionTokenClockLeniencyStart, appSessionTokenClockLeniencyEnd
, appBearerTokenClockLeniencyStart, appBearerTokenClockLeniencyEnd
, appUploadTokenClockLeniencyStart, appUploadTokenClockLeniencyEnd :: Maybe NominalDiffTime
, appMailObjectDomain :: Text
, appMailVerp :: VerpMode
, appMailRetainSent :: Maybe NominalDiffTime
, appMailEnvelopeFrom :: Text
, appMailFrom
, appMailSender
, appMailSupport :: Address
, appMailRerouteTo :: Maybe Address
, appMailUseReplyToInstead :: Bool
, appJobWorkers :: Natural
, appJobFlushInterval :: Maybe NominalDiffTime
, appJobCronInterval :: Maybe NominalDiffTime
, appJobStaleThreshold :: NominalDiffTime
, appJobMoveThreshold :: Maybe DiffTime
, appNotificationRateLimit :: NominalDiffTime
, appNotificationCollateDelay :: NominalDiffTime
, appNotificationExpiration :: NominalDiffTime
, appSessionTimeout :: NominalDiffTime
, appMaximumContentLength :: Maybe Word64
, appBearerExpiration :: Maybe NominalDiffTime
, appBearerEncoding :: JwtEncoding
, appHealthCheckInterval :: HealthCheck -> Maybe NominalDiffTime
, appHealthCheckDelayNotify :: Bool
, appHealthCheckHTTP :: Bool
, appHealthCheckActiveJobExecutorsTimeout :: NominalDiffTime
, appHealthCheckActiveWidgetMemcachedTimeout :: NominalDiffTime
, appHealthCheckSMTPConnectTimeout :: NominalDiffTime
, appHealthCheckLDAPAdminsTimeout :: NominalDiffTime
, appHealthCheckHTTPReachableTimeout :: NominalDiffTime
, appHealthCheckMatchingClusterConfigTimeout :: NominalDiffTime
, appSynchroniseLdapUsersWithin :: Maybe NominalDiffTime
, appSynchroniseLdapUsersInterval :: NominalDiffTime
, appSynchroniseLdapUsersExpire :: Maybe NominalDiffTime
, appSynchroniseAvsUsersWithin :: Maybe NominalDiffTime
, appSynchroniseAvsUsersInterval :: NominalDiffTime
, appLdapReTestFailover :: DiffTime
, appSessionFilesExpire :: NominalDiffTime
, appKeepUnreferencedFiles :: NominalDiffTime
, appPruneUnreferencedFilesWithin :: Maybe NominalDiffTime
, appPruneUnreferencedFilesInterval :: NominalDiffTime
, appInitialLogSettings :: LogSettings
, appTransactionLogIPRetentionTime :: NominalDiffTime
, appReloadTemplates :: Bool
-- ^ Use the reload version of templates
, appMutableStatic :: Bool
-- ^ Assume that files in the static dir may change after compilation
, appSkipCombining :: Bool
-- ^ Perform no stylesheet/script combining
, appAuthDummyLogin :: Bool
-- ^ Indicate if auth dummy login should be enabled.
, appAllowDeprecated :: Bool
-- ^ Indicate if deprecated routes are accessible for everyone
, appEncryptErrors :: Bool
, appClearCache :: Bool
, appUserDefaults :: UserDefaultConf
, appAuthPWHash :: PWHashConf
, appExternalApisPingInterval
, appExternalApisPongTimeout
, appExternalApisExpiry :: NominalDiffTime
, appCookieSettings :: RegisteredCookie -> CookieSettings
, appMemcachedConf :: Maybe MemcachedConf
, appMemcacheAuth :: Bool
, appUploadCacheConf :: Maybe Minio.ConnectInfo
, appUploadCacheBucket, appUploadTmpBucket :: Minio.Bucket
, appInjectFiles :: Maybe NominalDiffTime
, appRechunkFiles :: Maybe NominalDiffTime
, appCheckMissingFiles :: Maybe NominalDiffTime
, appFileUploadDBChunksize :: Int
, appFavouritesQuickActionsBurstsize
, appFavouritesQuickActionsAvgInverseRate :: Word64
, appFavouritesQuickActionsTimeout :: DiffTime
, appFavouritesQuickActionsCacheTTL :: Maybe DiffTime
, appPersistentTokenBuckets :: TokenBucketIdent -> TokenBucketConf
, appFallbackPersonalisedSheetFilesKeysExpire :: NominalDiffTime
, appDownloadTokenExpire :: NominalDiffTime
, appInitialInstanceID :: Maybe (Either FilePath UUID)
, appRibbon :: Maybe Text
, appJobMode :: JobMode
, appStudyFeaturesRecacheRelevanceWithin :: Maybe NominalDiffTime
, appStudyFeaturesRecacheRelevanceInterval :: NominalDiffTime
, appJobLmsQualificationsEnqueueHour :: Maybe Natural
, appJobLmsQualificationsDequeueHour :: Maybe Natural
, appBotMitigations :: Set SettingBotMitigation
, appVolatileClusterSettingsCacheTime :: DiffTime
, appJobMaxFlush :: Maybe Natural
, appCommunicationAttachmentsMaxSize :: Maybe Natural
, appCommunicationGlobalCC :: Maybe UserEmail
, appFileChunkingParams :: FastCDCParameters
, appLegalExternal :: Set LegalExternal
} deriving Show
data JobMode = JobsLocal { jobsAcceptOffload :: Bool }
| JobsOffload
| JobsDrop
@ -305,15 +136,29 @@ instance FromJSON PWHashConf where
return PWHashConf{..}
data LdapConf = LdapConf
{ ldapHost :: Ldap.Host, ldapPort :: Ldap.PortNumber
, ldapDn :: Ldap.Dn, ldapPassword :: Ldap.Password
, ldapBase :: Ldap.Dn
, ldapScope :: Ldap.Scope
, ldapTimeout :: NominalDiffTime
, ldapSearchTimeout :: Int32
, ldapPool :: ResourcePoolConf
} deriving (Show)
data AuthSourceConf = AuthSourceConfLdap LdapConf | AuthSourceConfAzureAdV2 AzureConf
deriving (Show)
newtype UserAuthConf =
UserAuthConfSingleSource -- ^ use only one specific source
{ userAuthConfSingleSource :: AuthSourceConf
}
-- TODO: other modes yet to be implemented
-- | UserAuthConfFailover -- ^ use only one user source at a time, but failover to the next-best database if the current source is unavailable
-- { userAuthConfFailoverSources :: PointedList UserSource
-- , userAuthConfFailoverRetest :: NominalDiffTime
-- }
-- | UserAuthConfMultiSource -- ^ Multiple coequal user sources
-- { userAuthConfMultiSources :: Set UserSource
-- }
-- | UserAuthConfNoSource -- ^ allow no external sources at all -- TODO: either this, or make user-auth in settings.yml optional
deriving (Show)
mkAuthSourceIdent :: AuthSourceConf -> AuthSourceIdent
mkAuthSourceIdent = \case
AuthSourceConfAzureAdV2 AzureConf{..} -> AuthSourceIdAzure azureConfClientId
AuthSourceConfLdap LdapConf{..} -> AuthSourceIdLdap ldapConfSourceId
data LmsConf = LmsConf
{ lmsUploadHeader :: Bool
@ -394,12 +239,6 @@ instance FromJSON WidgetMemcachedConf where
widgetMemcachedBaseUrl <- o .:? "base-url" .!= ""
return WidgetMemcachedConf{..}
data ResourcePoolConf = ResourcePoolConf
{ poolStripes :: Int
, poolTimeout :: NominalDiffTime
, poolLimit :: Int
} deriving (Show)
data SmtpSslMode = SmtpSslNone | SmtpSslSmtps | SmtpSslStarttls
deriving (Show)
@ -452,7 +291,6 @@ deriveJSON defaultOptions
{ fieldLabelModifier = camelToPathPiece' 2
} ''TokenBucketConf
deriveFromJSON defaultOptions ''Ldap.Scope
deriveFromJSON defaultOptions
{ fieldLabelModifier = camelToPathPiece' 2
} ''UserDefaultConf
@ -469,47 +307,31 @@ pathPieceJSONKey ''SettingBotMitigation
makePrisms ''JobMode
makeLenses_ ''JobMode
makePrisms ''AuthSourceConf
makeLenses_ ''UserAuthConf
makePrisms ''UserAuthConf
instance FromJSON LdapConf where
parseJSON = withObject "LdapConf" $ \o -> do
ldapTls <- o .:? "tls"
tlsSettings <- case ldapTls :: Maybe String of
Just spec
| spec == "insecure" -> return $ Just Ldap.insecureTlsSettings
| spec == "default" -> return $ Just Ldap.defaultTlsSettings
| spec == "none" -> return Nothing
| spec == "notls" -> return Nothing
| null spec -> return Nothing
Nothing -> return Nothing
_otherwise -> fail "Could not parse LDAP TLSSettings"
ldapHost <- maybe Ldap.Plain (flip Ldap.Tls) tlsSettings <$> o .:? "host" .!= ""
ldapPort <- (fromIntegral :: Int -> Ldap.PortNumber) <$> o .: "port"
ldapDn <- Ldap.Dn <$> o .:? "user" .!= ""
ldapPassword <- Ldap.Password . Text.encodeUtf8 <$> o .:? "pass" .!= ""
ldapBase <- Ldap.Dn <$> o .:? "baseDN" .!= ""
ldapScope <- o .: "scope"
ldapTimeout <- o .: "timeout"
ldapSearchTimeout <- o .: "search-timeout"
ldapPool <- o .: "pool"
return LdapConf{..}
deriveFromJSON defaultOptions
{ constructorTagModifier = toLower . dropPrefix "AuthSourceConf"
, sumEncoding = TaggedObject "protocol" "config"
} ''AuthSourceConf
deriveFromJSON
defaultOptions
{ fieldLabelModifier = intercalate "-" . map toLower . drop 1 . splitCamel
}
''ResourcePoolConf
deriveFromJSON defaultOptions
{ constructorTagModifier = camelToPathPiece' 3
, fieldLabelModifier = camelToPathPiece' 3
, sumEncoding = UntaggedValue -- TaggedObject "mode" "config"
, unwrapUnaryRecords = True
} ''UserAuthConf
instance FromJSON HaskellNet.PortNumber where
parseJSON = withScientific "PortNumber" $ \sciNum -> case Scientific.toBoundedInteger sciNum of
Just int -> return $ fromIntegral (int :: Word16)
Nothing -> fail "Expected whole number of plausible size to denote port"
deriveFromJSON
defaultOptions
{ constructorTagModifier = unpack . intercalate "-" . Text.splitOn "_" . toLower . pack
, allNullaryToStringTag = True
}
''HaskellNet.AuthType
deriveFromJSON defaultOptions
{ constructorTagModifier = unpack . intercalate "-" . Text.splitOn "_" . toLower . pack
, allNullaryToStringTag = True
} ''HaskellNet.AuthType
instance FromJSON LmsConf where
parseJSON = withObject "LmsConf" $ \o -> do
@ -602,7 +424,6 @@ instance FromJSON Minio.ConnectInfo where
connectDisableTLSCertValidation <- o .:? "disable-cert-validation" .!= False
return Minio.ConnectInfo{..}
instance FromJSON ServerSessionSettings where
parseJSON = withObject "ServerSession.State" $ \o -> do
idleTimeout <- o .:? "idle-timeout"
@ -625,6 +446,188 @@ instance FromJSON LegalExternal where
externalPayments <- o .: "payments"
return LegalExternal{..}
submissionBlacklist :: [Pattern]
submissionBlacklist = $$(patternFile compDefault "config/submission-blacklist")
personalisedSheetFilesCollatable :: Map Text Pattern
personalisedSheetFilesCollatable = $$(patternFile' compDefault "config/personalised-sheet-files-collate")
-- | Runtime settings to configure this application. These settings can be
-- loaded from various sources: defaults, environment variables, config files,
-- theoretically even a database.
data AppSettings = AppSettings
{ appStaticDir :: FilePath
-- ^ Directory from which to serve static files.
, appBundlerEntrypoints :: FilePath
, appWellKnownDir :: FilePath
, appWellKnownLinkFile :: FilePath
, appDatabaseConf :: PostgresConf
-- ^ Configuration settings for accessing the database.
, appAutoDbMigrate :: Bool
, appUserAuthConf :: UserAuthConf
, appSingleSignOn :: Bool
-- ^ Enable OIDC single sign-on
, appAutoSignOn :: Bool
-- ^ Automatically redirect to SSO route when not signed on
-- ^ Note: This will force authentication, thus the site will be inaccessible without external credentials. Only use this option when it is ensured that every user that should be able to access the site has valid external credentials!
, appLmsConf :: LmsConf
-- ^ Configuration settings for CSV export/import to LMS (= Learn Management System) -- TODO, TODISCUSS: reimplement as user-auth source?
, appAvsConf :: Maybe AvsConf
-- ^ Configuration settings for accessing AVS Server (= Ausweis Verwaltungs System) -- TODO, TODISCUSS: reimplement as user-auth source?
, appAvsLicenceSynchConf :: Maybe AvsLicenceSynchConf
, appLprConf :: LprConf
-- ^ Configuration settings for accessing a printer queue via lpr for letter mailing
, appSmtpConf :: Maybe SmtpConf
-- ^ Configuration settings for accessing a SMTP Mailserver
, appWidgetMemcachedConf :: Maybe WidgetMemcachedConf
-- ^ Configuration settings for accessing a Memcached instance for use with `addStaticContent`
, appRoot :: ApprootScope -> Maybe Text
-- ^ Base for all generated URLs. If @Nothing@, determined from the request headers.
, appHost :: HostPreference
-- ^ Host/interface the server should bind to.
, appPort :: Int
-- ^ Port to listen on
, appIpFromHeader :: Bool
-- ^ Get the IP address from the header when logging. Useful when sitting behind a reverse proxy.
, appServerSessionConfig :: ServerSessionSettings
, appServerSessionAcidFallback :: Bool
, appSessionMemcachedConf :: Maybe MemcachedConf
, appSessionTokenStart
, appSessionTokenExpiration :: Maybe NominalDiffTime
, appSessionTokenEncoding :: JwtEncoding
, appSessionTokenClockLeniencyStart, appSessionTokenClockLeniencyEnd
, appBearerTokenClockLeniencyStart, appBearerTokenClockLeniencyEnd
, appUploadTokenClockLeniencyStart, appUploadTokenClockLeniencyEnd :: Maybe NominalDiffTime
, appMailObjectDomain :: Text
, appMailVerp :: VerpMode
, appMailRetainSent :: Maybe NominalDiffTime
, appMailEnvelopeFrom :: Text
, appMailFrom
, appMailSender
, appMailSupport :: Address
, appMailRerouteTo :: Maybe Address
, appMailUseReplyToInstead :: Bool
, appJobWorkers :: Natural
, appJobFlushInterval :: Maybe NominalDiffTime
, appJobCronInterval :: Maybe NominalDiffTime
, appJobStaleThreshold :: NominalDiffTime
, appJobMoveThreshold :: Maybe DiffTime
, appNotificationRateLimit :: NominalDiffTime
, appNotificationCollateDelay :: NominalDiffTime
, appNotificationExpiration :: NominalDiffTime
, appSessionTimeout :: NominalDiffTime
, appMaximumContentLength :: Maybe Word64
, appBearerExpiration :: Maybe NominalDiffTime
, appBearerEncoding :: JwtEncoding
, appHealthCheckInterval :: HealthCheck -> Maybe NominalDiffTime
, appHealthCheckDelayNotify :: Bool
, appHealthCheckHTTP :: Bool
, appHealthCheckActiveJobExecutorsTimeout :: NominalDiffTime
, appHealthCheckActiveWidgetMemcachedTimeout :: NominalDiffTime
, appHealthCheckSMTPConnectTimeout :: NominalDiffTime
, appHealthCheckLDAPAdminsTimeout :: NominalDiffTime -- TODO: either generalize over every external auth sources, or otherwise reimplement for different semantics
, appHealthCheckHTTPReachableTimeout :: NominalDiffTime
, appHealthCheckMatchingClusterConfigTimeout :: NominalDiffTime
-- , appUserRetestFailover :: DiffTime -- TODO: reintroduce and move into failover settings once failover mode has been reimplemented
-- TODO; maybe implement syncWithin and syncInterval per auth source
, appUserSyncWithin :: Maybe NominalDiffTime
, appUserSyncInterval :: NominalDiffTime
-- , appSynchroniseLdapUsersExpire :: Maybe NominalDiffTime -- TODO: migrate
, appLdapPoolConf :: Maybe ResourcePoolConf -- TODO: generalize for arbitrary auth protocols
-- TODO: maybe use separate pools for external databases?
, appSynchroniseAvsUsersWithin :: Maybe NominalDiffTime
, appSynchroniseAvsUsersInterval :: NominalDiffTime
, appSynchroniseLdapUsersExpire :: Maybe NominalDiffTime
, appSessionFilesExpire :: NominalDiffTime
, appKeepUnreferencedFiles :: NominalDiffTime
, appPruneUnreferencedFilesWithin :: Maybe NominalDiffTime
, appPruneUnreferencedFilesInterval :: NominalDiffTime
, appInitialLogSettings :: LogSettings
, appTransactionLogIPRetentionTime :: NominalDiffTime
, appReloadTemplates :: Bool
-- ^ Use the reload version of templates
, appMutableStatic :: Bool
-- ^ Assume that files in the static dir may change after compilation
, appSkipCombining :: Bool
-- ^ Perform no stylesheet/script combining
, appAuthDummyLogin :: Bool
-- ^ Indicate if auth dummy login should be enabled.
, appAllowDeprecated :: Bool
-- ^ Indicate if deprecated routes are accessible for everyone
, appEncryptErrors :: Bool
, appClearCache :: Bool
, appUserDefaults :: UserDefaultConf
, appAuthPWHash :: PWHashConf
, appExternalApisPingInterval
, appExternalApisPongTimeout
, appExternalApisExpiry :: NominalDiffTime
, appCookieSettings :: RegisteredCookie -> CookieSettings
, appMemcachedConf :: Maybe MemcachedConf
, appMemcacheAuth :: Bool
, appUploadCacheConf :: Maybe Minio.ConnectInfo
, appUploadCacheBucket, appUploadTmpBucket :: Minio.Bucket
, appInjectFiles :: Maybe NominalDiffTime
, appRechunkFiles :: Maybe NominalDiffTime
, appCheckMissingFiles :: Maybe NominalDiffTime
, appFileUploadDBChunksize :: Int
, appFavouritesQuickActionsBurstsize
, appFavouritesQuickActionsAvgInverseRate :: Word64
, appFavouritesQuickActionsTimeout :: DiffTime
, appFavouritesQuickActionsCacheTTL :: Maybe DiffTime
, appPersistentTokenBuckets :: TokenBucketIdent -> TokenBucketConf
, appFallbackPersonalisedSheetFilesKeysExpire :: NominalDiffTime
, appDownloadTokenExpire :: NominalDiffTime
, appInitialInstanceID :: Maybe (Either FilePath UUID)
, appRibbon :: Maybe Text
, appJobMode :: JobMode
, appStudyFeaturesRecacheRelevanceWithin :: Maybe NominalDiffTime
, appStudyFeaturesRecacheRelevanceInterval :: NominalDiffTime
, appJobLmsQualificationsEnqueueHour :: Maybe Natural
, appJobLmsQualificationsDequeueHour :: Maybe Natural
, appBotMitigations :: Set SettingBotMitigation
, appVolatileClusterSettingsCacheTime :: DiffTime
, appJobMaxFlush :: Maybe Natural
, appCommunicationAttachmentsMaxSize :: Maybe Natural
, appCommunicationGlobalCC :: Maybe UserEmail
, appFileChunkingParams :: FastCDCParameters
, appLegalExternal :: Set LegalExternal
} deriving Show
instance FromJSON AppSettings where
parseJSON = withObject "AppSettings" $ \o -> do
let defaultDev =
@ -639,10 +642,16 @@ instance FromJSON AppSettings where
appBundlerEntrypoints <- o .: "bundler-manifest"
appDatabaseConf <- o .: "database"
appAutoDbMigrate <- o .: "auto-db-migrate"
let nonEmptyHost LdapConf{..} = case ldapHost of
Ldap.Tls host _ -> not $ null host
Ldap.Plain host -> not $ null host
appLdapConf <- P.fromList . mapMaybe (assertM nonEmptyHost) <$> o .:? "ldap" .!= []
-- TODO: reintroduce non-emptyness check for ldap hosts
-- let nonEmptyHost (UserDbLdap LdapConf{..}) = case ldapHost of
-- Ldap.Tls host _ -> not $ null host
-- Ldap.Plain host -> not $ null host
-- nonEmptyHost (UserDbOAuth2 OAuth2Conf{..}) = not $ or [ null oauth2TenantId, null oauth2ClientId, null oauth2ClientSecret ]
appUserAuthConf <- o .: "user-auth"
-- P.fromList . mapMaybe (assertM nonEmptyHost) <$> o .:? "user-database" .!= []
appLdapPoolConf <- o .:? "ldap-pool"
appSingleSignOn <- o .:? "single-sign-on" .!= False
appAutoSignOn <- o .:? "auto-sign-on" .!= False
appLmsConf <- o .: "lms-direct"
appAvsConf <- assertM (not . null . avsPass) <$> o .:? "avs"
appAvsLicenceSynchConf <- o .:? "avs-licence-synch" .!= def
@ -707,15 +716,14 @@ instance FromJSON AppSettings where
appSessionTimeout <- o .: "session-timeout"
appSynchroniseLdapUsersWithin <- o .:? "synchronise-ldap-users-within"
appSynchroniseLdapUsersInterval <- o .: "synchronise-ldap-users-interval"
-- appUserRetestFailover <- o .: "userdb-retest-failover"
appUserSyncWithin <- o .:? "user-sync-within"
appUserSyncInterval <- o .: "user-sync-interval"
appSynchroniseLdapUsersExpire <- o .:? "synrchonise-ldap-users-expire" -- time after last synch to delete LDAP sepcific data
appSynchroniseAvsUsersWithin <- o .:? "synchronise-avs-users-within"
appSynchroniseAvsUsersInterval <- o .: "synchronise-avs-users-interval"
appLdapReTestFailover <- o .: "ldap-re-test-failover"
appSessionFilesExpire <- o .: "session-files-expire"
appKeepUnreferencedFiles <- o .:? "keep-unreferenced-files" .!= 0
appInjectFiles <- o .:? "inject-files"
@ -817,6 +825,26 @@ instance FromJSON AppSettings where
makeClassy_ ''AppSettings
-- | Raw bytes at compile time of @config/settings.yml@
configSettingsYmlBS :: ByteString
configSettingsYmlBS = $(embedFile configSettingsYml)
-- | @config/settings.yml@, parsed to a @Value@.
configSettingsYmlValue :: Value
configSettingsYmlValue = either Exception.throw id
$ decodeEither' configSettingsYmlBS
-- | A version of @AppSettings@ parsed at compile time from @config/settings.yml@.
compileTimeAppSettings :: AppSettings
compileTimeAppSettings =
case fromJSON $ applyEnvValue False mempty configSettingsYmlValue of
Aeson.Error e -> error e
Aeson.Success settings -> settings
-- Since widgetFile above also add "templates" directory, requires import Text.Hamlet (hamletFile)
-- hamletFile' :: FilePath -> Q Exp
-- hamletFile' nameBase = hamletFile $ "templates" </> nameBase
-- | Settings for 'widgetFile', such as which template languages to support and
-- default Hamlet settings.
--
@ -826,16 +854,6 @@ makeClassy_ ''AppSettings
widgetFileSettings :: WidgetFileSettings
widgetFileSettings = def
submissionBlacklist :: [Pattern]
submissionBlacklist = $$(patternFile compDefault "config/submission-blacklist")
personalisedSheetFilesCollatable :: Map Text Pattern
personalisedSheetFilesCollatable = $$(patternFile' compDefault "config/personalised-sheet-files-collate")
-- The rest of this file contains settings which rarely need changing by a
-- user.
widgetFile :: String -> Q Exp
#ifdef DEVELOPMENT
widgetFile nameBase = do

63
src/Settings/Ldap.hs Normal file
View File

@ -0,0 +1,63 @@
-- SPDX-FileCopyrightText: 2024 Sarah Vaupel <sarah.vaupel@uniworx.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
module Settings.Ldap
( LdapConf(..)
, _ldapConfHost, _ldapConfPort, _ldapConfSourceId, _ldapConfDn, _ldapConfPassword, _ldapConfBase, _ldapConfScope, _ldapConfTimeout, _ldapConfSearchTimeout
) where
import ClassyPrelude
import Utils.Lens.TH
import Control.Monad.Fail (fail)
import Data.Aeson
import qualified Data.Text.Encoding as Text
import Data.Time.Clock
import qualified Ldap.Client as Ldap
import Ldap.Client.Instances ()
data LdapConf = LdapConf
{ ldapConfHost :: Ldap.Host
, ldapConfPort :: Ldap.PortNumber
, ldapConfSourceId :: Text
-- ^ Some unique identifier for this LDAP instance, e.g. hostname or hostname:port
, ldapConfDn :: Ldap.Dn
, ldapConfPassword :: Ldap.Password
, ldapConfBase :: Ldap.Dn
, ldapConfScope :: Ldap.Scope
, ldapConfTimeout :: NominalDiffTime
, ldapConfSearchTimeout :: Int32
} deriving (Show)
makeLenses_ ''LdapConf
instance FromJSON LdapConf where
parseJSON = withObject "LdapConf" $ \o -> do
ldapConfTls <- o .:? "tls"
tlsSettings <- case ldapConfTls :: Maybe String of
Just spec
| spec == "insecure" -> return $ Just Ldap.insecureTlsSettings
| spec == "default" -> return $ Just Ldap.defaultTlsSettings
| spec == "none" -> return Nothing
| spec == "notls" -> return Nothing
| null spec -> return Nothing
Nothing -> return Nothing
_otherwise -> fail "Could not parse LDAP TLSSettings"
hostname :: Text <- o .: "host"
port :: Int <- o .: "port"
let
ldapConfHost = maybe Ldap.Plain (flip Ldap.Tls) tlsSettings $ show hostname
ldapConfPort = fromIntegral port
ldapConfSourceId <- o .:? "source-id" .!= hostname
ldapConfDn <- Ldap.Dn <$> o .:? "user" .!= ""
ldapConfPassword <- Ldap.Password . Text.encodeUtf8 <$> o .:? "pass" .!= ""
ldapConfBase <- Ldap.Dn <$> o .:? "baseDN" .!= ""
ldapConfScope <- o .: "scope"
ldapConfTimeout <- o .: "timeout"
ldapConfSearchTimeout <- o .: "search-timeout"
return LdapConf{..}

32
src/Settings/OAuth2.hs Normal file
View File

@ -0,0 +1,32 @@
-- SPDX-FileCopyrightText: 2024 Sarah Vaupel <sarah.vaupel@uniworx.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
module Settings.OAuth2
( AzureConf(..)
, _azureConfClientId, _azureConfClientSecret, _azureConfTenantId, _azureConfScopes
) where
import ClassyPrelude
import Data.Aeson
import Data.Aeson.TH
import Data.UUID
import Utils.Lens.TH
import Utils.PathPiece (camelToPathPiece')
data AzureConf = AzureConf
{ azureConfClientId :: UUID
, azureConfClientSecret :: Text
, azureConfTenantId :: UUID
, azureConfScopes :: Set Text -- TODO: use AzureScopes type?
}
deriving (Show)
makeLenses_ ''AzureConf
deriveFromJSON defaultOptions
{ fieldLabelModifier = camelToPathPiece' 2
} ''AzureConf

View File

@ -0,0 +1,30 @@
-- SPDX-FileCopyrightText: 2024 Sarah Vaupel <sarah.vaupel@uniworx.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
module Settings.ResourcePool
( ResourcePoolConf(..)
, _poolStripes, _poolTimeout, _poolLimit
) where
import ClassyPrelude
import Utils.Lens.TH
import Utils.PathPiece (camelToPathPiece')
import Data.Aeson
import Data.Aeson.TH
import Data.Time.Clock
data ResourcePoolConf = ResourcePoolConf
{ poolStripes :: Int
, poolTimeout :: NominalDiffTime
, poolLimit :: Int
} deriving (Show)
makeLenses_ ''ResourcePoolConf
deriveFromJSON defaultOptions
{ fieldLabelModifier = camelToPathPiece' 1
} ''ResourcePoolConf

View File

@ -1,4 +1,4 @@
-- SPDX-FileCopyrightText: 2023-2025 Felix Hamann <felix.hamann@campus.lmu.de>,Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Sarah Vaupel <vaupel.sarah@campus.lmu.de>,Steffen Jost <jost@cip.ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Wolfgang Witt <Wolfgang.Witt@campus.lmu.de>,Steffen Jost <s.jost@fraport.de>
-- SPDX-FileCopyrightText: 2023-2025 Sarah Vaupel <sarah.vaupel@uniworx.de>, Felix Hamann <felix.hamann@campus.lmu.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>, Sarah Vaupel <sarah.vaupel@ifi.lmu.de>, Sarah Vaupel <vaupel.sarah@campus.lmu.de>, Steffen Jost <jost@cip.ifi.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>, Wolfgang Witt <Wolfgang.Witt@campus.lmu.de>,Steffen Jost <s.jost@fraport.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
@ -19,7 +19,7 @@ import Settings
import Utils.Parameters
import Utils.Lens
import Text.Blaze (Markup)
import Text.Blaze (Markup, toMarkup)
import qualified Text.Blaze.Internal as Blaze (null)
import qualified Data.Text as T
import qualified Data.Char as C
@ -27,6 +27,7 @@ import qualified Data.Char as C
import Data.CaseInsensitive (CI)
import qualified Data.CaseInsensitive as CI
import Data.Universe
import qualified Data.UUID as UUID
import Data.List (nub, (!!))
import Data.Map.Lazy ((!))
@ -81,6 +82,9 @@ import qualified Data.ByteString.Base64.URL as Base64 (encodeUnpadded)
import qualified Data.ByteString as BS
fvWidget :: FieldView site -> WidgetFor site ()
fvWidget FieldView{..} = $(widgetFile "widgets/field-view/field-view")
------------
-- Fields --
------------
@ -116,6 +120,17 @@ commentField msg = Field {..}
fieldView _ _ _ _ _ = msg2widget msg
fieldEnctype = UrlEncoded
uuidField :: Monad m => Field m UUID
uuidField = Field{..}
where
fieldParse = parseHelperGen $ maybe (Left $ tshow "Invalid UUID!") Right . UUID.fromText
fieldView fvId (toMarkup -> fvLabel) fvAttrs fvInput' fvRequired = fvWidget FieldView{..}
where fvTooltip = Nothing
fvErrors = either (Just . toMarkup) (const Nothing) fvInput'
fvInput = [whamlet|<input type="text" *{fvAttrs} name=#{fvLabel} :fvRequired:required value=#{fvValue}>|]
fvValue = either id UUID.toText fvInput'
fieldEnctype = UrlEncoded
-- | Fields of sets are only allowed indirectly
mkSetField :: (Ord a, Functor m) => Field m [a] -> Field m (Set a)
mkSetField = convertField Set.fromList Set.toList
@ -1273,10 +1288,6 @@ formSection formSectionTitle = do
, fvInput = mempty
})
fvWidget :: FieldView site -> WidgetFor site ()
fvWidget FieldView{..} = $(widgetFile "widgets/field-view/field-view")
doFormHoneypots :: ( MonadHandler m
, HasAppSettings (HandlerSite m)
, YesodAuth (HandlerSite m)

View File

@ -1,4 +1,4 @@
-- SPDX-FileCopyrightText: 2022-2024 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Sarah Vaupel <vaupel.sarah@campus.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Wolfgang Witt <Wolfgang.Witt@campus.lmu.de>,David Mosbach <david.mosbach@uniworx.de>
-- SPDX-FileCopyrightText: 2022-2024 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Sarah Vaupel <vaupel.sarah@campus.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Wolfgang Witt <Wolfgang.Witt@campus.lmu.de>,David Mosbach <david.mosbach@uniworx.de>,Steffen Jost <s.jost@fraport.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
@ -87,6 +87,7 @@ data Icon
| IconNavContainerClose | IconPageActionChildrenClose
| IconMenuNews
| IconMenuHelp
| IconMenuAccount
| IconMenuProfile
| IconMenuLogin | IconMenuLogout
| IconBreadcrumbsHome

View File

@ -269,8 +269,6 @@ makeLenses_ ''ExamOccurrence
makeLenses_ ''ExamOfficeLabel
makePrisms ''AuthenticationMode
makeLenses_ ''CourseUserNote
makeLenses_ ''CourseParticipant

View File

@ -1,4 +1,4 @@
-- SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>
-- SPDX-FileCopyrightText: 2022-2024 Gregor Kleen <gregor.kleen@ifi.lmu.de>, David Mosbach <david.mosbach@uniworx.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
@ -20,6 +20,7 @@ data SessionKey = SessionActiveAuthTags | SessionInactiveAuthTags
| SessionLang
| SessionError
| SessionFiles
| SessionOAuth2Token
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic)
deriving anyclass (Universe, Finite)

View File

@ -1,4 +1,4 @@
-- SPDX-FileCopyrightText: 2022 Steffen Jost <jost@tcs.ifi.lmu.de>
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Steffen Jost <jost@tcs.ifi.lmu.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
@ -12,24 +12,6 @@ module Utils.Users
import Import
data AuthenticationKind = AuthKindLDAP | AuthKindPWHash | AuthKindNoLogin
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Universe, Finite)
--instance Universe AuthenticationKind
--instance Finite AuthenticationKind
embedRenderMessage ''UniWorX ''AuthenticationKind id
nullaryPathPiece ''AuthenticationKind $ camelToPathPiece' 2
mkAuthMode :: AuthenticationKind -> AuthenticationMode
mkAuthMode AuthKindLDAP = AuthLDAP
mkAuthMode AuthKindPWHash = AuthPWHash ""
mkAuthMode AuthKindNoLogin = AuthNoLogin
{-
classifyAuth :: AuthenticationMode -> AuthenticationKind
classifyAuth AuthLDAP = AuthKindLDAP
classifyAuth AuthPWHash{} = AuthKindPWHash
classifyAuth AuthNoLogin = AuthKindNoLogin
-}
data AddUserData = AddUserData
{ audTitle :: Maybe Text
@ -70,44 +52,46 @@ addNewUserDB aud = do
makeUser :: MonadIO m => UserDefaultConf -> AddUserData -> m User
makeUser UserDefaultConf{..} AddUserData{..} = do
now <- liftIO getCurrentTime
return User
{ userIdent = audIdent
, userMaxFavourites = userDefaultMaxFavourites
, userMaxFavouriteTerms = userDefaultMaxFavouriteTerms
, userTheme = userDefaultTheme
, userDateTimeFormat = userDefaultDateTimeFormat
, userDateFormat = userDefaultDateFormat
, userTimeFormat = userDefaultTimeFormat
, userDownloadFiles = userDefaultDownloadFiles
, userWarningDays = userDefaultWarningDays
, userShowSex = userDefaultShowSex
, userExamOfficeGetSynced = userDefaultExamOfficeGetSynced
, userExamOfficeGetLabels = userDefaultExamOfficeGetLabels
, userNotificationSettings = def
, userLanguages = Nothing
, userCsvOptions = def { csvFormat = review csvPreset CsvPresetXlsx }
, userTokensIssuedAfter = Nothing
, userCreated = now
, userLastLdapSynchronisation = Nothing
, userLdapPrimaryKey = audFPersonalNumber
, userLastAuthentication = Nothing
, userEmail = audEmail
, userDisplayName = audDisplayName
, userDisplayEmail = audDisplayEmail
, userFirstName = audFirstName
, userSurname = audSurname
, userTitle = audTitle
, userSex = audSex
, userBirthday = audBirthday
, userMobile = audMobile
, userTelephone = audTelephone
, userCompanyPersonalNumber = audFPersonalNumber
, userCompanyDepartment = audFDepartment
, userPostAddress = audPostAddress
, userPostLastUpdate = Nothing
, userPrefersPostal = audPrefersPostal
, userPinPassword = audPinPassword
, userMatrikelnummer = audMatriculation
, userAuthentication = mkAuthMode audAuth
}
UserDefaultConf{..} <- getsYesod $ view _appUserDefaults
let
newUser = User
{ userIdent = audIdent
, userMaxFavourites = userDefaultMaxFavourites
, userMaxFavouriteTerms = userDefaultMaxFavouriteTerms
, userTheme = userDefaultTheme
, userDateTimeFormat = userDefaultDateTimeFormat
, userDateFormat = userDefaultDateFormat
, userTimeFormat = userDefaultTimeFormat
, userDownloadFiles = userDefaultDownloadFiles
, userWarningDays = userDefaultWarningDays
, userShowSex = userDefaultShowSex
, userExamOfficeGetSynced = userDefaultExamOfficeGetSynced
, userExamOfficeGetLabels = userDefaultExamOfficeGetLabels
, userNotificationSettings = def
, userLanguages = Nothing
, userCsvOptions = def { csvFormat = review csvPreset CsvPresetXlsx }
, userTokensIssuedAfter = Nothing
, userCreated = now
, userLastLdapSynchronisation = Nothing
, userLdapPrimaryKey = audFPersonalNumber
, userLastAuthentication = Nothing
, userEmail = audEmail
, userDisplayName = audDisplayName
, userDisplayEmail = audDisplayEmail
, userFirstName = audFirstName
, userSurname = audSurname
, userTitle = audTitle
, userSex = audSex
, userBirthday = audBirthday
, userMobile = audMobile
, userTelephone = audTelephone
, userCompanyPersonalNumber = audFPersonalNumber
, userCompanyDepartment = audFDepartment
, userPostAddress = audPostAddress
, userPostLastUpdate = Nothing
, userPrefersPostal = audPrefersPostal
, userPinPassword = audPinPassword
, userMatrikelnummer = audMatriculation
, userLastSync = Nothing -- TODO: combine add user with external sync?
}
runDB $ insertUnique newUser

View File

@ -30,7 +30,7 @@ import Control.Lens.Extras
import Foundation.Servant.Types
import Utils hiding (HasRoute)
import Model.Types.Security
import Model.Types.Auth
import Yesod.Core ( Yesod
, RenderRoute(..), ParseRoute(..)

View File

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>
# SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>
#
# SPDX-License-Identifier: AGPL-3.0-or-later

View File

@ -0,0 +1,46 @@
$newline never
$# SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Steffen Jost <jost@tcs.ifi.lmu.de>
$#
$# SPDX-License-Identifier: AGPL-3.0-or-later
<section>
<p>
Query external user databases:
^{personForm}
$maybe responses <- mbData
<h1>
Responses: #
<dl .deflist>
$forall (source,responses) <- responses
<dt .deflist__dt>
$case source
$of AuthSourceIdAzure tenantId
Azure Tenant ID: #
#{tshow tenantId}
$of AuthSourceIdLdap ldapHost
LDAP host: #
#{ldapHost}
<dd .deflist__dd>
<pre>
#{responses}
$# <dl .deflist>
$# $forall (k,(numv,vUtf8,vLatin1)) <- responses
$# <dt .deflist__dt>
$# #{k}
$# $if 1 < numv
$# \ (#{show numv})
$# <dd .deflist__dd>
$# UTF8: #{vUtf8}
$# &#8212;
$# Latin: #{vLatin1}
<section>
<p>
Upsert user from external database:
^{upsertForm}
$maybe response <- mbUpsert
<h1>
Response: #
<p>
#{tshow response}

View File

@ -1,6 +1,6 @@
$newline never
$# SPDX-FileCopyrightText: 2022 Felix Hamann <felix.hamann@campus.lmu.de>,Sarah Vaupel <vaupel.sarah@campus.lmu.de>,Steffen Jost <jost@cip.ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>
$# SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Felix Hamann <felix.hamann@campus.lmu.de>, Sarah Vaupel <vaupel.sarah@campus.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>
$#
$# SPDX-License-Identifier: AGPL-3.0-or-later

View File

@ -0,0 +1,7 @@
$newline never
$# SPDX-FileCopyrightText: 2024 Sarah Vaupel <sarah.vaupel@uniworx.de>
$#
$# SPDX-License-Identifier: AGPL-3.0-or-later
FRADrive-Benutzer können sich nun über ihr Azure-Benutzerkonto in FRADrive anmelden

View File

@ -0,0 +1,7 @@
$newline never
$# SPDX-FileCopyrightText: 2024 Sarah Vaupel <sarah.vaupel@uniworx.de>
$#
$# SPDX-License-Identifier: AGPL-3.0-or-later
FRADrive users can now log in to FRADrive using their Azure user account

View File

@ -1,33 +0,0 @@
$newline never
$# SPDX-FileCopyrightText: 2022 Steffen Jost <jost@tcs.ifi.lmu.de>
$#
$# SPDX-License-Identifier: AGPL-3.0-or-later
<section>
<p>
LDAP Person Search:
^{personForm}
$maybe answers <- mbLdapData
<h1>
Antwort: #
<dl .deflist>
$forall (lk, lv) <- answers
$with numv <- length lv
<dt>
#{show lk}
$if 1 < numv
\ (#{show numv})
<dd>
UTF8: #{presentUtf8 lv}
&#8212;
Latin: #{presentLatin1 lv}
<section>
<p>
LDAP Upsert user in DB:
^{upsertForm}
$maybe answer <- mbLdapUpsert
<h1>
Antwort: #
<p>
#{tshow answer}

View File

@ -1,20 +1,33 @@
$newline never
$# SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>
$# SPDX-FileCopyrightText: 2022-2024 Gregor Kleen <gregor.kleen@ifi.lmu.de>, David Mosbach <david.mosbach@uniworx.de>
$#
$# SPDX-License-Identifier: AGPL-3.0-or-later
$forall AuthPlugin{apName, apLogin} <- plugins
$if apName == "LDAP"
$if apName == apAzure
<section>
<h2>Azure
^{apLogin toParent}
$elseif apName == apAzureMock
<section>
<h2>_{MsgDummyLoginTitle}
^{apLogin toParent}
$elseif apName == apLdap
<section>
<h2>_{MsgLDAPLoginTitle}
^{apLogin toParent}
$elseif apName == "PWHash"
$elseif apName == apHash
<section>
<h2>_{MsgPWHashLoginTitle}
<p>_{MsgPWHashLoginNote}
^{apLogin toParent}
$elseif apName == "dummy"
$elseif apName == apDummy
<section>
<h2>_{MsgDummyLoginTitle}
^{apLogin toParent}
$maybe port <- mPort
<section>
<h2>SSO Dev Test
<a href=http://localhost:#{port}/test-sso>Test login via single sign-on

View File

@ -1,6 +1,6 @@
$newline never
$# SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>
$# SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>
$#
$# SPDX-License-Identifier: AGPL-3.0-or-later
@ -19,21 +19,16 @@ $# SPDX-License-Identifier: AGPL-3.0-or-later
}
<body>
<h1>
$case userAuthentication
$of AuthLDAP
_{SomeMessage MsgUserAuthModeChangedToLDAP}
$of AuthPWHash _
_{SomeMessage MsgUserAuthModeChangedToPWHash}
$of AuthNoLogin
_{SomeMessage MsgUserAuthModeChangedToNoLogin}
$if is _Just userPasswordHash
_{SomeMessage MsgUserAuthPasswordEnabled}
$else
_{SomeMessage MsgUserAuthPasswordDisabled}
<p>
<a href=@{NewsR}>
_{SomeMessage MsgMailFradrive} #
_{SomeMessage MsgMailBodyFradrive}
$if is _AuthPWHash userAuthentication
<p>
_{SomeMessage MsgAuthPWHashTip}
$if is _Just userPasswordHash
<dl>
<dt>
_{SomeMessage MsgPWHashIdent}
@ -42,6 +37,9 @@ $# SPDX-License-Identifier: AGPL-3.0-or-later
<dt>_{SomeMessage MsgPWHashPassword}
<dd>
_{SomeMessage MsgPasswordResetEmailIncoming}
$else
<p>
_{SomeMessage MsgAuthExternalLoginTip}
$if is _Just userLastAuthentication
^{editNotifications}

View File

@ -1,6 +1,6 @@
$newline never
$# SPDX-FileCopyrightText: 2022-25 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <vaupel.sarah@campus.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>,Steffen Jost <s.jost@fraport.de>
$# SPDX-FileCopyrightText: 2022-2025 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <vaupel.sarah@campus.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>,Steffen Jost <s.jost@fraport.de>
$#
$# SPDX-License-Identifier: AGPL-3.0-or-later
@ -135,6 +135,25 @@ $# SPDX-License-Identifier: AGPL-3.0-or-later
_{MsgUserCreated}
<dd .deflist__dd>
^{formatTimeW SelFormatDateTime userCreated}
<dt .deflist__dt>
_{MsgAdminUserAuthentication}
<dd .deflist__dd>
$if null externalUsers && is _Nothing userPasswordHash
_{MsgAuthKindNoLogin}
$else
<ul>
$if is _Just userPasswordHash
<li>_{MsgAuthKindPWHash}
$forall (authIdent, sourceIdent, lsync) <- externalUsers
<li>
$case sourceIdent
$of AuthSourceIdAzure _clientId
_{MsgAuthKindAzure}: #
$of AuthSourceIdLdap _sourceId
_{MsgAuthKindLDAP}: #
#{authIdent} #
<span .comment>
(_{MsgAdminUserAuthLastSync}: ^{formatTimeW SelFormatDateTime lsync})
<dt .deflist__dt>
_{MsgLastLogin}
<dd .deflist__dd>
@ -142,18 +161,6 @@ $# SPDX-License-Identifier: AGPL-3.0-or-later
^{formatTimeW SelFormatDateTime llogin}
$nothing
_{MsgNeverSet}
<dt .deflist__dt>
_{MsgProfileLastLdapSynchronisation}
<dd .deflist__dd>
$maybe lsync <- userLastLdapSynchronisation
^{formatTimeW SelFormatDateTime lsync}
$nothing
_{MsgNeverSet}
$maybe pKey <- userLdapPrimaryKey
<dt .deflist__dt>
_{MsgProfileLdapPrimaryKey}
<dd .deflist__dd .ldap-primary-key>
#{pKey}
<dt .deflist__dt>
_{MsgTokensLastReset}
<dd .deflist__dd>

View File

@ -83,8 +83,9 @@ fillDb = do
gkleen <- insert User
{ userIdent = "G.Kleen@campus.lmu.de"
, userAuthentication = AuthLDAP
, userPasswordHash = Nothing
, userLastAuthentication = Just now
, userLastSync = Just now
, userTokensIssuedAfter = Just now
, userMatrikelnummer = Just "99"
, userEmail = "G.Kleen@campus.lmu.de"
@ -104,8 +105,6 @@ fillDb = do
, userLanguages = Just $ Languages ["en"]
, userNotificationSettings = def
, userCreated = now
, userLastLdapSynchronisation = Nothing
, userLdapPrimaryKey = Nothing
, userCsvOptions = def { csvFormat = csvPreset # CsvPresetRFC }
, userSex = Just SexMale
, userBirthday = Nothing
@ -123,8 +122,9 @@ fillDb = do
}
fhamann <- insert User
{ userIdent = "felix.hamann@campus.lmu.de"
, userAuthentication = AuthLDAP
, userPasswordHash = Nothing
, userLastAuthentication = Nothing
, userLastSync = Nothing
, userTokensIssuedAfter = Nothing
, userMatrikelnummer = Nothing
, userEmail = "AVSNO:123456"
@ -144,8 +144,6 @@ fillDb = do
, userLanguages = Nothing
, userNotificationSettings = def
, userCreated = now
, userLastLdapSynchronisation = Nothing
, userLdapPrimaryKey = Nothing
, userCsvOptions = def { csvFormat = csvPreset # CsvPresetExcel }
, userSex = Just SexMale
, userShowSex = userDefaultShowSex
@ -165,12 +163,12 @@ fillDb = do
let pw = "123.456"
PWHashConf{..} <- getsYesod $ view _appAuthPWHash
pwHash <- liftIO $ PWStore.makePasswordWith pwHashAlgorithm pw pwHashStrength
return $ AuthPWHash $ TEnc.decodeUtf8 pwHash
return $ TEnc.decodeUtf8 pwHash
jost <- insert User
{ userIdent = "jost@tcs.ifi.lmu.de"
-- , userAuthentication = AuthLDAP
, userAuthentication = pwSimple
, userPasswordHash = Just pwSimple
, userLastAuthentication = Nothing
, userLastSync = Nothing
, userTokensIssuedAfter = Nothing
, userMatrikelnummer = Just "12345678"
, userEmail = "S.Jost@Fraport.de"
@ -190,8 +188,6 @@ fillDb = do
, userLanguages = Nothing
, userNotificationSettings = def
, userCreated = now
, userLastLdapSynchronisation = Nothing
, userLdapPrimaryKey = Nothing
, userSex = Just SexMale
, userBirthday = Just $ n_day $ 35 * (-365)
, userCsvOptions = def
@ -209,8 +205,9 @@ fillDb = do
}
maxMuster <- insert User
{ userIdent = "max@campus.lmu.de"
, userAuthentication = AuthLDAP
, userPasswordHash = Nothing
, userLastAuthentication = Just now
, userLastSync = Just now
, userTokensIssuedAfter = Nothing
, userMatrikelnummer = Just "1299"
, userEmail = "max@campus.lmu.de"
@ -230,8 +227,6 @@ fillDb = do
, userLanguages = Just $ Languages ["de"]
, userNotificationSettings = def
, userCreated = now
, userLastLdapSynchronisation = Nothing
, userLdapPrimaryKey = Nothing
, userCsvOptions = def
, userSex = Just SexMale
, userBirthday = Just $ n_day $ 27 * (-365)
@ -249,8 +244,9 @@ fillDb = do
}
tinaTester <- insert $ User
{ userIdent = "tester@campus.lmu.de"
, userAuthentication = AuthNoLogin
, userPasswordHash = Nothing
, userLastAuthentication = Nothing
, userLastSync = Nothing
, userTokensIssuedAfter = Nothing
, userMatrikelnummer = Just "999"
, userEmail = "tester@campus.lmu.de"
@ -270,8 +266,6 @@ fillDb = do
, userLanguages = Just $ Languages ["sn"]
, userNotificationSettings = def
, userCreated = now
, userLastLdapSynchronisation = Nothing
, userLdapPrimaryKey = Nothing
, userCsvOptions = def
, userSex = Just SexNotApplicable
, userBirthday = Just $ n_day 3
@ -289,8 +283,9 @@ fillDb = do
}
svaupel <- insert User
{ userIdent = "vaupel.sarah@campus.lmu.de"
, userAuthentication = AuthLDAP
, userPasswordHash = Nothing
, userLastAuthentication = Nothing
, userLastSync = Nothing
, userTokensIssuedAfter = Nothing
, userMatrikelnummer = Just "365"
, userEmail = "vaupel.sarah@campus.lmu.de"
@ -310,8 +305,6 @@ fillDb = do
, userLanguages = Nothing
, userNotificationSettings = def
, userCreated = now
, userLastLdapSynchronisation = Nothing
, userLdapPrimaryKey = Nothing
, userCsvOptions = def
, userSex = Just SexFemale
, userBirthday = Nothing
@ -329,8 +322,9 @@ fillDb = do
}
sbarth <- insert User
{ userIdent = "Stephan.Barth@campus.lmu.de"
, userAuthentication = AuthLDAP
, userPasswordHash = Nothing
, userLastAuthentication = Nothing
, userLastSync = Nothing
, userTokensIssuedAfter = Nothing
, userMatrikelnummer = Nothing
, userEmail = "Stephan.Barth@lmu.de"
@ -350,8 +344,6 @@ fillDb = do
, userLanguages = Nothing
, userNotificationSettings = def
, userCreated = now
, userLastLdapSynchronisation = Nothing
, userLdapPrimaryKey = Nothing
, userCsvOptions = def
, userSex = Just SexMale
, userBirthday = Nothing
@ -369,8 +361,9 @@ fillDb = do
}
stranger1 <- insert User
{ userIdent = "AVSID:996699"
, userAuthentication = AuthLDAP
, userPasswordHash = Nothing
, userLastAuthentication = Nothing
, userLastSync = Nothing
, userTokensIssuedAfter = Nothing
, userMatrikelnummer = Nothing
, userEmail = "E996699@fraport.de"
@ -390,8 +383,6 @@ fillDb = do
, userLanguages = Nothing
, userNotificationSettings = def
, userCreated = now
, userLastLdapSynchronisation = Nothing
, userLdapPrimaryKey = Nothing
, userCsvOptions = def
, userSex = Just SexMale
, userBirthday = Nothing
@ -409,8 +400,9 @@ fillDb = do
}
stranger2 <- insert User
{ userIdent = "AVSID:669966"
, userAuthentication = AuthLDAP
, userPasswordHash = Nothing
, userLastAuthentication = Nothing
, userLastSync = Nothing
, userTokensIssuedAfter = Nothing
, userMatrikelnummer = Nothing
, userEmail = "E669966@fraport.de"
@ -430,8 +422,6 @@ fillDb = do
, userLanguages = Nothing
, userNotificationSettings = def
, userCreated = now
, userLastLdapSynchronisation = Nothing
, userLdapPrimaryKey = Nothing
, userCsvOptions = def
, userSex = Just SexMale
, userBirthday = Nothing
@ -449,8 +439,9 @@ fillDb = do
}
stranger3 <- insert User
{ userIdent = "AVSID:6969"
, userAuthentication = AuthLDAP
, userPasswordHash = Nothing
, userLastAuthentication = Nothing
, userLastSync = Nothing
, userTokensIssuedAfter = Nothing
, userMatrikelnummer = Nothing
, userEmail = "E6969@fraport.de"
@ -470,8 +461,6 @@ fillDb = do
, userLanguages = Nothing
, userNotificationSettings = def
, userCreated = now
, userLastLdapSynchronisation = Nothing
, userLdapPrimaryKey = Nothing
, userCsvOptions = def
, userSex = Just SexMale
, userBirthday = Nothing
@ -528,8 +517,9 @@ fillDb = do
middlenames = [ Nothing, Just "Jamesson", Just "Theresa", Just "Ally", Just "Tiberius", Just "Maria" ]
manyUser (firstName, middleName, userSurname) userMatrikelnummer' = User
{ userIdent
, userAuthentication = AuthLDAP
, userPasswordHash = Nothing
, userLastAuthentication = Nothing
, userLastSync = Nothing
, userTokensIssuedAfter = Nothing
, userMatrikelnummer = Just userMatrikelnummer'
, userEmail = userEmail'
@ -551,8 +541,6 @@ fillDb = do
, userLanguages = Nothing
, userNotificationSettings = def
, userCreated = now
, userLastLdapSynchronisation = Nothing
, userLdapPrimaryKey = Nothing
, userCsvOptions = def
, userSex = Nothing
, userBirthday = Nothing

View File

@ -0,0 +1,231 @@
# SPDX-FileCopyrightText: 2024 David Mosbach <david.mosbach@uniworx.de>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
special-users:
- default: &default-user
userIdent: null
userAuthentication: AuthLDAP
userLastAuthentication: null
userTokensIssuedAfter: null
userMatrikelnummer: null
userEmail: ""
userDisplayEmail: null
userDisplayName: null
userSurname: ""
userFirstName: ""
userTitle: null
userMaxFavourites: userDefaultMaxFavourites
userMaxFavouriteTerms: userDefaultMaxFavouriteTerms
userTheme: ThemeDefault
userDateTimeFormat: userDefaultDateTimeFormat
userDateFormat: userDefaultDateFormat
userTimeFormat: userDefaultTimeFormat
userDownloadFiles: userDefaultDownloadFiles
userWarningDays: userDefaultWarningDays
userLanguages: null
userCreated: now
userNotificationSettings: def
userLastLdapSynchronisation: null
userLdapPrimaryKey: null
userCsvOptions: def
userSex: null
userBirthday: null
userShowSex: userDefaultShowSex
userTelephone: null
userMobile: null
userCompanyPersonalNumber: null
userCompanyDepartment: null
userPinPassword: null
userPostAddress: null
userPostLastUpdate: null
userPrefersPostal: true
userExamOfficeGetSynced: userDefaultExamOfficeGetSynced
userExamOfficeGetLabels: userDefaultExamOfficeGetLabels
- gkleen:
<<: *default-user
userIdent: "G.Kleen@campus.lmu.de"
userLastAuthentication: now
userTokensIssuedAfter: now
userEmail: "G.Kleen@campus.lmu.de"
userDisplayEmail: "gregor.kleen@ifi.lmu.de"
userDisplayName: "Gregor Kleen"
userSurname: "Kleen"
userFirstName: "Gregor Julius Arthur"
userMaxFavourites: 6
userMaxFavouriteTerms: 1
userLanguages: ["en"]
# userCsvOptions = def { csvFormat = csvPreset # CsvPresetRFC }
userSex: SexMale
userCompanyPersonalNumber: "00000"
userPostAddress: "Büro 127 \nMathematisches Institut der Ludwig-Maximilians-Universität München \nTheresienstr. 39 \nD-80333 München"
- fhamann:
<<: *default-user
userIdent: "felix.hamann@campus.lmu.de"
userEmail: "noEmailKnown"
userDisplayEmail: "felix.hamann@campus.lmu.de"
userDisplayName: "Felix Hamann"
userSurname: "Hamann"
userFirstName: "Felix"
# userCsvOptions = def { csvFormat = csvPreset # CsvPresetExcel }
userSex: SexMale
userPinPassword: "tomatenmarmelade"
userPostAddress: "Erdbeerweg 24 \n12345 Schlumpfhausen \nTraumland"
- jost:
<<: *default-user
userIdent: "jost@tcs.ifi.lmu.de"
userAuthentication: pwSimple
userMatrikelnummer: "12345678"
userEmail: "S.Jost@Fraport.de"
userDisplayEmail: "jost@tcs.ifi.lmu.de"
userDisplayName: "Steffen Jost"
userSurname: "Jost"
userFirstName: "Steffen"
userTitle: "Dr."
userMaxFavourites: 14
userMaxFavouriteTerms: 4
userTheme: ThemeMossGreen
userSex: SexMale
# userBirthday = Just $ n_day $ 35 * (-365)
userTelephone: "+49 69 690-71706"
userMobile: "0173 69 99 646"
userCompanyPersonalNumber: "57138"
userCompanyDepartment: "AVN-AR2"
- maxMuster:
<<: *default-user
userIdent: "max@campus.lmu.de"
userLastAuthentication: now
userMatrikelnummer: "1299"
userEmail: "max@campus.lmu.de"
userDisplayEmail: "max@max.com"
userDisplayName: "Max Musterstudent"
userSurname: "Musterstudent"
userFirstName: "Max"
userMaxFavourites: 7
userTheme: ThemeAberdeenReds
userLanguages: ["de"]
userSex: SexMale
# userBirthday = Just $ n_day $ 27 * (-365)
userPrefersPostal: false
- tinaTester:
<<: *default-user
userIdent: "tester@campus.lmu.de"
userAuthentication: null
userMatrikelnummer: "999"
userEmail: "tester@campus.lmu.de"
userDisplayEmail: "tina@tester.example"
userDisplayName: "Tina Tester"
userSurname: "vön Tërrör¿"
userFirstName: "Sabrina"
userTitle: "Magister"
userMaxFavourites: 5
userTheme: ThemeAberdeenReds
userLanguages: ["sn"]
userSex: SexNotApplicable
# userBirthday = Just $ n_day 3
userCompanyPersonalNumber: "12345"
userPrefersPostal: false
- svaupel:
<<: *default-user
userIdent: "vaupel.sarah@campus.lmu.de"
userEmail: "vaupel.sarah@campus.lmu.de"
userDisplayEmail: "vaupel.sarah@campus.lmu.de"
userDisplayName: "Sarah Vaupel"
userSurname: "Vaupel"
userFirstName: "Sarah"
userMaxFavourites: 14
userMaxFavouriteTerms: 4
userTheme: ThemeMossGreen
userLanguages: null
userSex: SexFemale
userPrefersPostal: false
- sbarth:
<<: *default-user
userIdent: "Stephan.Barth@campus.lmu.de"
userEmail: "Stephan.Barth@lmu.de"
userDisplayEmail: "stephan.barth@ifi.lmu.de"
userDisplayName: "Stephan Barth"
userSurname: "Barth"
userFirstName: "Stephan"
userTheme: ThemeMossGreen
userSex: SexMale
userPrefersPostal: false
userExamOfficeGetSynced: false
userExamOfficeGetLabels: true
- _stranger1:
userIdent: "AVSID:996699"
userEmail: "E996699@fraport.de"
userDisplayEmail: ""
userDisplayName: "Stranger One"
userSurname: "One"
userFirstName: "Stranger"
userTheme: ThemeMossGreen
userSex: SexMale
userCompanyPersonalNumber: "E996699"
userCompanyDepartment: "AVN-Strange"
userPrefersPostal: false
userExamOfficeGetSynced: false
userExamOfficeGetLabels: true
- _stranger2:
userIdent: "AVSID:669966"
userEmail: "E669966@fraport.de"
userDisplayEmail: ""
userDisplayName: "Stranger Two"
userSurname: "Stranger"
userFirstName: "Two"
userTheme: ThemeMossGreen
userSex: SexMale
userCompanyPersonalNumber: "669966"
userCompanyDepartment: "AVN-Strange"
userPrefersPostal: false
userExamOfficeGetSynced: false
userExamOfficeGetLabels: true
- _stranger3:
userIdent: "AVSID:6969"
userEmail: "E6969@fraport.de"
userDisplayEmail: ""
userDisplayName: "Stranger 3 Three"
userSurname: "Three"
userFirstName: "Stranger"
userTheme: ThemeMossGreen
userSex: SexMale
userCompanyPersonalNumber: "E996699"
userCompanyDepartment: "AVN-Strange"
userPostAddress: "Kartoffelweg 12 \n666 Höllensumpf \nFreiland"
userPrefersPostal: false
userExamOfficeGetSynced: false
userExamOfficeGetLabels: true
random-users:
firstNames: [ "James", "John", "Robert", "Michael"
, "William", "David", "Mary", "Richard"
, "Joseph", "Thomas", "Charles", "Daniel"
, "Matthew", "Patricia", "Jennifer", "Linda"
, "Elizabeth", "Barbara", "Anthony", "Donald"
, "Mark", "Paul", "Steven", "Andrew"
, "Kenneth", "Joshua", "George", "Kevin"
, "Brian", "Edward", "Susan", "Ronald"
]
surnames: [ "Smith", "Johnson", "Williams", "Brown"
, "Jones", "Miller", "Davis", "Garcia"
, "Rodriguez", "Wilson", "Martinez", "Anderson"
, "Taylor", "Thomas", "Hernandez", "Moore"
, "Martin", "Jackson", "Thompson", "White"
, "Lopez", "Lee", "Gonzalez", "Harris"
, "Clark", "Lewis", "Robinson", "Walker"
, "Perez", "Hall", "Young", "Allen"
]
middlenames: [ null, "Jamesson" ]

View File

@ -51,7 +51,6 @@ makeUsers (fromIntegral -> n) = do
let baseid = "user." <> tshow i
u' = u { userIdent = CI.mk baseid
, userEmail = CI.mk $ baseid <> "@example.com"
, userLdapPrimaryKey = Just $ baseid <> ".ldap"
}
return u'
uids <- insertMany users

View File

@ -10,7 +10,6 @@ module Model.TypesSpec
import TestImport
import TestInstances ()
import Settings
import Utils (guardOn)
@ -21,7 +20,6 @@ import qualified Data.Aeson.Types as Aeson
import Model.Types.LanguagesSpec ()
import System.IO.Unsafe
import Yesod.Auth.Util.PasswordStore
import Database.Persist.Sql (SqlBackend, fromSqlKey, toSqlKey)
@ -218,21 +216,6 @@ instance Arbitrary Value where
arbitrary' = scale (`div` 2) arbitrary
shrink = genericShrink
instance Arbitrary AuthenticationMode where
arbitrary = oneof
[ pure AuthLDAP
, do
pw <- encodeUtf8 . pack . getPrintableString <$> arbitrary
let
PWHashConf{..} = appAuthPWHash compileTimeAppSettings
authPWHash = unsafePerformIO . fmap decodeUtf8 $ makePasswordWith pwHashAlgorithm pw (pwHashStrength `div` 2)
return $ AuthPWHash{..}
]
shrink AuthLDAP = []
shrink AuthNoLogin = []
shrink (AuthPWHash _) = [AuthLDAP]
instance Arbitrary LecturerType where
arbitrary = genericArbitrary
shrink = genericShrink
@ -469,8 +452,6 @@ spec = do
[ eqLaws, ordLaws, boundedEnumLaws, showReadLaws, jsonLaws, finiteLaws, pathPieceLaws, persistFieldLaws ]
lawsCheckHspec (Proxy @CorrectorState)
[ eqLaws, ordLaws, showReadLaws, boundedEnumLaws, jsonLaws, finiteLaws, pathPieceLaws, persistFieldLaws ]
lawsCheckHspec (Proxy @AuthenticationMode)
[ eqLaws, ordLaws, showReadLaws, jsonLaws, persistFieldLaws ]
lawsCheckHspec (Proxy @Value)
[ persistFieldLaws ]
lawsCheckHspec (Proxy @Scientific)

View File

@ -1,4 +1,4 @@
-- SPDX-FileCopyrightText: 2022-2024 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@cip.ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@cip.ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
@ -9,7 +9,7 @@ module ModelSpec where
import TestImport
import Settings (getTimeLocale', VerpMode(..))
import Settings
import Model.TypesSpec ()
import MailSpec ()
@ -34,9 +34,10 @@ import qualified Data.CryptoID.Class.ImplicitNamespace as Implicit
import qualified Data.CryptoID.Class as Explicit
import Data.Binary.SerializationLength
import Control.Monad.Catch.Pure (Catch, runCatch)
import System.IO.Unsafe
import Yesod.Auth.Util.PasswordStore
import System.IO.Unsafe (unsafePerformIO)
import Control.Monad.Catch.Pure (Catch, runCatch)
import Data.Universe
@ -103,7 +104,12 @@ instance Arbitrary User where
[ getPrintableString <$> arbitrary
, on (\l d -> l <> "@" <> d) getPrintableString <$> arbitrary <*> arbitrary
]
userAuthentication <- arbitrary
userPasswordHash <-
let genPwd = do
pw <- encodeUtf8 . pack . getPrintableString <$> arbitrary
let PWHashConf{..} = appAuthPWHash compileTimeAppSettings
return . unsafePerformIO . fmap decodeUtf8 $ makePasswordWith pwHashAlgorithm pw (pwHashStrength `div` 2)
in oneof [ pure Nothing, Just <$> genPwd ]
userLastAuthentication <- arbitrary
userTokensIssuedAfter <- arbitrary
userMatrikelnummer <- fmap pack . assertM' (not . null) <$> listOf (elements ['0'..'9'])
@ -147,14 +153,7 @@ instance Arbitrary User where
userExamOfficeGetLabels <- arbitrary
userCreated <- arbitrary
userLastLdapSynchronisation <- arbitrary
userLdapPrimaryKey <- oneof
[ pure Nothing
, fmap Just $ pack <$> oneof
[ getPrintableString <$> arbitrary
, on (\l d -> l <> "@" <> d) getPrintableString <$> arbitrary <*> arbitrary
]
]
userLastSync <- arbitrary
return User{..}
shrink = genericShrink

View File

@ -1,4 +1,4 @@
-- SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
@ -9,7 +9,8 @@ module TestImport
, module X
) where
import Application (makeFoundation, makeMiddleware, shutdownApp)
import Application (makeFoundation, shutdownApp)
import Middleware (makeMiddleware)
import ClassyPrelude as X
hiding ( delete, deleteBy
, Handler, Index

Some files were not shown because too many files have changed in this diff Show More