diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d1d47e48e..48adc6a88 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -14,6 +14,12 @@ variables: POSTGRES_DB: uniworx_test POSTGRES_USER: uniworx POSTGRES_PASSWORD: uniworx + MINIO_ACCESS_KEY: gOel7KvadwNKgjjy + MINIO_SECRET_KEY: ugO5pkEla7F0JW9MdPwLi4MWLT5ZbqAL + UPLOAD_S3_HOST: localhost + UPLOAD_S3_PORT: 9000 + UPLOAD_S3_KEY_ID: gOel7KvadwNKgjjy + UPLOAD_S3_KEY: ugO5pkEla7F0JW9MdPwLi4MWLT5ZbqAL N_PREFIX: "${HOME}/.n" stages: @@ -82,9 +88,12 @@ frontend:lint: interruptible: true yesod:build:dev: - services: + services: &build-services - name: postgres:10.10 alias: postgres + - name: minio/minio:RELEASE.2020-08-27T05-16-20Z + alias: minio + command: ["minio", "server", "/data"] stage: yesod:build script: @@ -114,9 +123,7 @@ yesod:build:dev: interruptible: true yesod:build: - services: - - name: postgres:10.10 - alias: postgres + services: *build-services stage: yesod:build script: diff --git a/CHANGELOG.md b/CHANGELOG.md index 452d377d4..67e64602c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,86 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [20.1.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v20.0.0...v20.1.0) (2020-09-17) + + +### Features + +* **sheet:** warn about no submission without not graded ([9373266](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/937326639a02c576f278b79b8ebb441a2652bece)), closes [#342](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/342) + + +### Bug Fixes + +* **eexamlistr:** allow access for users with exam results ([885de44](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/885de4403c0172b3e9c3b59c277628106a7e925b)) +* **files:** fix download of non-injected files ([ce54adc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ce54adce6b67f3de95d65d74ff62b36cccdba47e)) + +## [20.0.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v19.3.1...v20.0.0) (2020-09-11) + + +### ⚠ BREAKING CHANGES + +* **files:** files now chunked + +### Features + +* **files:** avoid initial unnecessary rechunking ([e80f7d7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e80f7d7a89e205ce53a70178e0b44d9b0ddf5b97)) +* **files:** chunk prune-unreferenced-files finer ([58c2420](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/58c242045887673f69c368668803574d829cc823)) +* **files:** chunking ([8f608c1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8f608c19552ef7bd6ce61af92496b3d5f5bf61b1)) +* **files:** content dependent chunking ([d624a95](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d624a951c54bda86e04d440eba9901d2a65153b9)) + + +### Bug Fixes + +* zip handling & tests ([350ee79](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/350ee79af3c8fcc480970166a559596873beab2a)) + +### [19.3.1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v19.3.0...v19.3.1) (2020-09-10) + + +### Bug Fixes + +* **dbtable:** calculate height of header correctly ([5659f2d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5659f2df1e6ea473794075d85f2a43fc1037fce9)), closes [#634](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/634) + +## [19.3.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v19.2.2...v19.3.0) (2020-08-28) + + +### Features + +* add user-system-function ([abc37ac](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/abc37aca9c2aa5eafe7eea9333886b43189d5591)) +* automatically sync system functions from ldap ([297ff4f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/297ff4f02591339dda7f3270cc9cd332e18febb7)) +* course applications study features ([44eeffc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/44eeffcc70a8b4c119e1a88a9ef01c687fe2e10a)) +* generated columns tooltip ([2c4080d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2c4080d0e0d7f59829238830a5200116a9d884ec)) +* implement system-exam-office ([42aee66](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/42aee66d1f9c189a6a6b13b1970c61e0299630ae)) +* log ldap error messages on invalid-credentials ([0b4fade](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0b4fadedd2d7ffbb58598d9844e1c7d97cabc447)) +* reduce number of study features for courses ([51a98f0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/51a98f067086bcef3daff601b53d5eb45f4a27f0)) +* restore study features in all tables ([363f7ab](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/363f7abc192872ebd2a609b8bd89b58032bc9131)) +* study feature filtering ([96d0ba8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/96d0ba8f7a1c8d8d4e895541b66e36d35392fb25)) +* support for ldap primary keys ([bbfd182](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/bbfd182ed93d1e602229a2fd1ac1e0fa4c4439ef)) +* **study-features:** add study-features-first-observed ([dcb83d9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/dcb83d96fc0e52c0c322e50d9467d9a2bed90359)) +* **study-features:** further restriction by course ([f7a9bc8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f7a9bc831a3b0ef58fcbf7918be9f5e3b262641e)) + + +### Bug Fixes + +* don't set user-last-authentication during ldap sync ([fdaad16](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fdaad16e713e69a7b47f80a690a97d2ff5eb9986)) +* missing translations ([dcfdb51](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/dcfdb5130d19e737147bfe9065a6ccb5edf49a77)) +* order of on in exam office auth ([f44f150](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f44f1507471a9310a9c88738ca5b3d8268afc136)) +* tests ([018d26f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/018d26f4a1a1cf411324aeac56ce4d4203670942)) +* tests ([5541619](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5541619372f4a4e46ccc403004e869afdfaed7b0)) + +### [19.2.2](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v19.2.1...v19.2.2) (2020-08-26) + + +### Bug Fixes + +* have exam deregistration always delete stored grades ([24f428b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/24f428b13bb181bec99417b4e69fc538e35acbcf)) + +### [19.2.1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v19.2.0...v19.2.1) (2020-08-26) + + +### Bug Fixes + +* improve hidecolumns behaviour ([9a4f30b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9a4f30b811fdf8c58ec5c50c185628eb3158931a)) + ## [19.2.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v19.1.5...v19.2.0) (2020-08-24) diff --git a/config/settings.yml b/config/settings.yml index 3824a9f2b..d8b8b0330 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -35,7 +35,8 @@ bearer-expiration: 604800 bearer-encoding: HS256 maximum-content-length: "_env:MAX_UPLOAD_SIZE:134217728" session-files-expire: 3600 -prune-unreferenced-files: 28800 +prune-unreferenced-files-within: 57600 +prune-unreferenced-files-interval: 3600 keep-unreferenced-files: 86400 health-check-interval: matching-cluster-config: "_env:HEALTHCHECK_INTERVAL_MATCHING_CLUSTER_CONFIG:600" @@ -158,7 +159,13 @@ upload-cache: auto-discover-region: "_env:UPLOAD_S3_AUTO_DISCOVER_REGION:true" disable-cert-validation: "_env:UPLOAD_S3_DISABLE_CERT_VALIDATION:false" upload-cache-bucket: "uni2work-uploads" -inject-files: 10 + +inject-files: 307 +rechunk-files: 601 + +file-upload-db-chunksize: 4194304 # 4MiB +file-chunking-target-exponent: 21 # 2MiB +file-chunking-hash-window: 4096 server-sessions: idle-timeout: 28807 @@ -229,6 +236,9 @@ token-buckets: depth: 1572864000 # 1500MiB inv-rate: 1.9e-6 # 2MiB/s initial-value: 0 - + rechunk-files: + depth: 20971520 # 20MiB + inv-rate: 9.5e-7 # 1MiB/s + initial-value: 0 fallback-personalised-sheet-files-keys-expire: 2419200 diff --git a/config/test-settings.yml b/config/test-settings.yml index 7ba4552eb..905639ac1 100644 --- a/config/test-settings.yml +++ b/config/test-settings.yml @@ -1,5 +1,6 @@ database: database: "_env:PGDATABASE_TEST:uniworx_test" +upload-cache-bucket: "uni2work-test-uploads" log-settings: detailed: true @@ -10,4 +11,5 @@ log-settings: auth-dummy-login: true server-session-acid-fallback: true +job-cron-interval: null job-workers: 1 diff --git a/frontend/src/_common.sass b/frontend/src/_common.sass index cf4fab2cf..00bf18dfe 100644 --- a/frontend/src/_common.sass +++ b/frontend/src/_common.sass @@ -5,5 +5,5 @@ @use "~@fortawesome/fontawesome-pro/scss/solid" @use "~typeface-roboto" as roboto - @use "~typeface-source-sans-pro" as source-sans-pro +@use "~typeface-source-code-pro" as source-code-pro diff --git a/frontend/src/app.sass b/frontend/src/app.sass index 2c4e1a45d..d8c5f6e5d 100644 --- a/frontend/src/app.sass +++ b/frontend/src/app.sass @@ -22,6 +22,7 @@ // FONTS --font-base: "Source Sans Pro", "Trebuchet MS", sans-serif --font-logo: "Roboto", var(--font-base) + --font-monospace: "Source Code Pro", monospace // DIMENSIONS --header-height: 100px @@ -594,9 +595,6 @@ section border-bottom: none padding-bottom: 0px -.pseudonym - font-family: monospace - .headline-one margin-bottom: 10px @@ -726,8 +724,13 @@ section background-color: hsla($hue, 75%, 50%, $opacity) !important -.uuid - font-family: monospace +.uuid, .pseudonym, .ldap-primary-key, .email, .file-path, .metric-value, .metric-label + font-family: var(--font-monospace) + +.token + font-family: var(--font-monospace) + white-space: pre-wrap + word-break: break-all .form--inline display: inline-block @@ -841,7 +844,7 @@ section .comment, .literal-error white-space: pre-wrap - font-family: monospace + font-family: var(--font-monospace) th vertical-align: top @@ -1108,12 +1111,12 @@ th, td #changelog font-size: 14px white-space: pre-wrap - font-family: monospace + font-family: var(--font-monospace) #gitrev font-size: 12px white-space: pre-wrap - font-family: monospace + font-family: var(--font-monospace) .breadcrumbs__container position: relative @@ -1234,12 +1237,12 @@ a.breadcrumbs__home top: 5px .table__td--csv, .table__th--csv - font-family: monospace + font-family: var(--font-monospace) .confirmationText white-space: pre-wrap font-size: 14px - font-family: monospace + font-family: var(--font-monospace) .func-field__wrapper, .allocation-missing-prios, .allocation-users__accept, .corrections-overview__section max-height: 75vh @@ -1304,7 +1307,7 @@ a.breadcrumbs__home .csv-parse-error white-space: pre-wrap - font-family: monospace + font-family: var(--font-monospace) overflow: auto max-height: 75vh diff --git a/frontend/src/utils/form/datepicker.css b/frontend/src/utils/form/datepicker.css index 7c1172da1..b052a1e7c 100644 --- a/frontend/src/utils/form/datepicker.css +++ b/frontend/src/utils/form/datepicker.css @@ -29,7 +29,7 @@ visibility: hidden; direction: ltr; border-collapse: separate; - font-family: "Open Sans", Calibri, Arial, sans-serif; + /* font-family: "Open Sans", Calibri, Arial, sans-serif; */ background-color: white; border-width: 0; border-style: solid; @@ -724,4 +724,4 @@ } /* @end RTL */ -/*# sourceMappingURL=tail.datetime-default-green.map */ \ No newline at end of file +/*# sourceMappingURL=tail.datetime-default-green.map */ diff --git a/frontend/src/utils/inputs/inputs.sass b/frontend/src/utils/inputs/inputs.sass index 374d37189..022efa71d 100644 --- a/frontend/src/utils/inputs/inputs.sass +++ b/frontend/src/utils/inputs/inputs.sass @@ -235,7 +235,7 @@ option padding-bottom: 0 .file-input__list-item - font-family: monospace + font-family: var(--font-monospace) font-size: 15px // PREVIOUSLY UPLOADED FILES diff --git a/messages/uniworx/de-de-formal.msg b/messages/uniworx/de-de-formal.msg index abf0768af..d02d9acd1 100644 --- a/messages/uniworx/de-de-formal.msg +++ b/messages/uniworx/de-de-formal.msg @@ -389,6 +389,7 @@ SheetWarnNoActiveTo: "Aktiv bis/Ende Abgabezeitraum" sollte stets angegeben werd SheetNoCurrent: Es gibt momentan kein aktives Übungsblatt. SheetNoOldUnassigned: Alle Abgaben inaktiver Blätter sind bereits einen Korrektor zugeteilt. SheetsUnassignable name@Text: Momentan keine Abgaben zuteilbar für #{name} +SheetSubmissionModeNoneWithoutNotGraded: Es wurde "Keine Abgabe" eingestellt, jedoch nicht "Keine Bewertung". Kursteilnehmer werden nicht abgeben können. Deadline: Abgabe Done: Eingereicht @@ -478,6 +479,7 @@ UnauthorizedSchoolAdmin: Sie sind nicht als Administrator für dieses Institut e UnauthorizedAdminEscalation: Sie sind nicht Administrator für alle Institute, für die dieser Nutzer Administrator oder Veranstalter ist. UnauthorizedExamOffice: Sie sind nicht mit Prüfungsverwaltung beauftragt. UnauthorizedExamExamOffice: Es existieren keine Prüfungsergebnisse für Nutzer, für die Sie mit der Prüfungsverwaltung beauftragt sind. +UnauthorizedSystemExamOffice: Sie sind nicht mit systemweiter Prüfungsverwaltung beauftragt. UnauthorizedExternalExamExamOffice: Es existieren keine Prüfungsergebnisse für Nutzer, für die Sie mit der Prüfungsverwaltung beauftragt sind. UnauthorizedEvaluation: Sie sind nicht mit der Kursumfragenverwaltung beauftragt. UnauthorizedAllocationAdmin: Sie sind nicht mit der Administration von Zentralanmeldungen beauftragt. @@ -786,6 +788,9 @@ CorrectorsFor n@Int: #{pluralDE n "Korrektor" "Korrektoren"} UserListTitle: Komprehensive Benutzerliste AccessRightsSaved: Berechtigungen erfolgreich verändert AccessRightsNotChanged: Berechtigungen wurden nicht verändert +UserSystemFunctions: Systemweite Rollen +UserSystemFunctionsSaved: Systemweite Rollen gespeichert +UserSystemFunctionsNotChanged: Es wurden keine systemweiten Rollen angepasst LecturersForN n@Int: #{pluralDE n "Dozent" "Dozenten"} @@ -900,6 +905,8 @@ SubmissionReplace: Abgabe ersetzen SubmissionCreated: Abgabe erfolgreich angelegt SubmissionUpdated: Abgabe erfolgreich ersetzt +ColumnStudyFeatures: Studiendaten + AdminFeaturesHeading: Studiengänge StudyTerms: Studiengänge StudyTerm: Studiengang @@ -1033,6 +1040,10 @@ MailUserRightsIntro name@Text email@UserEmail: #{name} <#{email}> hat folgende U MailNoLecturerRights: Sie haben derzeit keine Dozenten-Rechte. MailLecturerRights n@Int: Als Dozent dürfen Sie Veranstaltungen innerhalb #{pluralDE n "Ihres Instituts" "Ihrer Institute"} anlegen. +MailSubjectUserSystemFunctionsUpdate name@Text: Berechtigungen für #{name} aktualisiert +MailUserSystemFunctionsIntro name@Text email@UserEmail: #{name} <#{email}> hat folgende Uni2work nicht-institutsbezogene Berechtigungen: +MailUserSystemFunctionsNoFunctions: Keine + MailSubjectUserAuthModeUpdate: Ihr Uni2work-Login UserAuthModePWHashChangedToLDAP: Sie können sich nun mit Ihrer Campus-Kennung in Uni2work einloggen UserAuthModeLDAPChangedToPWHash: Sie können sich nun mit einer Uni2work-internen Kennung in Uni2work einloggen @@ -1468,6 +1479,7 @@ AuthPredsActiveChanged: Authorisierungseinstellungen für aktuelle Sitzung gespe AuthTagFree: Seite ist universell zugänglich AuthTagAdmin: Nutzer ist Administrator AuthTagExamOffice: Nutzer ist mit Prüfungsverwaltung beauftragt +AuthTagSystemExamOffice: Nutzer ist mit systemweiter Prüfungsverwaltung beauftragt AuthTagEvaluation: Nutzer ist mit Kursumfragenverwaltung beauftragt AuthTagAllocationAdmin: Nutzer ist mit der Administration von Zentralanmeldungen beauftragt AuthTagToken: Nutzer präsentiert Authorisierungs-Token @@ -2020,7 +2032,7 @@ CsvColumnUserName: Voller Name des Teilnehmers CsvColumnUserMatriculation: Matrikelnummer des Teilnehmers CsvColumnUserSex: Geschlecht CsvColumnUserEmail: E-Mail-Adresse des Teilnehmers -CsvColumnUserStudyFeatures: Alle aktiven Studiendaten des Teilnehmers als Semikolon (;) separierte Liste +CsvColumnUserStudyFeatures: Alle relevanten Studiendaten des Teilnehmers als Semikolon (;) separierte Liste CsvColumnUserField: Studienfach, mit dem der Teilnehmer seine Kursanmeldung assoziiert hat CsvColumnUserDegree: Abschluss, den der Teilnehmer im assoziierten Studienfach anstrebt CsvColumnUserSemester: Fachsemester des Teilnehmers im assoziierten Studienfach @@ -2046,6 +2058,11 @@ CsvColumnApplicationsVeto: Bewerber mit Veto werden garantiert nicht dem Kurs zu CsvColumnApplicationsRating: Bewertung der Bewerbung; "1.0", "1.3", "1.7", ..., "4.0", "5.0" (Leer wird behandelt wie eine Note zwischen 2.3 und 2.7) CsvColumnApplicationsComment: Kommentar zur Bewerbung; je nach Kurs-Einstellungen entweder nur als Notiz für die Kursverwalter oder Feedback für den Bewerber +ApplicationGeneratedColumns: Stammdaten +ApplicationGeneratedColumnsTip: Stammdaten eines Bewerbers sind Daten, welche dem System zu diesem Benutzer bekannt sind und welche der Benutzer im Zuge der Bewerbung nicht beeinflussen kann. +ApplicationUserColumns: Bewerbung +ApplicationRatingColumns: Bewertung + Action: Aktion ActionNoUsersSelected: Keine Benutzer ausgewählt @@ -2501,6 +2518,9 @@ StudyTermsDefaultFieldType: Default Typ MenuLanguage: Sprache LanguageChanged: Sprache erfolgreich geändert +ProfileLastLdapSynchronisation: Letzte LDAP-Synchronisation +ProfileLdapPrimaryKey: LDAP-Primärschlüssel + ProfileCorrector: Korrektor ProfileCourses: Eigene Kurse ProfileCourseParticipations: Kursanmeldungen @@ -2766,3 +2786,6 @@ SheetPersonalisedFilesUsersList: Liste von Teilnehmern mit personalisierten Übu AdminCrontabNotGenerated: (Noch) keine Crontab generiert CronMatchAsap: ASAP CronMatchNone: Nie + +SystemExamOffice: Prüfungsverwaltung +SystemFaculty: Fakultätsmitglied \ No newline at end of file diff --git a/messages/uniworx/en-eu.msg b/messages/uniworx/en-eu.msg index 159d750c1..01e686a3e 100644 --- a/messages/uniworx/en-eu.msg +++ b/messages/uniworx/en-eu.msg @@ -389,6 +389,7 @@ SheetWarnNoActiveTo: “Active to/Submission period end” should always be spec SheetNoCurrent: There is no currently active exercise sheet SheetNoOldUnassigned: All submissions for inactive sheets are already assigned to correctors. SheetsUnassignable name: Submission for #{name} may not currently be assigned to correctors. +SheetSubmissionModeNoneWithoutNotGraded: The sheet was configured to be "No submission" but not "Not marked". Course participants will not be able to submit. Deadline: Deadline Done: Submitted @@ -479,6 +480,7 @@ UnauthorizedExamOffice: You are not part of an exam office. UnauthorizedEvaluation: You are not charged with course evaluation. UnauthorizedAllocationAdmin: You are not charged with the administration of central allocations. UnauthorizedExamExamOffice: You are not part of the appropriate exam office for any of the participants of this exam. +UnauthorizedSystemExamOffice: You are not charged with system wide exam administration UnauthorizedExternalExamExamOffice: You are not part of the appropriate exam office for any of the participants of this exam. UnauthorizedSchoolLecturer: You are no lecturer for this department. UnauthorizedLecturer: You are no administrator for this course. @@ -784,6 +786,9 @@ CorrectorsFor n: #{pluralEN n "Corrector" "Correctors"} UserListTitle: Comprehensive list of users AccessRightsSaved: Successfully updated permissions AccessRightsNotChanged: Permissions left unchanged +UserSystemFunctions: System wide roles +UserSystemFunctionsSaved: Successfully saved system wide roles +UserSystemFunctionsNotChanged: No system wide roles were changed LecturersForN n: #{pluralEN n "Lecturer" "Lecturers"} @@ -898,6 +903,8 @@ SubmissionReplace: Replace submission SubmissionCreated: Successfully created submission SubmissionUpdated: Successfully replaced submission +ColumnStudyFeatures: Features of study + AdminFeaturesHeading: Features of study StudyTerms: Fields of study StudyTerm: Field of study @@ -1034,6 +1041,10 @@ MailUserRightsIntro name email: #{name} <#{email}> now has the following permiss MailNoLecturerRights: You don't currently have lecturer permissions for any department. MailLecturerRights n: As a lecturer you may create new courses within your #{pluralEN n "department" "departments"}. +MailSubjectUserSystemFunctionsUpdate name: Permissions for #{name} changed +MailUserSystemFunctionsIntro name email: #{name} <#{email}> now has the following, not school restricted, permissions: +MailUserSystemFunctionsNoFunctions: None + MailSubjectUserAuthModeUpdate: Your Uni2work login UserAuthModePWHashChangedToLDAP: You can now log in to Uni2work using your Campus-account UserAuthModeLDAPChangedToPWHash: You can now log in to Uni2work using your Uni2work-internal account @@ -1469,6 +1480,7 @@ AuthPredsActiveChanged: Authorisation settings saved for the current session AuthTagFree: Page is freely accessable AuthTagAdmin: User is administrator AuthTagExamOffice: User is part of an exam office +AuthTagSystemExamOffice: User is charged with system wide exam administration AuthTagEvaluation: User is charged with course evaluation AuthTagAllocationAdmin: User is charged with administration of central allocations AuthTagToken: User is presenting an authorisation-token @@ -2019,7 +2031,7 @@ CsvColumnUserName: Participant's full name CsvColumnUserMatriculation: Participant's matriculation CsvColumnUserSex: Participant's sex CsvColumnUserEmail: Participant's email address -CsvColumnUserStudyFeatures: All active fields of study for the participant, separated by semicolon (;) +CsvColumnUserStudyFeatures: All relevant features of study for the participant, separated by semicolon (;) CsvColumnUserField: Field of study the participant specified when enrolling for the course CsvColumnUserDegree: Degree the participant pursues in their associated field of study CsvColumnUserSemester: Semester the participant is in wrt. to their associated field of study @@ -2045,6 +2057,11 @@ CsvColumnApplicationsVeto: Vetoed applicants are never assigned to the course; " CsvColumnApplicationsRating: Application grading; Any number grade ("1.0", "1.3", "1.7", ..., "4.0", "5.0"); Empty cells will be treated as if they contained a grade between 2.3 and 2.7 CsvColumnApplicationsComment: Application comment; depending on course settings this might purely be a note for course administrators or be feedback for the applicant +ApplicationGeneratedColumns: Master data +ApplicationGeneratedColumnsTip: An applicant's master data is data which is known to the system about this user and which the user cannot modify when applying for the course. +ApplicationUserColumns: Application +ApplicationRatingColumns: Rating + Action: Action ActionNoUsersSelected: No users selected @@ -2501,6 +2518,9 @@ StudyTermsDefaultFieldType: Default type MenuLanguage: Language LanguageChanged: Language changed successfully +ProfileLastLdapSynchronisation: Last LDAP synchronisation +ProfileLdapPrimaryKey: LDAP primary key + ProfileCorrector: Corrector ProfileCourses: Own courses ProfileCourseParticipations: Course registrations @@ -2767,3 +2787,6 @@ SheetPersonalisedFilesUsersList: List of course participants who have personalis AdminCrontabNotGenerated: Crontab not (yet) generated CronMatchAsap: ASAP CronMatchNone: Never + +SystemExamOffice: Exam office +SystemFaculty: Faculty member diff --git a/models/courses.model b/models/courses.model index 509a49d5f..137c0cdf1 100644 --- a/models/courses.model +++ b/models/courses.model @@ -56,7 +56,7 @@ CourseParticipant -- course enrolement course CourseId user UserId registration UTCTime -- time of last enrolement for this course - field StudyFeaturesId Maybe -- associated degree course, user-defined; required for communicating grades + field StudyFeaturesId Maybe MigrationOnly allocated AllocationId Maybe -- participant was centrally allocated state CourseParticipantState UniqueParticipant user course diff --git a/models/courses/applications.model b/models/courses/applications.model index b4648a60e..8e7d6c8d5 100644 --- a/models/courses/applications.model +++ b/models/courses/applications.model @@ -1,7 +1,7 @@ CourseApplication course CourseId user UserId - field StudyFeaturesId Maybe -- associated degree course, user-defined; required for communicating grades + field StudyFeaturesId Maybe MigrationOnly text Text Maybe -- free text entered by user ratingVeto Bool default=false ratingPoints ExamGrade Maybe diff --git a/models/files.model b/models/files.model index 428331b36..2a8656a3e 100644 --- a/models/files.model +++ b/models/files.model @@ -1,9 +1,20 @@ -FileContent +FileContentEntry hash FileContentReference + ix Natural + chunkHash FileContentChunkId + UniqueFileContentEntry hash ix + +FileContentChunk + hash FileContentChunkReference content ByteString - unreferencedSince UTCTime Maybe + contentBased Bool default=false -- For Migration Primary hash +FileContentChunkUnreferenced + hash FileContentChunkId + since UTCTime + UniqueFileContentChunkUnreferenced hash + SessionFile content FileContentReference Maybe touched UTCTime @@ -12,3 +23,8 @@ FileLock content FileContentReference instance InstanceId time UTCTime + +FileChunkLock + hash FileContentChunkReference + instance InstanceId + time UTCTime \ No newline at end of file diff --git a/models/users.model b/models/users.model index 657669910..b3b92e2d3 100644 --- a/models/users.model +++ b/models/users.model @@ -17,6 +17,7 @@ User json -- Each Uni2work user has a corresponding row in this table; create lastAuthentication UTCTime Maybe -- last login date created UTCTime default=now() lastLdapSynchronisation UTCTime Maybe + ldapPrimaryKey Text Maybe tokensIssuedAfter UTCTime Maybe -- do not accept bearer tokens issued before this time (accept all tokens if null) matrikelnummer UserMatriculation Maybe -- optional immatriculation-string; usually a number, but not always (e.g. lecturers, pupils, guests,...) firstName Text -- For export in tables, pre-split firstName from displayName @@ -42,6 +43,12 @@ UserFunction -- Administratively assigned functions (lecturer, admin, evaluation school SchoolId function SchoolFunction UniqueUserFunction user school function +UserSystemFunction + user UserId + function SystemFunction + manual Bool + isOptOut Bool + UniqueUserSystemFunction user function UserExamOffice user UserId field StudyTermsId @@ -58,8 +65,9 @@ StudyFeatures -- multiple entries possible for students pursuing several degree superField StudyTermsId Maybe type StudyFieldType -- Major or minor, i.e. Haupt-/Nebenfach semester Int - updated UTCTime default=now() -- last update from LDAP - valid Bool default=true -- marked as active in LDAP (students may switch, but LDAP never forgets) + firstObserved UTCTime Maybe + lastObserved UTCTime default=now() -- last update from LDAP + valid Bool default=true UniqueStudyFeatures user degree field type semester deriving Eq Show -- UniqueUserSubject ubuser degree field -- There exists a counterexample diff --git a/package-lock.json b/package-lock.json index 756462fda..5a78f8bad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "uni2work", - "version": "19.2.0", + "version": "20.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -20773,6 +20773,11 @@ "integrity": "sha512-VrR/IiH00Z1tFP4vDGfwZ1esNqTiDMchBEXYY9kilT6wRGgFoCAlgkEUMHb1E3mB0FsfZhv756IF0+R+SFPfdg==", "dev": true }, + "typeface-source-code-pro": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/typeface-source-code-pro/-/typeface-source-code-pro-1.1.3.tgz", + "integrity": "sha512-BAQ8I7Xy5zS5+KuG+gjRPNYCdfwL8vSF9jT8q9wzCRiiOG4h7id5zt8wcQx59riGRbRsgycRfs/ubyAm2z/FJQ==" + }, "typeface-source-sans-pro": { "version": "0.0.75", "resolved": "https://registry.npmjs.org/typeface-source-sans-pro/-/typeface-source-sans-pro-0.0.75.tgz", diff --git a/package.json b/package.json index 2b75424df..92a1f56d5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "uni2work", - "version": "19.2.0", + "version": "20.1.0", "description": "", "keywords": [], "author": "", @@ -109,6 +109,7 @@ "tmp": "^0.1.0", "typeface-roboto": "0.0.75", "typeface-source-sans-pro": "0.0.75", + "typeface-source-code-pro": "^1.1.3", "webpack": "^4.44.1", "webpack-cli": "^3.3.12", "webpack-manifest-plugin": "^2.2.0", diff --git a/package.yaml b/package.yaml index 513b569a9..fe4f0c6ce 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: uniworx -version: 19.2.0 +version: 20.1.0 dependencies: - base @@ -151,6 +151,7 @@ dependencies: - minio-hs - network-ip - data-textual + - fastcdc other-extensions: - GeneralizedNewtypeDeriving @@ -310,6 +311,7 @@ tests: - generic-arbitrary - http-types - yesod-persistent + - quickcheck-io ghc-options: - -fno-warn-orphans - -threaded -rtsopts "-with-rtsopts=-N -T" diff --git a/routes b/routes index e6420957e..d0c137c64 100644 --- a/routes +++ b/routes @@ -79,10 +79,10 @@ /user/storage-key StorageKeyR POST !free /exam-office ExamOfficeR !exam-office: - / EOExamsR GET + / EOExamsR GET !system-exam-office /fields EOFieldsR GET POST - /users EOUsersR GET POST - /users/invite EOUsersInviteR GET POST + /users EOUsersR GET POST !system-exam-office + /users/invite EOUsersInviteR GET POST !system-exam-office /external-exam EExamListR GET !lecturer !¬empty /external-exam/new EExamNewR GET POST !lecturer diff --git a/src/Application.hs b/src/Application.hs index 0325d9ef8..2eb0b9d46 100644 --- a/src/Application.hs +++ b/src/Application.hs @@ -228,13 +228,15 @@ makeFoundation appSettings'@AppSettings{..} = do forM_ ldapPool $ registerFailoverMetrics "ldap" -- Perform database migration using our application's logging settings. - if - | appAutoDbMigrate -> do - $logDebugS "setup" "Migration" - migrateAll `runSqlPool` sqlPool - | otherwise -> whenM (requiresMigration `runSqlPool` sqlPool) $ do - $logErrorS "setup" "Migration required" - liftIO . exitWith $ ExitFailure 2 + flip runReaderT tempFoundation $ + if + | appAutoDbMigrate -> do + $logDebugS "setup" "Migration" + migrateAll `runSqlPool` sqlPool + | otherwise -> whenM (requiresMigration `runSqlPool` sqlPool) $ do + $logErrorS "setup" "Migration required" + liftIO . exitWith $ ExitFailure 2 + $logDebugS "setup" "Cluster-Config" appCryptoIDKey <- clusterSetting (Proxy :: Proxy 'ClusterCryptoIDKey) `runSqlPool` sqlPool appSecretBoxKey <- clusterSetting (Proxy :: Proxy 'ClusterSecretBoxKey) `runSqlPool` sqlPool diff --git a/src/Auth/Dummy.hs b/src/Auth/Dummy.hs index 859b04554..351893fc9 100644 --- a/src/Auth/Dummy.hs +++ b/src/Auth/Dummy.hs @@ -1,5 +1,6 @@ module Auth.Dummy - ( dummyLogin + ( apDummy + , dummyLogin , DummyMessage(..) ) where @@ -32,6 +33,9 @@ dummyForm = do userList = fmap mkOptionList . runDB $ withReaderT projectBackend (map toOption <$> selectList [] [Asc UserIdent] :: ReaderT SqlBackend _ [Option UserIdent]) toOption (Entity _ User{..}) = Option userDisplayName userIdent (CI.original userIdent) +apDummy :: Text +apDummy = "dummy" + dummyLogin :: forall site. ( YesodAuth site , YesodPersist site @@ -44,7 +48,7 @@ dummyLogin :: forall site. dummyLogin = AuthPlugin{..} where apName :: Text - apName = "dummy" + apName = apDummy apDispatch :: forall m. MonadAuthHandler site m => Text -> [Text] -> m TypedContent apDispatch method [] | encodeUtf8 method == methodPost = liftSubHandler $ do diff --git a/src/Auth/LDAP.hs b/src/Auth/LDAP.hs index 9b57c8904..007178793 100644 --- a/src/Auth/LDAP.hs +++ b/src/Auth/LDAP.hs @@ -10,6 +10,7 @@ module Auth.LDAP , ldapUserMatriculation, ldapUserFirstName, ldapUserSurname , ldapUserTitle, ldapUserStudyFeatures, ldapUserFieldName , ldapUserSchoolAssociation, ldapUserSubTermsSemester, ldapSex + , ldapAffiliation, ldapPrimaryKey ) where import Import.NoFoundation @@ -68,7 +69,7 @@ userSearchSettings LdapConf{..} = mconcat , Ldap.derefAliases Ldap.DerefAlways ] -ldapUserPrincipalName, ldapUserDisplayName, ldapUserMatriculation, ldapUserFirstName, ldapUserSurname, ldapUserTitle, ldapUserStudyFeatures, ldapUserFieldName, ldapUserSchoolAssociation, ldapSex, ldapUserSubTermsSemester :: Ldap.Attr +ldapUserPrincipalName, ldapUserDisplayName, ldapUserMatriculation, ldapUserFirstName, ldapUserSurname, ldapUserTitle, ldapUserStudyFeatures, ldapUserFieldName, ldapUserSchoolAssociation, ldapSex, ldapUserSubTermsSemester, ldapAffiliation, ldapPrimaryKey :: Ldap.Attr ldapUserPrincipalName = Ldap.Attr "userPrincipalName" ldapUserDisplayName = Ldap.Attr "displayName" ldapUserMatriculation = Ldap.Attr "LMU-Stud-Matrikelnummer" @@ -80,6 +81,8 @@ ldapUserFieldName = Ldap.Attr "LMU-Stg-Fach" ldapUserSchoolAssociation = Ldap.Attr "LMU-IFI-eduPersonOrgUnitDNString" ldapSex = Ldap.Attr "schacGender" ldapUserSubTermsSemester = Ldap.Attr "LMU-Stg-FachundFS" +ldapAffiliation = Ldap.Attr "eduPersonAffiliation" +ldapPrimaryKey = Ldap.Attr "eduPersonPrincipalName" ldapUserEmail :: NonEmpty Ldap.Attr ldapUserEmail = Ldap.Attr "mail" :| @@ -200,7 +203,11 @@ campusLogin pool mode = AuthPlugin{..} $logErrorS apName $ "Error during login: " <> tshow err observeLoginOutcome apName LoginError loginErrorMessageI LoginR Msg.AuthError - Right (Left _bindErr) -> do + Right (Left bindErr) -> do + case bindErr of + Ldap.ResponseErrorCode _ _ _ errTxt -> + $logInfoS apName [st|#{campusIdent}: #{errTxt}|] + _other -> return () $logDebugS apName "Invalid credentials" observeLoginOutcome apName LoginInvalidCredentials loginErrorMessageI LoginR Msg.InvalidLogin diff --git a/src/Auth/PWHash.hs b/src/Auth/PWHash.hs index c5c7d53a8..9cca6d440 100644 --- a/src/Auth/PWHash.hs +++ b/src/Auth/PWHash.hs @@ -1,5 +1,6 @@ module Auth.PWHash - ( hashLogin + ( apHash + , hashLogin , PWHashMessage(..) ) where @@ -39,6 +40,8 @@ hashForm = do <$> areq ciField (fslpI MsgPWHashIdent (mr MsgPWHashIdentPlaceholder)) Nothing <*> areq passwordField (fslpI MsgPWHashPassword (mr MsgPWHashPasswordPlaceholder)) Nothing +apHash :: Text +apHash = "PWHash" hashLogin :: forall site. ( YesodAuth site @@ -53,7 +56,7 @@ hashLogin :: forall site. hashLogin pwHashAlgo = AuthPlugin{..} where apName :: Text - apName = "PWHash" + apName = apHash apDispatch :: forall m. MonadAuthHandler site m => Text -> [Text] -> m TypedContent apDispatch method [] | encodeUtf8 method == methodPost = liftSubHandler $ do diff --git a/src/Crypto/Hash/Instances.hs b/src/Crypto/Hash/Instances.hs index 93bf63516..0be90af18 100644 --- a/src/Crypto/Hash/Instances.hs +++ b/src/Crypto/Hash/Instances.hs @@ -18,6 +18,8 @@ import Data.Aeson as Aeson import Control.Monad.Fail +import Language.Haskell.TH.Syntax (Lift(liftTyped)) +import Instances.TH.Lift () instance HashAlgorithm hash => PersistField (Digest hash) where toPersistValue = PersistByteString . convert @@ -46,3 +48,6 @@ instance HashAlgorithm hash => FromJSON (Digest hash) where instance Hashable (Digest hash) where hashWithSalt s = (hashWithSalt s :: ByteString -> Int) . convert + +instance HashAlgorithm hash => Lift (Digest hash) where + liftTyped dgst = [||fromMaybe (error "Lifted digest has wrong length") $ digestFromByteString $$(liftTyped (convert dgst :: ByteString))||] diff --git a/src/Database/Esqueleto/Utils.hs b/src/Database/Esqueleto/Utils.hs index b31708c48..7db0e3c39 100644 --- a/src/Database/Esqueleto/Utils.hs +++ b/src/Database/Esqueleto/Utils.hs @@ -6,8 +6,10 @@ module Database.Esqueleto.Utils , justVal, justValList , isJust , isInfixOf, hasInfix + , strConcat, substring , or, and , any, all + , subSelectAnd, subSelectOr , mkExactFilter, mkExactFilterWith , mkContainsFilter, mkContainsFilterWith , mkExistsFilter @@ -21,22 +23,25 @@ module Database.Esqueleto.Utils , maybe, maybeEq, unsafeCoalesce , bool , max, min + , abs , SqlProject(..) , (->.) , fromSqlKey , selectCountRows , selectMaybe + , day, diffDays , module Database.Esqueleto.Utils.TH ) where -import ClassyPrelude.Yesod hiding (isInfixOf, any, all, or, and, isJust, maybe, bool, max, min) +import ClassyPrelude.Yesod hiding (isInfixOf, any, all, or, and, isJust, maybe, bool, max, min, abs) import Data.Universe import qualified Data.Set as Set import qualified Data.List as List import qualified Data.Foldable as F import qualified Database.Esqueleto as E -import qualified Database.Esqueleto.Internal.Sql as E +import qualified Database.Esqueleto.PostgreSQL as E +import qualified Database.Esqueleto.Internal.Internal as E import Database.Esqueleto.Utils.TH import qualified Data.Text.Lazy as Lazy (Text) @@ -93,6 +98,42 @@ hasInfix :: ( E.SqlString s1 => E.SqlExpr (E.Value s2) -> E.SqlExpr (E.Value s1) -> E.SqlExpr (E.Value Bool) hasInfix = flip isInfixOf +infixl 6 `strConcat` + +strConcat :: E.SqlString s + => E.SqlExpr (E.Value s) -> E.SqlExpr (E.Value s) -> E.SqlExpr (E.Value s) +strConcat = E.unsafeSqlBinOp " || " + +substring :: ( E.SqlString str + , Num from, Num for + ) + => E.SqlExpr (E.Value str) + -> E.SqlExpr (E.Value from) + -> E.SqlExpr (E.Value for) + -> E.SqlExpr (E.Value str) +substring (E.ERaw p1 f1) (E.ERaw p2 f2) (E.ERaw p3 f3) + = E.ERaw E.Never $ \info -> + let (strTLB, strVals) = f1 info + (fromiTLB, fromiVals) = f2 info + (foriTLB, foriVals) = f3 info + in ( "SUBSTRING" <> E.parens (E.parensM p1 strTLB <> " FROM " <> E.parensM p2 fromiTLB <> " FOR " <> E.parensM p3 foriTLB) + , strVals <> fromiVals <> foriVals + ) +substring a b c = substring (construct a) (construct b) (construct c) + where construct :: E.SqlExpr (E.Value a) -> E.SqlExpr (E.Value a) + construct (E.ERaw p f) = E.ERaw E.Parens $ \info -> + let (b1, vals) = f info + build ("?", [E.PersistList vals']) = + (E.uncommas $ replicate (length vals') "?", vals') + build expr = expr + in build (E.parensM p b1, vals) + construct (E.ECompositeKey f) = + E.ERaw E.Parens $ \info -> (E.uncommas $ f info, mempty) + construct (E.EAliasedValue i _) = + E.ERaw E.Never $ E.aliasedValueIdentToRawSql i + construct (E.EValueReference i i') = + E.ERaw E.Never $ E.valueReferenceToRawSql i i' + and, or :: Foldable f => f (E.SqlExpr (E.Value Bool)) -> E.SqlExpr (E.Value Bool) and = F.foldr (E.&&.) true or = F.foldr (E.||.) false @@ -107,6 +148,13 @@ any test = or . map test . otoList all :: MonoFoldable f => (Element f -> E.SqlExpr (E.Value Bool)) -> f -> E.SqlExpr (E.Value Bool) all test = and . map test . otoList +subSelectAnd, subSelectOr :: E.SqlQuery (E.SqlExpr (E.Value Bool)) -> E.SqlExpr (E.Value Bool) +subSelectAnd q = parens . E.subSelectUnsafe $ flip (E.unsafeSqlAggregateFunction "bool_and" E.AggModeAll) [] <$> q +subSelectOr q = parens . E.subSelectUnsafe $ flip (E.unsafeSqlAggregateFunction "bool_or" E.AggModeAll) [] <$> q + +parens :: E.SqlExpr (E.Value a) -> E.SqlExpr (E.Value a) +parens = E.unsafeSqlFunction "" + -- Allow usage of Tuples as DbtRowKey, i.e. SqlIn instances for tuples $(sqlInTuples [2..16]) @@ -289,6 +337,11 @@ max, min :: PersistField a max a b = bool a b $ b E.>. a min a b = bool a b $ b E.<. a +abs :: (PersistField a, Num a) + => E.SqlExpr (E.Value a) + -> E.SqlExpr (E.Value a) +abs x = bool (E.val 0 E.-. x) x $ x E.>. E.val 0 + unsafeCoalesce :: E.PersistField a => [E.SqlExpr (E.Value (Maybe a))] -> E.SqlExpr (E.Value a) unsafeCoalesce = E.veryUnsafeCoerceSqlExprValue . E.coalesce @@ -325,3 +378,13 @@ selectCountRows q = do selectMaybe :: (E.SqlSelect a r, MonadIO m) => E.SqlQuery a -> E.SqlReadT m (Maybe r) selectMaybe = fmap listToMaybe . E.select . (<* E.limit 1) + + +day :: E.SqlExpr (E.Value UTCTime) -> E.SqlExpr (E.Value Day) +day = E.unsafeSqlCastAs "date" + +infixl 6 `diffDays` + +diffDays :: E.SqlExpr (E.Value Day) -> E.SqlExpr (E.Value Day) -> E.SqlExpr (E.Value Int) +-- ^ PostgreSQL is weird. +diffDays a b = E.veryUnsafeCoerceSqlExprValue $ a E.-. b diff --git a/src/Foundation/Authorization.hs b/src/Foundation/Authorization.hs index 991224b2f..9b7b211bd 100644 --- a/src/Foundation/Authorization.hs +++ b/src/Foundation/Authorization.hs @@ -324,6 +324,11 @@ tagAccessPredicate AuthAdmin = APDB $ \mAuthId route _ -> case route of adrights <- lift $ selectFirst [UserFunctionUser ==. authId, UserFunctionFunction ==. SchoolAdmin] [] guardMExceptT (isJust adrights) (unauthorizedI MsgUnauthorizedSiteAdmin) return Authorized +tagAccessPredicate AuthSystemExamOffice = APDB $ \mAuthId _ _ -> $cachedHereBinary mAuthId . exceptT return return $ do + authId <- maybeExceptT AuthenticationRequired $ return mAuthId + isExamOffice <- lift $ exists [UserSystemFunctionUser ==. authId, UserSystemFunctionFunction ==. SystemExamOffice, UserSystemFunctionIsOptOut ==. False] + guardMExceptT isExamOffice $ unauthorizedI MsgUnauthorizedSystemExamOffice + return Authorized tagAccessPredicate AuthExamOffice = APDB $ \mAuthId route _ -> case route of CExamR tid ssh csh examn _ -> $cachedHereBinary (mAuthId, tid, ssh, csh, examn) . exceptT return return $ do authId <- maybeExceptT AuthenticationRequired $ return mAuthId @@ -1191,6 +1196,10 @@ tagAccessPredicate AuthEmpty = APDB $ \mAuthId route _ -> case route of hasExternalExams <- $cachedHereBinary authId . lift . E.selectExists . E.from $ \(eexam `E.InnerJoin` eexamStaff) -> do E.on $ eexam E.^. ExternalExamId E.==. eexamStaff E.^. ExternalExamStaffExam E.where_ $ eexamStaff E.^. ExternalExamStaffUser E.==. E.val authId + E.||. E.exists (E.from $ \externalExamResult -> + E.where_ $ externalExamResult E.^. ExternalExamResultExam E.==. eexam E.^. ExternalExamId + E.&&. externalExamResult E.^. ExternalExamResultUser E.==. E.val authId + ) guardMExceptT (not hasExternalExams) $ unauthorizedI MsgUnauthorizedExternalExamListNotEmpty return Authorized CourseR tid ssh csh _ -> maybeT (unauthorizedI MsgCourseNotEmpty) $ do diff --git a/src/Foundation/I18n.hs b/src/Foundation/I18n.hs index aa514a72d..71543f2d9 100644 --- a/src/Foundation/I18n.hs +++ b/src/Foundation/I18n.hs @@ -219,6 +219,7 @@ embedRenderMessage ''UniWorX ''UploadModeDescr id embedRenderMessage ''UniWorX ''SecretJSONFieldException id embedRenderMessage ''UniWorX ''AFormMessage $ concat . drop 2 . splitCamel embedRenderMessage ''UniWorX ''SchoolFunction id +embedRenderMessage ''UniWorX ''SystemFunction id embedRenderMessage ''UniWorX ''CsvPreset id embedRenderMessage ''UniWorX ''Quoting ("Csv" <>) embedRenderMessage ''UniWorX ''FavouriteReason id diff --git a/src/Foundation/Type.hs b/src/Foundation/Type.hs index 5595127e8..5257f1c35 100644 --- a/src/Foundation/Type.hs +++ b/src/Foundation/Type.hs @@ -7,7 +7,7 @@ module Foundation.Type , _SessionStorageMemcachedSql, _SessionStorageAcid , SMTPPool , _appSettings', _appStatic, _appConnPool, _appSmtpPool, _appLdapPool, _appWidgetMemcached, _appHttpManager, _appLogger, _appLogSettings, _appCryptoIDKey, _appClusterID, _appInstanceID, _appJobState, _appSessionStore, _appSecretBoxKey, _appJSONWebKeySet, _appHealthReport - , DB, Form, MsgRenderer, MailM + , DB, Form, MsgRenderer, MailM, DBFile ) where import Import.NoFoundation @@ -81,3 +81,4 @@ type DB = YesodDB UniWorX type Form x = Html -> MForm (HandlerFor UniWorX) (FormResult x, WidgetFor UniWorX ()) type MsgRenderer = MsgRendererS UniWorX -- see Utils type MailM a = MailT (HandlerFor UniWorX) a +type DBFile = File (YesodDB UniWorX) diff --git a/src/Foundation/Types.hs b/src/Foundation/Types.hs index 4e21dce2f..7cfa5dc0a 100644 --- a/src/Foundation/Types.hs +++ b/src/Foundation/Types.hs @@ -1,6 +1,6 @@ module Foundation.Types ( UpsertCampusUserMode(..) - , _UpsertCampusUser, _UpsertCampusUserDummy, _UpsertCampusUserOther + , _UpsertCampusUserLoginLdap, _UpsertCampusUserLoginDummy, _UpsertCampusUserLoginOther, _UpsertCampusUserLdapSync, _UpsertCampusUserGuessUser , _upsertCampusUserIdent ) where @@ -8,9 +8,11 @@ import Import.NoFoundation data UpsertCampusUserMode - = UpsertCampusUser - | UpsertCampusUserDummy { upsertCampusUserIdent :: UserIdent } - | UpsertCampusUserOther { uspertCampusUserIdent :: UserIdent } + = UpsertCampusUserLoginLdap + | UpsertCampusUserLoginDummy { upsertCampusUserIdent :: UserIdent } + | UpsertCampusUserLoginOther { upsertCampusUserIdent :: UserIdent } + | UpsertCampusUserLdapSync { upsertCampusUserIdent :: UserIdent } + | UpsertCampusUserGuessUser deriving (Eq, Ord, Read, Show, Generic, Typeable) makeLenses_ ''UpsertCampusUserMode diff --git a/src/Foundation/Yesod/Auth.hs b/src/Foundation/Yesod/Auth.hs index 66941c9f6..1c8bd1e61 100644 --- a/src/Foundation/Yesod/Auth.hs +++ b/src/Foundation/Yesod/Auth.hs @@ -14,14 +14,17 @@ import Foundation.I18n import Handler.Utils.Profile import Handler.Utils.StudyFeatures import Handler.Utils.SchoolLdap +import Handler.Utils.LdapSystemFunctions import Yesod.Auth.Message import Auth.LDAP +import Auth.PWHash (apHash) +import Auth.Dummy (apDummy) import qualified Data.CaseInsensitive as CI import qualified Control.Monad.Catch as C (Handler(..)) -import qualified Data.List.NonEmpty as NonEmpty import qualified Ldap.Client as Ldap +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 @@ -53,8 +56,8 @@ authenticate creds@Creds{..} = liftHandler . runDB . withReaderT projectBackend uAuth = UniqueAuthentication $ CI.mk credsIdent upsertMode = creds ^? _upsertCampusUserMode - isDummy = is (_Just . _UpsertCampusUserDummy) upsertMode - isOther = is (_Just . _UpsertCampusUserOther) upsertMode + isDummy = is (_Just . _UpsertCampusUserLoginDummy) upsertMode + isOther = is (_Just . _UpsertCampusUserLoginOther) upsertMode excRecovery res | isDummy || isOther @@ -127,31 +130,37 @@ data CampusUserConversionException _upsertCampusUserMode :: Traversal' (Creds UniWorX) UpsertCampusUserMode _upsertCampusUserMode mMode cs@Creds{..} - | credsPlugin == "dummy" = setMode <$> mMode (UpsertCampusUserDummy $ CI.mk credsIdent) - | credsPlugin `elem` others = setMode <$> mMode (UpsertCampusUserOther $ CI.mk credsIdent) - | otherwise = setMode <$> mMode UpsertCampusUser + | credsPlugin == apDummy = setMode <$> mMode (UpsertCampusUserLoginDummy $ CI.mk credsIdent) + | credsPlugin == apLdap = setMode <$> mMode UpsertCampusUserLoginLdap + | otherwise = setMode <$> mMode (UpsertCampusUserLoginOther $ CI.mk credsIdent) where - setMode UpsertCampusUser - = cs{ credsPlugin = "LDAP" } - setMode (UpsertCampusUserDummy ident) - = cs{ credsPlugin = "dummy", credsIdent = CI.original ident } - setMode (UpsertCampusUserOther ident) - = cs{ credsPlugin = bool (NonEmpty.head others) credsPlugin (credsPlugin `elem` others), credsIdent = CI.original ident } + 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 _ = cs - others = "PWHash" :| [] + defaultOther = apHash upsertCampusUser :: forall m. ( MonadHandler m, HandlerSite m ~ UniWorX , MonadThrow m ) => UpsertCampusUserMode -> Ldap.AttrList [] -> SqlPersistT m (Entity User) -upsertCampusUser plugin ldapData = do +upsertCampusUser upsertMode ldapData = do now <- liftIO getCurrentTime UserDefaultConf{..} <- getsYesod $ view _appUserDefaults let userIdent'' = fold [ v | (k, v) <- ldapData, k == ldapUserPrincipalName ] userMatrikelnummer' = fold [ v | (k, v) <- ldapData, k == ldapUserMatriculation ] + userLdapPrimaryKey' = fold [ v | (k, v) <- ldapData, k == ldapPrimaryKey ] userEmail' = fold $ do k' <- toList ldapUserEmail (k, v) <- ldapData @@ -164,17 +173,18 @@ upsertCampusUser plugin ldapData = do userSex' = fold [ v | (k, v) <- ldapData, k == ldapSex ] userAuthentication - | is _UpsertCampusUserOther plugin - = error "PWHash should only work for users that are already known" + | is _UpsertCampusUserLoginOther upsertMode + = error "Non-LDAP logins should only work for users that are already known" | otherwise = AuthLDAP - userLastAuthentication = now <$ guard (isn't _UpsertCampusUserDummy plugin) + userLastAuthentication = guardOn isLogin now + isLogin = has (_UpsertCampusUserLoginLdap <> _UpsertCampusUserLoginOther . united) upsertMode userIdent <- if | [bs] <- userIdent'' , Right userIdent' <- CI.mk <$> Text.decodeUtf8' bs - , hasn't _upsertCampusUserIdent plugin || has (_upsertCampusUserIdent . only userIdent') plugin + , hasn't _upsertCampusUserIdent upsertMode || has (_upsertCampusUserIdent . only userIdent') upsertMode -> return userIdent' - | Just userIdent' <- plugin ^? _upsertCampusUserIdent + | Just userIdent' <- upsertMode ^? _upsertCampusUserIdent -> return userIdent' | otherwise -> throwM CampusUserInvalidIdent @@ -227,6 +237,13 @@ upsertCampusUser plugin ldapData = do -> return Nothing | otherwise -> throwM CampusUserInvalidSex + userLdapPrimaryKey <- if + | [bs] <- userLdapPrimaryKey' + , Right userLdapPrimaryKey'' <- Text.decodeUtf8' bs + , Just userLdapPrimaryKey''' <- assertM' (not . Text.null) $ Text.strip userLdapPrimaryKey'' + -> return $ Just userLdapPrimaryKey''' + | otherwise + -> return Nothing let newUser = User @@ -257,10 +274,15 @@ upsertCampusUser plugin ldapData = do , UserEmail =. userEmail , UserSex =. userSex , UserLastLdapSynchronisation =. Just now + , UserLdapPrimaryKey =. userLdapPrimaryKey ] ++ - [ UserLastAuthentication =. Just now | isn't _UpsertCampusUserDummy plugin ] + [ UserLastAuthentication =. Just now | isLogin ] - user@(Entity userId userRec) <- upsertBy (UniqueAuthentication userIdent) newUser userUpdate + oldUsers <- for userLdapPrimaryKey $ \pKey -> selectKeysList [ UserLdapPrimaryKey ==. Just pKey ] [] + + user@(Entity userId userRec) <- case oldUsers of + Just [oldUserId] -> updateGetEntity oldUserId userUpdate + _other -> upsertBy (UniqueAuthentication userIdent) newUser userUpdate unless (validDisplayName userTitle userFirstName userSurname $ userDisplayName userRec) $ update userId [ UserDisplayName =. userDisplayName' ] @@ -321,7 +343,7 @@ upsertCampusUser plugin ldapData = do , Just defType <- studyTermsDefaultType -> do $logDebugS "Campus" [st|Applying default for standalone study term “#{tshow subterm}”|] - (:) (StudyFeatures userId defDegree subterm Nothing defType subSemester now True) <$> assimilateSubTerms subterms unusedFeats + (:) (StudyFeatures userId defDegree subterm Nothing defType subSemester (Just now) now True) <$> assimilateSubTerms subterms unusedFeats Nothing | [] <- unusedFeats -> do $logDebugS "Campus" [st|Saw subterm “#{tshow subterm}” when no fos-terms remain|] @@ -389,26 +411,11 @@ upsertCampusUser plugin ldapData = do forM_ fs $ \f@StudyFeatures{..} -> do insertMaybe studyFeaturesDegree $ StudyDegree (unStudyDegreeKey studyFeaturesDegree) Nothing Nothing insertMaybe studyFeaturesField $ StudyTerms (unStudyTermsKey studyFeaturesField) Nothing Nothing Nothing Nothing - oldFs <- selectKeysList - [ StudyFeaturesUser ==. studyFeaturesUser - , StudyFeaturesDegree ==. studyFeaturesDegree - , StudyFeaturesField ==. studyFeaturesField - , StudyFeaturesType ==. studyFeaturesType - , StudyFeaturesSemester ==. studyFeaturesSemester - ] - [] - case oldFs of - [oldF] -> update oldF - [ StudyFeaturesUpdated =. now - , StudyFeaturesValid =. True - , StudyFeaturesField =. studyFeaturesField - , StudyFeaturesSuperField =. studyFeaturesSuperField - ] - _other -> void $ upsert f - [ StudyFeaturesUpdated =. now - , StudyFeaturesValid =. True - , StudyFeaturesSuperField =. studyFeaturesSuperField - ] + void $ upsert f + [ StudyFeaturesLastObserved =. now + , StudyFeaturesValid =. True + , StudyFeaturesSuperField =. studyFeaturesSuperField + ] associateUserSchoolsByTerms userId let @@ -440,6 +447,19 @@ upsertCampusUser plugin ldapData = do forM_ ss $ void . insertUnique . SchoolLdap Nothing + 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 + + iforM_ userSystemFunctions $ \func preset -> if + | preset -> void $ upsert (UserSystemFunction userId func False False) [] + | otherwise -> deleteWhere [UserSystemFunctionUser ==. userId, UserSystemFunctionFunction ==. func, UserSystemFunctionIsOptOut ==. False, UserSystemFunctionManual ==. False] + return user where insertMaybe key val = get key >>= maybe (insert_ val) (\_ -> return ()) diff --git a/src/Foundation/Yesod/ErrorHandler.hs b/src/Foundation/Yesod/ErrorHandler.hs index 025b4098d..f24a7ea85 100644 --- a/src/Foundation/Yesod/ErrorHandler.hs +++ b/src/Foundation/Yesod/ErrorHandler.hs @@ -44,29 +44,24 @@ errorHandler err = do [whamlet|

_{MsgErrorResponseEncrypted} -

+                  
                     #{ciphertext}
                 |]
             | otherwise -> plaintext
 
         errPage = case err of
           NotFound -> [whamlet|

_{MsgErrorResponseNotFound}|] - InternalError err' -> encrypted err' [whamlet|

#{err'}|] + InternalError err' -> encrypted err' [whamlet|

#{err'}|] InvalidArgs errs -> [whamlet|