Compare commits

...
This repository has been archived on 2024-10-24. You can view files and clone it, but cannot push or open issues or pull requests.

222 Commits

Author SHA1 Message Date
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
103 changed files with 5261 additions and 1934 deletions

View File

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2022-2023 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor@kleen.consulting>
# SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor@kleen.consulting>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
@ -35,6 +35,7 @@ node dependencies:
stage: frontend:build
script:
- nix -L build -o result ".#uniworxNodeDependencies"
- nix-store --gc
- nix-store --export $(nix-store -qR result) | xz -T0 -2 > node-dependencies.nar.xz
before_script: &nix-before
- git config --global init.defaultBranch master
@ -54,8 +55,12 @@ node dependencies:
well known:
stage: frontend:build
script:
# - xzcat node-dependencies.nar.xz > node-dependencies-debug.nar
# - nix-shell -p util-linux --command "hexdump -C node-dependencies-debug.nar | head -n 10"
# - nix nar ls node-dependencies-debug.nar /
- xzcat node-dependencies.nar.xz | nix-store --import
- nix -L build -o result ".#uniworxWellKnown"
- nix-store --gc
- nix-store --export $(nix-store -qR result) | xz -T0 -2 > well-known.nar.xz
before_script: *nix-before
needs:
@ -75,6 +80,7 @@ frontend:
- xzcat node-dependencies.nar.xz | nix-store --import
- xzcat well-known.nar.xz | nix-store --import
- nix -L build -o result ".#uniworxFrontend"
- nix-store --gc
- nix-store --export $(nix-store -qR result) | xz -T0 -2 > frontend.nar.xz
before_script: *nix-before
needs:
@ -95,6 +101,7 @@ uniworx:lib:uniworx:
script:
- xzcat frontend.nar.xz | nix-store --import
- nix -L build -o result ".#uniworx:lib:uniworx"
- nix-store --gc
- nix-store --export $(nix-store -qR result) | xz -T0 -2 > uniworx:lib:uniworx.nar.xz
before_script: *nix-before
needs:
@ -117,6 +124,7 @@ uniworx:exe:uniworx:
script:
- xzcat uniworx:lib:uniworx.nar.xz | nix-store --import
- nix -L build -o result ".#uniworx:exe:uniworx"
- nix-store --gc
- nix-store --export $(nix-store -qR result) | xz -T0 -2 > uniworx:exe:uniworx.nar.xz
before_script: *nix-before
needs:
@ -141,6 +149,7 @@ uniworx:exe:uniworxdb:
script:
- xzcat uniworx:lib:uniworx.nar.xz | nix-store --import
- nix -L build -o result ".#uniworx:exe:uniworxdb"
- nix-store --gc
- nix-store --export $(nix-store -qR result) | xz -T0 -2 > uniworx:exe:uniworxdb.nar.xz
before_script: *nix-before
needs:
@ -165,6 +174,7 @@ uniworx:exe:uniworxload:
script:
- xzcat uniworx:lib:uniworx.nar.xz | nix-store --import
- nix -L build -o result ".#uniworx:exe:uniworxload"
- nix-store --gc
- nix-store --export $(nix-store -qR result) | xz -T0 -2 > uniworx:exe:uniworxload.nar.xz
before_script: *nix-before
needs:
@ -207,8 +217,13 @@ container:
stage: container:build
script:
- xzcat uniworx:exe:uniworx.nar.xz | nix-store --import
- cp -pr --reflink=auto -L $(nix build --print-out-paths ".#uniworxDocker") uniworx.tar.gz
# - &container-remove-nodejs
# "for i in `ls /nix/store/ | grep -E -i '^[a-z0-9]+-nodejs-'` ; do ( nix store delete --ignore-liveness \"/nix/store/$i\" || ( echo \"Could not remove NodeJS from /nix/store/ due to: \" && ( nix-store --query --roots \"/nix/store/$i\" | cat ) && echo \"Removing NodeJS by brute force...\" && rm -rf \"/nix/store/$i\" ) ) ; done"
- cp -p --reflink=auto -L $(nix build --print-out-paths ".#uniworxDocker") uniworx.tar.gz
before_script: *nix-before
# TODO: reintroduce working version of after_script
# after_script: &container-fail-on-nodejs
# - (ls /nix/store/ | grep -E -i '^[a-z0-9]+-nodejs-') && (echo "NodeJS remainder in container /nix/store!" 1>&2; exit 1)
needs:
- job: node dependencies # transitive
artifacts: false
@ -235,8 +250,10 @@ test container:
stage: container:build
script:
- xzcat uniworx:exe:uniworx.nar.xz | nix-store --import
- cp -pr --reflink=auto -L $(nix build --print-out-paths ".#uniworxTestDocker") uniworx.tar.gz
# - *container-remove-nodejs
- cp -p --reflink=auto -L $(nix build --print-out-paths ".#uniworxTestDocker") uniworx.tar.gz
before_script: *nix-before
# after_script: *container-fail-on-nodejs
needs:
- job: node dependencies # transitive
artifacts: false
@ -259,6 +276,128 @@ test container:
interruptible: true
rules: &test-release-rules
- if: $CI_COMMIT_TAG =~ /^t/
dev container:
stage: container:build
script:
- xzcat uniworx:exe:uniworx.nar.xz | nix-store --import
- cp -p --reflink=auto -L $(nix build --print-out-paths ".#uniworxDevDocker") uniworx.tar.gz
before_script: *nix-before
needs:
- job: node dependencies # transitive
artifacts: false
- job: well known # transitive
artifacts: false
- job: frontend # tranitive
artifacts: false
- job: uniworx:lib:uniworx # transitive
artifacts: false
- job: uniworx:exe:uniworx
artifacts: true
- job: check # sanity
artifacts: false
artifacts:
paths:
- uniworx.tar.gz
name: "${CI_JOB_NAME}-${CI_COMMIT_SHORT_SHA}"
expire_in: "1 day"
retry: 2
interruptible: true
rules: &dev-release-rules
- if: $CI_COMMIT_TAG =~ /^d/
sanitize container:
stage: container:build
needs:
- job: node dependencies # transitive
artifacts: false
- job: well known # transitive
artifacts: false
- job: frontend # transitive
artifacts: false
- job: uniworx:lib:uniworx # transitive
artifacts: false
- job: uniworx:exe:uniworx # transitive
artifacts: false
- job: check # sanity
artifacts: false
- job: container
artifacts: true
before_script: *nix-before
script:
- nix shell nixpkgs#perl --command ./.gitlab-ci/sanitize-docker.pl
after_script:
- tar xzvf uniworx-sanitized.tar.gz
- for i in `tar tf */layer.tar | grep 'nix/store/[0-9a-z]*-nodejs'`; do echo "NodeJS remainer found in /nix/store!"; echo "$i"; exit 1; done
artifacts:
paths:
- uniworx-sanitized.tar.gz
name: "${CI_JOB_NAME}-${CI_COMMIT_SHORT_SHA}"
expire_in: "1 day"
retry: 2
interruptible: true
rules: *release-rules
sanitize test container:
stage: container:build
needs:
- job: node dependencies # transitive
artifacts: false
- job: well known # transitive
artifacts: false
- job: frontend # transitive
artifacts: false
- job: uniworx:lib:uniworx # transitive
artifacts: false
- job: uniworx:exe:uniworx # transitive
artifacts: false
- job: check # sanity
artifacts: false
- job: test container
artifacts: true
before_script: *nix-before
script:
- nix shell nixpkgs#perl --command ./.gitlab-ci/sanitize-docker.pl
after_script:
- tar xzvf uniworx-sanitized.tar.gz
- for i in `tar tf */layer.tar | grep 'nix/store/[0-9a-z]*-nodejs'`; do echo "NodeJS remainer found in /nix/store!"; echo "$i"; exit 1; done
artifacts:
paths:
- uniworx-sanitized.tar.gz
name: "${CI_JOB_NAME}-${CI_COMMIT_SHORT_SHA}"
expire_in: "1 day"
retry: 2
interruptible: true
rules: *test-release-rules
sanitize dev container:
stage: container:build
needs:
- job: node dependencies # transitive
artifacts: false
- job: well known # transitive
artifacts: false
- job: frontend # transitive
artifacts: false
- job: uniworx:lib:uniworx # transitive
artifacts: false
- job: uniworx:exe:uniworx # transitive
artifacts: false
- job: check # sanity
artifacts: false
- job: dev container
artifacts: true
before_script: *nix-before
script:
- nix shell nixpkgs#perl --command ./.gitlab-ci/sanitize-docker.pl
after_script:
- tar xzvf uniworx-sanitized.tar.gz
- for i in `tar tf */layer.tar | grep 'nix/store/[0-9a-z]*-nodejs'`; do echo "NodeJS remainer found in /nix/store!"; echo "$i"; exit 1; done
artifacts:
paths:
- uniworx-sanitized.tar.gz
name: "${CI_JOB_NAME}-${CI_COMMIT_SHORT_SHA}"
expire_in: "1 day"
retry: 2
interruptible: true
rules: *dev-release-rules
parse changelog:
stage: prepare release
@ -311,25 +450,27 @@ upload container:
stage: release
image: quay.io/skopeo/stable:latest
script:
- skopeo --insecure-policy copy --dest-creds "${CI_REGISTRY_USER}:${CI_JOB_TOKEN}" docker-archive://$(pwd)/uniworx.tar.gz docker://${CI_REGISTRY_IMAGE}:${VERSION}
- skopeo --insecure-policy copy --dest-creds "${CI_REGISTRY_USER}:${CI_JOB_TOKEN}" docker-archive://$(pwd)/uniworx-sanitized.tar.gz docker://${CI_REGISTRY_IMAGE}:${VERSION}
- skopeo --insecure-policy copy --src-creds "${CI_REGISTRY_USER}:${CI_JOB_TOKEN}" --dest-creds "${CI_REGISTRY_USER}:${CI_JOB_TOKEN}" docker://${CI_REGISTRY_IMAGE}:${VERSION} docker://${CI_REGISTRY_IMAGE}:latest
needs:
- job: node dependencies # transitive
artifacts: false
- job: well known # transitive
artifacts: false
- job: frontend # tranitive
- job: frontend # transitive
artifacts: false
- job: uniworx:lib:uniworx # transitive
artifacts: false
- job: uniworx:exe:uniworx # transitive
artifacts: false
- job: container
artifacts: true
- job: container # transitive
artifacts: false
- job: parse changelog
artifacts: true
- job: check # sanity
artifacts: false
- job: sanitize container
artifacts: true
rules: *release-rules
retry: 2
upload test container:
@ -338,27 +479,56 @@ upload test container:
stage: release
image: quay.io/skopeo/stable:latest
script:
- skopeo --insecure-policy copy --dest-creds "${CI_REGISTRY_USER}:${CI_JOB_TOKEN}" docker-archive://$(pwd)/uniworx.tar.gz docker://${CI_REGISTRY}/fradrive/fradrive/test:${CI_COMMIT_REF_NAME}
- skopeo --insecure-policy copy --dest-creds "${CI_REGISTRY_USER}:${CI_JOB_TOKEN}" docker-archive://$(pwd)/uniworx-sanitized.tar.gz docker://${CI_REGISTRY}/fradrive/fradrive/test:${CI_COMMIT_REF_NAME}
- skopeo --insecure-policy copy --src-creds "${CI_REGISTRY_USER}:${CI_JOB_TOKEN}" --dest-creds "${CI_REGISTRY_USER}:${CI_JOB_TOKEN}" docker://${CI_REGISTRY}/fradrive/fradrive/test:${CI_COMMIT_REF_NAME} docker://${CI_REGISTRY}/fradrive/fradrive/test:latest
needs:
- job: node dependencies # transitive
artifacts: false
- job: well known # transitive
artifacts: false
- job: frontend # tranitive
- job: frontend # transitive
artifacts: false
- job: uniworx:lib:uniworx # transitive
artifacts: false
- job: uniworx:exe:uniworx # transitive
artifacts: false
- job: test container
artifacts: true
- job: test container # transitive
artifacts: false
- job: parse test changelog
artifacts: true
- job: check # sanity
artifacts: false
- job: sanitize test container
artifacts: true
rules: *test-release-rules
retry: 2
upload dev container:
variables:
GIT_STRATEGY: none
stage: release
image: quay.io/skopeo/stable:latest
script:
- skopeo --insecure-policy copy --dest-creds "${CI_REGISTRY_USER}:${CI_JOB_TOKEN}" docker-archive://$(pwd)/uniworx-sanitized.tar.gz docker://${CI_REGISTRY}/fradrive/fradrive/dev:${CI_COMMIT_REF_NAME}
- skopeo --insecure-policy copy --src-creds "${CI_REGISTRY_USER}:${CI_JOB_TOKEN}" --dest-creds "${CI_REGISTRY_USER}:${CI_JOB_TOKEN}" docker://${CI_REGISTRY}/fradrive/fradrive/dev:${CI_COMMIT_REF_NAME} docker://${CI_REGISTRY}/fradrive/fradrive/dev:latest
needs:
- job: node dependencies # transitive
artifacts: false
- job: well known # transitive
artifacts: false
- job: frontend # transitive
artifacts: false
- job: uniworx:lib:uniworx # transitive
artifacts: false
- job: uniworx:exe:uniworx # transitive
artifacts: false
- job: dev container # transitive
artifacts: false
- job: check # sanity
artifacts: false
- job: sanitize dev container
artifacts: true
rules: *dev-release-rules
retry: 2
release:
variables:
@ -394,3 +564,18 @@ test release:
artifacts: false
- job: parse test changelog
artifacts: true
dev release:
variables:
GIT_STRATEGY: none
stage: release
image: registry.gitlab.com/gitlab-org/release-cli:latest
rules: *dev-release-rules
script:
- echo "Will create dev release ${VERSION}-dev..."
release:
name: "${VERSION}-dev"
tag_name: '$CI_COMMIT_TAG'
description: .current-changelog.md
needs:
- job: check # sanity
artifacts: false

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

File diff suppressed because it is too large Load Diff

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>, 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>, Wolfgang Witt <Wolfgang.Witt@campus.lmu.de>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
@ -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,13 +77,10 @@ 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"
synchronise-ldap-users-within: "_env:SYNCHRONISE_LDAP_WITHIN:1209600" # 14 Tage in Sekunden
synchronise-ldap-users-interval: "_env:SYNCHRONISE_LDAP_INTERVAL:3600" # jede Stunde
synchronise-avs-users-within: "_env:SYNCHRONISE_AVS_WITHIN:5702400" # alle 66 Tage
synchronise-avs-users-interval: "_env:SYNCHRONISE_AVS_INTERVAL:21600" # alle 6 Stunden
@ -129,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: 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"
@ -165,7 +185,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:"
@ -188,7 +208,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

@ -16,8 +16,13 @@ fi
branch="$(git rev-parse --abbrev-ref HEAD)"
if [[ $branch != "master" && $branch != "test" ]]; then
if echo $@ | grep -xqe '--dev';
then
: # dev-releases possible on any branch
else
echo "Not on master or test" >&2
exit 1
fi
fi
ourHash=$(git rev-parse HEAD)

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
@ -128,6 +128,8 @@ InterfaceName: Schnittstelle
InterfaceLastSynch: Zuletzt
InterfaceSubtype: Betreffend
InterfaceWrite: Schreibend
AdminUserPassword: Passwort
InterfaceSuccess: Rückmeldung
InterfaceInfo: Nachricht
InterfaceFreshness: Prüfungszeitraum (h)
InterfaceFreshness: Prüfungszeitraum (h)

View File

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2022 Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>
# SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Sarah Vaupel <sarah.vaupel@ifi.lmu.de>, Winnie Ros <winnie.ros@campus.lmu.de>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
@ -128,6 +128,8 @@ InterfaceName: Interface
InterfaceLastSynch: Last
InterfaceSubtype: Affecting
InterfaceWrite: Write
AdminUserPassword: Password
InterfaceSuccess: Returned
InterfaceInfo: Message
InterfaceFreshness: Check hours
InterfaceFreshness: Check hours

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 type 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
@ -38,8 +38,8 @@ AuthPWHashConfigured: Nutzer:in meldet sich nun mit FRADrive spezifischer Kennun
UsersCourseSchool: Bereich
ActionNoUsersSelected: Keine Benutzer:innen ausgewählt
SynchroniseAvsUserQueued n@Int: AVS-Synchronisation von #{n} #{pluralDE n "Benutzer:in" "Benutzer:innen"} angestoßen
SynchroniseLdapUserQueued n@Int: LDAP-Synchronisation von #{n} #{pluralDE n "Benutzer:in" "Benutzer:innen"} angestoßen
SynchroniseLdapAllUsersQueued: LDAP-Synchronisation von allen Benutzer:innen angestoßen
SynchroniseUserdbUserQueued n@Int: Benutzerdatenbank-Synchronisation von #{n} #{pluralDE n "Benutzer:in" "Benutzer:innen"} angestoßen
SynchroniseUserdbAllUsersQueued: Benutzerdatenbank-Synchronisation von allen Benutzer:innen angestoßen
UserListTitle: Komprehensive Benutzerliste
AccessRightsSaved: Berechtigungen erfolgreich verändert
AccessRightsNotChanged: Berechtigungen wurden nicht verändert
@ -48,6 +48,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
@ -95,10 +96,14 @@ UserAddSupervisor: Ansprechpartner hinzufügen
UserSetSupervisor: Ansprechpartner ersetzen
UserRemoveSupervisor: Alle Ansprechpartner entfernen
UserIsSupervisor: Ist Ansprechpartner
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!
UsersRemoveSupervisors usr@Int: Alle Ansprechpartner für #{tshow usr} Benutzer gelöscht.
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
@ -38,8 +38,8 @@ AuthPWHashConfigured: User now logs in using their FRADrive specific account
UsersCourseSchool: Department
ActionNoUsersSelected: No users selected
SynchroniseAvsUserQueued n: Triggered AVS synchronisation of #{n} #{pluralEN n "user" "users"}.
SynchroniseLdapUserQueued n: Triggered LDAP synchronisation of #{n} #{pluralEN n "user" "users"}.
SynchroniseLdapAllUsersQueued: Triggered LDAP synchronisation of all users
SynchroniseUserdbUserQueued n: Triggered user database synchronisation of #{n} #{pluralEN n "user" "users"}.
SynchroniseUserdbAllUsersQueued: Triggered user database synchronisation of all users
UserListTitle: Comprehensive list of users
AccessRightsSaved: Successfully updated permissions
AccessRightsNotChanged: Permissions left unchanged
@ -48,6 +48,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
@ -95,10 +96,14 @@ UserAddSupervisor: Add supervisor
UserSetSupervisor: Replace supervisors
UserRemoveSupervisor: Set to unsupervised
UserIsSupervisor: Is supervisor
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!
UsersRemoveSupervisors usr: Removed all supervisors for #{pluralENsN usr "user"}.
UsersRemoveSupervisors usr: Removed all supervisors for #{pluralENsN usr "user"}.
AdminUserAuthentication: Authentifizierung
AdminUserAuthLastSync: Last synchronised
AuthKindLDAP: Fraport AG account (LDAP)
AuthKindAzure: Azure account
AuthKindPWHash: Internal FRADrive login
AuthKindNoLogin: No login

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@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>, 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>
#
# 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
@ -142,7 +143,7 @@ MenuSap: SAP Schnittstelle
MenuAvs: AVS Schnittstelle
MenuAvsSynchError: AVS Problemübersicht
MenuLdap: LDAP Schnittstelle
MenuExternalUser: Externe Benutzer
MenuApc: Druckerei
MenuPrintSend: Manueller Briefversand
MenuPrintDownload: Brief herunterladen

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
@ -25,6 +25,7 @@ MenuInstance: Instance identification
MenuHealth: Instance health
MenuHealthInterface: Interface health
MenuHelp: Support
MenuAccount: Account
MenuProfile: Settings
MenuLogin: Login
MenuLogout: Logout
@ -142,7 +143,7 @@ MenuSap: SAP Interface
MenuAvs: AVS Interface
MenuAvsSynchError: AVS Problem Overview
MenuLdap: LDAP Interface
MenuExternalUser: External users
MenuApc: Printing
MenuPrintSend: Send Letter
MenuPrintDownload: Download Letter

View File

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2023 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>
# SPDX-FileCopyrightText: 2023-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>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
@ -158,4 +158,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 Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>
# SPDX-FileCopyrightText: 2023-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>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
@ -158,4 +158,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

@ -40,21 +40,22 @@ 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
continuous Bool -- expiring precondition blocks qualification
deriving Generic
-- 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
-- continuous Bool -- expiring precondition blocks qualification
-- deriving Generic
-- Maybe an alternative for online qualification validity checking, transitivity through recursive CTEs? (already available in our version)
-- QualificationRequirement
--QualificationRequirement
-- qualification QualificationId OnDeleteCascade OnUpdateCascade
-- requirement QualificationId OnDeleteCascade OnUpdateCascade
-- group Text -- OR: several requirements within the same group are considered equivalent
-- UniqueQualificationRequirement qualification requirement
-- deriving Generic
--
-- TODO: connect Qualification with Exams!
-- TODO: connect Qualifications with Exams!?
QualificationEdit
user UserId
@ -81,6 +82,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 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>, 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
-- 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,17 +11,16 @@
-- 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.
ident UserIdent -- Case-insensitive user-identifier
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?
surname UserSurname -- Display user names always through 'nameWidget displayName surname'
displayName UserDisplayName
displayEmail UserEmail
email UserEmail -- Case-insensitive eMail address, used for sending TODO: make this nullable
ident UserIdent -- Case-insensitive user-identifier
authentication AuthenticationMode -- 'AuthLDAP' or ('AuthPWHash'+password-hash)
lastAuthentication UTCTime Maybe -- last login date
created UTCTime default=now()
lastLdapSynchronisation UTCTime Maybe
ldapPrimaryKey UserEduPersonPrincipalName Maybe
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!
firstName Text -- For export in tables, pre-split firstName from displayName
@ -50,11 +49,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
@ -99,4 +107,3 @@ UserSupervisor
rerouteNotifications Bool -- User can be his own supervisor to receive notifications as well
UniqueUserSupervisor supervisor user -- each supervisor/user combination is unique (same supervisor can superviser the same user only once)
deriving Generic

View File

@ -14,8 +14,8 @@ let
'');
in if self ? lastModified then fromDate else "1970-01-01T00:00:01Z";
mkUniworxDocker = { isTest }: prev.dockerTools.buildImage {
name = "uniworx${optionalString isTest "-test"}";
mkUniworxDocker = { scope, isTest }: prev.dockerTools.buildImage {
name = "uniworx" + (if scope == null then "" else "-${scope}");
tag =
let
versionFile = if isTest then ./test-version.json else ./version.json;
@ -111,6 +111,7 @@ let
};
in
mapAttrs (_name: mkUniworxDocker) {
uniworxTestDocker = { isTest = true; };
uniworxDocker = { isTest = false; };
uniworxDocker = { isTest = false; scope = null; };
uniworxTestDocker = { isTest = false; scope = "test"; };
uniworxDevDocker = { isTest = false; scope = "dev"; };
}

View File

@ -1,3 +1,3 @@
{
"version": "27.4.59"
"version": "28.1.0"
}

2
package-lock.json generated
View File

@ -1,6 +1,6 @@
{
"name": "uni2work",
"version": "27.4.59",
"version": "28.1.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@ -1,6 +1,6 @@
{
"name": "uni2work",
"version": "27.4.59",
"version": "28.1.0",
"description": "",
"keywords": [],
"author": "",
@ -25,7 +25,9 @@
"i18n:test": "./missing-translations.sh",
"prerelease": "./is-clean.sh && npm run test",
"release": "./release.sh",
"release-dev": "./release.sh --dev",
"postrelease": "git push --follow-tags",
"postrelease-dev": "git push --follow-tags",
"parse-changelog": "changelog-parser ./CHANGELOG.md > changelog.json"
},
"husky": {

View File

@ -1,11 +1,12 @@
name: uniworx
version: 27.4.59
version: 28.1.0
dependencies:
- base
- yesod
- yesod-core
- yesod-persistent
- yesod-auth
- yesod-auth-oauth2
- yesod-static
- yesod-form
- yesod-persistent
@ -20,7 +21,6 @@ dependencies:
- template-haskell
- shakespeare
- monad-control
- wai-extra
- yaml
- http-conduit
- directory
@ -30,7 +30,6 @@ dependencies:
- conduit
- monad-logger
- fast-logger
- wai-logger
- foreign-store
- file-embed
- unordered-containers
@ -39,6 +38,10 @@ dependencies:
- time
- case-insensitive
- wai
- wai-cors
- wai-extra
- wai-logger
- wai-middleware-prometheus
- cryptonite
- cryptonite-conduit
- saltine
@ -143,7 +146,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

37
routes
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>,David Mosbach <david.mosbach@uniworx.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
@ -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
@ -46,6 +46,9 @@
/static StaticR EmbeddedStatic appStatic !free
/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
@ -59,23 +62,23 @@
/users/#CryptoUUIDUser/password UserPasswordR GET POST !selfANDis-pw-hash
!/users/functionary-invite/new AdminNewFunctionaryInviteR GET POST
!/users/functionary-invite AdminFunctionaryInviteR GET POST
!/users/add AdminUserAddR GET POST
/admin AdminR GET
/admin/test AdminTestR GET POST
/admin/test/pdf AdminTestPdfR GET
/admin/errMsg AdminErrMsgR GET POST
/admin/tokens AdminTokensR GET POST
/admin/crontab AdminCrontabR GET
/admin/crontab/jobs AdminJobsR GET POST
/admin/avs AdminAvsR GET POST
/admin/avs/#CryptoUUIDUser AdminAvsUserR GET
/admin/ldap AdminLdapR GET POST
/admin/problems AdminProblemsR GET
!/users/add AdminUserAddR GET POST
/admin AdminR GET
/admin/test AdminTestR GET POST
/admin/test/pdf AdminTestPdfR GET
/admin/errMsg AdminErrMsgR GET POST
/admin/tokens AdminTokensR GET POST
/admin/crontab AdminCrontabR GET
/admin/crontab/jobs AdminJobsR GET POST
/admin/avs AdminAvsR GET POST
/admin/avs/#CryptoUUIDUser AdminAvsUserR GET
/admin/external-user AdminExternalUserR GET POST
/admin/problems AdminProblemsR GET
/admin/problems/no-contact ProblemUnreachableR GET
/admin/problems/no-avs-id ProblemWithoutAvsId GET
/admin/problems/r-without-f ProblemFbutNoR GET
/admin/problems/avs ProblemAvsSynchR GET POST
/admin/problems/avs/errors ProblemAvsErrorR GET
/admin/problems/avs ProblemAvsSynchR GET POST
/admin/problems/avs/errors ProblemAvsErrorR GET
/print PrintCenterR GET POST !system-printer
/print/acknowledge/#Day/#Int/#Int PrintAckR GET POST !system-printer

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
@ -271,6 +302,9 @@ in pkgs.mkShell {
export CHROME_BIN=${pkgs.chromium}/bin/chromium
'';
OAUTH2_HBA = oauth2Hba;
OAUTH2_DB_SCHEMA = oauth2Schema;
OAUTH2_TEST_USERS = ./test/Database/test-users.yaml;
nativeBuildInputs = [develop inDevelop killallUni2work diffRunning]
++ (with pkgs;
[ stack nodejs-14_x postgresql_12 openldap exiftool memcached minio minio-client

View File

@ -1,4 +1,4 @@
-- 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>, David Mosbach <david.mosbach@uniworx.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,111 +18,98 @@ 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 System.Log.FastLogger.Date
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(..))
import GHC.RTS.Flags (getRTSFlags)
import Yesod.Auth.OAuth2.AzureAD (oauth2AzureADScoped)
import Yesod.Auth.Util.PasswordStore
import qualified Yesod.Core.Types as Yesod (Logger(..))
import qualified Prometheus
#ifdef DEVELOPMENT
import Data.Maybe (fromJust)
import Auth.OAuth2 (azureMockServer)
#endif
import qualified Data.IntervalMap.Strict as IntervalMap
import qualified Utils.Pool as Custom
import Utils.Postgresql
import Handler.Utils.Memcached (manageMemcachedLocalInvalidations)
import qualified System.Clock as Clock
import Utils.Avs
-- Import all relevant handler modules here.
-- (HPack takes care to add new modules to our cabal file nowadays.)
@ -161,11 +146,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
@ -236,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 appMemcachedLocal appUploadCache appVerpSecret appAuthKey appPersonalisedSheetFilesSeedKey appVolatileClusterSettingsCache appAvsQuery = UniWorX{..}
mkFoundation appSettings' appConnPool appSmtpPool appLdapPool appCryptoIDKey appSessionStore appSecretBoxKey appWidgetMemcached appJSONWebKeySet appClusterID appMemcached appMemcachedLocal appUploadCache appVerpSecret appAuthKey appAuthPlugins appPersonalisedSheetFilesSeedKey appVolatileClusterSettingsCache appAvsQuery = UniWorX{..}
tempFoundation = mkFoundation
(error "appSettings' forced in tempFoundation")
(error "connPool forced in tempFoundation")
@ -253,10 +238,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''
@ -291,13 +278,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 $
@ -318,6 +324,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'
@ -377,7 +411,8 @@ makeFoundation appSettings''@AppSettings{..} = do
$logDebugS "Runtime configuration" $ tshowCrop appSettings'
let foundation = mkFoundation appSettings' sqlPool smtpPool ldapPool appCryptoIDKey appSessionStore appSecretBoxKey appWidgetMemcached appJSONWebKeySet appClusterID appMemcached appMemcachedLocal 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 appMemcachedLocal appUploadCache appVerpSecret appAuthKey appAuthPlugins appPersonalisedSheetFilesSeedKey appVolatileClusterSettingsCache appAvsQuery
-- Return the foundation
$logInfoS "setup" "*** DONE ***"
@ -476,66 +511,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
@ -616,6 +591,8 @@ appMain = runResourceT $ do
foundation <- makeFoundation settings
runAppLoggingT foundation $ do
$logDebugS "AppSettings" $ tshow settings
$logInfoS "setup" "Job-Handling"
handleJobs foundation
@ -732,7 +709,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
@ -757,7 +734,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
@ -1521,7 +1521,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
@ -1529,13 +1529,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
@ -1543,11 +1547,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

@ -384,8 +384,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 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>, 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
@ -42,6 +46,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

@ -73,6 +73,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
@ -115,7 +117,7 @@ breadcrumb AdminCrontabR = i18nCrumb MsgBreadcrumbAdminCrontab $ Just
breadcrumb AdminJobsR = i18nCrumb MsgBreadcrumbAdminJobs $ Just AdminCrontabR
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
@ -542,42 +544,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
@ -856,8 +853,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
@ -1214,8 +1211,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

@ -156,6 +156,10 @@ siteLayout' overrideHeading widget = do
-- isParent r = r == (fst parents)
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

View File

@ -79,7 +79,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)
@ -97,6 +97,7 @@ data UniWorX = UniWorX
, appUploadCache :: Maybe MinioConn
, appVerpSecret :: VerpSecret
, appAuthKey :: Auth.Key
, appAuthPlugins :: [AuthPlugin UniWorX]
, appFileSourceARC :: Maybe (ARCHandle (FileContentChunkReference, (Int, Int)) Int ByteString)
, appFileSourcePrewarm :: Maybe (LRUHandle (FileContentChunkReference, (Int, Int)) UTCTime Int ByteString)
, appFileInjectInhibit :: TVar (IntervalMap UTCTime (Set FileContentReference))

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,231 +104,300 @@ 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
(newUser,userUpdate) <- decodeUser now userDefaultConf upsertData
--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)
(newUser ^. _userFirstName)
(newUser ^. _userSurname)
(userRec ^. _userDisplayName)) $
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) ]
unless (null emUps) $ update userId emUps
-- Attempt to update ident, too:
unless (validEmail' (userRec ^. _userIdent)) $
void $ maybeCatchAll (update userId [ UserIdent =. (newUser ^. _userEmail) ] >> return (Just ()))
user@(Entity userId _userRec) <- case oldUsers of
[oldUserId] -> updateGetEntity oldUserId userUpdate
_other -> upsertBy (UniqueAuthentication (newUser ^. _userIdent)) newUser userUpdate
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]))
decodeUserTest mbIdent ldapData = do
now <- liftIO getCurrentTime
userDefaultConf <- getsYesod $ view _appUserDefaults
let mode = maybe UpsertCampusUserLoginLdap UpsertCampusUserLoginDummy mbIdent
try $ decodeUser now userDefaultConf mode ldapData
upsertUser :: forall m.
( MonadHandler m
, HandlerSite m ~ UniWorX
, MonadCatch m
)
=> UpsertUserMode
-> NonEmpty UpsertUserData
-> SqlPersistT m (Entity User)
upsertUser upsertMode upsertData = maybeUpsertUser upsertMode (Just upsertData) >>= \case
Nothing -> error "upsertUser: No user result from maybeUpsertUser!"
Just user -> return user
decodeUser :: (MonadThrow m) => UTCTime -> UserDefaultConf -> UpsertCampusUserMode -> Ldap.AttrList [] -> m (User,_)
decodeUser now UserDefaultConf{..} upsertMode ldapData = do
let
userTelephone = decodeLdap ldapUserTelephone
userMobile = decodeLdap ldapUserMobile
userCompanyPersonalNumber = decodeLdap ldapUserFraportPersonalnummer
userCompanyDepartment = decodeLdap ldapUserFraportAbteilung
userAuthentication
| is _UpsertCampusUserLoginOther upsertMode
= AuthNoLogin -- AuthPWHash (error "Non-LDAP logins should only work for users that are already known")
| otherwise = AuthLDAP
userLastAuthentication = guardOn isLogin now
isLogin = has (_UpsertCampusUserLoginLdap <> _UpsertCampusUserLoginOther . united) upsertMode
userTitle = decodeLdap ldapUserTitle -- CampusUserInvalidTitle
userFirstName = decodeLdap' ldapUserFirstName -- CampusUserInvalidGivenName
userSurname = decodeLdap' ldapUserSurname -- CampusUserInvalidSurname
userDisplayName <- decodeLdap1 ldapUserDisplayName CampusUserInvalidDisplayName <&> fixDisplayName -- do not check LDAP-given userDisplayName
--userDisplayName <- decodeLdap1 ldapUserDisplayName CampusUserInvalidDisplayName >>=
-- (maybeThrow CampusUserInvalidDisplayName . checkDisplayName userTitle userFirstName userSurname)
decodeUser :: ( MonadThrow m
)
=> UTCTime -- ^ Now
-> UserDefaultConf
-> NonEmpty UpsertUserData -- ^ Raw source data
-> m (User,_) -- ^ Data for new User entry and updating existing User entries
decodeUser now UserDefaultConf{..} upsertData = do
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
userEmail <- if -- TODO: refactor! NOTE: LDAP doesnt know email for all users; we use userPrincialName instead; however validEmail refutes `E<number@fraport.de` here, too strong! Make Email-Field optional!
| userEmail : _ <- mapMaybe (assertM (elem '@') . either (const Nothing) Just . Text.decodeUtf8') (lookupSome ldapMap $ toList ldapUserEmail)
-> return $ CI.mk userEmail
-- | userEmail : _ <- mapMaybe (assertM validEmail . either (const Nothing) Just . Text.decodeUtf8') (lookupSome ldapMap $ toList ldapUserEmail) -- TOO STRONG, see above!
-- -> return $ CI.mk userEmail
| otherwise
-> throwM CampusUserInvalidEmail
userLdapPrimaryKey <- if
| [bs] <- ldapMap !!! ldapPrimaryKey
, Right userLdapPrimaryKey'' <- Text.decodeUtf8' bs
, Just userLdapPrimaryKey''' <- assertM' (not . Text.null) $ Text.strip userLdapPrimaryKey''
-> return $ Just userLdapPrimaryKey'''
| otherwise
-> return Nothing
-> throwM DecodeUserInvalidIdent
let
(azureSurname, azureFirstName, azureDisplayName, azureEmail, azureTelephone, azureMobile, azureLanguages)
| Just azureData <- mbAzureData
= ( azureData `decodeAzure` azureUserSurname
, azureData `decodeAzure` azureUserGivenName
, azureData `decodeAzure` azureUserDisplayName
, azureData `decodeAzure` azureUserMail
, azureData `decodeAzure` azureUserTelephone
, azureData `decodeAzure` azureUserMobile
, Nothing -- azureData `decodeAzure` azureUserPreferredLanguage -- TODO: parse Languages from azureUserPreferredLanguage
)
| otherwise
= ( Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing )
(ldapSurname, ldapFirstName, ldapDisplayName, ldapEmail, ldapTelephone, ldapMobile, ldapCompanyPersonalNumber, ldapCompanyDepartment)
| Just ldapData <- mbLdapData
= ( ldapData `decodeLdap` ldapUserSurname
, ldapData `decodeLdap` ldapUserFirstName
, ldapData `decodeLdap` ldapUserDisplayName
, ldapData `decodeLdap` Ldap.Attr "mail" -- TODO: use ldapUserEmail?
, ldapData `decodeLdap` ldapUserTelephone
, ldapData `decodeLdap` ldapUserMobile
, ldapData `decodeLdap` ldapUserFraportPersonalnummer
, ldapData `decodeLdap` ldapUserFraportAbteilung
)
| otherwise
= ( Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing )
-- TODO: throw on collisions?
-- TODO: use user-auth precedence from app config when implementing multi-source support
let
userSurname = fromMaybe mempty $ azureSurname <|> ldapSurname
userFirstName = fromMaybe mempty $ azureFirstName <|> ldapFirstName
userDisplayName = fromMaybe mempty $ azureDisplayName <|> ldapDisplayName
userEmail = maybe mempty CI.mk $ azureEmail <|> ldapEmail
userTelephone = azureTelephone <|> ldapTelephone
userMobile = azureMobile <|> ldapMobile
userLanguages = azureLanguages
userCompanyPersonalNumber = ldapCompanyPersonalNumber
userCompanyDepartment = ldapCompanyDepartment
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 =
[ UserLastAuthentication =. Just now | isLogin ] ++
[ UserEmail =. userEmail | validEmail' userEmail ] ++
[
-- UserDisplayName =. userDisplayName -- not updated here, since users are allowed to change their DisplayName; see line 272
UserFirstName =. userFirstName
, UserSurname =. userSurname
, UserLastLdapSynchronisation =. Just now
, UserLdapPrimaryKey =. userLdapPrimaryKey
, UserMobile =. userMobile
, UserTelephone =. userTelephone
, UserCompanyPersonalNumber =. userCompanyPersonalNumber
, UserCompanyDepartment =. userCompanyDepartment
userUpdate =
[ UserSurname =. userSurname
, UserFirstName =. userFirstName
-- , UserDisplayName =. userDisplayName -- not updated, since users are allowed to change their DisplayName
, UserEmail =. userEmail
, UserTelephone =. userTelephone
, UserMobile =. userMobile
, UserCompanyPersonalNumber =. userCompanyPersonalNumber
, UserCompanyDepartment =. userCompanyDepartment
, UserLastSync =. Just now
]
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
@ -343,11 +409,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
@ -357,6 +423,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
@ -371,11 +448,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"]
@ -406,7 +486,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 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
@ -31,7 +31,7 @@ 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
getAdminR :: Handler Html

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 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
@ -321,8 +321,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

@ -204,7 +204,6 @@ data UserTableCsv = UserTableCsv
, csvUserSex :: Maybe Sex
, csvUserBirthday :: Maybe Day
, csvUserMatriculation :: Maybe UserMatriculation
, csvUserEPPN :: Maybe UserEduPersonPrincipalName
, csvUserEmail :: UserEmail
, csvUserQualifications :: [QualificationName]
, csvUserSubmissionGroup :: Maybe SubmissionGroupName
@ -224,7 +223,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
@ -286,7 +284,6 @@ data UserTableJson = UserTableJson
, jsonUserName :: UserDisplayName
, jsonUserSex :: Maybe (Maybe Sex)
, jsonUserMatriculation :: Maybe UserMatriculation
, jsonUserEPPN :: Maybe UserEduPersonPrincipalName
, jsonUserEmail :: UserEmail
, jsonUserQualifications :: Set QualificationName
, jsonUserSubmissionGroup :: Maybe SubmissionGroupName
@ -323,7 +320,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
@ -566,7 +562,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)
@ -598,7 +593,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

@ -187,7 +187,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)
@ -208,7 +207,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
@ -234,7 +232,6 @@ instance FromNamedRecord ExamUserTableCsv where
<*> csv .:?? "first-name"
<*> csv .:?? "name"
<*> csv .:?? "matriculation"
<*> csv .:?? "eduPersonPrincipalName"
<*> pure mempty
<*> csv .:?? "occurrence"
<*> fmap Just (csv .:?? "exercise-points")
@ -277,7 +274,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"
@ -615,7 +612,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)
@ -939,7 +935,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

@ -67,7 +67,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
@ -83,15 +83,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

@ -584,6 +584,8 @@ makeProfileData :: Entity User -> DB Widget
makeProfileData (Entity uid User{..}) = do
now <- liftIO getCurrentTime
avsId <- entityVal <<$>> getBy (UniqueUserAvsUser uid)
externalUsers <- (\(Entity _ ExternalUser{..}) -> (externalUserUser, externalUserSource, externalUserLastSync)) <<$>> selectList [ ExternalUserUser ==. userIdent ] []
-- avsCards <- maybe (pure mempty) (\a -> selectList [UserAvsCardPersonId ==. userAvsPersonId a] []) avsId
functions <- Map.fromListWith Set.union . map (\(Entity _ UserFunction{..}) -> (userFunctionFunction, Set.singleton userFunctionSchool)) <$> selectList [UserFunctionUser ==. uid] []
lecture_corrector <- E.select $ E.distinct $ E.from $ \(sheet `E.InnerJoin` corrector `E.InnerJoin` course) -> do

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
@ -96,8 +94,16 @@ compileBlocks dStart dEnd = go (dStart, True)
getQualificationSAPDirectR :: Handler TypedContent
getQualificationSAPDirectR = do
now <- liftIO getCurrentTime
fdate <- formatTime' "%Y%m%d_%H-%M" now
let ldap_cutoff = addDiffDaysRollOver (fromMonths $ -3) now
fdate <- formatTime' "%Y%m%d_%H-%M" now
userAuthConf <- getsYesod $ view _appUserAuthConf
let
ldapSources = case userAuthConf of
UserAuthConfSingleSource (AuthSourceConfLdap LdapConf{..})
-> singleton $ AuthSourceIdLdap ldapConfSourceId
_other -> mempty
ldapCutoff = addDiffDaysRollOver (fromMonths $ -3) now
qualUsers <- runDB $ E.select $ do
(qual :& qualUser :& user :& qualBlock) <-
E.from $ E.table @Qualification
@ -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-23 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
@ -11,14 +11,11 @@ module Handler.Users
import Import
import Jobs
-- import Data.Text
import Handler.Utils
import Handler.Utils.Users
import Handler.Utils.Invitations
import Handler.Utils.Avs
import qualified Auth.LDAP as Auth
import qualified Data.CaseInsensitive as CI
import qualified Data.Set as Set
@ -35,8 +32,6 @@ 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.HashSet as HashSet
@ -130,8 +125,8 @@ postUsersR = do
icnReroute = text2widget " " <> toWgt (icon IconLetter)
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 "ldap-sync") (i18nCell MsgLdapSynced) $ \DBRow{ dbrOutput = Entity _ User{..} } -> maybe mempty dateTimeCell userLastLdapSynchronisation
-- , 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
, flip foldMap universeF $ \function ->
sortable (Just $ SortingKey $ CI.mk $ toPathPiece function) (i18nCell function) $ \DBRow{ dbrOutput = Entity uid _ } -> flip (set' cellContents) mempty $ do
schools <- liftHandler . runDB . E.select . E.from $ \(school `E.InnerJoin` userFunction) -> do
@ -226,15 +221,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
@ -276,24 +271,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
, ( "user-company", FilterColumn . E.mkExistsFilter $ \user criterion ->
E.from $ \(usrComp `E.InnerJoin` comp) -> do
let testname = (E.val criterion :: E.SqlExpr (E.Value (CI Text))) `E.isInfixOf`
@ -335,8 +330,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
]
, dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout }
, dbtParams = DBParamsForm
@ -364,8 +359,8 @@ postUsersR = do
| Set.null usersSet && isNotSetSupervisor act ->
addMessageI Info MsgActionNoUsersSelected
(UserLdapSyncData, userSet) -> do
forM_ userSet $ \uid -> queueJob' $ JobSynchroniseLdapUser uid
addMessageI Success . MsgSynchroniseLdapUserQueued $ Set.size userSet
forM_ userSet $ queueJob' . JobSynchroniseUser
addMessageI Success . MsgSynchroniseUserdbUserQueued $ Set.size userSet
redirectKeepGetParams UsersR
(UserAvsSyncData, userSet) -> do
forM_ userSet $ \uid -> queueJob' $ JobSynchroniseAvsUser uid Nothing
@ -400,8 +395,8 @@ postUsersR = do
formResult allUsersRes $ \case
AllUsersLdapSync -> do
runDBJobs . runConduit $ selectSource [] [] .| C.mapM_ (queueDBJob . JobSynchroniseLdapUser . entityKey)
addMessageI Success MsgSynchroniseLdapAllUsersQueued
runDBJobs . runConduit $ selectSource [] [] .| C.mapM_ (queueDBJob . JobSynchroniseUser . entityKey)
addMessageI Success MsgSynchroniseUserdbAllUsersQueued
redirect UsersR
let allUsersWgt' = wrapForm allUsersWgt def
{ formSubmit = FormNoSubmit
@ -506,7 +501,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
@ -532,33 +527,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
@ -718,18 +721,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)
@ -746,7 +749,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-23 Steffen Jost <s.jost@fraport.de>
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Steffen Jost <s.jost@fraport.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
@ -35,8 +35,7 @@ import qualified Data.Set as Set
import qualified Data.Map as Map
import qualified Data.Text as Text
import qualified Data.CaseInsensitive as CI
-- import Auth.LDAP (ldapUserPrincipalName)
import Foundation.Yesod.Auth (ldapLookupAndUpsert) -- , CampusUserConversionException())
import Foundation.Yesod.Auth (userLookupAndUpsert)
import Handler.Utils.Company
import Handler.Utils.Qualification
@ -79,7 +78,7 @@ instance Exception AvsException
Connect AVS query to LDAP queries for automatic synchronisation:
- add query to Auth.LDAP.campusUserMatr
- add query to Auth.LDAP.campusLogin
- jobs.Handler.dispatchJobSynchroniseLdap
- jobs.Handler.dispatchJobSynchroniseUserdb
-}
@ -355,12 +354,12 @@ guessAvsUser someid = do
[Entity uid _] -> return $ Just uid
_ -> return Nothing
uid -> return uid
Nothing -> try (runDB $ ldapLookupAndUpsert someid) >>= \case
Right Entity{entityKey=uid, entityVal=User{userCompanyPersonalNumber=Just persNo}} ->
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 Entity{entityKey=uid} -> return $ Just uid
Right (Just Entity{entityKey=uid}) -> return $ Just uid
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
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)
@ -370,8 +369,8 @@ guessAvsUser someid = do
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 $ ldapLookupAndUpsert otherId) >>= \case
Right Entity{entityVal=User{userCompanyPersonalNumber=Just persNo}} -> maybeCatchAll $ upsertAvsUserByCard (Right $ mkAvsInternalPersonalNo persNo)
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
@ -420,13 +419,16 @@ upsertAvsUserById api = do
[uid] -> $logInfoS "AVS" "Matching user found, linking." >> insertUniqueEntity (UserAvs api uid avsPersonPersonNo now Nothing)
(_:_) -> throwM $ AvsUserAmbiguous api
[] -> do
upsRes :: Either SomeException (Entity User)
<- try $ ldapLookupAndUpsert persNo
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 Entity{entityKey=uid} -> insertUniqueEntity $ UserAvs api uid avsPersonPersonNo now Nothing -- pin/addr are updated in next step anyway
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 LDAP: " <> tshow err
$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
@ -460,9 +462,10 @@ upsertAvsUserById api = do
, 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
, audAuth = maybe AuthKindNoLogin (const AuthKindLDAP) avsPersonInternalPersonalNo -- FUTURE TODO: if email is known, use AuthKinfPWHash for email invite, if no internal personnel number is known
, 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 JobSynchroniseLdapUser, JobSendPasswordReset and NotificationUserAutoModeUpdate -- TODO: check if these are failsafe
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

View File

@ -115,7 +115,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
@ -203,8 +203,8 @@ randomText extra n = fmap pack . evalRandTIO . replicateM n $ uniform range
-- 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-23 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>, 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
@ -24,8 +24,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)
@ -37,7 +36,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 ((:&)(..))
@ -131,15 +129,13 @@ getSupervisees = do
return $ Set.insert uid $ Set.fromAscList svs
computeUserAuthenticationDigest :: AuthenticationMode -> Digest SHA3_256
computeUserAuthenticationDigest :: Maybe Text -> Digest SHA3_256
computeUserAuthenticationDigest = hashlazy . JSON.encode
data GuessUserInfo
= GuessUserMatrikelnummer
{ guessUserMatrikelnummer :: UserMatriculation }
| GuessUserEduPersonPrincipalName
{ guessUserEduPersonPrincipalName :: UserEduPersonPrincipalName }
| GuessUserDisplayName
{ guessUserDisplayName :: UserDisplayName }
| GuessUserSurname
@ -191,12 +187,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
@ -238,11 +233,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
@ -258,25 +249,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 . catMaybes) $ 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
@ -912,8 +903,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

@ -252,6 +252,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

@ -71,7 +71,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
@ -493,7 +493,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
@ -310,15 +310,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
@ -326,8 +325,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,64 +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
, 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)}: 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)

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-24 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
@ -92,11 +92,11 @@ data Job
| JobTruncateTransactionLog
| JobPruneInvitations
| JobDeleteTransactionLogIPs
| JobSynchroniseLdap { jNumIterations
| JobSynchroniseUsers { jNumIterations
, jEpoch
, jIteration :: Natural
}
| JobSynchroniseLdapUser { jUser :: UserId }
| JobSynchroniseUser { jUser :: UserId }
| JobSynchroniseAvs { jNumIterations
, jEpoch
, jIteration :: Natural
@ -348,8 +348,8 @@ jobNoQueueSame = \case
JobTruncateTransactionLog{} -> Just JobNoQueueSame
JobPruneInvitations{} -> Just JobNoQueueSame
JobDeleteTransactionLogIPs{} -> Just JobNoQueueSame
JobSynchroniseLdap{} -> Just JobNoQueueSame
JobSynchroniseLdapUser{} -> Just JobNoQueueSame
JobSynchroniseUsers{} -> Just JobNoQueueSame
JobSynchroniseUser{} -> Just JobNoQueueSame
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

@ -50,6 +50,7 @@ data ManualMigration
= Migration20230524QualificationUserBlock
| Migration20230703LmsUserStatus
| Migration20240212InitInterfaceHealth -- create table interface_health and fill with default values
| Migration20240312OAuth2
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic)
deriving anyclass (Universe, Finite)
@ -81,7 +82,7 @@ migrateManual = do
, ("user_lower_ident", "CREATE INDEX user_lower_ident ON \"user\" (lower(ident))" )
, ("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")
@ -195,7 +196,24 @@ customMigrations = mapF $ \case
('Printer', 'Acknowledge', True, 168)
, ('AVS' , 'Synch' , True , 96)
ON CONFLICT DO NOTHING;
|]
|]
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";
|]
tableExists :: MonadIO m => Text -> ReaderT SqlBackend m Bool

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 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
@ -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 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>,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,6 +85,361 @@ import Utils.Lens.TH
import qualified Data.Set as Set
data JobMode = JobsLocal { jobsAcceptOffload :: Bool }
| JobsOffload
| JobsDrop
{ jobsAcceptOffload :: Bool
, jobsWriteFakeLastExec :: Bool
}
deriving (Eq, Ord, Read, Show, Generic)
deriving anyclass (Hashable)
data ApprootScope = ApprootUserGenerated | ApprootDefault
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic)
deriving anyclass (Universe, Finite, Hashable)
newtype ServerSessionSettings
= ServerSessionSettings { applyServerSessionSettings :: forall a. ServerSession.State a -> ServerSession.State a }
instance Show ServerSessionSettings where
showsPrec d _ = showParen (d > 10) $ showString "ServerSessionSettings _"
data UserDefaultConf = UserDefaultConf
{ userDefaultTheme :: Theme
, userDefaultMaxFavourites, userDefaultMaxFavouriteTerms :: Int
, userDefaultDateTimeFormat, userDefaultDateFormat, userDefaultTimeFormat :: DateTimeFormat
, userDefaultDownloadFiles :: Bool
, userDefaultWarningDays :: NominalDiffTime
, userDefaultShowSex :: Bool
, userDefaultExamOfficeGetSynced :: Bool
, userDefaultExamOfficeGetLabels :: Bool
, userDefaultPrefersPostal :: Bool
} deriving (Show)
data PWHashConf = PWHashConf
{ pwHashAlgorithm :: PWHashAlgorithm
, pwHashStrength :: Int
}
instance Show PWHashConf where
show PWHashConf{..} = "PWHashConf { pwHashStrength = " <> show pwHashStrength <> ", .. }"
instance FromJSON PWHashConf where
parseJSON = withObject "PWHashConf" $ \o -> do
pwHashAlgorithm' <- o .: "algorithm" :: Aeson.Parser Text
pwHashAlgorithm <- if
| pwHashAlgorithm' == "pbkdf1" -> return PWStore.pbkdf1
| pwHashAlgorithm' == "pbkdf2" -> return PWStore.pbkdf2
| otherwise -> fail "Unsupported hash algorithm"
pwHashStrength <- o .: "strength"
return PWHashConf{..}
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
, lmsUploadDelimiter :: Maybe Char
, lmsDownloadHeader :: Bool
, lmsDownloadDelimiter :: Char
, lmsDownloadCrLf :: Bool
, lmsDeletionDays :: Int
} deriving (Show)
data AvsConf = AvsConf
{ avsHost :: String
, avsPort :: Int
, avsUser :: ByteString
, avsPass :: ByteString
} deriving (Show)
data LprConf = LprConf
{ lprHost :: String
, lprPort :: Int
, lprQueue:: String
} deriving (Show)
data SmtpConf = SmtpConf
{ smtpHost :: HaskellNet.HostName
, smtpPort :: HaskellNet.PortNumber
, smtpAuth :: Maybe SmtpAuthConf
, smtpSsl :: SmtpSslMode
, smtpPool :: ResourcePoolConf
} deriving (Show)
data WidgetMemcachedConf = WidgetMemcachedConf
{ widgetMemcachedConf :: MemcachedConf
, widgetMemcachedBaseUrl :: Text
} deriving (Show)
data MemcachedConf = MemcachedConf
{ memcachedConnectInfo :: Memcached.ConnectInfo
, memcachedExpiry :: Maybe NominalDiffTime
} deriving (Show)
instance FromJSON Memcached.Auth where
parseJSON = Aeson.withText "Auth" $ \(Text.breakOn "@" -> (encodeUtf8 -> user, encodeUtf8 -> pw)) -> return $ Memcached.Plain user pw
instance FromJSON MemcachedConf where
parseJSON = withObject "MemcachedConf" $ \o -> do
connectHost <- o .:? "host" .!= ""
connectPort <- o .: "port"
connectAuth <- o .: "auth"
numConnection <- o .: "limit"
connectionIdleTime <- o .: "timeout"
memcachedExpiry <- assertM (maybe True $ \t -> 0 < t && t <= 30 * nominalDay) $ o .:? "expiration"
return MemcachedConf{ memcachedConnectInfo = Memcached.ConnectInfo{..}, .. }
instance FromJSON WidgetMemcachedConf where
parseJSON v = flip (withObject "WidgetMemcachedConf") v $ \o -> do
widgetMemcachedConf <- parseJSON v
widgetMemcachedBaseUrl <- o .:? "base-url" .!= ""
return WidgetMemcachedConf{..}
data SmtpSslMode = SmtpSslNone | SmtpSslSmtps | SmtpSslStarttls
deriving (Show)
data SmtpAuthConf = SmtpAuthConf
{ smtpAuthType :: HaskellNet.AuthType
, smtpAuthUsername :: HaskellNet.UserName
, smtpAuthPassword :: HaskellNet.Password
} deriving (Show)
data TokenBucketConf = TokenBucketConf
{ tokenBucketDepth :: Word64
, tokenBucketInvRate :: NominalDiffTime
, tokenBucketInitialValue :: Int64
} deriving (Eq, Ord, Show, Generic)
data VerpMode = VerpNone
| Verp { verpPrefix :: Text, verpSeparator :: Char }
deriving (Eq, Show, Read, Generic)
data ARCConf w = ARCConf
{ arccMaximumGhost :: Int
, arccMaximumWeight :: w
} deriving (Eq, Ord, Read, Show, Generic)
data PrewarmCacheConf = PrewarmCacheConf
{ precMaximumWeight :: Int
, precStart, precEnd, precInhibit :: NominalDiffTime -- ^ Prewarming cache starts at @t - precStart@ and should be finished by @t - precEnd@; injecting from minio to database is inhibited from @t - precStart@ until @t - precStart + precInhibit@
, precSteps :: Natural
, precMaxSpeedup :: Rational
} deriving (Eq, Ord, Read, Show, Generic)
data SettingBotMitigation
= SettingBotMitigationOnlyLoggedInTableSorting
| SettingBotMitigationUnauthorizedFormHoneypots
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic)
deriving anyclass (Universe, Finite)
data LegalExternal = LegalExternal
{ externalLanguage :: Lang
, externalImprint :: Text
, externalDataProtection :: Text
, externalTermsOfUse :: Text
, externalPayments :: Text
}
deriving (Eq, Ord, Read, Show, Generic)
makeLenses_ ''LegalExternal
nullaryPathPiece ''ApprootScope $ camelToPathPiece' 1
pathPieceJSON ''ApprootScope
pathPieceJSONKey ''ApprootScope
pathPieceBinary ''ApprootScope
pathPieceHttpApiData ''ApprootScope
deriveJSON defaultOptions
{ constructorTagModifier = camelToPathPiece' 1
, fieldLabelModifier = camelToPathPiece' 1
, sumEncoding = UntaggedValue
} ''VerpMode
deriveJSON defaultOptions
{ fieldLabelModifier = camelToPathPiece' 2
} ''TokenBucketConf
deriveFromJSON defaultOptions
{ fieldLabelModifier = camelToPathPiece' 2
} ''UserDefaultConf
deriveJSON defaultOptions
{ fieldLabelModifier = camelToPathPiece' 1
, constructorTagModifier = camelToPathPiece' 1
} ''JobMode
deriveJSON defaultOptions
{ fieldLabelModifier = camelToPathPiece' 1
} ''ARCConf
deriveJSON defaultOptions
{ fieldLabelModifier = camelToPathPiece' 1
} ''PrewarmCacheConf
makeLenses_ ''PrewarmCacheConf
nullaryPathPiece ''SettingBotMitigation $ camelToPathPiece' 3
pathPieceJSON ''SettingBotMitigation
pathPieceJSONKey ''SettingBotMitigation
makePrisms ''JobMode
makeLenses_ ''JobMode
makePrisms ''AuthSourceConf
makeLenses_ ''UserAuthConf
makePrisms ''UserAuthConf
deriveFromJSON defaultOptions
{ constructorTagModifier = toLower . dropPrefix "AuthSourceConf"
, sumEncoding = TaggedObject "protocol" "config"
} ''AuthSourceConf
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
instance FromJSON LmsConf where
parseJSON = withObject "LmsConf" $ \o -> do
lmsUploadHeader <- o .: "upload-header"
lmsUploadDelimiter <- o .:? "upload-delimiter"
lmsDownloadHeader <- o .: "download-header"
lmsDownloadDelimiter <- o .: "download-delimiter"
lmsDownloadCrLf <- o .: "download-cr-lf"
lmsDeletionDays <- o .: "deletion-days"
return LmsConf{..}
makeLenses_ ''LmsConf
instance FromJSON AvsConf where
parseJSON = withObject "AvsConf" $ \o -> do
avsHost <- o .: "host"
avsPort <- o .: "port"
avsUser <- o .: "user"
avsPass <- o .:? "pass" .!= ""
return AvsConf{..}
instance FromJSON LprConf where
parseJSON = withObject "LprConf" $ \o -> do
lprHost <- o .: "host"
lprPort <- o .: "port"
lprQueue <- o .: "queue"
return LprConf{..}
instance FromJSON SmtpConf where
parseJSON = withObject "SmtpConf" $ \o -> do
smtpHost <- o .:? "host" .!= ""
smtpPort <- o .: "port"
smtpAuth <- assertM (not . null . smtpAuthUsername) <$> o .:? "auth"
smtpSsl <- o .: "ssl"
smtpPool <- o .: "pool"
return SmtpConf{..}
deriveFromJSON
defaultOptions
{ constructorTagModifier = intercalate "-" . map toLower . drop 2 . splitCamel
, allNullaryToStringTag = True
}
''SmtpSslMode
instance FromJSON SmtpAuthConf where
parseJSON = withObject "SmtpAuthConf" $ \o -> do
smtpAuthType <- o .: "type"
smtpAuthUsername <- o .:? "user" .!= ""
smtpAuthPassword <- o .:? "pass" .!= ""
return SmtpAuthConf{..}
instance FromJSON JwtEncoding where
parseJSON v@(String _) = JwsEncoding <$> parseJSON v
parseJSON v = flip (withObject "JwtEncoding") v $ \obj -> asum
[ do
alg <- obj .: "alg"
return $ JwsEncoding alg
, do
alg <- obj .: "alg"
enc <- obj .: "enc"
return $ JweEncoding alg enc
]
instance FromJSON Minio.ConnectInfo where
parseJSON v@(String _) = fromString <$> parseJSON v
parseJSON v = flip (withObject "ConnectInfo") v $ \o -> do
connectHost <- o .:? "host" .!= ""
connectPort <- o .: "port"
connectAccessKey <- o .:? "access-key" .!= ""
connectSecretKey <- o .:? "secret-key" .!= ""
connectIsSecure <- o .: "is-secure"
connectRegion <- o .:? "region" .!= ""
connectAutoDiscoverRegion <- o .:? "auto-discover-region" .!= True
connectDisableTLSCertValidation <- o .:? "disable-cert-validation" .!= False
return Minio.ConnectInfo{..}
instance FromJSON ServerSessionSettings where
parseJSON = withObject "ServerSession.State" $ \o -> do
idleTimeout <- o .:? "idle-timeout"
absoluteTimeout <- o .:? "absolute-timeout"
timeoutResolution <- o .:? "timeout-resolution"
persistentCookies <- o .:? "persistent-cookies"
return $ ServerSessionSettings (appEndo . foldMap Endo $ catMaybes
[ pure $ ServerSession.setIdleTimeout idleTimeout
, pure $ ServerSession.setAbsoluteTimeout absoluteTimeout
, pure $ ServerSession.setTimeoutResolution timeoutResolution
, ServerSession.setPersistentCookies <$> persistentCookies
])
instance FromJSON LegalExternal where
parseJSON = withObject "LegalExternal" $ \o -> do
externalLanguage <- o .: "language"
externalImprint <- o .: "imprint"
externalDataProtection <- o .: "data-protection"
externalTermsOfUse<- o .: "terms-of-use"
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.
@ -96,12 +452,16 @@ data AppSettings = AppSettings
, 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)
, 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 accessing the LDAP-directory
-- ^ 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)
-- ^ Configuration settings for accessing AVS Server (= Ausweis Verwaltungs System) -- TODO, TODISCUSS: reimplement as user-auth source?
, appLprConf :: LprConf
-- ^ Configuration settings for accessing a printer queue via lpr for letter mailing
, appSmtpConf :: Maybe SmtpConf
@ -109,15 +469,13 @@ data AppSettings = AppSettings
, 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.
-- ^ 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.
-- ^ Get the IP address from the header when logging. Useful when sitting behind a reverse proxy.
, appServerSessionConfig :: ServerSessionSettings
, appServerSessionAcidFallback :: Bool
@ -158,18 +516,21 @@ data AppSettings = AppSettings
, appHealthCheckActiveJobExecutorsTimeout :: NominalDiffTime
, appHealthCheckActiveWidgetMemcachedTimeout :: NominalDiffTime
, appHealthCheckSMTPConnectTimeout :: NominalDiffTime
, appHealthCheckLDAPAdminsTimeout :: NominalDiffTime
, appHealthCheckLDAPAdminsTimeout :: NominalDiffTime -- TODO: either generalize over every external auth sources, or otherwise reimplement for different semantics
, appHealthCheckHTTPReachableTimeout :: NominalDiffTime
, appHealthCheckMatchingClusterConfigTimeout :: NominalDiffTime
, appSynchroniseLdapUsersWithin :: Maybe NominalDiffTime
, appSynchroniseLdapUsersInterval :: 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
, appSynchroniseAvsUsersWithin :: Maybe NominalDiffTime
, appLdapPoolConf :: Maybe ResourcePoolConf -- TODO: generalize for arbitrary auth protocols
-- TODO: maybe use separate pools for external databases?
, appSynchroniseAvsUsersWithin :: Maybe NominalDiffTime
, appSynchroniseAvsUsersInterval :: NominalDiffTime
, appLdapReTestFailover :: DiffTime
, appSessionFilesExpire :: NominalDiffTime
, appKeepUnreferencedFiles :: NominalDiffTime
@ -254,365 +615,6 @@ data AppSettings = AppSettings
} deriving Show
data JobMode = JobsLocal { jobsAcceptOffload :: Bool }
| JobsOffload
| JobsDrop
{ jobsAcceptOffload :: Bool
, jobsWriteFakeLastExec :: Bool
}
deriving (Eq, Ord, Read, Show, Generic)
deriving anyclass (Hashable)
data ApprootScope = ApprootUserGenerated | ApprootDefault
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic)
deriving anyclass (Universe, Finite, Hashable)
newtype ServerSessionSettings
= ServerSessionSettings { applyServerSessionSettings :: forall a. ServerSession.State a -> ServerSession.State a }
instance Show ServerSessionSettings where
showsPrec d _ = showParen (d > 10) $ showString "ServerSessionSettings _"
data UserDefaultConf = UserDefaultConf
{ userDefaultTheme :: Theme
, userDefaultMaxFavourites, userDefaultMaxFavouriteTerms :: Int
, userDefaultDateTimeFormat, userDefaultDateFormat, userDefaultTimeFormat :: DateTimeFormat
, userDefaultDownloadFiles :: Bool
, userDefaultWarningDays :: NominalDiffTime
, userDefaultShowSex :: Bool
, userDefaultExamOfficeGetSynced :: Bool
, userDefaultExamOfficeGetLabels :: Bool
, userDefaultPrefersPostal :: Bool
} deriving (Show)
data PWHashConf = PWHashConf
{ pwHashAlgorithm :: PWHashAlgorithm
, pwHashStrength :: Int
}
instance Show PWHashConf where
show PWHashConf{..} = "PWHashConf { pwHashStrength = " <> show pwHashStrength <> ", .. }"
instance FromJSON PWHashConf where
parseJSON = withObject "PWHashConf" $ \o -> do
pwHashAlgorithm' <- o .: "algorithm" :: Aeson.Parser Text
pwHashAlgorithm <- if
| pwHashAlgorithm' == "pbkdf1" -> return PWStore.pbkdf1
| pwHashAlgorithm' == "pbkdf2" -> return PWStore.pbkdf2
| otherwise -> fail "Unsupported hash algorithm"
pwHashStrength <- o .: "strength"
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 LmsConf = LmsConf
{ lmsUploadHeader :: Bool
, lmsUploadDelimiter :: Maybe Char
, lmsDownloadHeader :: Bool
, lmsDownloadDelimiter :: Char
, lmsDownloadCrLf :: Bool
, lmsDeletionDays :: Int
} deriving (Show)
data AvsConf = AvsConf
{ avsHost :: String
, avsPort :: Int
, avsUser :: ByteString
, avsPass :: ByteString
} deriving (Show)
data LprConf = LprConf
{ lprHost :: String
, lprPort :: Int
, lprQueue:: String
} deriving (Show)
data SmtpConf = SmtpConf
{ smtpHost :: HaskellNet.HostName
, smtpPort :: HaskellNet.PortNumber
, smtpAuth :: Maybe SmtpAuthConf
, smtpSsl :: SmtpSslMode
, smtpPool :: ResourcePoolConf
} deriving (Show)
data WidgetMemcachedConf = WidgetMemcachedConf
{ widgetMemcachedConf :: MemcachedConf
, widgetMemcachedBaseUrl :: Text
} deriving (Show)
data MemcachedConf = MemcachedConf
{ memcachedConnectInfo :: Memcached.ConnectInfo
, memcachedExpiry :: Maybe NominalDiffTime
} deriving (Show)
instance FromJSON Memcached.Auth where
parseJSON = Aeson.withText "Auth" $ \(Text.breakOn "@" -> (encodeUtf8 -> user, encodeUtf8 -> pw)) -> return $ Memcached.Plain user pw
instance FromJSON MemcachedConf where
parseJSON = withObject "MemcachedConf" $ \o -> do
connectHost <- o .:? "host" .!= ""
connectPort <- o .: "port"
connectAuth <- o .: "auth"
numConnection <- o .: "limit"
connectionIdleTime <- o .: "timeout"
memcachedExpiry <- assertM (maybe True $ \t -> 0 < t && t <= 30 * nominalDay) $ o .:? "expiration"
return MemcachedConf{ memcachedConnectInfo = Memcached.ConnectInfo{..}, .. }
instance FromJSON WidgetMemcachedConf where
parseJSON v = flip (withObject "WidgetMemcachedConf") v $ \o -> do
widgetMemcachedConf <- parseJSON v
widgetMemcachedBaseUrl <- o .:? "base-url" .!= ""
return WidgetMemcachedConf{..}
data ResourcePoolConf = ResourcePoolConf
{ poolStripes :: Int
, poolTimeout :: NominalDiffTime
, poolLimit :: Int
} deriving (Show)
data SmtpSslMode = SmtpSslNone | SmtpSslSmtps | SmtpSslStarttls
deriving (Show)
data SmtpAuthConf = SmtpAuthConf
{ smtpAuthType :: HaskellNet.AuthType
, smtpAuthUsername :: HaskellNet.UserName
, smtpAuthPassword :: HaskellNet.Password
} deriving (Show)
data TokenBucketConf = TokenBucketConf
{ tokenBucketDepth :: Word64
, tokenBucketInvRate :: NominalDiffTime
, tokenBucketInitialValue :: Int64
} deriving (Eq, Ord, Show, Generic)
data VerpMode = VerpNone
| Verp { verpPrefix :: Text, verpSeparator :: Char }
deriving (Eq, Show, Read, Generic)
data ARCConf w = ARCConf
{ arccMaximumGhost :: Int
, arccMaximumWeight :: w
} deriving (Eq, Ord, Read, Show, Generic)
data PrewarmCacheConf = PrewarmCacheConf
{ precMaximumWeight :: Int
, precStart, precEnd, precInhibit :: NominalDiffTime -- ^ Prewarming cache starts at @t - precStart@ and should be finished by @t - precEnd@; injecting from minio to database is inhibited from @t - precStart@ until @t - precStart + precInhibit@
, precSteps :: Natural
, precMaxSpeedup :: Rational
} deriving (Eq, Ord, Read, Show, Generic)
data SettingBotMitigation
= SettingBotMitigationOnlyLoggedInTableSorting
| SettingBotMitigationUnauthorizedFormHoneypots
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic)
deriving anyclass (Universe, Finite)
data LegalExternal = LegalExternal
{ externalLanguage :: Lang
, externalImprint :: Text
, externalDataProtection :: Text
, externalTermsOfUse :: Text
, externalPayments :: Text
}
deriving (Eq, Ord, Read, Show, Generic)
makeLenses_ ''LegalExternal
nullaryPathPiece ''ApprootScope $ camelToPathPiece' 1
pathPieceJSON ''ApprootScope
pathPieceJSONKey ''ApprootScope
pathPieceBinary ''ApprootScope
pathPieceHttpApiData ''ApprootScope
deriveJSON defaultOptions
{ constructorTagModifier = camelToPathPiece' 1
, fieldLabelModifier = camelToPathPiece' 1
, sumEncoding = UntaggedValue
} ''VerpMode
deriveJSON defaultOptions
{ fieldLabelModifier = camelToPathPiece' 2
} ''TokenBucketConf
deriveFromJSON defaultOptions ''Ldap.Scope
deriveFromJSON defaultOptions
{ fieldLabelModifier = camelToPathPiece' 2
} ''UserDefaultConf
deriveJSON defaultOptions
{ fieldLabelModifier = camelToPathPiece' 1
, constructorTagModifier = camelToPathPiece' 1
} ''JobMode
deriveJSON defaultOptions
{ fieldLabelModifier = camelToPathPiece' 1
} ''ARCConf
deriveJSON defaultOptions
{ fieldLabelModifier = camelToPathPiece' 1
} ''PrewarmCacheConf
makeLenses_ ''PrewarmCacheConf
nullaryPathPiece ''SettingBotMitigation $ camelToPathPiece' 3
pathPieceJSON ''SettingBotMitigation
pathPieceJSONKey ''SettingBotMitigation
makePrisms ''JobMode
makeLenses_ ''JobMode
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
{ fieldLabelModifier = intercalate "-" . map toLower . drop 1 . splitCamel
}
''ResourcePoolConf
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
instance FromJSON LmsConf where
parseJSON = withObject "LmsConf" $ \o -> do
lmsUploadHeader <- o .: "upload-header"
lmsUploadDelimiter <- o .:? "upload-delimiter"
lmsDownloadHeader <- o .: "download-header"
lmsDownloadDelimiter <- o .: "download-delimiter"
lmsDownloadCrLf <- o .: "download-cr-lf"
lmsDeletionDays <- o .: "deletion-days"
return LmsConf{..}
makeLenses_ ''LmsConf
instance FromJSON AvsConf where
parseJSON = withObject "AvsConf" $ \o -> do
avsHost <- o .: "host"
avsPort <- o .: "port"
avsUser <- o .: "user"
avsPass <- o .:? "pass" .!= ""
return AvsConf{..}
instance FromJSON LprConf where
parseJSON = withObject "LprConf" $ \o -> do
lprHost <- o .: "host"
lprPort <- o .: "port"
lprQueue <- o .: "queue"
return LprConf{..}
instance FromJSON SmtpConf where
parseJSON = withObject "SmtpConf" $ \o -> do
smtpHost <- o .:? "host" .!= ""
smtpPort <- o .: "port"
smtpAuth <- assertM (not . null . smtpAuthUsername) <$> o .:? "auth"
smtpSsl <- o .: "ssl"
smtpPool <- o .: "pool"
return SmtpConf{..}
deriveFromJSON
defaultOptions
{ constructorTagModifier = intercalate "-" . map toLower . drop 2 . splitCamel
, allNullaryToStringTag = True
}
''SmtpSslMode
instance FromJSON SmtpAuthConf where
parseJSON = withObject "SmtpAuthConf" $ \o -> do
smtpAuthType <- o .: "type"
smtpAuthUsername <- o .:? "user" .!= ""
smtpAuthPassword <- o .:? "pass" .!= ""
return SmtpAuthConf{..}
instance FromJSON JwtEncoding where
parseJSON v@(String _) = JwsEncoding <$> parseJSON v
parseJSON v = flip (withObject "JwtEncoding") v $ \obj -> asum
[ do
alg <- obj .: "alg"
return $ JwsEncoding alg
, do
alg <- obj .: "alg"
enc <- obj .: "enc"
return $ JweEncoding alg enc
]
instance FromJSON Minio.ConnectInfo where
parseJSON v@(String _) = fromString <$> parseJSON v
parseJSON v = flip (withObject "ConnectInfo") v $ \o -> do
connectHost <- o .:? "host" .!= ""
connectPort <- o .: "port"
connectAccessKey <- o .:? "access-key" .!= ""
connectSecretKey <- o .:? "secret-key" .!= ""
connectIsSecure <- o .: "is-secure"
connectRegion <- o .:? "region" .!= ""
connectAutoDiscoverRegion <- o .:? "auto-discover-region" .!= True
connectDisableTLSCertValidation <- o .:? "disable-cert-validation" .!= False
return Minio.ConnectInfo{..}
instance FromJSON ServerSessionSettings where
parseJSON = withObject "ServerSession.State" $ \o -> do
idleTimeout <- o .:? "idle-timeout"
absoluteTimeout <- o .:? "absolute-timeout"
timeoutResolution <- o .:? "timeout-resolution"
persistentCookies <- o .:? "persistent-cookies"
return $ ServerSessionSettings (appEndo . foldMap Endo $ catMaybes
[ pure $ ServerSession.setIdleTimeout idleTimeout
, pure $ ServerSession.setAbsoluteTimeout absoluteTimeout
, pure $ ServerSession.setTimeoutResolution timeoutResolution
, ServerSession.setPersistentCookies <$> persistentCookies
])
instance FromJSON LegalExternal where
parseJSON = withObject "LegalExternal" $ \o -> do
externalLanguage <- o .: "language"
externalImprint <- o .: "imprint"
externalDataProtection <- o .: "data-protection"
externalTermsOfUse<- o .: "terms-of-use"
externalPayments <- o .: "payments"
return LegalExternal{..}
instance FromJSON AppSettings where
parseJSON = withObject "AppSettings" $ \o -> do
let defaultDev =
@ -627,10 +629,16 @@ instance FromJSON AppSettings where
appWebpackEntrypoints <- o .: "webpack-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"
appLprConf <- o .: "lpr"
@ -695,14 +703,13 @@ 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"
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"
@ -816,6 +823,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.
--
@ -825,16 +852,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
@ -855,24 +872,3 @@ widgetFile
| otherwise
= widgetFileNoReload widgetFileSettings
#endif
-- Since widgetFile above also add "templates" directory, requires import Text.Hamlet (hamletFile)
-- hamletFile' :: FilePath -> Q Exp
-- hamletFile' nameBase = hamletFile $ "templates" </> nameBase
-- | 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

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 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>
-- SPDX-FileCopyrightText: 2023-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>, 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>
--
-- 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
--------------------
-- Field Settings --
--------------------
@ -1259,10 +1274,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-23 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>,Steffen Jost <s.jost@fraport.de>
-- SPDX-FileCopyrightText: 2022-24 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>,Steffen Jost <s.jost@fraport.de>,David Mosbach <david.mosbach@uniworx.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
@ -81,6 +81,7 @@ data Icon
| IconNavContainerClose | IconPageActionChildrenClose
| IconMenuNews
| IconMenuHelp
| IconMenuAccount
| IconMenuProfile
| IconMenuLogin | IconMenuLogout
| IconBreadcrumbsHome
@ -173,6 +174,7 @@ iconText = \case
IconPageActionChildrenClose -> "chevron-up"
IconMenuNews -> "megaphone"
IconMenuHelp -> "question"
IconMenuAccount -> "user"
IconMenuProfile -> "cogs"
IconMenuLogin -> "sign-in-alt"
IconMenuLogout -> "sign-out-alt"

View File

@ -260,8 +260,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,35 +1,16 @@
-- 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
{-# OPTIONS_GHC -fno-warn-unused-top-binds #-}
module Utils.Users
( AuthenticationKind(..)
, AddUserData(..)
( AddUserData(..)
, addNewUser
) where
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
@ -49,10 +30,11 @@ data AddUserData = AddUserData
, audPinPassword :: Maybe Text
, audEmail :: UserEmail
, audIdent :: UserIdent
, audAuth :: AuthenticationKind
, audPassword :: Maybe Text
}
-- | Adds a new user to database, no background jobs are scheduled, no notifications send
-- | Adds a new user to database; no background jobs are scheduled, no notifications sent
addNewUser :: AddUserData -> Handler (Maybe UserId)
addNewUser AddUserData{..} = do
now <- liftIO getCurrentTime
@ -60,6 +42,8 @@ addNewUser AddUserData{..} = do
let
newUser = User
{ userIdent = audIdent
, userLastAuthentication = Nothing
, userPasswordHash = audPassword
, userMaxFavourites = userDefaultMaxFavourites
, userMaxFavouriteTerms = userDefaultMaxFavouriteTerms
, userTheme = userDefaultTheme
@ -76,9 +60,6 @@ addNewUser AddUserData{..} = do
, userCsvOptions = def { csvFormat = review csvPreset CsvPresetXlsx }
, userTokensIssuedAfter = Nothing
, userCreated = now
, userLastLdapSynchronisation = Nothing
, userLdapPrimaryKey = audFPersonalNumber
, userLastAuthentication = Nothing
, userEmail = audEmail
, userDisplayName = audDisplayName
, userDisplayEmail = audDisplayEmail
@ -96,6 +77,6 @@ addNewUser AddUserData{..} = do
, userPrefersPostal = audPrefersPostal
, userPinPassword = audPinPassword
, userMatrikelnummer = audMatriculation
, userAuthentication = mkAuthMode audAuth
, userLastSync = Nothing -- TODO: combine add user with external sync?
}
runDB $ insertUnique newUser
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
@ -26,33 +26,33 @@ packages:
- .
extra-deps:
- git: https://gitlab.ifi.lmu.de/uni2work/haskell/encoding.git
- git: https://gitlab.uniworx.de/haskell/encoding.git
commit: 22fc3bb14841d8d50997aa47f1be3852e666f787
- git: https://gitlab.ifi.lmu.de/uni2work/haskell/memcached-binary.git
- git: https://gitlab.uniworx.de/haskell/memcached-binary.git
commit: b7071df50bad3a251a544b984e4bf98fa09b8fae
- git: https://gitlab.ifi.lmu.de/uni2work/haskell/conduit-resumablesink.git
- git: https://gitlab.uniworx.de/haskell/conduit-resumablesink.git
commit: cbea6159c2975d42f948525e03e12fc390da53c5
- git: https://github.com/jtdaugherty/HaskellNet.git
commit: 5aa1f3b009253b02c4822005ac59ee208a10a347
- git: https://gitlab.ifi.lmu.de/uni2work/haskell/HaskellNet-SSL.git
- git: https://gitlab.uniworx.de/haskell/HaskellNet-SSL.git
commit: 40393c938111ac78232dc2c7eec5edb4a22d03e8
- git: https://gitlab.ifi.lmu.de/uni2work/haskell/ldap-client.git
- git: https://gitlab.uniworx.de/haskell/ldap-client.git
commit: 01afaf599ba6f8a9d804c269e91d3190b249d3f0
- git: https://gitlab.ifi.lmu.de/uni2work/haskell/serversession.git
- git: https://gitlab.uniworx.de/haskell/serversession.git
commit: fda3a000f9039e35e76e28f8e88c4942fac9fd69
subdirs:
- serversession
- serversession-backend-acid-state
- git: https://gitlab.ifi.lmu.de/uni2work/haskell/xss-sanitize.git
- git: https://gitlab.uniworx.de/haskell/xss-sanitize.git
commit: dc928c3a456074b8777603bea20e81937321777f
- git: https://gitlab.ifi.lmu.de/uni2work/haskell/colonnade.git
- git: https://gitlab.uniworx.de/haskell/colonnade.git
commit: f8170266ab25b533576e96715bedffc5aa4f19fa
subdirs:
- colonnade
- git: https://gitlab.ifi.lmu.de/uni2work/haskell/minio-hs.git
- git: https://gitlab.uniworx.de/haskell/minio-hs.git
commit: 42103ab247057c04c8ce7a83d9d4c160713a3df1
- git: https://gitlab.ifi.lmu.de/uni2work/haskell/cryptoids.git
- git: https://gitlab.uniworx.de/haskell/cryptoids.git
commit: 130b0dcbf2b09ccdf387b50262f1efbbbf1819e3
subdirs:
- cryptoids-class
@ -67,10 +67,10 @@ extra-deps:
- gearhash
- fastcdc
- git: https://gitlab.ifi.lmu.de/uni2work/haskell/zip-stream.git
- git: https://gitlab.uniworx.de/haskell/zip-stream.git
commit: 843683d024f767de236f74d24a3348f69181a720
- git: https://gitlab.ifi.lmu.de/uni2work/haskell/yesod.git
- git: https://gitlab.uniworx.de/haskell/yesod.git
commit: aa671eb41fdad360f2f7cb844f8de03479efe3f7 # <- references debug with traceStack; master ref: cb75191e0c5490246ae2cbcc2a00e7985cf2aadb
subdirs:
- yesod-core
@ -88,10 +88,13 @@ extra-deps:
- yesod-eventsource
- yesod-websockets
- git: https://gitlab.ifi.lmu.de/uni2work/haskell/cryptonite.git
- git: https://github.com/freckle/yesod-auth-oauth2
commit: 342dac80e40b10f07694a7e9aa8bab6d03ed6d66
- git: https://gitlab.uniworx.de/haskell/cryptonite.git
commit: 71a630edaf5f22c464e24fac8d9d310f4055ea1f
- git: https://gitlab.ifi.lmu.de/uni2work/haskell/esqueleto.git
- git: https://gitlab.uniworx.de/haskell/esqueleto.git
commit: e18dd125c5ea26fa4e88bed079b61d8c1365ee37
- classy-prelude-yesod-1.5.0@sha256:8f7e183bdfd6d2ea9674284c4f285294ab086aff60d9be4e5d7d2f3c1a2b05b7,1330

View File

@ -5,539 +5,550 @@
packages:
- completed:
commit: 22fc3bb14841d8d50997aa47f1be3852e666f787
git: https://gitlab.uniworx.de/haskell/encoding.git
name: encoding
version: 0.8.2
git: https://gitlab.ifi.lmu.de/uni2work/haskell/encoding.git
pantry-tree:
size: 5723
sha256: fec12328951021bb4d9326ae0b35f0c459e65f28442366efd4366cd1e18abe19
commit: 22fc3bb14841d8d50997aa47f1be3852e666f787
size: 5723
version: 0.8.2
original:
git: https://gitlab.ifi.lmu.de/uni2work/haskell/encoding.git
commit: 22fc3bb14841d8d50997aa47f1be3852e666f787
git: https://gitlab.uniworx.de/haskell/encoding.git
- completed:
commit: b7071df50bad3a251a544b984e4bf98fa09b8fae
git: https://gitlab.uniworx.de/haskell/memcached-binary.git
name: memcached-binary
version: 0.2.0
git: https://gitlab.ifi.lmu.de/uni2work/haskell/memcached-binary.git
pantry-tree:
size: 1277
sha256: 0da0539b7b9a56d03a116dcd666bc1bbbef085659910420849484d1418aa0857
commit: b7071df50bad3a251a544b984e4bf98fa09b8fae
size: 1277
version: 0.2.0
original:
git: https://gitlab.ifi.lmu.de/uni2work/haskell/memcached-binary.git
commit: b7071df50bad3a251a544b984e4bf98fa09b8fae
git: https://gitlab.uniworx.de/haskell/memcached-binary.git
- completed:
commit: cbea6159c2975d42f948525e03e12fc390da53c5
git: https://gitlab.uniworx.de/haskell/conduit-resumablesink.git
name: conduit-resumablesink
version: '0.3'
git: https://gitlab.ifi.lmu.de/uni2work/haskell/conduit-resumablesink.git
pantry-tree:
size: 394
sha256: 0cccf4684bbd84f81d2d3d53dd81c46cb103b5322f1d8e89e9b222211281e1b7
commit: cbea6159c2975d42f948525e03e12fc390da53c5
size: 394
version: '0.3'
original:
git: https://gitlab.ifi.lmu.de/uni2work/haskell/conduit-resumablesink.git
commit: cbea6159c2975d42f948525e03e12fc390da53c5
git: https://gitlab.uniworx.de/haskell/conduit-resumablesink.git
- completed:
commit: 5aa1f3b009253b02c4822005ac59ee208a10a347
git: https://github.com/jtdaugherty/HaskellNet.git
name: HaskellNet
version: 0.5.1
git: https://github.com/jtdaugherty/HaskellNet.git
pantry-tree:
size: 4011
sha256: 921b437ef18ccb04f889301c407263d6b5b72c5864803a000b1e61328988ce70
commit: 5aa1f3b009253b02c4822005ac59ee208a10a347
size: 4011
version: 0.5.1
original:
commit: 5aa1f3b009253b02c4822005ac59ee208a10a347
git: https://github.com/jtdaugherty/HaskellNet.git
commit: 5aa1f3b009253b02c4822005ac59ee208a10a347
- completed:
commit: 40393c938111ac78232dc2c7eec5edb4a22d03e8
git: https://gitlab.uniworx.de/haskell/HaskellNet-SSL.git
name: HaskellNet-SSL
version: 0.3.4.1
git: https://gitlab.ifi.lmu.de/uni2work/haskell/HaskellNet-SSL.git
pantry-tree:
size: 841
sha256: 95dcec22fdb8af986e59f0f60aa76d4a48f34a546dca799bd571e1d183f773e0
commit: 40393c938111ac78232dc2c7eec5edb4a22d03e8
size: 841
version: 0.3.4.1
original:
git: https://gitlab.ifi.lmu.de/uni2work/haskell/HaskellNet-SSL.git
commit: 40393c938111ac78232dc2c7eec5edb4a22d03e8
git: https://gitlab.uniworx.de/haskell/HaskellNet-SSL.git
- completed:
commit: 01afaf599ba6f8a9d804c269e91d3190b249d3f0
git: https://gitlab.uniworx.de/haskell/ldap-client.git
name: ldap-client
version: 0.4.0
git: https://gitlab.ifi.lmu.de/uni2work/haskell/ldap-client.git
pantry-tree:
size: 6176
sha256: 3fa8f102427b437b2baaec15cf884e88b47a1621b1c3fd4d8919f0263fde8656
commit: 01afaf599ba6f8a9d804c269e91d3190b249d3f0
size: 6176
version: 0.4.0
original:
git: https://gitlab.ifi.lmu.de/uni2work/haskell/ldap-client.git
commit: 01afaf599ba6f8a9d804c269e91d3190b249d3f0
git: https://gitlab.uniworx.de/haskell/ldap-client.git
- completed:
subdir: serversession
commit: fda3a000f9039e35e76e28f8e88c4942fac9fd69
git: https://gitlab.uniworx.de/haskell/serversession.git
name: serversession
version: 1.0.2
git: https://gitlab.ifi.lmu.de/uni2work/haskell/serversession.git
pantry-tree:
size: 545
sha256: 83ac78a987399db3da62f84bbd335fead11aadebd57251d0688127fca984db23
commit: fda3a000f9039e35e76e28f8e88c4942fac9fd69
original:
size: 545
subdir: serversession
git: https://gitlab.ifi.lmu.de/uni2work/haskell/serversession.git
version: 1.0.2
original:
commit: fda3a000f9039e35e76e28f8e88c4942fac9fd69
git: https://gitlab.uniworx.de/haskell/serversession.git
subdir: serversession
- completed:
subdir: serversession-backend-acid-state
commit: fda3a000f9039e35e76e28f8e88c4942fac9fd69
git: https://gitlab.uniworx.de/haskell/serversession.git
name: serversession-backend-acid-state
version: 1.0.4
git: https://gitlab.ifi.lmu.de/uni2work/haskell/serversession.git
pantry-tree:
size: 544
sha256: 4804260c6245c12e1728c78dd33bf16e95b7f2b69b38b6900a4e65b1ef3e04b7
commit: fda3a000f9039e35e76e28f8e88c4942fac9fd69
original:
size: 544
subdir: serversession-backend-acid-state
git: https://gitlab.ifi.lmu.de/uni2work/haskell/serversession.git
version: 1.0.4
original:
commit: fda3a000f9039e35e76e28f8e88c4942fac9fd69
git: https://gitlab.uniworx.de/haskell/serversession.git
subdir: serversession-backend-acid-state
- completed:
commit: dc928c3a456074b8777603bea20e81937321777f
git: https://gitlab.uniworx.de/haskell/xss-sanitize.git
name: xss-sanitize
version: 0.3.6
git: https://gitlab.ifi.lmu.de/uni2work/haskell/xss-sanitize.git
pantry-tree:
size: 750
sha256: f567a1c834448daaa164f2029fad164e6c8df2d4c92b51f811bae19cc0c95975
commit: dc928c3a456074b8777603bea20e81937321777f
size: 750
version: 0.3.6
original:
git: https://gitlab.ifi.lmu.de/uni2work/haskell/xss-sanitize.git
commit: dc928c3a456074b8777603bea20e81937321777f
git: https://gitlab.uniworx.de/haskell/xss-sanitize.git
- completed:
subdir: colonnade
commit: f8170266ab25b533576e96715bedffc5aa4f19fa
git: https://gitlab.uniworx.de/haskell/colonnade.git
name: colonnade
version: 1.2.0.2
git: https://gitlab.ifi.lmu.de/uni2work/haskell/colonnade.git
pantry-tree:
size: 481
sha256: 392393652cc0f354d351482557b9385c8e6122e706359b030373656565f2e045
commit: f8170266ab25b533576e96715bedffc5aa4f19fa
original:
size: 481
subdir: colonnade
git: https://gitlab.ifi.lmu.de/uni2work/haskell/colonnade.git
version: 1.2.0.2
original:
commit: f8170266ab25b533576e96715bedffc5aa4f19fa
git: https://gitlab.uniworx.de/haskell/colonnade.git
subdir: colonnade
- completed:
commit: 42103ab247057c04c8ce7a83d9d4c160713a3df1
git: https://gitlab.uniworx.de/haskell/minio-hs.git
name: minio-hs
version: 1.5.2
git: https://gitlab.ifi.lmu.de/uni2work/haskell/minio-hs.git
pantry-tree:
size: 4560
sha256: c5faff15fa22a7a63f45cd903c9bd11ae03f422c26f24750f5c44cb4d0db70fc
commit: 42103ab247057c04c8ce7a83d9d4c160713a3df1
size: 4560
version: 1.5.2
original:
git: https://gitlab.ifi.lmu.de/uni2work/haskell/minio-hs.git
commit: 42103ab247057c04c8ce7a83d9d4c160713a3df1
git: https://gitlab.uniworx.de/haskell/minio-hs.git
- completed:
subdir: cryptoids-class
commit: 130b0dcbf2b09ccdf387b50262f1efbbbf1819e3
git: https://gitlab.uniworx.de/haskell/cryptoids.git
name: cryptoids-class
version: 0.0.0
git: https://gitlab.ifi.lmu.de/uni2work/haskell/cryptoids.git
pantry-tree:
size: 412
sha256: 30466648d273ffb1d580b7961188d67a0bedb3703d6d5f8cca3c15a45295f203
commit: 130b0dcbf2b09ccdf387b50262f1efbbbf1819e3
original:
size: 412
subdir: cryptoids-class
git: https://gitlab.ifi.lmu.de/uni2work/haskell/cryptoids.git
commit: 130b0dcbf2b09ccdf387b50262f1efbbbf1819e3
- completed:
subdir: cryptoids-types
name: cryptoids-types
version: 1.0.0
git: https://gitlab.ifi.lmu.de/uni2work/haskell/cryptoids.git
pantry-tree:
size: 320
sha256: 824ac5c55c2ad553bd401bb5a99731bbdccc828ecc5d71f174e9375c4e03c46e
commit: 130b0dcbf2b09ccdf387b50262f1efbbbf1819e3
original:
subdir: cryptoids-types
git: https://gitlab.ifi.lmu.de/uni2work/haskell/cryptoids.git
commit: 130b0dcbf2b09ccdf387b50262f1efbbbf1819e3
- completed:
subdir: cryptoids
name: cryptoids
version: 0.5.1.0
git: https://gitlab.ifi.lmu.de/uni2work/haskell/cryptoids.git
pantry-tree:
size: 566
sha256: b1f49dde76ff7e78b76e7f2f3b3f76c55e5e61555d1df5415ad3b6eb80dda2cb
commit: 130b0dcbf2b09ccdf387b50262f1efbbbf1819e3
original:
subdir: cryptoids
git: https://gitlab.ifi.lmu.de/uni2work/haskell/cryptoids.git
commit: 130b0dcbf2b09ccdf387b50262f1efbbbf1819e3
- completed:
subdir: filepath-crypto
name: filepath-crypto
version: 0.1.0.0
git: https://gitlab.ifi.lmu.de/uni2work/haskell/cryptoids.git
pantry-tree:
size: 676
sha256: 9c31a2ffb2b1c86f9ba34eb83529c7a5a7dc68a49f89813c9b553427474654d9
commit: 130b0dcbf2b09ccdf387b50262f1efbbbf1819e3
original:
subdir: filepath-crypto
git: https://gitlab.ifi.lmu.de/uni2work/haskell/cryptoids.git
commit: 130b0dcbf2b09ccdf387b50262f1efbbbf1819e3
- completed:
subdir: uuid-crypto
name: uuid-crypto
version: 1.4.0.0
git: https://gitlab.ifi.lmu.de/uni2work/haskell/cryptoids.git
pantry-tree:
size: 417
sha256: 852e59807df1f2cf4b5a3748c46fa149d15a78651c93addfe5fc31d2d94c47f4
commit: 130b0dcbf2b09ccdf387b50262f1efbbbf1819e3
original:
subdir: uuid-crypto
git: https://gitlab.ifi.lmu.de/uni2work/haskell/cryptoids.git
commit: 130b0dcbf2b09ccdf387b50262f1efbbbf1819e3
- completed:
subdir: gearhash
name: gearhash
version: 1.0.0
git: https://github.com/gkleen/FastCDC.git
pantry-tree:
size: 551
sha256: 89c58554f6780bff2a2cab86e94d2f562eea34e8025a9925bfdc25b56c925d3e
commit: f216e3c0a1efa11a62fd4c9c2db38f7e2b7ac72d
original:
subdir: gearhash
git: https://github.com/gkleen/FastCDC.git
commit: f216e3c0a1efa11a62fd4c9c2db38f7e2b7ac72d
- completed:
subdir: fastcdc
name: fastcdc
version: 0.0.0
git: https://github.com/gkleen/FastCDC.git
original:
commit: 130b0dcbf2b09ccdf387b50262f1efbbbf1819e3
git: https://gitlab.uniworx.de/haskell/cryptoids.git
subdir: cryptoids-class
- completed:
commit: 130b0dcbf2b09ccdf387b50262f1efbbbf1819e3
git: https://gitlab.uniworx.de/haskell/cryptoids.git
name: cryptoids-types
pantry-tree:
sha256: 824ac5c55c2ad553bd401bb5a99731bbdccc828ecc5d71f174e9375c4e03c46e
size: 320
subdir: cryptoids-types
version: 1.0.0
original:
commit: 130b0dcbf2b09ccdf387b50262f1efbbbf1819e3
git: https://gitlab.uniworx.de/haskell/cryptoids.git
subdir: cryptoids-types
- completed:
commit: 130b0dcbf2b09ccdf387b50262f1efbbbf1819e3
git: https://gitlab.uniworx.de/haskell/cryptoids.git
name: cryptoids
pantry-tree:
sha256: b1f49dde76ff7e78b76e7f2f3b3f76c55e5e61555d1df5415ad3b6eb80dda2cb
size: 566
subdir: cryptoids
version: 0.5.1.0
original:
commit: 130b0dcbf2b09ccdf387b50262f1efbbbf1819e3
git: https://gitlab.uniworx.de/haskell/cryptoids.git
subdir: cryptoids
- completed:
commit: 130b0dcbf2b09ccdf387b50262f1efbbbf1819e3
git: https://gitlab.uniworx.de/haskell/cryptoids.git
name: filepath-crypto
pantry-tree:
sha256: 9c31a2ffb2b1c86f9ba34eb83529c7a5a7dc68a49f89813c9b553427474654d9
size: 676
subdir: filepath-crypto
version: 0.1.0.0
original:
commit: 130b0dcbf2b09ccdf387b50262f1efbbbf1819e3
git: https://gitlab.uniworx.de/haskell/cryptoids.git
subdir: filepath-crypto
- completed:
commit: 130b0dcbf2b09ccdf387b50262f1efbbbf1819e3
git: https://gitlab.uniworx.de/haskell/cryptoids.git
name: uuid-crypto
pantry-tree:
sha256: 852e59807df1f2cf4b5a3748c46fa149d15a78651c93addfe5fc31d2d94c47f4
size: 417
subdir: uuid-crypto
version: 1.4.0.0
original:
commit: 130b0dcbf2b09ccdf387b50262f1efbbbf1819e3
git: https://gitlab.uniworx.de/haskell/cryptoids.git
subdir: uuid-crypto
- completed:
commit: f216e3c0a1efa11a62fd4c9c2db38f7e2b7ac72d
git: https://github.com/gkleen/FastCDC.git
name: gearhash
pantry-tree:
sha256: 89c58554f6780bff2a2cab86e94d2f562eea34e8025a9925bfdc25b56c925d3e
size: 551
subdir: gearhash
version: 1.0.0
original:
commit: f216e3c0a1efa11a62fd4c9c2db38f7e2b7ac72d
git: https://github.com/gkleen/FastCDC.git
subdir: gearhash
- completed:
commit: f216e3c0a1efa11a62fd4c9c2db38f7e2b7ac72d
git: https://github.com/gkleen/FastCDC.git
name: fastcdc
pantry-tree:
size: 292
sha256: aa588b55c7c9c079e39569489a8089ec312f0538d02cf0e1fffe2f0e058566b8
commit: f216e3c0a1efa11a62fd4c9c2db38f7e2b7ac72d
original:
size: 292
subdir: fastcdc
git: https://github.com/gkleen/FastCDC.git
version: 0.0.0
original:
commit: f216e3c0a1efa11a62fd4c9c2db38f7e2b7ac72d
git: https://github.com/gkleen/FastCDC.git
subdir: fastcdc
- completed:
commit: 843683d024f767de236f74d24a3348f69181a720
git: https://gitlab.uniworx.de/haskell/zip-stream.git
name: zip-stream
version: 0.2.0.1
git: https://gitlab.ifi.lmu.de/uni2work/haskell/zip-stream.git
pantry-tree:
size: 812
sha256: 0da8bc38d73034962d2e2d1a7586b6dee848a629319fce9cbbf578348c61118c
commit: 843683d024f767de236f74d24a3348f69181a720
size: 812
version: 0.2.0.1
original:
git: https://gitlab.ifi.lmu.de/uni2work/haskell/zip-stream.git
commit: 843683d024f767de236f74d24a3348f69181a720
git: https://gitlab.uniworx.de/haskell/zip-stream.git
- completed:
subdir: yesod-core
commit: aa671eb41fdad360f2f7cb844f8de03479efe3f7
git: https://gitlab.uniworx.de/haskell/yesod.git
name: yesod-core
version: 1.6.20.2
git: https://gitlab.ifi.lmu.de/uni2work/haskell/yesod.git
pantry-tree:
size: 5954
sha256: 08c8da10b32c8d9f784238fd87232bf90b752e82f81ef2c52c62210f9aadda9a
commit: aa671eb41fdad360f2f7cb844f8de03479efe3f7
original:
size: 5954
subdir: yesod-core
git: https://gitlab.ifi.lmu.de/uni2work/haskell/yesod.git
version: 1.6.20.2
original:
commit: aa671eb41fdad360f2f7cb844f8de03479efe3f7
git: https://gitlab.uniworx.de/haskell/yesod.git
subdir: yesod-core
- completed:
subdir: yesod-static
commit: aa671eb41fdad360f2f7cb844f8de03479efe3f7
git: https://gitlab.uniworx.de/haskell/yesod.git
name: yesod-static
version: 1.6.1.0
git: https://gitlab.ifi.lmu.de/uni2work/haskell/yesod.git
pantry-tree:
size: 2949
sha256: 32c1608243a5309005ce11e2aa379ac1d6f8c380c529785eb510770118f3da06
commit: aa671eb41fdad360f2f7cb844f8de03479efe3f7
original:
size: 2949
subdir: yesod-static
git: https://gitlab.ifi.lmu.de/uni2work/haskell/yesod.git
version: 1.6.1.0
original:
commit: aa671eb41fdad360f2f7cb844f8de03479efe3f7
git: https://gitlab.uniworx.de/haskell/yesod.git
subdir: yesod-static
- completed:
subdir: yesod-persistent
commit: aa671eb41fdad360f2f7cb844f8de03479efe3f7
git: https://gitlab.uniworx.de/haskell/yesod.git
name: yesod-persistent
version: 1.6.0.7
git: https://gitlab.ifi.lmu.de/uni2work/haskell/yesod.git
pantry-tree:
size: 497
sha256: 3778ef2964e1a3890afc22cc9124eacb40e64b62bed4983a85d3b99897f54c5c
commit: aa671eb41fdad360f2f7cb844f8de03479efe3f7
original:
size: 497
subdir: yesod-persistent
git: https://gitlab.ifi.lmu.de/uni2work/haskell/yesod.git
version: 1.6.0.7
original:
commit: aa671eb41fdad360f2f7cb844f8de03479efe3f7
git: https://gitlab.uniworx.de/haskell/yesod.git
subdir: yesod-persistent
- completed:
subdir: yesod-newsfeed
commit: aa671eb41fdad360f2f7cb844f8de03479efe3f7
git: https://gitlab.uniworx.de/haskell/yesod.git
name: yesod-newsfeed
version: 1.7.0.0
git: https://gitlab.ifi.lmu.de/uni2work/haskell/yesod.git
pantry-tree:
size: 488
sha256: 53ebad62655863a657dcf749ffd3de46f6af90dd71f55bc4d50805ac48ddb099
commit: aa671eb41fdad360f2f7cb844f8de03479efe3f7
original:
size: 488
subdir: yesod-newsfeed
git: https://gitlab.ifi.lmu.de/uni2work/haskell/yesod.git
version: 1.7.0.0
original:
commit: aa671eb41fdad360f2f7cb844f8de03479efe3f7
git: https://gitlab.uniworx.de/haskell/yesod.git
subdir: yesod-newsfeed
- completed:
subdir: yesod-form
commit: aa671eb41fdad360f2f7cb844f8de03479efe3f7
git: https://gitlab.uniworx.de/haskell/yesod.git
name: yesod-form
version: 1.7.0
git: https://gitlab.ifi.lmu.de/uni2work/haskell/yesod.git
pantry-tree:
size: 1914
sha256: 260b7f16a8e1d58da137eb91aeed3a11ccbe59ba3e614457a635b9dc3e71426f
commit: aa671eb41fdad360f2f7cb844f8de03479efe3f7
original:
size: 1914
subdir: yesod-form
git: https://gitlab.ifi.lmu.de/uni2work/haskell/yesod.git
version: 1.7.0
original:
commit: aa671eb41fdad360f2f7cb844f8de03479efe3f7
git: https://gitlab.uniworx.de/haskell/yesod.git
subdir: yesod-form
- completed:
subdir: yesod-form-multi
commit: aa671eb41fdad360f2f7cb844f8de03479efe3f7
git: https://gitlab.uniworx.de/haskell/yesod.git
name: yesod-form-multi
version: 1.7.0.2
git: https://gitlab.ifi.lmu.de/uni2work/haskell/yesod.git
pantry-tree:
size: 328
sha256: b21fc50db43733dfe6e285345856610ba4feb83329e9cf953bf8047ba18ecbd6
commit: aa671eb41fdad360f2f7cb844f8de03479efe3f7
original:
size: 328
subdir: yesod-form-multi
git: https://gitlab.ifi.lmu.de/uni2work/haskell/yesod.git
version: 1.7.0.2
original:
commit: aa671eb41fdad360f2f7cb844f8de03479efe3f7
git: https://gitlab.uniworx.de/haskell/yesod.git
subdir: yesod-form-multi
- completed:
subdir: yesod-auth
commit: aa671eb41fdad360f2f7cb844f8de03479efe3f7
git: https://gitlab.uniworx.de/haskell/yesod.git
name: yesod-auth
version: 1.6.10.3
git: https://gitlab.ifi.lmu.de/uni2work/haskell/yesod.git
pantry-tree:
size: 1212
sha256: d335b940a207f8155f421b7146746a72d20db6ad54412154f2c829a59bf21e08
commit: aa671eb41fdad360f2f7cb844f8de03479efe3f7
original:
size: 1212
subdir: yesod-auth
git: https://gitlab.ifi.lmu.de/uni2work/haskell/yesod.git
version: 1.6.10.3
original:
commit: aa671eb41fdad360f2f7cb844f8de03479efe3f7
git: https://gitlab.uniworx.de/haskell/yesod.git
subdir: yesod-auth
- completed:
subdir: yesod-auth-oauth
commit: aa671eb41fdad360f2f7cb844f8de03479efe3f7
git: https://gitlab.uniworx.de/haskell/yesod.git
name: yesod-auth-oauth
version: 1.6.0.3
git: https://gitlab.ifi.lmu.de/uni2work/haskell/yesod.git
pantry-tree:
size: 321
sha256: 39d2f7d5d1abb3a2953858c5f23880e60ecfcdad0549ddc2570204f9c47649f4
commit: aa671eb41fdad360f2f7cb844f8de03479efe3f7
original:
size: 321
subdir: yesod-auth-oauth
git: https://gitlab.ifi.lmu.de/uni2work/haskell/yesod.git
version: 1.6.0.3
original:
commit: aa671eb41fdad360f2f7cb844f8de03479efe3f7
git: https://gitlab.uniworx.de/haskell/yesod.git
subdir: yesod-auth-oauth
- completed:
subdir: yesod-sitemap
commit: aa671eb41fdad360f2f7cb844f8de03479efe3f7
git: https://gitlab.uniworx.de/haskell/yesod.git
name: yesod-sitemap
version: 1.6.0
git: https://gitlab.ifi.lmu.de/uni2work/haskell/yesod.git
pantry-tree:
size: 314
sha256: 971f48af7011ff7816872d067e5de9cadafdd371bdf209170b77df36001abd27
commit: aa671eb41fdad360f2f7cb844f8de03479efe3f7
original:
size: 314
subdir: yesod-sitemap
git: https://gitlab.ifi.lmu.de/uni2work/haskell/yesod.git
version: 1.6.0
original:
commit: aa671eb41fdad360f2f7cb844f8de03479efe3f7
git: https://gitlab.uniworx.de/haskell/yesod.git
subdir: yesod-sitemap
- completed:
subdir: yesod-test
commit: aa671eb41fdad360f2f7cb844f8de03479efe3f7
git: https://gitlab.uniworx.de/haskell/yesod.git
name: yesod-test
version: 1.6.12
git: https://gitlab.ifi.lmu.de/uni2work/haskell/yesod.git
pantry-tree:
size: 563
sha256: 3d5022e8e3f8e77abcf075c42cf49efaa26f4951159bbb5ab50b69fdfeacb7c1
commit: aa671eb41fdad360f2f7cb844f8de03479efe3f7
original:
size: 563
subdir: yesod-test
git: https://gitlab.ifi.lmu.de/uni2work/haskell/yesod.git
version: 1.6.12
original:
commit: aa671eb41fdad360f2f7cb844f8de03479efe3f7
git: https://gitlab.uniworx.de/haskell/yesod.git
subdir: yesod-test
- completed:
subdir: yesod-bin
commit: aa671eb41fdad360f2f7cb844f8de03479efe3f7
git: https://gitlab.uniworx.de/haskell/yesod.git
name: yesod-bin
version: 1.6.1
git: https://gitlab.ifi.lmu.de/uni2work/haskell/yesod.git
pantry-tree:
size: 1295
sha256: 422d7816965b79826c6c24582d76dadbacd1bfb3e9a8f31208867cd788f2a5b8
commit: aa671eb41fdad360f2f7cb844f8de03479efe3f7
original:
size: 1295
subdir: yesod-bin
git: https://gitlab.ifi.lmu.de/uni2work/haskell/yesod.git
version: 1.6.1
original:
commit: aa671eb41fdad360f2f7cb844f8de03479efe3f7
git: https://gitlab.uniworx.de/haskell/yesod.git
subdir: yesod-bin
- completed:
subdir: yesod
commit: aa671eb41fdad360f2f7cb844f8de03479efe3f7
git: https://gitlab.uniworx.de/haskell/yesod.git
name: yesod
version: 1.6.1.1
git: https://gitlab.ifi.lmu.de/uni2work/haskell/yesod.git
pantry-tree:
size: 666
sha256: cb53ef3f2036185d2b4752d6fbc5d78470b4504e646e7eb4dd2397f2599daf42
commit: aa671eb41fdad360f2f7cb844f8de03479efe3f7
original:
size: 666
subdir: yesod
git: https://gitlab.ifi.lmu.de/uni2work/haskell/yesod.git
version: 1.6.1.1
original:
commit: aa671eb41fdad360f2f7cb844f8de03479efe3f7
git: https://gitlab.uniworx.de/haskell/yesod.git
subdir: yesod
- completed:
subdir: yesod-eventsource
commit: aa671eb41fdad360f2f7cb844f8de03479efe3f7
git: https://gitlab.uniworx.de/haskell/yesod.git
name: yesod-eventsource
version: 1.6.0.1
git: https://gitlab.ifi.lmu.de/uni2work/haskell/yesod.git
pantry-tree:
size: 324
sha256: 6d393201852cd024e377159ba836398e24d191563e08165430113d3c1384aff2
commit: aa671eb41fdad360f2f7cb844f8de03479efe3f7
original:
size: 324
subdir: yesod-eventsource
git: https://gitlab.ifi.lmu.de/uni2work/haskell/yesod.git
version: 1.6.0.1
original:
commit: aa671eb41fdad360f2f7cb844f8de03479efe3f7
git: https://gitlab.uniworx.de/haskell/yesod.git
subdir: yesod-eventsource
- completed:
subdir: yesod-websockets
commit: aa671eb41fdad360f2f7cb844f8de03479efe3f7
git: https://gitlab.uniworx.de/haskell/yesod.git
name: yesod-websockets
version: 0.3.0.3
git: https://gitlab.ifi.lmu.de/uni2work/haskell/yesod.git
pantry-tree:
size: 485
sha256: 02df6117e9b74a77879ea750130ba2d8ad8d3c99e14ca678320cb578984301e5
commit: aa671eb41fdad360f2f7cb844f8de03479efe3f7
original:
size: 485
subdir: yesod-websockets
git: https://gitlab.ifi.lmu.de/uni2work/haskell/yesod.git
version: 0.3.0.3
original:
commit: aa671eb41fdad360f2f7cb844f8de03479efe3f7
git: https://gitlab.uniworx.de/haskell/yesod.git
subdir: yesod-websockets
- completed:
commit: 342dac80e40b10f07694a7e9aa8bab6d03ed6d66
git: https://github.com/freckle/yesod-auth-oauth2
name: yesod-auth-oauth2
pantry-tree:
sha256: 22e8be5c8128e2f0fb976cb904ac93cefb49e6feef6bcadb7746641be11dcb13
size: 3054
version: 0.6.3.4
original:
commit: 342dac80e40b10f07694a7e9aa8bab6d03ed6d66
git: https://github.com/freckle/yesod-auth-oauth2
- completed:
commit: 71a630edaf5f22c464e24fac8d9d310f4055ea1f
git: https://gitlab.uniworx.de/haskell/cryptonite.git
name: cryptonite
version: '0.29'
git: https://gitlab.ifi.lmu.de/uni2work/haskell/cryptonite.git
pantry-tree:
size: 25056
sha256: 19e49259fa5e3c257495d72b3c7c3c49537aeafd508c780c2430ddca2ef71a91
commit: 71a630edaf5f22c464e24fac8d9d310f4055ea1f
size: 25056
version: '0.29'
original:
git: https://gitlab.ifi.lmu.de/uni2work/haskell/cryptonite.git
commit: 71a630edaf5f22c464e24fac8d9d310f4055ea1f
git: https://gitlab.uniworx.de/haskell/cryptonite.git
- completed:
commit: e18dd125c5ea26fa4e88bed079b61d8c1365ee37
git: https://gitlab.uniworx.de/haskell/esqueleto.git
name: esqueleto
version: 3.5.4.0
git: https://gitlab.ifi.lmu.de/uni2work/haskell/esqueleto.git
pantry-tree:
size: 5633
sha256: 8a93dc98eb4529ff64aa5bcdaa3c00dcdf0378033ad675864e2b0fc3d869d947
commit: e18dd125c5ea26fa4e88bed079b61d8c1365ee37
size: 5633
version: 3.5.4.0
original:
git: https://gitlab.ifi.lmu.de/uni2work/haskell/esqueleto.git
commit: e18dd125c5ea26fa4e88bed079b61d8c1365ee37
git: https://gitlab.uniworx.de/haskell/esqueleto.git
- completed:
hackage: classy-prelude-yesod-1.5.0@sha256:8f7e183bdfd6d2ea9674284c4f285294ab086aff60d9be4e5d7d2f3c1a2b05b7,1330
pantry-tree:
size: 330
sha256: ae84d4cc0e1daf985db6cdcf2ac92319531b8e60f547183cc46480d00aafbe20
size: 330
original:
hackage: classy-prelude-yesod-1.5.0@sha256:8f7e183bdfd6d2ea9674284c4f285294ab086aff60d9be4e5d7d2f3c1a2b05b7,1330
- completed:
hackage: acid-state-0.16.0.1@sha256:d43f6ee0b23338758156c500290c4405d769abefeb98e9bc112780dae09ece6f,6207
pantry-tree:
size: 13678
sha256: d57bcb2ad5e01fe7424abbcf9e58cf943027b5c4a8496d93625c57b6e1272274
size: 13678
original:
hackage: acid-state-0.16.0.1@sha256:d43f6ee0b23338758156c500290c4405d769abefeb98e9bc112780dae09ece6f,6207
- completed:
hackage: normaldistribution-1.1.0.3@sha256:2615b784c4112cbf6ffa0e2b55b76790290a9b9dff18a05d8c89aa374b213477,2160
pantry-tree:
size: 269
sha256: 856818862d12df8b030fa9cfef2c4ffa604d06f0eb057498db245dfffcd60e3c
size: 269
original:
hackage: normaldistribution-1.1.0.3@sha256:2615b784c4112cbf6ffa0e2b55b76790290a9b9dff18a05d8c89aa374b213477,2160
- completed:
hackage: pkcs7-1.0.0.1@sha256:b26e5181868667abbde3ce17f9a61cf705eb695da073cdf82e1f9dfd6cc11176,3594
pantry-tree:
size: 316
sha256: ab3c2d2880179a945ab3122c51d1657ab4a7a628292b646e047cd32b0751a80c
size: 316
original:
hackage: pkcs7-1.0.0.1@sha256:b26e5181868667abbde3ce17f9a61cf705eb695da073cdf82e1f9dfd6cc11176,3594
- completed:
hackage: system-locale-0.3.0.0@sha256:13b3982403d8ac8cc6138e68802be8d8e7cf7ebc4cbc7e47e99e3c0dd1be066a,1529
pantry-tree:
size: 446
sha256: 3b22af3e6315835bf614a0d30381ec7e47aca147b59ba601aeaa26f1fdc19373
size: 446
original:
hackage: system-locale-0.3.0.0@sha256:13b3982403d8ac8cc6138e68802be8d8e7cf7ebc4cbc7e47e99e3c0dd1be066a,1529
- completed:
hackage: token-bucket-0.1.0.1@sha256:d8e85f2fc373939975e7ace7907baee177531ab6e43df94e330a2357e64a2d11,1899
pantry-tree:
size: 399
sha256: b0b4a08ea1bf76bd108310f64d7f80e0f30b61ddc3d71f6cab7bdce329d2c1fa
size: 399
original:
hackage: token-bucket-0.1.0.1@sha256:d8e85f2fc373939975e7ace7907baee177531ab6e43df94e330a2357e64a2d11,1899
- completed:
hackage: tz-0.1.3.5@sha256:fb17ca50a7d943e511c0ca70342dc83f66aa2532de2745632f1f5f9b1ad783c4,5086
pantry-tree:
size: 1179
sha256: 6482698ea1b1a93bd684fca35836b35e8cdf53fe51b0fa6b215afa7da1f983a6
size: 1179
original:
hackage: tz-0.1.3.5@sha256:fb17ca50a7d943e511c0ca70342dc83f66aa2532de2745632f1f5f9b1ad783c4,5086
- completed:
hackage: unidecode-0.1.0.4@sha256:99581ee1ea334a4596a09ae3642e007808457c66893b587e965b31f15cbf8c4d,1144
pantry-tree:
size: 492
sha256: 4959068a0caf410dd4b8046f0b0138e3cf6471abb0cc865c9993db3b2930d283
size: 492
original:
hackage: unidecode-0.1.0.4@sha256:99581ee1ea334a4596a09ae3642e007808457c66893b587e965b31f15cbf8c4d,1144
- completed:
hackage: hlint-test-0.1.0.0@sha256:e427c0593433205fc629fb05b74c6b1deb1de72d1571f26142de008f0d5ee7a9,1814
pantry-tree:
size: 442
sha256: 347eac6c8a3c02fc0101444d6526b57b3c27785809149b12f90d8db57c721fea
size: 442
original:
hackage: hlint-test-0.1.0.0@sha256:e427c0593433205fc629fb05b74c6b1deb1de72d1571f26142de008f0d5ee7a9,1814
- completed:
hackage: servant-quickcheck-0.0.10.0@sha256:1d5849d703c2487752f8fc7391cca7c998ee24f54ca0bb72d238bf99b64ac667,3755
pantry-tree:
size: 976
sha256: 37dab60111c71d011fc4964e9a8b4b05ac544bc0ba8155e895518680066c2adb
size: 976
original:
hackage: servant-quickcheck-0.0.10.0@sha256:1d5849d703c2487752f8fc7391cca7c998ee24f54ca0bb72d238bf99b64ac667,3755
- completed:
hackage: servant-flatten-0.2@sha256:276896f7c5cdec5b8f8493f6205fded0cc602d050b58fdb09a6d7c85c3bb0837,1234
pantry-tree:
size: 325
sha256: 04f12c7bef2c3f9a25d94eb9489752ed498db8e243069fe95838dbb51df1dcb3
size: 325
original:
hackage: servant-flatten-0.2@sha256:276896f7c5cdec5b8f8493f6205fded0cc602d050b58fdb09a6d7c85c3bb0837,1234
- completed:
hackage: network-arbitrary-0.7.0.0@sha256:0cd381c80ae20c16048936edcdb018b1d9fbe2b6ac8c44e908df403a5c6d7cd5,2520
pantry-tree:
size: 912
sha256: a40b62eddfb12cfec753a10836a4ef5fe8ec94d7478e6957e1fe5729017928fb
size: 912
original:
hackage: network-arbitrary-0.7.0.0@sha256:0cd381c80ae20c16048936edcdb018b1d9fbe2b6ac8c44e908df403a5c6d7cd5,2520
- completed:
hackage: saltine-0.2.0.0@sha256:2232a285ef326b0942bbcbfa6f465933a020f27e19552213e688fe371d66dddd,5198
pantry-tree:
size: 5016
sha256: fdf4397f4b1ed7975f38d0b463eb6c9d206d0c85d157c41c19983e80b2005763
size: 5016
original:
hackage: saltine-0.2.0.0@sha256:2232a285ef326b0942bbcbfa6f465933a020f27e19552213e688fe371d66dddd,5198
- completed:
hackage: persistent-postgresql-2.13.0.3@sha256:43384bf8ed9c931c673e6abb763c8811113d1b7004095faaae1eb42e2cd52d8f,3601
pantry-tree:
size: 1059
sha256: 2d647a17372e42bc54331cfb35f5a55a71e6854dac8299b7ed6a1c69ae12734d
size: 1059
original:
hackage: persistent-postgresql-2.13.0.3@sha256:43384bf8ed9c931c673e6abb763c8811113d1b7004095faaae1eb42e2cd52d8f,3601
snapshots:
- completed:
sha256: c632012da648385b9fa3c29f4e0afd56ead299f1c5528ee789058be410e883c0
size: 585393
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/18/0.yaml
sha256: c632012da648385b9fa3c29f4e0afd56ead299f1c5528ee789058be410e883c0
original: lts-18.0

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 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>
$# SPDX-FileCopyrightText: 2022-2024 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>
$#
$# SPDX-License-Identifier: AGPL-3.0-or-later
@ -10,10 +10,6 @@ $# SPDX-License-Identifier: AGPL-3.0-or-later
_{MsgIdent}
<dd .deflist__dd .email>
#{userIdent}
<dt .deflist__dt>
_{MsgAuthModeSet}
<dd .deflist__dd>
_{userAuthentication}
$maybe avs <- avsId
<dt .deflist__dt>
_{MsgAvsPersonNo}
@ -124,6 +120,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>
@ -131,18 +146,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

@ -82,8 +82,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 = Nothing
, userEmail = "G.Kleen@campus.lmu.de"
@ -103,8 +104,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
@ -122,8 +121,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 = "noEmailKnown"
@ -143,8 +143,6 @@ fillDb = do
, userLanguages = Nothing
, userNotificationSettings = def
, userCreated = now
, userLastLdapSynchronisation = Nothing
, userLdapPrimaryKey = Nothing
, userCsvOptions = def { csvFormat = csvPreset # CsvPresetExcel }
, userSex = Just SexMale
, userShowSex = userDefaultShowSex
@ -164,12 +162,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"
@ -189,8 +187,6 @@ fillDb = do
, userLanguages = Nothing
, userNotificationSettings = def
, userCreated = now
, userLastLdapSynchronisation = Nothing
, userLdapPrimaryKey = Nothing
, userSex = Just SexMale
, userBirthday = Just $ n_day $ 35 * (-365)
, userCsvOptions = def
@ -208,8 +204,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"
@ -229,8 +226,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)
@ -248,8 +243,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"
@ -269,8 +265,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
@ -288,8 +282,9 @@ fillDb = do
}
svaupel <- insert User
{ userIdent = "vaupel.sarah@campus.lmu.de"
, userAuthentication = AuthLDAP
, userPasswordHash = Nothing
, userLastAuthentication = Nothing
, userLastSync = Nothing
, userTokensIssuedAfter = Nothing
, userMatrikelnummer = Nothing
, userEmail = "vaupel.sarah@campus.lmu.de"
@ -309,8 +304,6 @@ fillDb = do
, userLanguages = Nothing
, userNotificationSettings = def
, userCreated = now
, userLastLdapSynchronisation = Nothing
, userLdapPrimaryKey = Nothing
, userCsvOptions = def
, userSex = Just SexFemale
, userBirthday = Nothing
@ -328,8 +321,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"
@ -349,8 +343,6 @@ fillDb = do
, userLanguages = Nothing
, userNotificationSettings = def
, userCreated = now
, userLastLdapSynchronisation = Nothing
, userLdapPrimaryKey = Nothing
, userCsvOptions = def
, userSex = Just SexMale
, userBirthday = Nothing
@ -368,8 +360,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"
@ -389,8 +382,6 @@ fillDb = do
, userLanguages = Nothing
, userNotificationSettings = def
, userCreated = now
, userLastLdapSynchronisation = Nothing
, userLdapPrimaryKey = Nothing
, userCsvOptions = def
, userSex = Just SexMale
, userBirthday = Nothing
@ -408,8 +399,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"
@ -429,8 +421,6 @@ fillDb = do
, userLanguages = Nothing
, userNotificationSettings = def
, userCreated = now
, userLastLdapSynchronisation = Nothing
, userLdapPrimaryKey = Nothing
, userCsvOptions = def
, userSex = Just SexMale
, userBirthday = Nothing
@ -448,8 +438,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"
@ -469,8 +460,6 @@ fillDb = do
, userLanguages = Nothing
, userNotificationSettings = def
, userCreated = now
, userLastLdapSynchronisation = Nothing
, userLdapPrimaryKey = Nothing
, userCsvOptions = def
, userSex = Just SexMale
, userBirthday = Nothing
@ -526,8 +515,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'
@ -549,8 +539,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)
@ -217,21 +215,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
@ -462,8 +445,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)

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