Compare commits
235 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fc2aa31d77 | |||
| 29166b0b39 | |||
| 3949a60828 | |||
| 7bdd9b648a | |||
| 51164abc54 | |||
| d6607b4432 | |||
| 988dddc0a2 | |||
| 4eb28c3c5b | |||
| cac16b4e1c | |||
| dfa7746ff6 | |||
| be250afce5 | |||
| 81278d92a1 | |||
|
|
6693bbe166 | ||
|
|
1696135096 | ||
| c764182a6d | |||
|
|
a112ef2eca | ||
|
|
7e28517b82 | ||
|
|
3080ab995a | ||
|
|
7a510b315d | ||
|
|
dc701e5c49 | ||
| 233e9ca92f | |||
|
|
e1a25cdd31 | ||
|
|
5be23c0d52 | ||
|
|
de8cf11d4d | ||
|
|
666a50e163 | ||
| 0bd256cb09 | |||
| 0d46802862 | |||
| b190e25c88 | |||
| 8290f9dd23 | |||
| 22e57dc075 | |||
| aa6406f949 | |||
| 663ad01740 | |||
| 619c5975aa | |||
| 795c707a1f | |||
| 0599ec2512 | |||
| b1cb45ac7e | |||
| 274c86a820 | |||
| 3119dff6fe | |||
| f7f3532b30 | |||
| 1dd83af6aa | |||
| cea64da34d | |||
| cba9cadb41 | |||
| 9428bc05cc | |||
| 94d45c1f17 | |||
| 8be3e2ea78 | |||
| 7e33d9e5de | |||
| 923166b592 | |||
| dbfd3657a0 | |||
|
|
864175284d | ||
| a4eda81436 | |||
| 4db44733ca | |||
| 1fc43a8727 | |||
| 6cd1d829b6 | |||
| 85dc1fa0b5 | |||
| 2aa64f7360 | |||
| f3da2ac630 | |||
| d44b903b3e | |||
| c4501f1d08 | |||
| 560d1adf5f | |||
| acd6a3c11c | |||
| 2787bde8da | |||
| 6b82c26268 | |||
| 770c2f3182 | |||
| 843e6dbba2 | |||
| 3607a9da6d | |||
| 608bea5199 | |||
| 07dd91665c | |||
| 5662a2d1f1 | |||
| 72938e41ba | |||
|
|
cf6ae898c4 | ||
| 05acba8cbe | |||
| 9856272734 | |||
| 504490f593 | |||
|
|
4c109538ee | ||
|
|
1e5c4df163 | ||
| e1ebd528b8 | |||
| 708320e067 | |||
| 51298ba726 | |||
| 96e3eb613d | |||
| a2903da109 | |||
| c9fa627651 | |||
| 969cc4df63 | |||
| 2480efc345 | |||
| 8c4ec00c35 | |||
| 78a8442d07 | |||
| 95803db3a0 | |||
| d71ff014ea | |||
| aca5a79de2 | |||
| 4feb05a02e | |||
| 77a9100b2e | |||
|
|
b947037ea2 | ||
|
|
d88acf4634 | ||
|
|
fbe0e37d28 | ||
| bb03d28b7d | |||
| 2196e89208 | |||
| 4ff51c8f6f | |||
| 434eed2217 | |||
| f88e527fe4 | |||
| 40fe8ecfc6 | |||
| 13502d704e | |||
| d1e1f25162 | |||
| ac5bca2fcd | |||
| 064645d1b3 | |||
| 956c85a9f3 | |||
|
|
bee135ab48 | ||
| 42ecc91c22 | |||
| a37d4b369a | |||
| 039b1234c5 | |||
| 87b3214c84 | |||
| ad937cda8c | |||
| 899071e4d6 | |||
| 55bf8c0355 | |||
| b4a8ccf9cc | |||
| 76d3c57658 | |||
| 2490f8e69f | |||
| 6cd0152636 | |||
| 19433fdc56 | |||
| 012c75db21 | |||
| 71e2d6827e | |||
| 41b14f1ece | |||
| a2e01e74af | |||
| 8a353c357f | |||
| 9bf7033eac | |||
| 0a01490aa7 | |||
| 115452035d | |||
| b8e7ee2b3d | |||
| 3d1908d71a | |||
| ed54b666ec | |||
| a1d8dc2e7e | |||
|
|
956464659e | ||
| 9a5c487b2c | |||
| bcfcbd5c9b | |||
| 96038a4f22 | |||
| 5c4042e5f3 | |||
| c9f1bc4047 | |||
| bf13473954 | |||
| a0e7b2f96c | |||
| 848890d3cd | |||
| f8bf02df2b | |||
| 1489c27121 | |||
| 0c5f4cb430 | |||
| 9597663881 | |||
| 7ed5e7a326 | |||
| 1180ef6fd0 | |||
| 2c3292cadf | |||
| 7803b753cb | |||
|
|
bbeebc641e | ||
| 42c97924ec | |||
| 29fc201294 | |||
| 938423b832 | |||
| 54f2430b3e | |||
| 2e47df00b9 | |||
| 223ae0f2f8 | |||
| cc8bd19f85 | |||
|
|
3f5a22c85d | ||
| 12fe58fc81 | |||
|
|
fafa25a7b5 | ||
|
|
d4cfce317d | ||
| ac045fdc70 | |||
| a85a5be4cd | |||
| 1d7b46b4a4 | |||
|
|
453034100b | ||
| 9c608070ae | |||
| aa81de74a4 | |||
| d9ed893b52 | |||
| dfa774f655 | |||
| 608d8a3661 | |||
| 3c4e6b62fb | |||
| f39de71c02 | |||
| 24dbaf36bc | |||
| 43bf25a5bd | |||
| f4b8417deb | |||
| c8350722a4 | |||
| af09e02801 | |||
| 8e2a98c12b | |||
| 1cdb20eb60 | |||
|
|
c8fa509ace | ||
|
|
5a023a9e32 | ||
|
|
2763d2012a | ||
| 264aaab24c | |||
| c65dc04e8f | |||
| a1ba004efa | |||
| 514bca5257 | |||
| 9cbc35c263 | |||
| 84d7890ae4 | |||
| aa893062f1 | |||
| d4a3459adf | |||
|
|
8acfc1d10c | ||
| e9bbeffd7e | |||
| 7e3e772055 | |||
| 471982d245 | |||
| 3eec9ef8df | |||
| ff5b31929e | |||
| 12bb8b7145 | |||
| 2e005a90f2 | |||
| 843ac60aae | |||
| a42ccb0faa | |||
| c929d42ebd | |||
| 4051d1e11b | |||
| 71af64dc28 | |||
| 74f044919c | |||
| 9dc6ec461c | |||
| 1f31fe8cf2 | |||
| d56c9c3c31 | |||
| 55ed01cb40 | |||
|
|
9f299c854c | ||
|
|
35902daff6 | ||
|
|
31f657a15f | ||
|
|
7946e046e2 | ||
|
|
7ca12d064d | ||
|
|
5e85eae825 | ||
|
|
3e9e90ed86 | ||
|
|
a67697d159 | ||
|
|
ce8aa849f8 | ||
| 5c4f742745 | |||
| 7b7b82cba3 | |||
|
|
cf89722c7f | ||
|
|
44d082f8b9 | ||
|
|
9b9370fed0 | ||
|
|
2351388826 | ||
| aa41004c39 | |||
| 29df39f3b5 | |||
| de005691f1 | |||
| 050516c0bc | |||
| e63c8751eb | |||
| 2a4158303e | |||
| 1797d4eb9b | |||
| 307cda543e | |||
| de19073e11 | |||
| 18af65da10 | |||
| 45048ce62d | |||
| bc4594bea2 | |||
| e4883c62d0 | |||
| 6e5a58aa37 | |||
| d495a31ad8 |
272
.gitlab-ci/sanitize-docker.pl
Executable file
272
.gitlab-ci/sanitize-docker.pl
Executable 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 image’s 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 image’s 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
64
.ports/assign.hs
Normal 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
24
.ports/offsets
Normal 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
|
||||||
|
|
||||||
873
CHANGELOG.md
873
CHANGELOG.md
@ -2,6 +2,879 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||||
|
|
||||||
|
* **model:** separate user authentication data from User table; add ExternalAuth and InternalAuth models
|
||||||
|
* **model:** move user authentication data to new ExternalUser model
|
||||||
|
* **settings:** rename userdb app settings
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **auth:** added azure & mock server to login widget ([44d082f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/44d082f8b95ad1b2d1ee0e9ce71d84dfbcd23df4))
|
||||||
|
* **auth:** admin handler can query user data ([4530341](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/453034100b38540a884ebfa4d46fdba04cf90b77))
|
||||||
|
* **auth:** formatted output of user queries ([d4cfce3](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d4cfce317d00714404ea3640cae8ad25182594b0))
|
||||||
|
* **auth:** implemented single sign out ([b947037](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b947037ea29bb721f3c5cade28ba606ad3e9e26f))
|
||||||
|
* **auth:** integrated oauth2 mock server ([8acfc1d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8acfc1d10c740766b55d5315fa6b413dcad50df5))
|
||||||
|
* **auth:** link to sso test from dev login widget ([9564646](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/956464659eccdb519ad9a18ca05b908486904b27))
|
||||||
|
* **auth:** oidc based sso for auth protected routes ([fbe0e37](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fbe0e37d281e19bcdbb926eb9a128c69186dd596))
|
||||||
|
* **auth:** tokens can be stored & refreshed ([c8fa509](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c8fa509ace7cc0746ac1df5678b49e393f39d397))
|
||||||
|
* **auth:** WIP authorization function ([9b9370f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9b9370fed0f55098163b55d88aea5fd55ffd736c))
|
||||||
|
* **auth:** WIP support for OAuth2 ([2351388](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/235138882650f54410154243cc6c61122e556d6d))
|
||||||
|
* **sso:** redirect to login when auto-sign-on is enabled and user is not authenticated ([2aa64f7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2aa64f7360ec9fa66a2be1868ddcc7aa8abea71d))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **acs:** fix overzealous avs error catching resulting in unnecessary error messages ([fa5fd98](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fa5fd98619895191156d19a77897342e247c531e))
|
||||||
|
* add missing do ([55319c8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/55319c8c5060a0d8763abb56c27d30e852c51f52))
|
||||||
|
* add missing translations ([d798dc4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d798dc48be5dc182ac3acb9efe46f556a7e95d17))
|
||||||
|
* **add-users:** fix and refactor confirm post param handling ([727d78c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/727d78cabc01e9f520b7140336bdacebc6188e2b))
|
||||||
|
* **add-users:** fix confirm secret field decoding ([57c9535](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/57c9535733b9ca2888a01d940a4a8888ca342c97))
|
||||||
|
* **add-users:** fix typo in message ([5e02c99](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5e02c99c44783672a66f2562bc92d14287b0ff48))
|
||||||
|
* added check in async table and removeddebug log output ([f807e2a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f807e2af78aa0d1d135990d764df9da89a0e61d0))
|
||||||
|
* added uw-enter-as-tab to CCommR subject field ([93a829b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/93a829b81b45639b0841bac72dc57416b50ef01c))
|
||||||
|
* **admin-tokens:** avoid option none ([af3ec98](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/af3ec98de512f72220d363b9dd0c06532ae1a960))
|
||||||
|
* **admin-workflows:** fix workflow definition descriptions forms ([f9d933b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f9d933bdacaf21618da9dc74e7bd6bea5e369aa7))
|
||||||
|
* **admin:** minor fixes and translations for admin problem page ([30fae33](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/30fae33dedb1501e570e9edca288fea3c84ac84a))
|
||||||
|
* **aform:** show info about required fields in all aforms ([63f6d01](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/63f6d016191fd1529ad7545b795bd4d174e6586a)), closes [#418](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/418)
|
||||||
|
* **allocation-list:** fix default sorting ([9eff3cf](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9eff3cfa10806f90d655fb32f72116e30020afab))
|
||||||
|
* **allocation-list:** fix sorting ([33d9bac](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/33d9bacc8aa3e412e88251a76a389d19fd148210))
|
||||||
|
* **allocation:** don't restart cloneCount when allocating successors ([e1c6fd4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e1c6fd43b807abd3126b7ae8b948f585416f883c))
|
||||||
|
* **allocation:** fix allocation-results notifications ([ed700a3](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ed700a34295525cb760d3afa975238171fbaeda5))
|
||||||
|
* **allocations:** better explain capped allocation bounds ([a890e34](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a890e346c8f76fb2fb9467910085d4d41a40b7d8))
|
||||||
|
* **allocations:** better handle participants without applications ([05d37fb](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/05d37fbc0ce4edd737b86b6c7646bcd16dbf1746))
|
||||||
|
* **allocations:** don't show all allocation information to lecturers ([ad6c503](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ad6c503ef56a97fc8210e06a1c4dd9e0c19ae949))
|
||||||
|
* **allocations:** fix allocation-course-accept-substitutes ([b4df980](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b4df98069982752e36e69571f5557a6179b44cff))
|
||||||
|
* **allocations:** fix behaviour of "active" dbTable-filter ([b694a09](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b694a093d56c034df69be4805c76a84155871165))
|
||||||
|
* **allocations:** fix result notifications ([bb6703d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/bb6703de47cfca156be4e763a3cf5c32ec27f389))
|
||||||
|
* **allocations:** notify for new course upon registration ([9e0b43a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9e0b43a60d26a05f6e1b9d4dae2b2f75dd52fff1))
|
||||||
|
* **allocations:** show assignment green ([9d62b3a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9d62b3a79e7566cb2d32f8e4147a2237146a0c3a))
|
||||||
|
* **allocations:** work around yesod weirdness wrt "none" ([4a731ec](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4a731eca4e69b5ee080f229a602e76f5ae165c64))
|
||||||
|
* allow deregistering from full courses ([d7e1e67](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d7e1e671abd7504556f977d26de0af6ea2d56444))
|
||||||
|
* **apc:** apc cannot distinguish ij from ji, partial fix only. Needs new font ([b4ba0a3](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b4ba0a30dc7c513bb9e3c567ca771d5d75de4343))
|
||||||
|
* apply margin-left to both ol und ul ([c0d319e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c0d319e0fde393f543da5ee585e1e89802188ecf))
|
||||||
|
* **arc:** actually invalidate ([ef4734e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ef4734ebb671d9ef19c284a4c5cc9412d6e62874))
|
||||||
|
* **arc:** reduce lock contention ([1be391f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1be391f5f5bf2588939fea92809dd629c0a69d99))
|
||||||
|
* **assign correctors:** also show names of unenlisted correctors ([de49a77](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/de49a777ebc31463893555720ffc2d07cb618ab5))
|
||||||
|
* **assign-submissions:** avoid division by zero ([640326c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/640326ca5de12c21f87fe728bde69c21d8444320))
|
||||||
|
* async table js util now knows current random css prefix ([cc90faf](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/cc90faf7320de85fe9ace1b4b9dfc36f4f53fd14))
|
||||||
|
* **async-table:** bind callback in updateTableFrom call ([cd3e72c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/cd3e72c0f1389a80786df1f4e2433a2152cf3d55))
|
||||||
|
* **async-table:** fix condition for uw-async-table class ([9a87730](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9a87730517f019987f8dbd93e7496c7b8c459758))
|
||||||
|
* **async-table:** update legacy call to datepicker ([d56e12d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d56e12d2070ea81f0f082df5b30914234cdda3c1))
|
||||||
|
* **async-table:** uw-async-table instead of .uw-async-table ([a5d9bfc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a5d9bfc1a29ca7b6bfd3c20ba157e563e9f21e36))
|
||||||
|
* **audit:** add missing submission edit ([537e66e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/537e66e4877027b858d6ecc55d12ee40e87928b7))
|
||||||
|
* **auth-caching:** submission-group ([896bd41](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/896bd41e3b415283cce16cb84a8219b8d4c1702c))
|
||||||
|
* **auth:** authorize exam offices by school ([946a42b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/946a42b7f01016652d03dd214fdf2bc7202ab8ab))
|
||||||
|
* **auth:** fix infinite auth loop for workflow files ([21cf6cf](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/21cf6cfa873b841c2f9f8ab9f69c08ea72fc2420))
|
||||||
|
* **authorisation:** inverted logic for empty ([65814c0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/65814c005e2637bb5f6347bf1f35133654538e7a))
|
||||||
|
* **authorisation:** keep showing allocations (ro) to lecturers ([c8e1d51](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c8e1d51e252e037daa72aaf058239091694af74a))
|
||||||
|
* **authorization:** have AllocationTime consider ParticipantState ([b69481e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b69481e88fb20890b4ece7a0023dcfdad21604d6))
|
||||||
|
* **authorship-statements:** resolve exam-part to exam properly ([3a2d031](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3a2d031bb5f5b4d6e5df06f8ec82957a1bc81a72))
|
||||||
|
* **auth:** prettier active directory errors in help messages ([b631ed7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b631ed7d0620748fd833c4cda4b421dc147d0906))
|
||||||
|
* **auth:** properly restrict various auth by school ([6f04a6b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6f04a6b693e99b573efcc94023dab0be4d6d83bb))
|
||||||
|
* **auth:** tutors may see sheet list ([e0c05f3](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e0c05f39d4c162dc793745325b69807a59df0c5e))
|
||||||
|
* **auth:** wrong caching for external-exam-staff ([9d1f1c6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9d1f1c691085ec65ad0f19cc51602a59ee133fc4))
|
||||||
|
* avoid subSelectForeign join issues ([576fccb](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/576fccb5222a5dbd19db69f142a39b4155b7486d))
|
||||||
|
* **avs:** attempt to fix avs background jobs ([bbaa42e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/bbaa42eefaaae88982b091973adb295cdc0e80ff))
|
||||||
|
* **avs:** avs background synchs and lms userlist result no longer block handler ([0beb0e4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0beb0e4011745ea51906e018c53548bb2f6d978e))
|
||||||
|
* **avs:** background avs synch yielding undefined due to wrong monad ([2e59d3c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2e59d3c2ea4d5017be9b4e578b7da12c4da0e2fa))
|
||||||
|
* **avs:** background synch was only triggerd by manual synchs ([48ef25a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/48ef25aa8ffbbd96c1578ae85b76f090d9042595))
|
||||||
|
* **avs:** chunk avs status query automatically ([352ee21](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/352ee215b4075c70dbf9229434e62c8e6d847ae4))
|
||||||
|
* **avs:** eliminate call to undefined in Esqueleto.Internals ([240c6f8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/240c6f81f81d1872317da01411fa67ec97e3b16d))
|
||||||
|
* **avs:** fix [#7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/7) by sequencing avs background jobs one after another ([6dc3d8d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6dc3d8d059e132d19c119c5f1de906342fdf6d2c))
|
||||||
|
* **avs:** fix tests (do not exit with failure on empty avs config) ([89aff47](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/89aff471528ad9002e309d50d706a412b7e67eb6))
|
||||||
|
* **avs:** import avs users without ldap entry ([850c52b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/850c52b496a28b6ecced6cb1275a6cf7705ec2cf))
|
||||||
|
* **avs:** incomplete config throws error ([1d3c278](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1d3c27868277c28350dfb087802bc1f7c6732aeb))
|
||||||
|
* **avs:** normalize internal personal numbers between LDAP and AVS ([b20008d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b20008d3bcb730ff76a76ce2928364e6ce9e7c35))
|
||||||
|
* **avs:** preserve unset pin passwords in update ([8c4f848](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8c4f848675e1125547d1fdfa05560affe4794118))
|
||||||
|
* **avs:** strip trailing whitespace from avs names upon import only ([d47e8c4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d47e8c4909f0f8a7a3f84b8beae4b5d4e223dcff))
|
||||||
|
* **avs:** update names from avs too ([e2a8fee](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e2a8feee3b186881fb2b323ed9fd9e0cc93787c8))
|
||||||
|
* better concurrency behaviour ([a0392dd](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a0392dd329c871ed855ee832ee97230d9c72d59e))
|
||||||
|
* better pathPieceJoined ([adcd5d5](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/adcd5d5aee3d541fbf65a532b81d86f236575b7b))
|
||||||
|
* better translation for "exam office" ([edbdceb](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/edbdceb74847f79cda0fd8087e2441a0eff3952e))
|
||||||
|
* **block:** negate condition to test ([9cf7f39](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9cf7f3965aa95f0b8f2a1574dbad90c0257edafd))
|
||||||
|
* broken dom ([02e8825](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/02e8825cbaad54b8a0b5a8e2bde25268488c5311))
|
||||||
|
* build ([7c86293](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7c8629300529d18554aac0cd66cf6bb13814337e))
|
||||||
|
* build ([071df90](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/071df906da6c41afa226f944a90c2f294eeba243))
|
||||||
|
* build ([9fd95d1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9fd95d181c498d460eaf30436ff110f7c1f9413e))
|
||||||
|
* build ([5c709f1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5c709f1bbb077d981fbd5d59e9c0f30cddbb468d))
|
||||||
|
* build ([cf33f0a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/cf33f0af84166b040de4b9a685a58d9884bc67f8))
|
||||||
|
* build ([68b8b45](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/68b8b458b1e02ec992df811cca2cda08a2f77d9a))
|
||||||
|
* build ([6322fd4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6322fd449bd83edf2e1909b95c6aa4c795d49a54))
|
||||||
|
* build ([b1641ad](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b1641ad57e036c33d12cc9002f2355231676f9d1))
|
||||||
|
* build ([43bb0ab](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/43bb0abe7218f6d43b52d9a64e62f0dc29b9972e))
|
||||||
|
* build ([23a21b9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/23a21b905c902bea5fe88abd84da600e757a194e))
|
||||||
|
* build ([fa61b46](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fa61b46d308753354623df17241b5312f324321e))
|
||||||
|
* build ([7147bb4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7147bb478db7ac039afd2004c9300304b1aa94ea))
|
||||||
|
* build ([5684213](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/56842134d3d68c22ee875cca18cd05f31087f83e))
|
||||||
|
* build ([f92e555](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f92e555de2555a030f1b52859f56181ac54ba78d))
|
||||||
|
* **build:** accepting linter suggestions ([d25dd64](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d25dd64eec0a75b8d0e53795341088835f34fd85))
|
||||||
|
* **build:** add missing file ([1fd24f6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1fd24f608dc9202fa98f52f7908f4be908a18efc))
|
||||||
|
* **build:** add some guards at calls to (%) for issue [#34](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/34) ([d8d75ed](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d8d75edafef3af6886de3048a566fb85419c1aab))
|
||||||
|
* **build:** bump version numbers for containers ([6678ddc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6678ddcf1aef920e2b04a491f086ac721cae07c9))
|
||||||
|
* **build:** empty avs config is ignored again ([1720e12](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1720e1229df80b0fe6cbecdef1cc0b5ce7d7c65a))
|
||||||
|
* **build:** fix botched merge in fill ([fb5cd55](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fb5cd5558c7db634497f2b7d97489fc023ce59d6))
|
||||||
|
* **build:** fix build ([49dc413](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/49dc4139cf900ae3b46c5946fb38ef6cad7d5cab))
|
||||||
|
* **build:** fix frontend vulnerability ([c2e3693](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c2e3693490c5ac870df7b701dc9499dfa69228b7))
|
||||||
|
* **build:** fix whitespace in routes ([a24e44e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a24e44efc9a20d3934d96640bb9e21b3b6d55b96))
|
||||||
|
* **build:** hlint did not like unnecessary monadic code ([acb52c5](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/acb52c51f429b35f91c9c8a491af6b353c9b5e75))
|
||||||
|
* **build:** linter complains ([ac57b1c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ac57b1cd32909c8314f3a3b15431d280911af643))
|
||||||
|
* **build:** major qualfication block quirks fixed ([ab48e40](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ab48e40ac7e5024b7847b3995e6ae16d1c401c60))
|
||||||
|
* **build:** merge ci/cd changes from gitlab ([30d5af0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/30d5af00bfe6cf946ecbc489726212178c2b794a))
|
||||||
|
* **build:** minor ([954a239](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/954a23936a35ea6c32247d7e191312e63888c12d))
|
||||||
|
* **build:** minor ([f9930f2](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f9930f2a00d1e0f0af9b7f2af7c387bcc09cef5a))
|
||||||
|
* **build:** minor errors firm handler ([06bb44c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/06bb44cf715375b5dd0141a46f8e10924ad6cd9c))
|
||||||
|
* **build:** minor move parenthesis to make linter happy ([02bf1d9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/02bf1d9a2ca433e55cf7d1e06f0ff300b53c7efb))
|
||||||
|
* **build:** no change, just retry merge pipeline ([9e156f4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9e156f407d00c85ec5212ca08b4efb446ddee8b0))
|
||||||
|
* **build:** package-lock.json changed somehow see issue [#18](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/18) ([3caf0d5](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3caf0d55f75d43b7a25a32b0383ab66022cd9fc3))
|
||||||
|
* **build:** prevent migration on non-existing table ([5bb49cd](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5bb49cd88941e510a50759efaad88690f841ca47))
|
||||||
|
* **build:** reactivate optimisations and llvm backend ([172181f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/172181f1c6d7673de5422374ba5738c3eb4f7983))
|
||||||
|
* **build:** reduce container size by removing LaTeX, Pandoc ([d5a2dd0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d5a2dd07fc7465a3745f48ad706f0994e5142751))
|
||||||
|
* **build:** redundant parenthesis ([50eda5f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/50eda5f65f7394fe519546609fe748490cb4dd72))
|
||||||
|
* **build:** refix test commits somehow ([34ada53](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/34ada53de0cc5804468791854e824b730fcc84de))
|
||||||
|
* **build:** remove impossible ([90b38ca](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/90b38ca5dc319f2d175978242b2fdd4477568a3c))
|
||||||
|
* **build:** remove obsolete import ([a5d5d8d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a5d5d8dbd668c6c35019ce14e8b5e59f5374bf80))
|
||||||
|
* **build:** remove redundant constraints ([ea82d75](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ea82d75a0934f8e13f26af5cb8a06c11d32dc0c5))
|
||||||
|
* **build:** remove redundant import ([a35341d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a35341d4b77bb564bd026073915b986eacd65bd8))
|
||||||
|
* **build:** remove tests for workflows ([bb696d0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/bb696d0332839f0f95c940c56db6249585ba65fa))
|
||||||
|
* **build:** remove traces of wflint for removed workflows ([3a089d9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3a089d957b7af5149fd132ba8e0c791d93f83ac4))
|
||||||
|
* **build:** revert nix flake config to obtain container ([99b6724](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/99b672483a2e0ead889693c4460aa1b2864c11ec))
|
||||||
|
* **build:** schools.model examDiscouragedModes default contained whitespace, which is not allowed ([9ee7ec8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9ee7ec8d7ab74832d10b6384490b9d20263f49b9))
|
||||||
|
* **build:** tests were overzealous ([ed3ca8c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ed3ca8c3d6949c22a3918f3b1832ea06e1300cf7))
|
||||||
|
* **build:** update frontend hash ([74c361d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/74c361d7dec8f93863d1b0e91f89cbd3f646777e))
|
||||||
|
* **build:** Update ParticipantInvite.hs ([f888da3](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f888da3ab0df45bb3c515ebb7cbb43569fdaa1fa))
|
||||||
|
* **build:** Update ParticipantInvite.hs ([fa4f9b2](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fa4f9b24475261afc1e534541c8878a85e6a1b10))
|
||||||
|
* **build:** Update Utils.hs ([87f0b2e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/87f0b2edab2bcf696b7b776e47272ef2204c0b75))
|
||||||
|
* **build:** user basic texlive package with required packages only ([cba748e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/cba748e94e69e1839432d33d81fa81d4159fd76f))
|
||||||
|
* **build:** v2 ([ac77aa1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ac77aa176a3c3977c4a802e5ed534fa2850528fe))
|
||||||
|
* **build:** weird build error, probably whitespace in routes ([11cc45a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/11cc45aacf0546a24498b01a896aa58fd28150a0))
|
||||||
|
* **build:** while the blank is necessary to prevent unnecessary migrations, it is not allowed either, see [#133](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/133) ([a4b2af7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a4b2af7f157444ead8c9df989741b266f7c2b4f2))
|
||||||
|
* bump changelog ([60a7bb2](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/60a7bb2b194e47e924f56d9f461f07e17cea3f5e))
|
||||||
|
* bump changelog & translate ([a75f3eb](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a75f3eb2f14d3a392294fc85b0fcc1efbd75dbd1))
|
||||||
|
* buttons know about ALL actions from other buttons ([11664dc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/11664dcd82c13eef1c395e2e590c4fb0c587aa65))
|
||||||
|
* **cache:** atomicity & workflow instance invalidations ([ef7fde9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ef7fde937ebf1bc31e3706fba1da166bb82133c5))
|
||||||
|
* **cache:** remove risky caching for submissions ([4ae59fc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4ae59fc1fa658e1462139ddddd6dc80308d85872))
|
||||||
|
* **campus-auth:** properly handle login failures ([ec42d83](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ec42d834ee627401849910c44ed18ee696c8fc76))
|
||||||
|
* **campus-login:** add i18n for ident placeholder ([692e533](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/692e533da0380337838c63e32370fde060905ac7)), closes [#417](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/417)
|
||||||
|
* **campus:** fix corner case with study features ([76098cc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/76098cc3c84e1e51cfadc381347aae483d62dbeb))
|
||||||
|
* changed DEBUG_MODE back ([cc63f63](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/cc63f636e928c9071bbc3208b77aa20d88800a17))
|
||||||
|
* changed enter to tab behavior in CCommR ([7aeb8e6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7aeb8e61f46dcc5c918df20450df2fdf1d10f217))
|
||||||
|
* changed keypress to keydown. ([9288e5c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9288e5c203a88368ec853ba7001290df87069fc6))
|
||||||
|
* **changelog:** add date ([52a88f8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/52a88f8fadcc6a115a569842197fa0b06367c368))
|
||||||
|
* **changelog:** try not to crash on unknown changelog items ([850c8d4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/850c8d4dae47489e0dbf0eb46276eaf0002bf123))
|
||||||
|
* **changelog:** update changelog ([fa5358a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fa5358a91da997d041be6110b7a0482886dfe52f))
|
||||||
|
* check if number of relevant user is >0 to prevent crash ([317b95b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/317b95be317ea038ad9fa398fc0c0c456b53495d))
|
||||||
|
* check space of occurrences after ignoring ([fabf56c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fabf56c1640c94f806d43aaca264100cbc39b840))
|
||||||
|
* **check-all:** fix column collection ([9935efe](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9935efe96689e8208630523424f1eba285b77db0))
|
||||||
|
* **cicd:** remove wflint step, update .gitlab-ci.yml file ([a07c66c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a07c66c5ab622b6c80e28e056f3e5b73da317198))
|
||||||
|
* **communication:** make communication form more intuitive ([7a2b972](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7a2b972f9f78817688b344ac269ba99694f0854a)), closes [#387](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/387)
|
||||||
|
* configure sessions to be strictly same-site ([a7e64bc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a7e64bce7b3961c05e568d365381097e9239a7fb))
|
||||||
|
* correct (switch) sheetHint and sheetSolution mail templates ([d6f0d28](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d6f0d28a1fe7f9b3b01a05d177c1e604e893fa8f))
|
||||||
|
* correct rebase-sourced error ([02589e4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/02589e4d00de233d847d6be71e44f9fc451fbfe9))
|
||||||
|
* **correction assignment:** correcting lecturer's names are shown now ([16c556b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/16c556b852501a5e6c88094556f5054c4d4f352b))
|
||||||
|
* **correction-upload:** better error messages wrt rating files ([8bb3bc5](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8bb3bc50a24664d7a9425e83822c19592cd35056))
|
||||||
|
* **correction:** comment column made wide in online correction form ([d83b1f6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d83b1f696f0ebdc631bd460e87c762d16fbc3ade)), closes [#373](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/373)
|
||||||
|
* **corrections-grade-r:** add get following post ([14f9ab6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/14f9ab6a31147290ca5e6d446baf641ddc317f40)), closes [#532](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/532)
|
||||||
|
* **corrections-grade:** fix inFix ([2c2dd8d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2c2dd8d3ff3334df675972bafc367349e13d7ef5))
|
||||||
|
* **corrections-overview:** behavioural fixes ([e10cfe9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e10cfe9c581e7b3b9ca93aced2293592a57ac78b))
|
||||||
|
* **corrections-r:** allow filtering by matriculation ([1b6b781](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1b6b781e82c39bc29c8984c587ac836f0da77a02))
|
||||||
|
* **corrections:** properly link corrector emails ([9385595](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/93855957e62b7764f001a83a77419fdeb465326b))
|
||||||
|
* correctly apply suggestion ([67d6fd7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/67d6fd7d438a31b50e6f4e6e921873ee11b32e9c))
|
||||||
|
* correctly calculate maximum user name length ([cd07a56](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/cd07a56a9fd3ee99b74e5304581574671e3689a0))
|
||||||
|
* correctly handle original minimizeRooms-flag ([d5bd504](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d5bd5042ad920b26df847845cc437c3f0616575c))
|
||||||
|
* correctly report NoUsers for ExamRoomRandom ([16cbc78](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/16cbc78878615a8d123de5d8fda11136685a824c))
|
||||||
|
* **corrector assignment:** sheet tabel mixed up columns sorted ([d07f53e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d07f53e1d8db10407d26d4fdd6e7c4c8a34b973d))
|
||||||
|
* **corrector handling:** show correctors by a consistent order ([9c5ed5f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9c5ed5f8424b36723e4ef483b85db305e7c6cfa9))
|
||||||
|
* **course and exam registration:** distinguish registrations buttons ([ad825b6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ad825b66b80dc6676c2f881f70630ac293162aec)), closes [#416](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/416)
|
||||||
|
* **course list:** show complete registration span ([754d6ca](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/754d6caa1ba056de70ba5fa868a1a4f3976876f9)), closes [#446](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/446)
|
||||||
|
* **course-application:** better display of priorities ([64f7715](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/64f771518ede9e1b11aaeeaaeb3a2e7d449a13ed))
|
||||||
|
* **course-applications-csv:** record rating time ([c2c6974](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c2c6974a7700e4d9bad92c4477f64ad2b1eed5a2))
|
||||||
|
* **course-deregister:** only delete relevant users exam results ([3997857](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/39978574feff5c1a761f84ab4e040dd53cc7f739))
|
||||||
|
* **course-deregistration:** fix check on exam registration ([0b8c30f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0b8c30f534eb07d75fce44b16d5af68f4ab41863))
|
||||||
|
* **course-edit:** additional permission checks wrt allocations ([fca5caa](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fca5caaa3137f3e8a11d76fbacf4d0ed0f1b78dd))
|
||||||
|
* **course-edit:** edit courses without being school-wide lecturer ([d7d1f27](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d7d1f273036a2dba5113c04319cdf075fbdb829c)), closes [#464](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/464)
|
||||||
|
* **course-edit:** expand rights of allocation admins ([7f2dd78](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7f2dd7808ebca01a2e2b857b67d8ebcbd12492b9))
|
||||||
|
* **course-edit:** improve instructions ([9d53730](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9d537307c23f5385effd359b26cfd695102dd955))
|
||||||
|
* **course-edit:** only show allocation error message when relevant ([00a6ca8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/00a6ca83bcc096075b65a70cf18860c1d7bf5a6b))
|
||||||
|
* **course-edit:** show old allocation ([fc53497](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fc53497aa330e50dc6c61a864cd417e0bb0c5b30)), closes [#450](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/450)
|
||||||
|
* **course-news:** fix permissions ([9e5fde9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9e5fde902724ad50a8b79519a798f53c6945e786))
|
||||||
|
* **course-news:** prevent display of edit-functions unless auth'ed ([89cc9ad](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/89cc9ad35e34d2938746b4ef5b86c8473417988b))
|
||||||
|
* **course-register:** swapped warning message ([32c0605](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/32c060575ce4d17430b7dc6432da1cb6b138dcb9))
|
||||||
|
* **course-show:** show display-email for correctors & tutors ([a2e3699](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a2e36995ea4699718ef88f7b9ecda8e8264a9fd0))
|
||||||
|
* **course-teaser-css:** class name fixes ([8a92985](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8a92985e16f569f6478c06772e6d89f5f4b78590))
|
||||||
|
* **course-teaser:** don't collapse unless chevron is clicked ([fca99be](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fca99bebe63b6e63c4597d7becb2e273ec40260d))
|
||||||
|
* **course-user:** handle allocations when deregistering single users ([ef5bb70](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ef5bb70b652a739db4eefc5e663f804414a43ce8))
|
||||||
|
* **course-users:** add missing dbt sorting ([1bc14c9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1bc14c9e19c5c165c735f9c3d00062fb1dcf6077))
|
||||||
|
* **course-users:** deregistration w/ allocation & w/o reason ([4f237e1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4f237e19682d489bdfc87ee6a1b18dac7eb99bfa))
|
||||||
|
* **course-users:** insertUnique and only count and audit true inserts ([1325ff2](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1325ff2a95d75c6fc1cb9f1f2eb4d1e464aa34ad))
|
||||||
|
* **course-visibility:** (more) correct visibility check for favourites ([796a806](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/796a8066aaac4d4c2789b65563672fff0d07dfe5))
|
||||||
|
* **course-visibility:** account for active auth tags everywhere ([c99433c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c99433c291bda25b45bf0c36ca8469324a71202e))
|
||||||
|
* **course-visibility:** allow access for admin-like roles ([7569195](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7569195c8b26c02eeea85aa95e7c3952041b4e98))
|
||||||
|
* **course-visibility:** allow deregistration from invisible courses ([29da6e2](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/29da6e2ac51b0dd2e02fb114f5b5db75a4f0da30))
|
||||||
|
* **course-visibility:** allow for caching Nothing results of getBy ([f129ce6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f129ce6b2b12e4cfe856f1de728873868c7a0bba))
|
||||||
|
* **course-visibility:** check for mayEdit on course list ([b1d0893](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b1d08939930c56fdc0efaf0e133a5feffa1cee64))
|
||||||
|
* **course-visibility:** correctly count courses on AllocationListR ([7530287](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/753028778833ea9972d37c973fe1f339512774e8))
|
||||||
|
* **course-visibility:** fix favourites ([1ac3c08](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1ac3c08d01056e0d4b8c7f740ebd78eb19bc73a7))
|
||||||
|
* **course-visibility:** rework routes ([7ce60a3](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7ce60a36f4c28ef6d4ff6f93015444dc62ff27d5))
|
||||||
|
* **course-visibility:** show icon to lecturers only ([cbb8e72](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/cbb8e7217d2ec256fff0b09eed1665c7f7d30c1b))
|
||||||
|
* **course-visibility:** visibility for admin-like users ([43f625b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/43f625ba0c009519f7e37b833e33a99f1ac97069))
|
||||||
|
* **course:** add links between users & applications ([edaca1b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/edaca1b394b2eb624f4c2bedd4fcc7ef91b5c4f6))
|
||||||
|
* **course:** better explanation for material access ([78c5bc5](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/78c5bc5258c9305deafac18b010dc6a41e5ea864))
|
||||||
|
* **course:** don't delete applications when deregistering ([b666408](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b6664089f75dcb3b2c89dbd2941c064e8aa86404)), closes [#648](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/648)
|
||||||
|
* **course:** fix [#28](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/28) by allowing course deletion with inactive participants only ([9dfd91b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9dfd91b2f864424cf940259890afb8bf8cb0fdcf))
|
||||||
|
* **course:** grant qualifications now issues and unblocks ([5d8d8cf](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5d8d8cf17e634ecb950a1c329c859fb93f94ef77))
|
||||||
|
* **courses:** better defaults for application/registration ([1c2c8fe](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1c2c8fe3d99176e079d0473dd45039b44128c491))
|
||||||
|
* **cron-exec:** consider lastExec before executing job ([43833db](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/43833db3e110cad1224ee9599992cec64f24b8a4))
|
||||||
|
* **cron:** consider scheduling precision in all time comparisons ([4ded04b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4ded04b80df94a4655af52b64cbcd156394286fd))
|
||||||
|
* **cron:** disallow jobs executing twice within scheduling precision ([bc74c9e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/bc74c9ef10d8d5e115679f9b1ac43ea69030dd8e))
|
||||||
|
* **cron:** time out sheet notifications ([d5a897c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d5a897c020cb0e6ef05a85a4f683210ce7c44069))
|
||||||
|
* **cron:** work around extraneous sheet notifications ([cbe211b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/cbe211bf2377a3c41644000f562c2dc353aac01c))
|
||||||
|
* csp-sandbox downloads ([50cbba1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/50cbba114a850469ed6893e697d0c329c8e894e0))
|
||||||
|
* **csv exam import:** ignore unchanged noshow and voided ([a346524](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a3465240731423e3ea5d7f85f8f8c73935166b76))
|
||||||
|
* **csv import:** csv import preview help text adjusted ([b7321df](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b7321dfbc54eb58e405ceebd9150683aa23095de))
|
||||||
|
* **csv import:** fix spelling and expand help text ([2c57a77](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2c57a77316e41bdac940747111009fa2f9b7d43d))
|
||||||
|
* **csv upload exams:** allow ambiguous harmless study fields ([7d2937c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7d2937c71df50e2fdd1346629d2fc1ca0016cf57))
|
||||||
|
* **csv-export:** mime confusion ([8bdaae0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8bdaae0881fe98c4c5f69f1332ac2ffb0ca83081))
|
||||||
|
* **csv-import:** fix incorrect map merge ([0d283fd](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0d283fd9e5ebfd2cabfc70eecd9d53fc1fc87e31))
|
||||||
|
* **csv-import:** major usability improvements ([2dc6641](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2dc6641e68f73f048ee9187e1f6ccd924870577f))
|
||||||
|
* **csv:** ignore empty lines ([211ff5e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/211ff5eacc83bb47e564dd88e11bc18ae7e0a6af))
|
||||||
|
* **csv:** less quoting in semicolon separated lists ([42f1eab](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/42f1eabb2c984a7d30ea8b90710c68aff8af9f97))
|
||||||
|
* **cvs:** export company in e-learning view ([2093cf5](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2093cf501827ab2305f26ab5cf742f2b0be4a7de))
|
||||||
|
* date formatting ([0af3b87](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0af3b87a474544231f8c277ed2fdb421aabfe86a))
|
||||||
|
* **datepicker:** close datepickers on focus loss ([3f9ca5e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3f9ca5e230f0d8b5f83b5fc35c18fca6afcae6f7))
|
||||||
|
* **datepicker:** close datepickers on focusout or click outside ([7fa0124](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7fa0124fe25a53caa07c478e157dace3c4f2a6ee))
|
||||||
|
* **datepicker:** close on focusout of elements in document only ([ee0edc7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ee0edc7d21393e217f88486be56a9d79f542a510))
|
||||||
|
* **datepicker:** fix for empty or browser-filled inputs ([3c24e5f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3c24e5f187a2d516948b9f11c8b46fd8dd111b33))
|
||||||
|
* **datepicker:** fix selecting date from manual input in internal format ([8bdcc92](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8bdcc9254ef681e0d6061cb69f8c48d392384351))
|
||||||
|
* **datepicker:** fixes [#456](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/456) ([613426b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/613426b27459a84fc7ea9c8e85a729ba4a43e625))
|
||||||
|
* **datepicker:** format time on copy paste as well ([99d9efa](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/99d9efa9465f68d55fdbc276207987b7289302bd))
|
||||||
|
* **datepicker:** handle output format when reformatting ([09622bd](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/09622bdb12c08957d750121fbd8b9d1f1631530d))
|
||||||
|
* **datepicker:** hide number input spinners in datepicker ([2073130](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2073130867ffa10c3f9469643355b8dfb67fa413))
|
||||||
|
* **datepicker:** increase datepicker z-index in modals ([593a6a7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/593a6a72d2750135dbe6348b53f48b18db6032b5))
|
||||||
|
* **datepicker:** insert datepicker after the form ([b590995](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b5909950930b1bff36d5834d5fc8f9e6ca65d0e6))
|
||||||
|
* **datepicker:** manually add scroll offset based on scroll target ([3ecf834](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3ecf834756bb6b12fe8c335370b9988a2d8e4ab0))
|
||||||
|
* **datepicker:** no manual positioning; update tail.datetime ([3cd71d6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3cd71d6b197428729d2bf7bede676df855eafef3))
|
||||||
|
* **datepicker:** partial focusout and click fix ([434c0da](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/434c0daa239dc6d502f55be39783edd75c96703e))
|
||||||
|
* **datepicker:** quickfix to fix datepicker position in modals ([3f9454a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3f9454a7ef4ff2303827b692c52151d07a96c67d))
|
||||||
|
* **datepicker:** removes idle cancel and submit buttons ([805676f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/805676f2b9141318e488cbb2509346dcd23715eb))
|
||||||
|
* **datepicker:** select time from preselected date on edit ([d3375bb](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d3375bb2c150611e891d883674831ede574ad346))
|
||||||
|
* **datepicker:** workaround for new Date(..) inconsistency ([d24ebf8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d24ebf81455aa049a1c621d3e3c06befd08c45a2))
|
||||||
|
* **datetime:** remove redundant constraints ([9258ba7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9258ba766577a4a573ab70f87bdaaa38ad674ea6))
|
||||||
|
* **db:** migration qualification block ([3d59527](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3d595271d979f29ed8bbc546f495e5ad1deae5ca))
|
||||||
|
* **db:** prevent superfluous migrations ([b73557a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b73557a1eee4315911c6369032447f8d1836d964))
|
||||||
|
* **dbtable-ui:** fix position of submit button for pagesize ([cf35118](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/cf351180dcaa39a3af69f26291af6dc058dce01a))
|
||||||
|
* **dbtable:** calculate height of header correctly ([5659f2d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5659f2df1e6ea473794075d85f2a43fc1037fce9)), closes [#634](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/634)
|
||||||
|
* **dbtable:** fix pagination bug ([b43f236](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b43f2364bbb4d9fdf8f3d4c2540246cf16a6be7a))
|
||||||
|
* **dbtable:** improve sorting for haskell+sql ([fd8255d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fd8255de8ca5f3e8f9faa9170a45297ea021c9d5))
|
||||||
|
* **deletion:** fix usage of deleteR from POST handler ([c87c9c1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c87c9c13d1e1ab1957d59467ae0e97a5dd8ee1a1))
|
||||||
|
* design tweaks ([18ae758](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/18ae75890a83d82f49343778d18185258e1985a4))
|
||||||
|
* design tweaks ([68eb448](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/68eb44884ebeec8a48ac962c53bf7c2d813cd3a9))
|
||||||
|
* **displayable:** fixed faulty display of db keys (SchoolId, TermId) ([c7312e8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c7312e8ec662eea25dd7f8d6ff36c28af890ec59))
|
||||||
|
* divide by zero ([674b949](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/674b94938812e93958e991367c261337fe64f2f6))
|
||||||
|
* do not add async-table class to empty tables ([b8e2911](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b8e2911e49c2880cd493f2df872f9c050cb5c570))
|
||||||
|
* do not apply target link height fix on targets in tables ([e7ff384](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e7ff3846f2b758e6fa1c10a5157eef1747750d38))
|
||||||
|
* **docker:** remove missing docker dependency ([1ac35e0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1ac35e0bf816ccdccaf984ee5fe28b329c9cb258))
|
||||||
|
* don't set user-last-authentication during ldap sync ([fdaad16](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fdaad16e713e69a7b47f80a690a97d2ff5eb9986))
|
||||||
|
* don't treat ExamBonusManual as override ([16abcd2](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/16abcd2265136b63e28dbde252f44c94417d0aff))
|
||||||
|
* **downloads:** do download links via redirect ([3ba41d8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3ba41d8f24b4358ad7a045eba0f630e1e2b67663))
|
||||||
|
* **eecorrectr:** encrypt eeid ([5d9ca45](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5d9ca454fa5353009c33678e21d9f49bd45f6cc3))
|
||||||
|
* **eecorrectr:** use default time ([3369155](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/33691556abd40041a1707e3f902cfcf5d513342d))
|
||||||
|
* **eexamlistr:** allow access for users with exam results ([885de44](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/885de4403c0172b3e9c3b59c277628106a7e925b))
|
||||||
|
* **email:** avoid sending to invalid email address ([71ccac0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/71ccac0dfe3e165f5ea674165f0cf2ed740b1c84))
|
||||||
|
* **email:** better wording for qualifcation expired notice ([412c56e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/412c56e78ceaef263e4ca8b8678bb0e8ea2efb9a))
|
||||||
|
* **email:** ensure sending to valid emails only ([3865afb](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3865afbceb69f8941c25c814abf855b4b035201a))
|
||||||
|
* **email:** instead of sender set reply-to only ([4c8f7e1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4c8f7e1267fe50196d664d733eb794ffaf55aa1c))
|
||||||
|
* **email:** invert invalid email error indicator ([731d0ce](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/731d0ce7c70533039d4ef82e8eaf366368c69403))
|
||||||
|
* **email:** reenable ldap logins with invalid email addresses (missing mail field problem) ([88a85bb](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/88a85bb5b63470a0fb117a67cbf8c597c96f6904))
|
||||||
|
* **email:** remove test for E#[@fraport](https://gitlab2.rz.ifi.lmu.de/fraport).de ([7c2226e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7c2226e138addc2154c58f98233d7c875d2ab0f9))
|
||||||
|
* **email:** rename settings parameter and switch to safe default ([5aa096f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5aa096f56acb37269b681abafef67b8a375f4d64))
|
||||||
|
* ensure termination for non-{'A'..'Z']-names ([873d5a0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/873d5a02adae8f33db349bd9de3c7bd49331d27f))
|
||||||
|
* **errors:** better handling of errors from separated approots ([833b674](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/833b674c31ef3d4bf3b9b1af13201f33c98ef82f))
|
||||||
|
* **exam add users:** correctly differentiate and fix messages ([a473599](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a47359997cd12a8a3053df5aa459c3f7d2c59cfd))
|
||||||
|
* exam auto-occurrence by matriculation ([3ef10d9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3ef10d98a174ce6c9fb5e7aaf2f2642073ea65c9))
|
||||||
|
* **exam grading keys:** Fix spacing ([24aacef](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/24aacef6af65d1c6c0cec53bd121abe1d889de2d))
|
||||||
|
* **exam import:** inactive registered features may be selected ([3c4172c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3c4172cbc2abb8b692241cc7fe73b62384c92a94))
|
||||||
|
* **exam participant download:** fix icon not being shown ([a075b16](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a075b1648eb9b49c37f8b6f228ae38e83baaf9fe))
|
||||||
|
* **exam registration:** icons added to exam register message ([ce61528](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ce615287180976d24f24abd16ec8bac79a4a881d))
|
||||||
|
* **exam-bonus:** avoid divide by zero if all sheets are bonus ([0fd7e86](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0fd7e86695f47047c9e4e1bb8efe9477103707ab)), closes [#671](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/671)
|
||||||
|
* **exam-bonus:** fix rounding ([854fa6b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/854fa6b968ed01d60dea0d9ba6fc93d37e5ec361)), closes [#672](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/672)
|
||||||
|
* **exam-correct:** add additional exam result td; table layout ([af32789](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/af3278912c960ad405a749ebd0bbd39c77f664a8))
|
||||||
|
* **exam-correct:** add XSRF token to post header ([2fd996b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2fd996be220228807548bbebab6f9531335879d6))
|
||||||
|
* **exam-correct:** add XSRF token to post header ([2b30461](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2b3046164f1c503dcd7fc558fce4fe8fc052c892))
|
||||||
|
* **exam-correct:** also persist local time on non-success ([41a9539](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/41a9539c27c32a5a867173bd9295380c1d497b25))
|
||||||
|
* **exam-correct:** also persist local time on non-success ([dcb79d4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/dcb79d4cb8c1cb96f0af61eeab57181cbe20063b))
|
||||||
|
* **exam-correct:** correctly htmlify user on failure ([ef34755](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ef3475539b3c144da67399ed29eb3af22ee51eb1))
|
||||||
|
* **exam-correct:** correctly htmlify user on failure ([595f46d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/595f46d860f3f893cdeb6618e361d533b5c90b5c))
|
||||||
|
* **exam-correct:** cut off at maxPoints for now (TODO) ([af8d77c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/af8d77c4a4d97cdc66b7f2e09568c1481768aa73))
|
||||||
|
* **exam-correct:** different values for examResult options ([aa794c0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/aa794c06e0f8819582d52f2089d26f8b781a718b))
|
||||||
|
* **exam-correct:** fix addRow rowInfo ([88768eb](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/88768eb1d184cc545da8a41566c893ea81a03c03))
|
||||||
|
* **exam-correct:** fix addRow rowInfo ([792da22](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/792da2220466f9e6a880052613c35ea5246452cd))
|
||||||
|
* **exam-correct:** fix attended values and submit on only exam-result ([df0aaca](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/df0aaca759aadeba718f02e946828ad55fdd07b7))
|
||||||
|
* **exam-correct:** fix attributes in template ([62bf73a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/62bf73ac1f348e60767e7a687893d6ac1a397a0c))
|
||||||
|
* **exam-correct:** fix attributes in template ([000f97c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/000f97c270577194c1e1d5c5adae1969db28b7ea))
|
||||||
|
* **exam-correct:** fix hlint ([630194c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/630194c4c0f0c9bdacf7d7a2c10118b407bc9b21))
|
||||||
|
* **exam-correct:** fix hlint ([c520918](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c520918eb350adcfc7fbda2b6edb6b8915cc8ea7))
|
||||||
|
* **exam-correct:** fix request bodies ([0b186a5](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0b186a5e1a76c2d593a596188a6432a44a796dc5))
|
||||||
|
* **exam-correct:** fix result info and response handling ([cd479e2](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/cd479e2f0cedbc7e076d3768b919da7da8f769ba))
|
||||||
|
* **exam-correct:** fix returning null if old and new results are equal ([968c6de](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/968c6defa676abf5705e9d3bdd62248f592e3298))
|
||||||
|
* **exam-correct:** fix returning null if old and new results are equal ([2e7bca6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2e7bca6333deea697e98d4bd84a1aec6ef0c1f77))
|
||||||
|
* **exam-correct:** fix usage for non-lecturer ([dd7fe84](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/dd7fe84ffdf89780cf1476e2697d2d5ed3e82222))
|
||||||
|
* **exam-correct:** id on td instead of select ([1d0be2d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1d0be2d6828f1221110af71bee709612fc09f4b4))
|
||||||
|
* **exam-correct:** reintroduce examResults ([f7136bc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f7136bca152ef455e6e6ce38c9cfaa39b213370d))
|
||||||
|
* **exam-correct:** send correct results ([2ca56fb](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2ca56fb8b1fc653e165611af9f7beb3653854f79))
|
||||||
|
* **exam-correct:** temporarily disable exam results (WIP) ([533e748](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/533e7482c9baab10c6b82781f57e610f5ca45129))
|
||||||
|
* **exam-csv:** audit registrations/deregistrations ([a278cc5](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a278cc5048a3c15c393cca86ea12b3ea095ae65c))
|
||||||
|
* **exam-form:** allow finished without start ([fbc3680](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fbc36806b13a7d674be3aa64a4c6491e8f051587))
|
||||||
|
* **exam-form:** sort occurrences and parts ([6d47549](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6d475497c0caee49ad34c5c3c6e7b1bf91ca0ba2))
|
||||||
|
* **exam-office:** better logic for isSynced ([cb9ff32](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/cb9ff32063046871edd7fe84729d5ea175e132f0))
|
||||||
|
* **exam-users:** don't crash when participant doesn't have bonus ([0fa910a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0fa910ae7c440b94a153fd48cf0585cd034fad4a))
|
||||||
|
* **exam-users:** make csv import much more lenient ([2ddb566](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2ddb56640fd0b5ac6bc7757e03b1819007cabd3a))
|
||||||
|
* **exam-users:** prevent exam results without registration via csv ([1c6ac4c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1c6ac4cb4a52ac7e69e615e0e3ff96432b173962))
|
||||||
|
* examAutoOccurence no longer user >100% of a room ([eaf245b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/eaf245beaaa1f739d6b857712f1e4ea5b53e7c82))
|
||||||
|
* **exam:** fix warning message ([60869fd](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/60869fd4a0fdb3efd2565c3b02aa45fa0f9d4eb5))
|
||||||
|
* **exams:** allow occurrences after exam end ([3d63b35](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3d63b355eb15daf858b554afe41932b117b00dd7))
|
||||||
|
* **exams:** better behaviour for optional statements wrt school default ([fe78377](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fe78377fae8af7766f9720628aebef599656ed2f))
|
||||||
|
* **exams:** change heading to rooms if no occurrence times are shown ([5cb9404](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5cb9404b7d2eaaf674a9f80ca323f052b2a1d3eb))
|
||||||
|
* **exams:** cleanup exam interface ([05e7b52](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/05e7b52f08354b9a83d5db1be9c084955a8cba97))
|
||||||
|
* **exams:** correctly treat school-mode optional as off by default ([ac86832](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ac86832b34a605e5d64d56ef08a871bf307347a8))
|
||||||
|
* **exams:** default exam mode to Nothing ([4b459ea](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4b459ea1430a4947364562f1a9881596325696ad))
|
||||||
|
* **exams:** don't show manual bonus as inconsistent ([fb54c84](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fb54c8445aa8b9762b6a8e2b4dd18ae379b80d2e))
|
||||||
|
* **exams:** error messages for foreign key constraint violations ([ca29a66](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ca29a66330a977a1f28bbdbe9a733aef10371427))
|
||||||
|
* **exams:** exam-auto-occurrence introduced spurious MappingSpecial ([a1d5479](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a1d547990df712f1866113dfeedc01d573d730c5))
|
||||||
|
* **exams:** fix caculation of maximum exercise points ([a9e74ca](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a9e74ca4af31e6f392fc79ae30d2a771800828d3))
|
||||||
|
* **exams:** fix form validation wrt non-empty statements ([0082135](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0082135c56b7fc0e5db3af6910f8365e12920c46))
|
||||||
|
* **exams:** Fix registration ([1684da0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1684da07f2352f76df7cef3bd7b33aa32a8dda97))
|
||||||
|
* **exams:** fixhance exam authship form section ([4109db6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4109db6f815fbb49c861177b3caecb98c2a963d8))
|
||||||
|
* **exams:** include bonus points in sum for exam participants ([2bc6894](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2bc68946e379e7d29a85148017ca9fd14b01ab18))
|
||||||
|
* **exams:** make examClosed a button ([530a8c6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/530a8c688e93ad25a5c4e3691cb64003e3a3676c))
|
||||||
|
* **exams:** prefill with school authship statement in optional mode ([0cd8f4c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0cd8f4c02f383f43b5e3ea059cd3acd38595ab56))
|
||||||
|
* **exams:** provide bonus information in return of examBonusGrade ([731231d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/731231d5eae5ab91361fec09686c2d1977ab9bae))
|
||||||
|
* **exams:** remove deprecated/unnecessary form validation wrt. authship statements ([bf059a1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/bf059a132094e53c3ef956582b5e13517e9c133d))
|
||||||
|
* **exams:** set use-custom correctly if forced ([8bb6140](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8bb61401a77f20fcb35aa05401bf16285aad1d93))
|
||||||
|
* **explained-selection-field:** support linebreak in titles ([627a2df](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/627a2df7adf41651e698d8cd9d632d066fc2f868))
|
||||||
|
* **failover:** don't always record as failed ([16643b6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/16643b62447315362e25922f817b92c50d754a43))
|
||||||
|
* fallback for determining user email ([6a1a256](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6a1a256cc2ee6928131422afea6a61017fd50272))
|
||||||
|
* **faqs:** mention mail to set password ([32097d1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/32097d18f9d5411e1bf8a3923ad8f04dcc7b4c83))
|
||||||
|
* **faqs:** wording ([02d284f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/02d284fb876af3256ef2bd82b92c40f3be36c446))
|
||||||
|
* **favourites:** always move current course up ([56d89d7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/56d89d7f5840033985f1b053920ebe8a6fdc3dc7))
|
||||||
|
* **favourites:** clear old favourites when changing max number ([92fb6f2](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/92fb6f2270d06a8a6520be10cb58e8fad3d7ecda))
|
||||||
|
* **fe-async-table:** Emulate no-js behaviour when handling pagesize ([28dcc8d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/28dcc8dc377c5a7c942daad7743f473208f5b99c))
|
||||||
|
* **fe-check-all:** use arrow fn to keep scope in event listeners ([09e681e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/09e681eeb1ec77a1f8c263978322026683aae031))
|
||||||
|
* **fe-deflist:** avoid horizontal scroll on pages with deflist ([16d422d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/16d422d9d82467ea6e6800bee5d1af06b7fe1d3b))
|
||||||
|
* **fe-i18n-spec:** fix tests ([339fa39](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/339fa398b48eefd7b208ede9036a35795185725e))
|
||||||
|
* **fe:** style notifications acceptably for now ([fc80f08](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fc80f087242201ddcf2509dbed1f6034dc5ea73e))
|
||||||
|
* **file-jobs:** improve log messages ([e099e13](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e099e13816d2ca79cbcc6a84fe970052980c0feb))
|
||||||
|
* **file-upload-form:** don't check case of file extensions ([6c49c50](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6c49c509ac6819bb2a46345b3f75c29ecae50e3b))
|
||||||
|
* **file-upload:** fix inverted logic for when upload is required ([3868e8f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3868e8feaeb0ab7aa9cbdac46c0f80bee68b891a))
|
||||||
|
* **file-upload:** size limitation was inverted ([de53c80](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/de53c80a1e715d0346ef2d28710b01dea16af905))
|
||||||
|
* **files:** allow clobbering files during form submission ([a60ad1a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a60ad1abae341283b1342018f57f7fc736c239a6))
|
||||||
|
* **files:** better configuration for file batch jobs ([3a90c88](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3a90c88b359f3e0cb0ed03df6e81b1532509ea48))
|
||||||
|
* **files:** count personalised sheet files as alive ([e54b985](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e54b985815fbbc637d8f4681ac55b3d46e2263a3))
|
||||||
|
* **files:** don't inject serializable ([2ca024b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2ca024b9351df800b57d3235c4a00776cd669952))
|
||||||
|
* **files:** fix download of non-injected files ([ce54adc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ce54adce6b67f3de95d65d74ff62b36cccdba47e))
|
||||||
|
* **fill:** correct term start day guessing ([538aa5b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/538aa5b3b9f0741e1dba80cd9e2ba70adfce1938))
|
||||||
|
* **fill:** minor testdata fixes ([59a7e1c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/59a7e1ceb285412bd7e61cb79b4c9e64a2ecdc81))
|
||||||
|
* filter submission by not having corrector ([3bded50](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3bded5071bf0e32a6d4df0dcc49f6039f283785e))
|
||||||
|
* **firm:** add sql indices for frequent filters to greatly enhance performance ([63e6d94](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/63e6d94df2fd1ce879cb59d14bc854f3c2556586))
|
||||||
|
* **firm:** firm messaging now works fine ([65cdc8d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/65cdc8ddfef19eb3a5578c536575f91ba9717a13))
|
||||||
|
* **firm:** foreign supervisor counts correct and sortable ([601ce7a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/601ce7abdf2a392d30f1ff799a2338968be795f1))
|
||||||
|
* **firm:** group multi select field supervisor ([fc0ca7b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fc0ca7b854a686cf395dadf81b7423e530fd26b8))
|
||||||
|
* **firm:** improve supervisor filter by caching ([88f24fe](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/88f24fe6f199290a83af2d204ba9aa2a838d11b8))
|
||||||
|
=======
|
||||||
|
* **admin:** minor fixes and translations for admin problem page ([30fae33](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/30fae33dedb1501e570e9edca288fea3c84ac84a))
|
||||||
|
* **avs:** background synch was only triggerd by manual synchs ([48ef25a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/48ef25aa8ffbbd96c1578ae85b76f090d9042595))
|
||||||
|
* **avs:** preserve unset pin passwords in update ([8c4f848](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8c4f848675e1125547d1fdfa05560affe4794118))
|
||||||
|
* **build:** minor errors firm handler ([06bb44c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/06bb44cf715375b5dd0141a46f8e10924ad6cd9c))
|
||||||
|
* **build:** redundant parenthesis ([50eda5f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/50eda5f65f7394fe519546609fe748490cb4dd72))
|
||||||
|
* **build:** while the blank is necessary to prevent unnecessary migrations, it is not allowed either, see [#133](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/133) ([a4b2af7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a4b2af7f157444ead8c9df989741b266f7c2b4f2))
|
||||||
|
* **cache:** remove risky caching for submissions ([4ae59fc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4ae59fc1fa658e1462139ddddd6dc80308d85872))
|
||||||
|
* **course:** fix [#147](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/147) abort addd participant aborts now ([d332c0c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d332c0c11afd8b1dfe1343659f0b1626c968bbde))
|
||||||
|
* **db:** prevent superfluous migrations ([b73557a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b73557a1eee4315911c6369032447f8d1836d964))
|
||||||
|
* **doc:** minor haddock problems ([d4f8a6c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d4f8a6c77b2a4a4540935f7f0beca0d0605508c8))
|
||||||
|
* **firm:** add sql indices for frequent filters to greatly enhance performance ([63e6d94](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/63e6d94df2fd1ce879cb59d14bc854f3c2556586))
|
||||||
|
* **firm:** firm messaging now works fine ([65cdc8d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/65cdc8ddfef19eb3a5578c536575f91ba9717a13))
|
||||||
|
* **firm:** group multi select field supervisor ([fc0ca7b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fc0ca7b854a686cf395dadf81b7423e530fd26b8))
|
||||||
|
* **firm:** improve supervisor filter by caching ([88f24fe](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/88f24fe6f199290a83af2d204ba9aa2a838d11b8))
|
||||||
|
* **firm:** improve supervisor filter yet once more ([c7b5a3c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c7b5a3c6cb70c314ecbfbe25969b4b6be1d43161))
|
||||||
|
* **firm:** restrict firm access to company supervisors only ([0a06efd](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0a06efd76c63180c996657c2c7d78efc5bddd83d))
|
||||||
|
* **firm:** sending messages works, but not test messages ([42ff02d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/42ff02d27e431a8855db7bf3046a1b74d297e6da))
|
||||||
|
* **firm:** set supervisor field not all fields required ([9878956](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9878956716b04c7ae88989cb9b059d3edcb923dc))
|
||||||
|
* **firm:** show default supervisors with no employees too ([0f9a7a8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0f9a7a8c53d216ca7a6d0a25462b19ab1fa00bb4))
|
||||||
|
* **firm:** supervisor changes led to inconsistent DB ([1d3345c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1d3345cbba1cb65ee49c6f62e145750545439642))
|
||||||
|
* **firm:** supervisor filter ([3acb847](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3acb847915010d10358ea02000c231dbba7cba26))
|
||||||
|
* **firm:** supervisor filter performance ([db77850](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/db77850c4f4cd1d68bfd38e02e0ae24584e1e556))
|
||||||
|
* fix .dual-heated.degenerate ([6058692](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/605869204fdf89d11b9dbd07edb02ab31a11cde5))
|
||||||
|
* fix [#571](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/571) ([aefb7e0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/aefb7e0b426fb2319e098add3231fc5274aeccc4))
|
||||||
|
* fix app frontend test ([49bafe1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/49bafe1276e13f0961e9d327b6506b97de39d079))
|
||||||
|
* fix build ([69f4a80](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/69f4a80dc18c58e5980251d3932b469004abe8c1))
|
||||||
|
* fix build ([d13ace4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d13ace4eddb581167544de5e5f788ab6d5836041))
|
||||||
|
* fix build ([1a66716](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1a66716e8afee4b1f34785e0743590ee0cab0830))
|
||||||
|
* fix build ([caf4092](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/caf4092d12ce1785bf153a372609f6d64b9c19cd))
|
||||||
|
* fix build & minor refactor ([bb9b4f0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/bb9b4f06ae135e9af8b3333b42e78ff38baab3d8))
|
||||||
|
* fix collision with keyword "none" ([203dbd3](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/203dbd3705717660cb2bef3a735eb10ce0085155))
|
||||||
|
* fix course duplicate message & name -> title for courses ([d87e8b7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d87e8b7142d879eb0a5d320332e967e1c19a1b33))
|
||||||
|
* fix creating new terms ([9676615](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9676615c55cbecce02dca95b01ad69c1a2455f1d))
|
||||||
|
* fix form-notification styling ([0226593](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0226593450502165933529e926293031cbd620ee))
|
||||||
|
* fix grid blowout on definition lists ([3cb3dcd](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3cb3dcdc9b5e20bc4a64ac787773fdd22edc3580))
|
||||||
|
* fix hlint ([e60aef4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e60aef4f8bbf4b81a66b4a8c4b6769744890f3f0))
|
||||||
|
* fix hlint ([9ecffc8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9ecffc8d8c843441365e871a50fa74cfa36abd5c))
|
||||||
|
* fix hlint ([37f0936](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/37f0936d91b59b66b8c261b0e25676d6ad1606c4))
|
||||||
|
* fix i18n widget files ([e517a8e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e517a8e4701f2df2f1a39e3ed16db0e5e3393ea1))
|
||||||
|
* fix merge ([d19cca6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d19cca6a400e41f8ac2733b474fbaa6cad2b48f6))
|
||||||
|
* fix merge ([38afa90](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/38afa901bab462b889ea036f142022afe4b32498))
|
||||||
|
* fix migration ([d2478a3](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d2478a3657a483dccc6852544097709bd3aa1f30))
|
||||||
|
* fix migration & tests ([e05ea8e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e05ea8ea8ccfa8a5bf0a97cf7d3273ba158938d2))
|
||||||
|
* fix rendering of weekdays ([94b87a2](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/94b87a2d0d0ca30477e0e63b2fcb4707ab1059c1))
|
||||||
|
* fix startup on unix-socket ([39f1295](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/39f12957f55256db74960b47be3797e881c525b8))
|
||||||
|
* fix tests ([a671937](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a671937868dd65c6a92ee58b9d47d296d502b0ea))
|
||||||
|
* fix tutorial registration group applying globally ([d2ba173](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d2ba173776a6caa84ae53e6f87245205e344fa40))
|
||||||
|
* fix webpack config ([50e4212](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/50e421222422d0d2a13c2284aaaa4cb10af922a6))
|
||||||
|
* fix webpack config ([5393a55](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5393a55482db21b5cb04ae3774d0f97e37789a3c))
|
||||||
|
* **form:** multiSelectField working with grouped options ([3aa8901](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3aa89019a8b4393da0eca715871a3793c1e3abb2))
|
||||||
|
* **frontend:** fix typo in navigate-away-prompt ([061349e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/061349efb0ce65079e01ca9420b1c4eaabf731c2))
|
||||||
|
* **frontend:** improve performance of table-related utils ([eff273b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/eff273bf090518d41ba46b8a52b91f20cf9d7074)), closes [#603](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/603)
|
||||||
|
* **generic-file-field:** allow .zip when doUnpack ([46e9908](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/46e99081d9bad67b42f09792ac12e8a5f7d72723))
|
||||||
|
* **generic-file-field:** better explain extension restrictions ([342c64a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/342c64a93acd557f69c5953876391157f6193174)), closes [#509](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/509)
|
||||||
|
* **guess-user:** fix ldap-lookup condition and refactor ([ad4ae71](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ad4ae713c8c87c13616582389fda05a6dba8d962))
|
||||||
|
* **haddock:** fix accidental haddock comments ([882ca7c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/882ca7c5828fbf42fa9d08a5cb545fc2fd8bbba5))
|
||||||
|
* **haddock:** hoogle.sh fails on a comment, turned into normal comment ([c6264f7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c6264f75b4034c76168f07c2731af1b77ae8b16c))
|
||||||
|
* **haddock:** merge haddock fix from master ([e6c4125](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e6c41250e8f9a8a75f6a1d76e7da36964adf1336))
|
||||||
|
* handle rare cases where a mappingDescription with start>end would be produced ([c99d96e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c99d96ecb8a43400eb10dfe192bf751cb00a9d25))
|
||||||
|
* have exam deregistration always delete stored grades ([24f428b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/24f428b13bb181bec99417b4e69fc538e35acbcf))
|
||||||
|
* **health:** correct file path ([6214448](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/621444829e8c51e114f219285fb0ebc7a43a1829))
|
||||||
|
* **health:** include compile time instead of version number ([8130eb6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8130eb6b7f6a14be6fa6bbab73eb47622736e28e))
|
||||||
|
* **health:** ldap check only admins ([f889ec6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f889ec674e35af24f8d33acf7102a2bee5dcf68b))
|
||||||
|
* **health:** monitor flush by check interval not flush interval ([03226ec](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/03226eca6aba91e2f10f5828a38f2a15d747dd0c))
|
||||||
|
* **health:** more generous healthchecks ([466203d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/466203d866ba92a2ee6fed8a01dcf1e610e6e896))
|
||||||
|
* **Help Widget, Corrector Assignment:** Modal Form closes in place; assign alerts ([89d5364](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/89d5364c937132a642d7b7960e90b73868fe56f4)), closes [#195](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/195)
|
||||||
|
* **hide-columns:** account for undefined element in isTableHider ([ee5a005](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ee5a0052a1b1ae1842ce968818f6fae2b2bd1150))
|
||||||
|
* **hide-columns:** bump storage manager minor version ([9053b87](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9053b878c98fcc8cccd5c1a6b241eb36c372f087))
|
||||||
|
* **hide-columns:** check for content div in isEmptyColumn ([615555e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/615555eb597bf4a87b83020275cce7f30495d0ac))
|
||||||
|
* **hide-columns:** correctly hide hiders of previously hidden columns ([364991c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/364991c42bbb82301dd71cee8823d061f92da1ad))
|
||||||
|
* **hide-columns:** fix crash if no row is present ([827cecd](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/827cecda8f4244861f05ec988c00929591c94bec))
|
||||||
|
* **hide-columns:** fix repositioning of table hiders onclick ([9d8ca38](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9d8ca38f2e7016b753d7951a12b4b10ac3089c9f))
|
||||||
|
* **hide-columns:** fix vertical positioning of hider and minor refactor ([3fbb4db](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3fbb4db962c6493a1da2c243850d65e4f735ab4b))
|
||||||
|
* **hide-columns:** improve positioning ([e371412](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e371412db48c600589800890477531706a6313bd))
|
||||||
|
* **hide-columns:** no hide-columns in tail.datetime ([03bcf56](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/03bcf56487c5bf363bf6d9f595b735208e3fa87f))
|
||||||
|
* **hide-columns:** remove debug text from template ([9e449dd](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9e449ddaed31aba904d7a6bbd367cc256180d237))
|
||||||
|
* hlint ([7e14fef](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7e14fef5c57acf436669a7cbe637d0d019810031))
|
||||||
|
* hlint ([58c933c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/58c933c62466aae823b7c61a2e80f9150a41fae8))
|
||||||
|
* hlint ([662943b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/662943b256e013c02317181f9a30f6b2f7bd606a))
|
||||||
|
* hlint ([5ea7816](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5ea781692671ed60e2851297b7eecd6ce666ec34))
|
||||||
|
* hlint ([908e6de](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/908e6def80d8ac2b65e6d5722607db7571c007ea))
|
||||||
|
* hlint ([4348efc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4348efca35c80720e6d49b55bcd6256ae52a0b55))
|
||||||
|
* hlint ([b0b92b4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b0b92b4b26310b0a41b45d6928fe469d47e5df5d))
|
||||||
|
* hlint ([c19f427](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c19f427cd76d09533fab8a24c8b54e1eefa9ed08))
|
||||||
|
* hlint & build ([036c74e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/036c74ef49d33efebaaecec16d2daa5900170f94))
|
||||||
|
* **holidays:** add proper memoization to yet unused function ([d2938e3](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d2938e3ae90def29736ccb884bfebfcd275cb4a9))
|
||||||
|
* **holidays:** minor improvement to memoization ([f411fde](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f411fde42d913d5a9ef357674ab77934c8e1ba42))
|
||||||
|
* **home:** fix build ([551c4cb](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/551c4cb23cb9243acca793b3430e8732754336bb))
|
||||||
|
* **home:** fix hlint and other minor bugs ([839251e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/839251ede34f99446e371c4119abca535cf0b834))
|
||||||
|
* **hoogle:** remove erroneous comment ([c011d88](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c011d887cece8338920355b540aa4b233e0b994f))
|
||||||
|
* hopefully improve workflow auth performance ([1d3fd8c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1d3fd8c8a7824d6c6d043f4114067238af4bdc6e))
|
||||||
|
* hopefully speed up aeson via ffi ([a00ba10](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a00ba10e9cf1ffa534908b9125730e88179052eb))
|
||||||
|
* **html-field:** introduce stored-markup ([e25e8a2](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e25e8a2f4ca65afc29acc8a3884df9acf77d4398))
|
||||||
|
* **html-field:** remove warning about html-input ([d0358b4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d0358b4a500c64c6a25558cf8f5179f0c385cd45))
|
||||||
|
* **html:** use non-breakable dash in menu and column translations ([56c3c8f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/56c3c8fe40bffec2c83f085ed5afae9af06b9d2f))
|
||||||
|
* i18n ([3dd6e21](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3dd6e21f8ec95ca9ebfe71e4a3dc9743ab7bdbc8))
|
||||||
|
* **i18n:** add missing translations ([773c6c5](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/773c6c5dc02ed5626f3a83e309a229538fd4b27f))
|
||||||
|
* **i18n:** custom language inference ([205d768](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/205d7688bf821b1c899b9f3b4d3759a9d89de3cb))
|
||||||
|
* **i18n:** fix typos ([8af256e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8af256e55dd2c23276c2894f734e80ee5b5712bb))
|
||||||
|
* **i18n:** get started on i18n-breadcrumbs ([268d9e0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/268d9e0b1cfc2a41079d42415fccc075ba909ee7))
|
||||||
|
* **i18n:** i18n for all widgets ([3fe278e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3fe278ec3087b7cb219a94c3b9fe28c0f3e204fa))
|
||||||
|
* **i18n:** i18n in various places ([155ed1d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/155ed1d557e8b12fc40ac7de4efc9987a4e560f8))
|
||||||
|
* **i18n:** missing translations ([14b1706](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/14b17068a02004266a13b9770172fc7a6697644f))
|
||||||
|
* **i18n:** missing translations ([d0ce45b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d0ce45ba318ba324845c513b45e776f0d03e7c4c))
|
||||||
|
* **i18n:** missing translations ([b6a2412](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b6a24127055757f8dfe33035cd098335be5d6df7))
|
||||||
|
* **i18n:** missing translations & changelog ([76663b0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/76663b057d58bb4cb531f80076954c443ce1dc92))
|
||||||
|
* **i18n:** missing workflow translations ([ed4ee13](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ed4ee1320bd2eaeb462a1e6b72b0f4ff24e447f3))
|
||||||
|
* **i18n:** prepare translation file for en-eu ([281c98f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/281c98fe916d1d44805605d5ffca3c6b3aecca36))
|
||||||
|
* **i18n:** rename i18nWidgetFiles to proper language code ([33ddbfb](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/33ddbfb7ccd648bf875152ba2c626c0220088bb2))
|
||||||
|
* **i18n:** s/Typ/Art/ ([0e43851](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0e438513368ea0b44726e7d7e1aeddac2a4183cc)), closes [#493](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/493)
|
||||||
|
* **i18n:** submissionDownloadAnonymous ([e6af788](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e6af7888825e66624b94f4af45ad4d698fc8d3ae))
|
||||||
|
* **implementation:** spaces ([53471d1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/53471d166273fd1a8ef3a4c49f8d39c8a329dfbd))
|
||||||
|
* improve async behaviour ([cc7a528](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/cc7a5289a4ef7965b3464bb826e6a1e32a5d2929))
|
||||||
|
* improve csv import explanation ([729a8e8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/729a8e8bcea420a856a2d577520e943968807ce1))
|
||||||
|
* improve exam occurrence ui ([83fa9c9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/83fa9c9c69a6e986c341e8283698f6c23ced2a90))
|
||||||
|
* improve explanation of multiUserField invitations ([954bb78](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/954bb78aae1316bbcc4e8145d63ae072c63beff8))
|
||||||
|
* improve hidecolumns behaviour ([9a4f30b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9a4f30b811fdf8c58ec5c50c185628eb3158931a))
|
||||||
|
* improve labeling of button to switch exam occurrence ([727b89b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/727b89bf4b80b1f5c83e3f6eeebe5c9da7226596))
|
||||||
|
* increase size of test instances again (oops) ([4e76fe7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4e76fe7e504515845d468fc3251a38c90aaaaf66))
|
||||||
|
* **info-lecturer:** Touch ups ([e1e26ab](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e1e26abbbffd0f30a9e53c1a73f5bff7b1cb4afc))
|
||||||
|
* **info-lecturer:** translate german headline ([069d15a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/069d15abcd4cb811a0aeb07ef1cb55ced97729c1))
|
||||||
|
* **info:** minor whitespace correction ([0ce4dd1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0ce4dd181c57788de71a400c0635088634155d7e))
|
||||||
|
* inherit authorization of CAddUserR in more places ([3391904](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3391904cff75cf9646d647ee15d907a8080d00ce))
|
||||||
|
* **interactive-fieldset:** fix behaviour for nested fieldsets ([65b429a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/65b429a320b6876a7d72d40935a7d49257ef77d7))
|
||||||
|
* **interval jobs:** avoid accumulation, reduce job size ([24491b4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/24491b446b870698564adb9718e868e082873539))
|
||||||
|
* invalidate nav caches ([e88b6d6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e88b6d6bab3ea4577af3cd9465e66aa7e48177a2))
|
||||||
|
* **job:** fix [#95](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/95) by implementing queued job deletion for admins ([5b9a554](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5b9a5545457dbe506d20f7362fb6e0d6bae4f7f4))
|
||||||
|
* **jobs:** adjust job handling to hopefully reduce load ([ed38f93](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ed38f93537b57b5b3e4563dc0259d805760071bc))
|
||||||
|
* **jobs:** better flushing, correct metrics, better etas ([e4416e7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e4416e7f0e2ea2cf9db0e61cf2d20c27260ccaf8))
|
||||||
|
* **jobs:** cleaner shutdown of job-pool-manager ([adc8d46](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/adc8d466ac0948dcddf601fac439bb4e8d3bf619))
|
||||||
|
* **jobs:** delimit resource allocation to within handler ([7038099](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7038099389fcca684a9e1a3f28f76629e0c194bd))
|
||||||
|
* **jobs:** flush only partially for reliability ([59c7c17](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/59c7c1766588052383754b16e575347fa960ad6a))
|
||||||
|
* **jobs:** implement job priorities ([e29f042](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e29f042229d397bb37b692621a10c66ff9489db1))
|
||||||
|
* **jobs:** improve job worker healthchecks & logging ([2a84edc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2a84edccb4cdfddc2bdc03ebdd2b934fd7f53884))
|
||||||
|
* **jobs:** more general no queue same ([b1143cb](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b1143cb12bea48d75a2453f92122edcfb4fe51f1))
|
||||||
|
* **jobs:** only write CronLastExec after job has executed ([67eda82](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/67eda82bbcaf768fadda9735455f3d260532ee09))
|
||||||
|
* **jobs:** prevent offloading instances from deleting cron last exec ([e61b561](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e61b5611b1568180aa3ccfc3e3b981eb9a13cd53))
|
||||||
|
* **jobs:** queue certain jobs at most once ([1be9716](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1be971677b25689a895734d9efa5898fcbf0ca08))
|
||||||
|
* **jobs:** reduce likelihood for multiple queueing of notifications ([970ca78](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/970ca784b0286a0f8341356e9769d9a80ab60903))
|
||||||
|
* **jobs:** use more read only/deferrable transactions ([db48bbb](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/db48bbb7765604aaab8f8d5c540793b1ceaff16a))
|
||||||
|
* **jobs:** wake more often during waitUntil ([6115b83](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6115b83bb32d767ef54f2a2ae210ad1c7415e69c))
|
||||||
|
* **jobs:** weaken crontab guarantees for performance ([212e316](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/212e316c7e256f7883e0b883942e98bf795d870b))
|
||||||
|
* **js:** fix i18n not loading ([a3ee6f6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a3ee6f6fa6cf90f3a4e1ef4bed6ff104412318c8))
|
||||||
|
* **ldap-failover:** improve concurrency & error handling ([da1bf86](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/da1bf86d5e72b59448037487cbc4bf5194f5aac7))
|
||||||
|
* **ldap:** allow ldap update for mangled user entries ([006ab63](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/006ab632a394ed8d566f7930e8f6fd5a6ba86814))
|
||||||
|
* **ldap:** allow punctuation in displaynames ([61cfdc8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/61cfdc8758dcc6ea8aabf52b9d0c0e1d61d6e522))
|
||||||
|
* **ldap:** fix type in department descriptor ([9697d8c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9697d8c7fa8bbe6db6418107acb21c387cd4672c))
|
||||||
|
* **ldap:** improve debug message ([9fafb0b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9fafb0b7c3a13209be8a8d37c0c99c46608c9a81))
|
||||||
|
* **ldap:** update phone numbers and company data from ldap ([991ee9c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/991ee9c704dea31e65bb347af582f8a81a72aca4))
|
||||||
|
* **legal:** move anchor targets to headings ([a5c98e0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a5c98e092d7e242010e7598e8317ca8c10c39849))
|
||||||
|
* **letter:** email wrapper for renewal letter reinstated in full again ([1c02b85](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1c02b85fa256302b01c18baa64c8d0b7f9ffb671))
|
||||||
|
* **letter:** renewal reminder and renewal idents switched ([064b984](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/064b984945288172c094485d8d3622d196333e0e))
|
||||||
|
* **letter:** update receiver postal address before sending ([7d5c4bf](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7d5c4bff2512154c087133e029713efa0657fa5a))
|
||||||
|
* **lint:** remove redundant parenthesis ([4956e6b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4956e6bf57458a5e76efc582f6127b84a055c4de))
|
||||||
|
* **lms:** accept success for no-status learners and print several more debug messages processing reports ([a7ed659](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a7ed659866de1d4a178bbe4e8f9cd8fbc629c724))
|
||||||
|
* **lms:** add safeguard to LmsUserlist dispatch running twice, thus ending LMS prematurely ([a8df40d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a8df40d9f8943f2e0c4e219074486dbbf0eaf0fe))
|
||||||
|
* **lms:** correct lms table column sorting key ([9ee4767](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9ee476736ccd361559edd3570dc27ba83ff8a334))
|
||||||
|
* **lms:** direct upload did not commit to DB ([e7cea4a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e7cea4aa6c5b05285e5c47d815067a4d45315024))
|
||||||
|
* **lms:** disable workaround for late lms success ([cb9e09d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/cb9e09d071d22f41a92ab8140d7aaa643c748373))
|
||||||
|
* **lms:** do not mark lms users with open status as ended ([a848126](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a84812640f02981875275c96e37338de4ab49996))
|
||||||
|
* **lms:** ensure lms uniqueness across all qualifications ([b85c8bd](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b85c8bd74f8db526fb1cbb43ff12a24b93c07eb3))
|
||||||
|
* **lms:** filter by status ([a74c3d8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a74c3d80cada4f9d224365727dab9676cc905f54))
|
||||||
|
* **lms:** filtering qualifications by supervisor works properly now ([15f7a75](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/15f7a7576ab48a362a479f43034510b4e80bb1b2))
|
||||||
|
* **lms:** improve sorting for firm all ([3865bda](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3865bda64d488c161b55e1f6eb48ca1b742dff98))
|
||||||
|
* **lms:** lms admin renew pin actions were ignored ([242dd0b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/242dd0b8d46566e67d9a9e75c6f35650cd6da27e))
|
||||||
|
* **lms:** LMS restart failing due to old LmsUser entry ([6761767](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6761767c6ca8cab62a22aa6f755e6231e07ab411))
|
||||||
|
* **lms:** lms-direct/deletion-days setting now represent #days to presever lms (used to be #days+1) ([d02e62e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d02e62ec20b8cdc9dd6144de558895885ad1e692))
|
||||||
|
* **lms:** mark as ended only if not seen for at least one day ([8165892](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8165892b2e4f945780bb8420cfc4eed50fdd294d))
|
||||||
|
* **lms:** mark expired learners as ended with status expired ([db9ffa1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/db9ffa18302bf56649466ed78b5040f3c46e5e09))
|
||||||
|
* **lms:** negate learner locking condition ([a452b03](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a452b032c43dbdfd086ffa4793c83ecc32c450f8))
|
||||||
|
* **lms:** negating unsigned word auditDuration bug squashed ([7b152b6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7b152b67edfc20f9d2a5dd3573c2bd52e710ff41))
|
||||||
|
* **lms:** prevent duplicated LmsIdents and Letter sending ([1731d22](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1731d22ba56045fd1c636e2897748faa735053e6))
|
||||||
|
* **lms:** report log did not match qualification ([390ff31](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/390ff317ea3bb4ef8918c9cda858f5f228e4a882))
|
||||||
|
* **lms:** reset e-learning more lenient ([8b0737e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8b0737e2aabc7153ae3a3df4f97f86ffc8592e7a))
|
||||||
|
* **lms:** send e-learning failed qualification only once ([c62a42d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c62a42d5c2175c4a2fbfcb47e54cbff273441b51))
|
||||||
|
* **lms:** simultaneous block/unblock lets unblock win in all situations ([ecd1a0f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ecd1a0fc210d1340bff5c79d8bb676a47654b509))
|
||||||
|
* **lms:** sorting and filtering lms status ([f48862e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f48862efbcb95e92203a200267e1bcc613af4af1))
|
||||||
|
* **lms:** sorting and filtering lms status works throughout now ([ae44703](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ae4470333e2b1b5c271b38092210c094822f4a19))
|
||||||
|
* **lms:** transmit renewed pins to lms ([be3fb39](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/be3fb39171c1eb5d015ae006286bed747055a7a6))
|
||||||
|
* **lms:** treat simultaneous blocks/unblocks correctly ([11752dc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/11752dc5ac96f36ebf9a4cad43fa4e4b55c1b21c))
|
||||||
|
* **lms:** trigger userlist job after upload ([cceb600](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/cceb60074fbb26d7ed2d10a1c37297fa6e52292a))
|
||||||
|
* **lpr:** fix [#96](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/96) by various minor improvements to PrintCenter ([80c632d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/80c632df1ca4871c10cdac1141d87f92a7646cf7))
|
||||||
|
* **mail:** add debug info why setting reply to instead of sender does not work ([3453fc3](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3453fc34598581f88dd5696fd28ddd726b389ed1))
|
||||||
|
* **mail:** better separation of sender/from/envelope-from ([0dbf4f8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0dbf4f8bde99431cafeec954dc164a73227154ad))
|
||||||
|
* **mail:** fix various minor email attachment problems ([90a5f07](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/90a5f07c0412c6820f935b483db8645bcefba160))
|
||||||
|
* **mail:** honor userCsvOptions and userDisplayEmail ([89adf7f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/89adf7f2dc1caa90fc71adbcf0dc04936b685bd3))
|
||||||
|
* **mail:** mail-reroute-to now changes envelope-recipients as expected ([86d947f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/86d947f7e80b96362be8d662318c0140055d4786))
|
||||||
|
* **mails:** prevent emails being resent to due archiving errors ([8cf39dc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8cf39dcbe68cefcc50691ae8a7194315d18420d6))
|
||||||
|
* **mail:** use only RFC822-timezones ([59b8bb9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/59b8bb982d33b14a01133819c375884a8a7f7ce9))
|
||||||
|
* make migration idempotent again ([9778404](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/977840446e5a9b5836c6f7370c6134b144b8902e))
|
||||||
|
* make sure it compiles again + add 2-letter name ([d60f935](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d60f93561f5ee84d460645a945db35ac6b55e97d))
|
||||||
|
* make sure line-break algorithm respects available lines ([e487cef](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e487ceff5858671eb0bcbd813e9de0d3b4c74f75))
|
||||||
|
* make sure to report NoUsers, regardless of rule ([9c928b0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9c928b0375c1aab0c46768101849ce8daeae9b81))
|
||||||
|
* make sure unfortunate combination doesn't only produce 0-9 ranges for matrikelnummer ([8e4cb09](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8e4cb0917db1098f5b19be0dfad4c6fafb900c49))
|
||||||
|
* **many occurrences throughout the project:** Fix typo: occurence -> occurrence everywhere ([96387cb](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/96387cbed5bda9b901706318e1931e6e718a0680))
|
||||||
|
* mappingDescription doesn't overlap for the first n rooms/with small names/matrikelnummer ([fc35fd2](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fc35fd26c1eb699d6eb8aa1b9febb48641c26d05))
|
||||||
|
* **mass-input:** defaultValue is safe ([03f36ae](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/03f36aea1f1cb1998a030d1e3cf360faedea58c7))
|
||||||
|
* **mass-input:** properly escape query selector ([9a3f401](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9a3f401b38e86e2f9e7fa722698a437d853b422e))
|
||||||
|
* **massinput:** properly render massInputList ([7c28448](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7c2844807f217b675b1730b128a3a418caee92d1))
|
||||||
|
* **memcached:** don't 500 upon hitting item size limit ([d79a539](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d79a539f71e8250f677ac4e0b42c9ffd4de50af5))
|
||||||
|
* **memcached:** navAccess & quick actions cache invalidations ([d05306a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d05306a39a5324b2b09503bfc09ca4b7f2ee38f8))
|
||||||
|
* merge ([a9636af](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a9636af13a86d405b03b538e5489ecb2d30c4294))
|
||||||
|
* **merge:** fix build ([0bd0260](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0bd0260a3e916b6541f43e036744e80b8d0f00bb))
|
||||||
|
* **metrics:** allow free access to metrics during development only ([085c841](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/085c841035b3f808ce6d8ae3f6e0e9e6452028df))
|
||||||
|
* **metrics:** larger range for worker_state_duration ([34a5265](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/34a52653d71140bcc664cbe864cad069441b5c6e))
|
||||||
|
* **metrics:** sort metrics ([e5ae152](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e5ae1521a0577df35abe13b6bcc602f3a38a6f9c))
|
||||||
|
* migrate so as not to resend allocation notifications ([132a510](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/132a510a2372f15db671c05c84b5a815fdc0e0a1))
|
||||||
|
* migration ([dd23559](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/dd235590b47a90d70753458ffc7ab61c771f3d9b))
|
||||||
|
* migration ([4383eb1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4383eb1359c7ddb6d2cf9cbcd8d92523522c7d8b))
|
||||||
|
* **migration:** don't consider changelog in requiresMigration ([ea95d74](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ea95d74cb5572688531ba0fdeed3983fb70ab236))
|
||||||
|
* **migration:** drop more tables in w.a. for inconsistent 21→22 ([d79dca6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d79dca6be9a00c5dc1671e4779418f2fdc54aa02))
|
||||||
|
* **migration:** fix [#133](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/133) by removing old outdated migrations irrelevant to FRADrive ([d4f0d69](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d4f0d69428a4f7fc887cb6854cb59e3dea83b9bc))
|
||||||
|
* **migration:** handle deleted courses & users ([35621df](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/35621df03ece390d9513871c9def990828a51ecf))
|
||||||
|
* **migration:** ignore superfluous migration entries gracefully ([1d48b62](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1d48b627f6b8cf1b03e2ef63850c36c429c9d3d6))
|
||||||
|
* **migration:** make index migration truly idempotent ([7a17535](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7a17535600ef9408af2da5e0b01bea4b6e2fb63b))
|
||||||
|
* **migration:** omit index for old versions of postgres ([cf412a4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/cf412a4b54f7159601187305ad34f188a7e3e9a0))
|
||||||
|
* **migration:** typo ([fb7c7ef](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fb7c7efebd84ac46c1d1463e6cdb53d277834ef3))
|
||||||
|
* **migration:** typos ([e508277](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e508277496f7aa4114ab9685166e91caab941226))
|
||||||
|
* minor corrections, also fix luatex dependencies ([5015dba](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5015dba5e3dc287a8b6042ef43ed6a3952e9d9d1))
|
||||||
|
* minor heat correction for correction overview ([5546849](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/55468496e0842ca014af6d6b93e87c1c0f46f222))
|
||||||
|
* missing translations ([dcfdb51](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/dcfdb5130d19e737147bfe9065a6ccb5edf49a77))
|
||||||
|
* **missing-files:** properly account for workflows ([c272618](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c272618aa6dd68a1acb5b959c6d905978b26eb07))
|
||||||
|
* **models:** correct erroneous default values ([282a7d4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/282a7d44b24b70df0bcfa65a31c2c8c48bdee021))
|
||||||
|
* more verbose watchdog notification failures ([48028c4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/48028c40532577f74430340ed924af7116b8bd96))
|
||||||
|
* **navbar:** restore border to language buttons ([a2e9a9c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a2e9a9c32ddb78a487b2d69f048922c405d14ee5))
|
||||||
|
* **new-submissions:** always check for existing sub ([c7d23e6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c7d23e64ff565ac6920c2f4dc2ab4da20a1fb70c))
|
||||||
|
* **news-allocations:** i18n ([5a23d87](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5a23d87380badfe1ee8e6b4c93730050cc42305f))
|
||||||
|
* next input area is now selected via a css query ([1aaf254](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1aaf254c3c3c07f95b63bdf760791859a80aa4a4))
|
||||||
|
* nix eval ([effddc7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/effddc7bfb3beed997ff342e707b49b88a31c395))
|
||||||
|
* non-dev build ([dfea399](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/dfea39907cd114e6d8aea29ae3835dd323ca4df8))
|
||||||
|
* non-exhaustive patterns ([5bff34e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5bff34ed0a1b2d4d160c506cbe7090209d28da66))
|
||||||
|
* nonmoving-gc still segfaults ([c404ce9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c404ce9b3529cf402a0f9d649ca3299df09ba089))
|
||||||
|
* **notification-form:** define rules for all notification-triggers ([0261b39](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0261b3979df73e1b24a920371682cea14ea6d1fe)), closes [#561](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/561)
|
||||||
|
* **notifications:** direct notifications now respect user triggers ([3e5f271](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3e5f271cacfcc5dbd95aa68a342f56db566f8dee))
|
||||||
|
* **notifications:** qualification renewals are more robust and not sent multiple times at once ([1cdd52e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1cdd52e96c727139d6cd630da5117fd3b4aa5a7f))
|
||||||
|
* **number-input-fields:** number inputs made HTML5 compatible ([6098215](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/609821595b547999a571b4c0d1aa5a10ed58aa9a)), closes [#412](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/412)
|
||||||
|
* occurence exception end times not shown correctly ([725468b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/725468bfd3e27787875e8d2bf21693b3145da77f))
|
||||||
|
* oops ([f6cbf99](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f6cbf99245ffdd19a2d6c9acc7c0b9a7f8df45ca))
|
||||||
|
* order of on in exam office auth ([f44f150](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f44f1507471a9310a9c88738ca5b3d8268afc136))
|
||||||
|
* ordinalPriorities ([d4ab6f6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d4ab6f64e24109892b5665d154d8811420452038))
|
||||||
|
* **pageaction:** fixes [#463](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/463) ([849c6c4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/849c6c49ca254e493538685f2ca82e052d4aa931))
|
||||||
|
* **pdf:** embed din5008 templates within binary ([b76c414](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b76c414220bd73ea8fb67b226a007cdcb0bbd4fc))
|
||||||
|
* **personalised-sheet-files:** don't delete files when "keep" ([6008cb0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6008cb040dea268e0a096f6c2fafa87f321d115f))
|
||||||
|
* **personalised-sheet-files:** more thorough check wrt sub-warnings ([0b0eaff](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0b0eaff20daf9f38e42678d7ab159a0e75ebec66))
|
||||||
|
* possible workaround? ([757e148](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/757e1480329d11521c1ef7afb78702a251fd5b89))
|
||||||
|
* prevent deleting sheet-referenced exam parts ([9859c2e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9859c2e99c1e0c7531ee38864a24ff279a8e6a7c)), closes [#681](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/681)
|
||||||
|
* **print-center:** fix syntax ([957bf4c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/957bf4c966cfd4bccd7da443d02de559f58dc703))
|
||||||
|
* **print:** apc ident aliases did not stop at first success ([b7d4f69](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b7d4f6913d8b1a70c1b7ef73782cf29861dc11a7))
|
||||||
|
* **print:** disable default filter for print acknowledged ([f0b20a1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f0b20a1b263a072a9811ff677f25e6518d314133))
|
||||||
|
* **print:** keep print jobs on user merge and lms id deletion ([a15862e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a15862ea72bc374af870ef3a23f86ae32c2c67a9))
|
||||||
|
* **profile:** bad email indicator ([6699f1d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6699f1d72f148ccd2c82bebb3f582cf61d711425))
|
||||||
|
* **profile:** email validation inverted ([799f1fe](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/799f1fe184276aea2eb3659e5d439cd76f4ca4d8))
|
||||||
|
* properly apply auth to corrections in sheet table ([d59f686](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d59f6860215ebbffde61062a501b5eeeabdb58ae)), closes [#700](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/700)
|
||||||
|
* **qualification:** new block/unblock mechanism working now ([5397c7b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5397c7be353fc1b1e8310f66b49a9b93ee890253))
|
||||||
|
* **qualification:** prevent qualification mixups ([88d4356](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/88d43560ae8de1480502914d9c95d6376a3c68cc))
|
||||||
|
* **qualifications:** counts for lms/quals correct now ([33a847b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/33a847baa3310e6e261409f2cda9d964cf5a821d))
|
||||||
|
* **qualifications:** fix [#78](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/78) block/unblock no longer deletes company association ([3cb66c6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3cb66c6211b9f15127d88f448557acb4a3a2dd5c))
|
||||||
|
* **qualifications:** latest block could ignore itself ([bb708ca](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/bb708ca540557b41d33996cfea9a390a457ed855))
|
||||||
|
* **rating files:** better descriptions & tests ([5f04593](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5f04593b30c14fffc58e513b79ce1c98e75328b1))
|
||||||
|
* **rating-files:** support integral points values ([62dd7b9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/62dd7b9f047e504db783b2ceef19307c21d5550b)), closes [#604](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/604)
|
||||||
|
* **ratings:** disallow ratings for graded sheets without point value ([c0b90c4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c0b90c4c4ab9e26143c0ac76ad7af0fb4ac5fb0e))
|
||||||
|
* **ratings:** disallow ratings for graded sheets without point value ([463b2b7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/463b2b78780ecf24aa3b48f0740c18fce45e8d3c))
|
||||||
|
* **ratings:** improve decoding error reporting ([c873150](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c87315006df39550a58d89bcb5491a0afe0e4481))
|
||||||
|
* **ratio:** more attempts to fix ratio bug ([b813442](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b813442012cab26edb6e04552eb77aaea4103e03))
|
||||||
|
* **reachability:** account for e-users being assigned a useless company department ([bb27324](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/bb27324ee8dff257da09c1575468048d793bec8e))
|
||||||
|
* **release:** ought to fix issue [#4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/4) faulty version numbers for demo container ([934026f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/934026fc79e93d2aeb2d46f5697288ee2374e06f))
|
||||||
|
* remove cached-db-runner ([ff82700](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ff8270042f74d8019e121aebf8636472e1e4d79e))
|
||||||
|
* remove link icon on table sorting links ([e7e7d2b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e7e7d2bc6b4a96ce3dc6735b4a869e0cfe2bf4fe))
|
||||||
|
* remove manually inserted error for testing ([8c17f33](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8c17f3354a6e7768ecf427e4c0a899cbff9c7e0a))
|
||||||
|
* remove merge artifacts ([99e39bc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/99e39bc27ab1e68aed565751ae395cb2a4c8cf2a))
|
||||||
|
* removed duplicated code from merge ([9fb9540](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9fb9540bf7fd16334eeaf09d22c05a1ce80b3737))
|
||||||
|
* restore behaviour of waiting asynchronously for job-management ([5ebcd89](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5ebcd89e11841fd777f9ab6fbe1c4c46b02313a7))
|
||||||
|
* restore storting for exam-office exams ([5698e9c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5698e9ca0bb19585b9a9d2d3c10f8b5f99ae5db9))
|
||||||
|
* restore workflowWorkflowList columns ([e55c6d7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e55c6d795fd724bdb732e22d13c96d6b67ea7da1))
|
||||||
|
* restrict guessUser to consistent queries ([bcd5326](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/bcd532612914f33fc3bb053d00a3319ce2748d29))
|
||||||
|
* revert wrong hlint suggestion ([ba2ed97](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ba2ed97731a5ca8d95a9118785c9cce163244267))
|
||||||
|
* **rights:** split applicant off participant ([9d709ca](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9d709ca400ab39957e0c5b6b7a4c466b4587dc83))
|
||||||
|
* **rooms:** honor roomHidden ([ed5d871](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ed5d871182954e2f0a9a5063f61277d925628c40))
|
||||||
|
* **route:** correct typo in route /lms/../userlist/upload ([89be36e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/89be36e35b82713e20b3665396d551ca1788fdd8))
|
||||||
|
* **routes:** change ex to sheet ([9d9ead9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9d9ead95d847a96927b3c8d66d1adff9f31af2f4))
|
||||||
|
* **routes:** remove redundant auth tag ([5ef36f1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5ef36f1d1c0bd92773bbb58af417bae8307a3610))
|
||||||
|
* **sap:** combine immediate next day licence chnages for SAP ([f4adfdf](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f4adfdf87270930d4ca6611f2a9956613fcace53))
|
||||||
|
* **sap:** combine immediate next day licence chnages for SAP ([cbb44f1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/cbb44f106ad59e0a53ca04963ade5544120b7e21))
|
||||||
|
* **sap:** combineBlocks yet another bug squashed ([3924d14](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3924d14abd868305b42c9d04913536b4999dc45b))
|
||||||
|
* **sap:** compileBlocks ([b4a88ab](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b4a88abcf85783c350ad2bf3a5e973d13d1eb1f6))
|
||||||
|
* **sap:** do not export e-accounts ([086e49e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/086e49e2ae126f6acb9be774b0351d37443c31d8))
|
||||||
|
* **sap:** yet another fix for finding date intervals ([fde97b0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fde97b048ab04ab59c9e3f2a2f74bb2c1e996b22))
|
||||||
|
* **school:** fix [#133](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/133) by adjusting default value ([2509358](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/25093588784381a19f34e5b091677b908420ddea))
|
||||||
|
* **schools:** fix schools form wrt. discouraged modes ([53a8f1b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/53a8f1ba122466312947cdbdb49749a61acab37c))
|
||||||
|
* **schools:** insert correct authorship statement definition for exam-unrelated sheets ([2272647](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/227264743e0e8d0acf76839300a034b4bb1bf2a6))
|
||||||
|
* **schools:** perform authorship statement inserts ([579371c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/579371cffd87c247805bf4ead8bc2c278269a5ee))
|
||||||
|
* **schools:** rename messages ([0e62073](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0e6207376043af8fe0929019e3c39f80bcfea9a6))
|
||||||
|
* **schools:** switch authorship modes to required in form ([8fb49dd](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8fb49dd602f4eb854b300b5b399206aa2fbca87b))
|
||||||
|
* **schools:** use StoredMarkup instead of Html for authorship statement ([67c3016](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/67c30165ae90603e8a97ad2661d2bacb92e2e53f))
|
||||||
|
* **serversession-backend-memcached:** don't throw on deleteSession ([bcd3e46](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/bcd3e467d6a9e4f3ba210d0303412b3657530eb7))
|
||||||
|
* **set-serializable:** logging limit ([60be62b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/60be62b63bf328407e4ba0f01221d87020e89f24))
|
||||||
|
* **settings:** disable lms jobs by default ([daa1fe1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/daa1fe1a37b075ce420607723e4255fd0ff7979f))
|
||||||
|
* **settings:** memcached host defaults to localhost, because why not? ([3d5e532](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3d5e532e2d963cacdd74c1b3a7237f37d1fd3751))
|
||||||
|
* **sheet corrector assigment:** minor bugfix ([749cd2f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/749cd2f7bcf990f5eee18d90099843151cb56716))
|
||||||
|
* **sheet list:** do not show icons for inaccessible items ([0bb9a0f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0bb9a0fa60d83e91c91bb97833126a23a6f03989)), closes [#421](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/421)
|
||||||
|
* **sheet list:** only show corrections after they are finished ([d4907cd](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d4907cd776467dcd863ea761f9c0c88408e7248a)), closes [#533](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/533)
|
||||||
|
* **sheet type info:** give better tooltips and name to sheet types ([9dbef1f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9dbef1fe0f2d69eef5f6ff830d5ac338b84aa0f7)), closes [#402](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/402)
|
||||||
|
* **sheet-inactive-notification:** improve wording ([8af6bde](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8af6bde8a6efd35ec2c534eabe92c5aa6c3a87b0)), closes [#514](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/514)
|
||||||
|
* **sheet-show:** move message ([1d8a2ce](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1d8a2cef60a688bd514d529f8e1230e462811f1e))
|
||||||
|
* **sheets:** fixhance sheet authship form section ([7192cb5](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7192cb527c7f66c320308a80de9906a6edc6e9ec))
|
||||||
|
* **sheets:** integrate corrector interface into SheetEdit ([acfd312](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/acfd3129ec561725dded5a24c7f293086c9087e5))
|
||||||
|
* shown ranges "include" special mappings ([7e1b75c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7e1b75c2e167c75ebc3a05f881ad7fb07c29af55))
|
||||||
|
* shutdown behaviour & tests ([19b8b06](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/19b8b0616f306b57390b7a6e26023e8d59aa1239))
|
||||||
|
* **smtp:** case-insensitive from-domain comparison for reply-to instead option ([859f5b8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/859f5b8494ce326fcdf13ed8fcca9355273fb42e))
|
||||||
|
* **smtp:** use full email with name in reply-to field ([8cdc2b5](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8cdc2b5267095ecb816398037bc830c996250456))
|
||||||
|
* sort occurrences in the right order ([732df50](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/732df5053033c3533f52850cc6220dd06a7e3500))
|
||||||
|
* **specific file submission:** swap labels ([7fadcf5](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7fadcf52b7c99c6259e36f1caf46b92e08b2aafa))
|
||||||
|
* spelling plugin had a suggestion; actually Hello World commit :p ([7b0fd61](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7b0fd61f7f8bf1e995209bec7b44231b5ba011a6))
|
||||||
|
* **sql:** fix transaction behaviour of setSerializable ([e5acdad](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e5acdad134a7c4d8457c1cf8755d69306a52dd9e)), closes [#535](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/535)
|
||||||
|
* **sql:** quiet warnings in setSerializable ([859ae5e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/859ae5eea103cce3dee84d6ba9d104f21d120f43))
|
||||||
|
* **standard-version:** properly reset staging area before release ([5aa906e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5aa906e7eb3798d590a38ae3e18ce4c18741d9b4))
|
||||||
|
* **status:** module imports fixed ([c59ecf5](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c59ecf5019af67c849f32962b8ce9485a7adab1c))
|
||||||
|
* **status:** nix files inaccessible on build server ([1bb500a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1bb500ab027993a97848d357a3382c8975f113eb))
|
||||||
|
* **status:** route status exempt from approot normalisation, might not fix the issue yet ([074a33d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/074a33dc51bd7e9ed72434223ceb06d70f1044c5))
|
||||||
|
* **storage-key:** fix types ([a0d067f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a0d067fabfa91027831e86fc5c52a676e63ea9f7))
|
||||||
|
* **storage-key:** fix types ([a23a473](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a23a4735c2a4a8a372b6479d79223099d59e3bea))
|
||||||
|
* **storage-manager:** correctly use encryption key in decrypt call ([2667aac](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2667aac1a382c52d9dc6c465ee8ccb173e893a28))
|
||||||
|
* **storage-manager:** correctly use encryption key in decrypt call ([9e9726e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9e9726e173dbfa5a0a4dff0402a19af6ca8dfa51))
|
||||||
|
* **storage-manager:** post salt and timestamp only when fetching key ([6340509](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/63405093c4fc2fd30b919da7b26ad7f984c0ba9d))
|
||||||
|
* **storage-manager:** post salt and timestamp only when fetching key ([301c88f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/301c88f2ea9dd0b430e982f11adcf6fd29d74096))
|
||||||
|
* **storage-manager:** remove and clear SessionStorage ([e42452e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e42452e4da3eb6182f792f0711a94395ee430eee))
|
||||||
|
* **storage-manager:** remove and clear SessionStorage ([38b0a8e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/38b0a8eebc2e0774fb80e11e18736492e6ae2755))
|
||||||
|
* **storage-manager:** save salt and timestamp ([0282918](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0282918c2e3ce50b471dcff80a6438ba1f9fd197))
|
||||||
|
* **storage-manager:** save salt and timestamp ([8bee033](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8bee033efaee308731dd62486fee7aba2ae3ddc0))
|
||||||
|
* **study-features:** account for existing StudyFeatures ([b6cada4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b6cada43f218f3c688360c114f020e9037a87a21))
|
||||||
|
* **study-features:** also apply caching to table columns ([564c0b9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/564c0b975ae65881cb3a168855b36e4b1614a6cb))
|
||||||
|
* **style:** breadcrumb bar width ([7340fc1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7340fc1fa60c241b2b5a054386a3484e724e9681))
|
||||||
|
* **style:** padding of language buttons ([e704b23](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e704b23a53f0af2a1e474c181ebd079063e5e469))
|
||||||
|
* submission download token generation broke viewing ([e1b6084](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e1b60844cb77b1fd41900d0a3c4829ba21b6b3fe))
|
||||||
|
* submission user notification recipients for pseudonym subs ([a7b7bdb](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a7b7bdbea754873e11fea8d2af42bf3aacaff3f2))
|
||||||
|
* **submission-create:** ensure number of buddies is acceptable ([ec24a04](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ec24a04c9b1dfaefe8f95548206278a2d6ac9774))
|
||||||
|
* **submission-create:** sanity check submittors in form ([3bf37a4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3bf37a4c1ab89379794c57acd5c30516072c721a))
|
||||||
|
* **submission-form:** fix display of all courseParticipants ([b67819d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b67819d061fb3409fa9978a31cb94b700e9e86fb))
|
||||||
|
* **submission-groups:** prevent deleting group before insert ([f87cf7a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f87cf7a378ec2641ed3a9a13182b39b170f61c1f))
|
||||||
|
* **submission-groups:** wrong sql query for finding buddies ([0679626](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/06796269d4da73f9c31c978fc2e05e32fbe04b2f))
|
||||||
|
* **submission-multi-archive:** fix cleanup & improve ([27731ac](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/27731ac077ae6eee31eeb5cd39d24b0a0ea8f490))
|
||||||
|
* **submission-users:** properly delete old invitations ([91c926b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/91c926b1c5eb92a4d8bbf3077020a0121165f3eb))
|
||||||
|
* **submission:** allow non-group-subs when user isn't in sub-group ([9a35c85](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9a35c8542c358fbb8e88e6661e981631ba4c1fbb))
|
||||||
|
* **submission:** allow not modifying submissionUsers ([030fd7a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/030fd7abf1bdcd700d45bfa14cc192ac98dee24e))
|
||||||
|
* **submission:** ignore extension case within zips ([f8442cf](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f8442cfea9f5ab0c4b8b2b6959153034f390839e))
|
||||||
|
* **submission:** race condition allowed creating multiple subs ([02fc0d4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/02fc0d476f07c786cb4ef23600baaed36530fa24))
|
||||||
|
* **submissions:** allow user to resolve themself for auth'stmt' ([5bbb86a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5bbb86aa7750dd907f49cb3ba5daf2cee8485bae))
|
||||||
|
* **submissions:** cascade delete to authorship statements ([fcce16d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fcce16d838e5cba3187a82a5762b831d7df54cd0))
|
||||||
|
* **submissions:** don't leak info from corrected versions of files ([66f5e96](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/66f5e96eca4cbcb6cb092092b1b1b069ce30f159))
|
||||||
|
* **submissions:** fix ambiguity with multiple past co-submissions ([6e4f469](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6e4f4690237625280d24ce6cdfb6f3e57bc16d38))
|
||||||
|
* **submissions:** fix distribution without consideration for deficit ([5035dff](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5035dff9021260cd45dabfc175bb535bdc19dc71)), closes [#713](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/713)
|
||||||
|
* **submissions:** fix users being deleted for other submissions ([2462c68](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2462c68f49d9952eeb4c9f5413c53d307f10975a))
|
||||||
|
* **submissions:** hide correction-only files ([575fadc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/575fadcd8cbc4d899ed0ab5d58e3fa8aa64df111))
|
||||||
|
* **submissions:** improve submission process ([7219131](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/72191315b6daed78cd0f31b02627e1d27db620f3)), closes [#675](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/675)
|
||||||
|
* **submissions:** maintain anonymity ([0184a5f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0184a5fe3b1af635318fa0fa317e3497f24fbc90))
|
||||||
|
* **submissions:** more precise feedback ([d151b6f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d151b6fc14e5b32d9f07923149923d5ab7ea4880))
|
||||||
|
* **submissions:** off-by-one when isLecturer ([01e61f9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/01e61f9bfda777c09c5b64c4bf4368e590328545))
|
||||||
|
* **submissions:** only notify submittors if rating changes doneness ([4f1162c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4f1162c363d15d9577302d064e4dd352111fd628))
|
||||||
|
* **submissions:** only notify submittors if rating is done ([8e0c379](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8e0c379c71ca1226590b333f9a740c6cc0aa98be))
|
||||||
|
* **submissions:** submitting produces an success alert now ([bf20d6f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/bf20d6f4e84353d7d83626377ccf41204832ac2c)), closes [#286](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/286)
|
||||||
|
* **submissions:** take care when to display corrections ([a6390ec](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a6390eccbd164ee5e821d3ecb0fab794a417425a))
|
||||||
|
* **supervisors:** reroute to non-avs supervisors too ([1cc6240](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1cc62403541404e4ab4c448841ff5fa0da508ce8))
|
||||||
|
* suppress exceptions relating to expired sessions ([d47d6aa](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d47d6aa6ccaa3007aae64f555ceec519dd03f029))
|
||||||
|
* syntax ([7afd569](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7afd569eaa363487f1775489ab1fe667aee84fe7))
|
||||||
|
* **system-message:** lastChanged & unhide logic error ([36abb3e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/36abb3ee2675e4da5e9baeccd1057db6d20303fb))
|
||||||
|
* **termidentifier:** rational not working use derived day instances instead ([ecdb22a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ecdb22aa613757ee6f0197a2d4191dcb3d603da1))
|
||||||
|
* **test:** build ([443b871](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/443b87168074b60d0b7806450dfbf512ad5b2135))
|
||||||
|
* **test:** fixed compiler errors (oops) ([bc42f30](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/bc42f3072fd37ee6f37c70a0b3999d9ac793b240))
|
||||||
|
* **test:** isNullResultJustified reported false positives ([292f5cf](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/292f5cf91b56953189ee72e42b822d66761ff3bb))
|
||||||
|
* **test:** LmsStatus is no longer a semigroup ([bf8cd4f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/bf8cd4fa899bccd4a37906a4d899aca6ca25d726))
|
||||||
|
* **test:** resepct uniqueness for ldap primary keys ([d1badf1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d1badf16fc9c45ee547ec1e3fe677e16645683f0))
|
||||||
|
* **test:** resepect uniqueness for ldap, 2nd attempt ([d06448a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d06448a4a8c194368c077d5ca7bf8d0feca86d60))
|
||||||
|
* tests ([4803026](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4803026a2c091128a7370c12f0c06de9bd7b9180))
|
||||||
|
* tests ([3c322af](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3c322af49e2021b2963fef9fbe303fa70ec77e18))
|
||||||
|
* tests ([65e0688](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/65e06882d2491da5e30b1401db6ecc81efcac58b))
|
||||||
|
* tests ([ca81f3b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ca81f3b0f2913431cbaf399c33ed30a21979ce69))
|
||||||
|
* tests ([018d26f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/018d26f4a1a1cf411324aeac56ce4d4203670942))
|
||||||
|
* tests ([5541619](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5541619372f4a4e46ccc403004e869afdfaed7b0))
|
||||||
|
* tests ([b4b4a96](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b4b4a96aaeb468881c8fe510a9a871e4931eeb1a))
|
||||||
|
* tests ([4854d83](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4854d830fb14ecff54d32aa93a95eaaf67c0499f))
|
||||||
|
* tests ([96b3ba4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/96b3ba4392ff76e48e032a08fff36808e006d0f2))
|
||||||
|
* tests ([daa1f83](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/daa1f837c78e19ab5dd0f9a79363ba9227707083))
|
||||||
|
* tests & hlint ([4e9b618](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4e9b618b61a2e35577dc9414d4c8c923b01b783f))
|
||||||
|
* **tests:** explicit post parameter name for dummy login ([2ccd50f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2ccd50fa85b05751de6b36185bf52a1386458992))
|
||||||
|
* **tests:** fix build ([b0f2304](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b0f2304273506947922637fa29132135cf11931f))
|
||||||
|
* **tests:** generate sensible WorkflowPayloadLabels ([8a888d3](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8a888d3945f0fd0d67ef83bae621744c943b99de))
|
||||||
|
* **tests:** i18n changes ([9ba0e27](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9ba0e27ba2208ef835538f6fecc092aab85411ea))
|
||||||
|
* **tests:** remove invalid claim of commutativity ([d2f0361](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d2f0361e49114e6dc6c55e64b677b8c842e93bee))
|
||||||
|
* **test:** test for applyMetas handles duplicate keys in correct order now ([0366f8c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0366f8cccaa7cf644d25d99c5a27c0d73a5714b0))
|
||||||
|
* **time:** midnight timezone conversion bug eliminated ([dfa07a9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/dfa07a95eb29f1fceec258a466e1e7c779ff6e5c))
|
||||||
|
* **tokens:** introduce clock leniency and remove start for downloads ([8939a8b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8939a8b90a39a26614da18dd3985aee253cd191f))
|
||||||
|
* **tooltips:** add dark variants of theme independent colors ([e5c7aa0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e5c7aa03dbe59060a2303d378229b2de3a2ee3c6))
|
||||||
|
* **tooltips:** fixes font-color when used in tableheaders ([f4bb70e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f4bb70e19161afffb396da182df5aeda0191285e))
|
||||||
|
* translation ([80960f4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/80960f42c578c201f78e226653431e9dd965cfce))
|
||||||
|
* **translation:** fix typos in translations; add bug to known bugs ([ac3f7bb](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ac3f7bb8b48d64b0db05f292211b6c7df955649b))
|
||||||
|
* **tutorial:** fix [#94](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/94) tutorial renaming (de) and template naming ([1ce8f75](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1ce8f75c2d192051929b1a74b17f4e6494961901))
|
||||||
|
* **tutorials:** improve creation interface ([bc248d0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/bc248d0fc21d6b8361d66e3bd6fa17b368da71b5))
|
||||||
|
* **tutorial:** template moving works now ([b982e59](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b982e59b630fbdb3fe8f37c979de8e8726b78ea9))
|
||||||
|
* tweak debouncing & canceling ([6b51cc5](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6b51cc5e53ab7686f2394cd80bc4ee4fc426c8d5))
|
||||||
|
* **txt:** delete old txt file ([0c639b9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0c639b9c53591e8c68d9c288ffddd72117ca3711))
|
||||||
|
* **types:** move term identifier start/end information to type definition; simplifies fill ([aeafe31](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/aeafe3118bb6111cc6f5d4ff012b0fc6c70fe23b))
|
||||||
|
* typo ([26c3a60](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/26c3a60592c02570ceeed42cc977ad223baa16ae))
|
||||||
|
* typo ([f155a4b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f155a4bf08d169309c05e3efbb47a246f3010816))
|
||||||
|
* typo ([f931c67](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f931c67a9ecf37bd9a6c9814ee61de7cb054dcc5))
|
||||||
|
* typo ([a1b03e8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a1b03e819fc41da7ca83be8d27955a29a4e73904))
|
||||||
|
* typo ([52670bc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/52670bc90526490b3b11b639cc3e4bd27bb4a184))
|
||||||
|
* typo ([c06a472](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c06a4723591cc3d716b2d6b39f2757e17387ae47))
|
||||||
|
* typo ([4c58699](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4c58699d1f2efa0695909de8be4ec660578696ae))
|
||||||
|
* typo ([ad5494e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ad5494ef03604250fbec63d4e48f0d6bbd66f87f))
|
||||||
|
* typo ([23f4eb3](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/23f4eb3f2b8154e412d280ab22eb881941d7e736))
|
||||||
|
* typo ([a6e40f1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a6e40f1be8dff7fc9e2711988aba0e431b3eb6dd))
|
||||||
|
* typo ([fb1e42d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fb1e42dc6994e79692f38b93a14eeaaaf9d53578))
|
||||||
|
* typo ([fc5ffb7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fc5ffb7c5231302c478999cb944821d0cbf5bbf7))
|
||||||
|
* typo course-assistant ([c7ce167](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c7ce1679de799285ec7a9a0a62c0a202b9078eb3))
|
||||||
|
* typos ([97f62b9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/97f62b92c1cc8a68682f6df3b104bd20e0e035b7))
|
||||||
|
* typos ([b9c284c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b9c284cde268f0865cc32d2c973206b227042700))
|
||||||
|
* ui improvements for (external-)exams ([b3ce3dd](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b3ce3dd93a576dd3b5c6a8ecb1b278556067806a))
|
||||||
|
* unbreak arc ([8ecb460](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8ecb460f39f48557b5935b1cd18709ba197d3490))
|
||||||
|
* uniworxdb ([e5608d2](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e5608d2d5bf5349f33d83dbd2f54af84442fd93d))
|
||||||
|
* update imprint & add instructions for help ([eec9a39](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/eec9a3974fc4cde5cc70ab650d018667ce044a92))
|
||||||
|
* **uploadcache:** set default to localhost ([eeb22de](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/eeb22dec9737e014c20b852438da4373219884b0))
|
||||||
|
* use extraUsers instead of extraCapacity for unrestricted pseudo-capacity ([2be9d76](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2be9d76af2b3e9fd52284c639a4c3f6dc1c51779))
|
||||||
|
* use recent locales & tzdata ([bedb47e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/bedb47e9d47e98621efb56db0cc24615cb7a8de4))
|
||||||
|
* user with a pre-assigned room count towards the capacity limit ([4fc0535](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4fc05351fa8048752f2ec3260dcaac64f962c9a3))
|
||||||
|
* **user-deregister:** remove tutorial participation ([cfcb28d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/cfcb28d1d491f1cbc77b0c4d4d522c44c5f6b807))
|
||||||
|
* **user:** add new user failed due to AuthNoLogin not treated in notification template ([8456f18](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8456f18bf688ad475f8f5d78c83df9f393c11ae3))
|
||||||
|
* **user:** add new user failed due to AuthNoLogin not treated in notification template ([a1516d9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a1516d9116451fb9a24761cdaee8b22dcab58680))
|
||||||
|
* **user:** check reachability by post or email did not account for department ([ed147db](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ed147dbd20b89d32281dbcaed3a1ba7cb00c347b))
|
||||||
|
* **user:** display name may omit hyphenated given name parts ([ddb1a15](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ddb1a15c183f25b81153baaec51033e32436e98b))
|
||||||
|
* **users-add:** upsert tutorial only if users not empty ([e65d388](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e65d38898e78dffa3dedf89292bf28e39a2ce3cf))
|
||||||
|
* **users:** allow prefer postal setting for users with fraport department ([a9d56c5](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a9d56c51dcc727f8637b09a0e849372e75032f5e))
|
||||||
|
* **users:** assimilate merges possibly incomplete user fields ([52afd13](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/52afd13b6dc2b870ab8dbba956874e8950e07973))
|
||||||
|
* **users:** fallback email to name ([7bf018c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7bf018c2a4cc8cd2d087da1d1c6cfff755bd3003))
|
||||||
|
* **users:** fix [#112](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/112) and also add some convenience ([35096ac](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/35096ace01a2bc2a2d666794bb1ff92f52b3edec))
|
||||||
|
* **users:** fix [#112](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/112) working now ([88bf21c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/88bf21c9c5de3755ea6591c97dc1f99a928914d5))
|
||||||
|
* **users:** fix broken email fallback ([f4e9f2c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f4e9f2c973f4c3eccda0a7997d25696fbe286e5c))
|
||||||
|
* **users:** prevent accidental user hijacking ([014d479](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/014d479df8f36515915bc7991bb97bad24dcbef9))
|
||||||
|
* **users:** synchronise sex ([25912e0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/25912e0616e1b771b0d76fa7ffb85ed7f5f676c0))
|
||||||
|
* **util-registry:** fix initAll and tests ([2620fb2](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2620fb2f9569368fdaafe3924c02f2f5a2671818))
|
||||||
|
* **util-registry:** start setup instances and not all active instances ([ddf94bf](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ddf94bf5650addbf6de40894ecda9ba6ef36e143))
|
||||||
|
* valid binary ci instance ([8cfdd28](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8cfdd286517e0a9ca99dd31b9d220560adc6c93d))
|
||||||
|
* **volatile-cluster-config:** fix pathpiece instance ([dcd5ddd](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/dcd5dddec82da359a2100360cfeb6845ed320821))
|
||||||
|
* **watchdog:** improve status&watchdog notification ([2d4ccd6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2d4ccd693394ac75aef291cdadf5bdbc72e173ac))
|
||||||
|
* weight random token impersonation towards active users ([a314f64](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a314f64a70d9e7e427383c8d656d9bdceed5f9f3))
|
||||||
|
* weird sql casting ([eb9c676](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/eb9c6760b9a62d263f0c30531a643d43c7318b3f))
|
||||||
|
* work around conduit-bug releasing fh to early ([3ff2cf1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3ff2cf1fec1bf582fe1d5e1f6ee08dcc85d6bc00))
|
||||||
|
* work around regression in esqueleto ([25cf946](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/25cf94657067dfc60e9acdb370274873ae0b6a6e))
|
||||||
|
* **workflow-types:** fix Int64 workaround; update test defs ([ce9648e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ce9648e47a870e3a051591631c2eb3a26c6b4b3c))
|
||||||
|
* **workflow-types:** minor import fix ([b19c1b3](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b19c1b31b7b079f3abe6aff93e3b5603050c6131))
|
||||||
|
* **workflow-workflow-list:** restore default sorting ([454a917](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/454a91702bdbbed7e473ef94a603bcea2e716406))
|
||||||
|
* **workflow:** add missing optional ([8608e83](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8608e83ef80497b7ddcc451759a98a07b435fc08))
|
||||||
|
* **workflow:** fix false instance with atrocious instances ([8812f24](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8812f24d9060314d60c3ae495ea7daccdabff30d))
|
||||||
|
* **workflow:** fix node and graph FromJSON instances ([263fee1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/263fee19f25a4782bf426347e385eedd1742c8da))
|
||||||
|
* **workflow:** fix types ([ce1acec](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ce1acec444762d254ccf07b37c280ffb02934669))
|
||||||
|
* **workflow:** fix types ([4334253](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4334253122f4208fb4dc61f5cbf4436f122b14e2))
|
||||||
|
* **workflows:** add missing import/reexport ([5e92a6e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5e92a6e04aeb9a54ab361f83c7168e57ae5e9c33))
|
||||||
|
* **workflows:** cleanup ([0a3eaa2](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0a3eaa29946ba00ba0c9597d124f4ca5cc25620d))
|
||||||
|
* **workflows:** disabled warning for top workflows/instances ([17ed2fa](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/17ed2fad2230944c629c6a0c8d8181f6fec8983f))
|
||||||
|
* **workflows:** don't cache instance-list empty for correctness ([cb1e715](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/cb1e715e9b2da2f5ac0bd03b636de0f961307efd))
|
||||||
|
* **workflows:** integrate in new master ([99f3fca](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/99f3fca6d08f098b996931c8c4736eefbc9db77c))
|
||||||
|
* **workflows:** navigation order ([c5eea64](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c5eea64b270369ef69e1aa368ec87f0a8846e1fd))
|
||||||
|
* **workflows:** prefer payload label from target state ([2619b08](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2619b08ad1be2921d2cdd568f9419852c374df10))
|
||||||
|
* **workflows:** properly offer previous payload files ([aa0404a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/aa0404a0075acbcd4c6f94984acdbb4d68f08d0a))
|
||||||
|
* **workflows:** refer by id in model ([94f78a0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/94f78a07d9376670122a2adce01cf7180a64d33d))
|
||||||
|
* **workflows:** ui improvements ([c7f4fa0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c7f4fa0e412d2b920a3819ffed5b79b8aeea2842))
|
||||||
|
* **workflows:** workflow-definition edit translations ([5c5cbad](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5c5cbaddf8b33f455ff18789806a3e0f9ac447ed))
|
||||||
|
* **xss-sanitize:** use forked version ([fb50d5b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fb50d5b9d002b979d59b9f11c64048d335090082))
|
||||||
|
* zip handling & tests ([350ee79](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/350ee79af3c8fcc480970166a559596873beab2a))
|
||||||
|
|
||||||
|
|
||||||
|
* bump esqueleto & redo StudySubTerms ([0e027b1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0e027b129eada45ce90f6b32223aab3fde8cf9cd))
|
||||||
|
* bump to lts-15.0 ([cfaea9c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/cfaea9c08bcc52b2db33f24efe4967ef9bfbe9d2))
|
||||||
|
* bump versions ([67e3b38](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/67e3b38834ae079ec601633a60799359853f9b5e))
|
||||||
|
* remove applications and allocations ([66b4cf8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/66b4cf8542b1e8c16346861ceed2833d9a56f35f))
|
||||||
|
* split foundation & llvm ([c68a01d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c68a01d7ae26bfa61306e143d663d28f641d0998))
|
||||||
|
* **sub-study-fields:** reformulate as superStudyField ([b7d6f3c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b7d6f3c9e991790cda7920ced039a3d8b4ffa8ac)), closes [#531](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/531)
|
||||||
|
=======
|
||||||
|
* **form:** multiSelectField working with grouped options ([3aa8901](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3aa89019a8b4393da0eca715871a3793c1e3abb2))
|
||||||
|
* **health:** fix [#151](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/151) by offering route /health/interface/* ([c71814d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c71814d1ef1efc16c278136dfd6ebd86bd1d20db))
|
||||||
|
* **health:** fix [#153](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/153) and offer interface health route matching ([ce3852e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ce3852e3d365e62b32d181d58b7cbcc749e49373))
|
||||||
|
* **health:** negative interface routes working as intended now ([3303c4e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3303c4eebf928e527d2f9c1eb6e2495c10b94b13))
|
||||||
|
* **lms:** LMS restart failing due to old LmsUser entry ([6761767](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6761767c6ca8cab62a22aa6f755e6231e07ab411))
|
||||||
|
* **lms:** previouly failed notifications will be sent again ([263894b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/263894b05899ce55635d790f5334729fbc655ecc))
|
||||||
|
* **migration:** fix [#133](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/133) by removing old outdated migrations irrelevant to FRADrive ([d4f0d69](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d4f0d69428a4f7fc887cb6854cb59e3dea83b9bc))
|
||||||
|
* **migration:** ignore superfluous migration entries gracefully ([1d48b62](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1d48b627f6b8cf1b03e2ef63850c36c429c9d3d6))
|
||||||
|
* **print:** keep print jobs on user merge and lms id deletion ([a15862e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a15862ea72bc374af870ef3a23f86ae32c2c67a9))
|
||||||
|
* **school:** fix [#133](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/133) by adjusting default value ([2509358](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/25093588784381a19f34e5b091677b908420ddea))
|
||||||
|
* **sql:** remove potential bug in relation to missing parenthesis after not_ ([42695cf](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/42695cf5ef9f21691dc027f1ec97d57eec72f03e))
|
||||||
|
* **users:** fix [#121](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/121) by providing last login column, which was the last part missing ([decc5af](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/decc5af6829998e2d0db79382bbd9a7bad7b5b09))
|
||||||
|
|
||||||
|
|
||||||
|
* **model:** move user authentication data to new ExternalUser model ([12fe58f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/12fe58fc81eada015103f6eff4a486fd6f03cbec))
|
||||||
|
* **model:** separate user authentication data from User table; add ExternalAuth and InternalAuth models ([54f2430](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/54f2430b3e79d3b7c396ac4cf1d4d0da860e3d02))
|
||||||
|
* **settings:** rename userdb app settings ([9f299c8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9f299c854c9d2d2f1b1127c85a31b787f85fa210))
|
||||||
|
|
||||||
|
=======
|
||||||
## [27.4.79](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.78...v27.4.79) (2024-09-10)
|
## [27.4.79](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v27.4.78...v27.4.79) (2024-09-10)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -25,8 +25,8 @@ mail-from:
|
|||||||
mail-object-domain: "_env:MAILOBJECT_DOMAIN:localhost"
|
mail-object-domain: "_env:MAILOBJECT_DOMAIN:localhost"
|
||||||
mail-use-replyto-instead-sender: "_env:MAIL_USES_REPLYTO:true"
|
mail-use-replyto-instead-sender: "_env:MAIL_USES_REPLYTO:true"
|
||||||
mail-reroute-to:
|
mail-reroute-to:
|
||||||
name: "_env:MAIL_REROUTE_TO_NAME:"
|
name: "_env:MAIL_REROUTE_TO_NAME:"
|
||||||
email: "_env:MAIL_REROUTE_TO_EMAIL:"
|
email: "_env:MAIL_REROUTE_TO_EMAIL:"
|
||||||
#mail-verp:
|
#mail-verp:
|
||||||
# separator: "_env:VERP_SEPARATOR:+"
|
# separator: "_env:VERP_SEPARATOR:+"
|
||||||
# prefix: "_env:VERP_PREFIX:bounce"
|
# prefix: "_env:VERP_PREFIX:bounce"
|
||||||
@ -66,7 +66,7 @@ keep-unreferenced-files: 86400
|
|||||||
health-check-interval:
|
health-check-interval:
|
||||||
matching-cluster-config: "_env:HEALTHCHECK_INTERVAL_MATCHING_CLUSTER_CONFIG:600"
|
matching-cluster-config: "_env:HEALTHCHECK_INTERVAL_MATCHING_CLUSTER_CONFIG:600"
|
||||||
http-reachable: "_env:HEALTHCHECK_INTERVAL_HTTP_REACHABLE: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"
|
smtp-connect: "_env:HEALTHCHECK_INTERVAL_SMTP_CONNECT:600"
|
||||||
widget-memcached: "_env:HEALTHCHECK_INTERVAL_WIDGET_MEMCACHED:600"
|
widget-memcached: "_env:HEALTHCHECK_INTERVAL_WIDGET_MEMCACHED:600"
|
||||||
active-job-executors: "_env:HEALTHCHECK_INTERVAL_ACTIVE_JOB_EXECUTORS:60"
|
active-job-executors: "_env:HEALTHCHECK_INTERVAL_ACTIVE_JOB_EXECUTORS:60"
|
||||||
@ -77,7 +77,7 @@ health-check-http: "_env:HEALTHCHECK_HTTP:true" # Can we assume, that we can rea
|
|||||||
health-check-active-job-executors-timeout: "_env:HEALTHCHECK_ACTIVE_JOB_EXECUTORS_TIMEOUT:5"
|
health-check-active-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-active-widget-memcached-timeout: "_env:HEALTHCHECK_ACTIVE_WIDGET_MEMCACHED_TIMEOUT:2"
|
||||||
health-check-smtp-connect-timeout: "_env:HEALTHCHECK_SMTP_CONNECT_TIMEOUT:5"
|
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-http-reachable-timeout: "_env:HEALTHCHECK_HTTP_REACHABLE_TIMEOUT:2"
|
||||||
health-check-matching-cluster-config-timeout: "_env:HEALTHCHECK_MATCHING_CLUSTER_CONFIG_TIMEOUT:2"
|
health-check-matching-cluster-config-timeout: "_env:HEALTHCHECK_MATCHING_CLUSTER_CONFIG_TIMEOUT:2"
|
||||||
|
|
||||||
@ -126,24 +126,47 @@ database:
|
|||||||
database: "_env:PGDATABASE:uniworx"
|
database: "_env:PGDATABASE:uniworx"
|
||||||
poolsize: "_env:PGPOOLSIZE:990"
|
poolsize: "_env:PGPOOLSIZE:990"
|
||||||
|
|
||||||
auto-db-migrate: '_env:AUTO_DB_MIGRATE:true'
|
auto-db-migrate: "_env:AUTO_DB_MIGRATE:true"
|
||||||
|
|
||||||
ldap:
|
# External sources used for user authentication and userdata lookups
|
||||||
- host: "_env:LDAPHOST:"
|
user-auth:
|
||||||
tls: "_env:LDAPTLS:"
|
# mode: single-source
|
||||||
port: "_env:LDAPPORT:389"
|
protocol: "_env:USERAUTH_MODE:azureadv2"
|
||||||
user: "_env:LDAPUSER:"
|
config:
|
||||||
pass: "_env:LDAPPASS:"
|
client-id: "_env:AZURECLIENTID:00000000-0000-0000-0000-000000000000"
|
||||||
baseDN: "_env:LDAPBASE:"
|
client-secret: "_env:AZURECLIENTSECRET:''"
|
||||||
scope: "_env:LDAPSCOPE:WholeSubtree"
|
tenant-id: "_env:AZURETENANTID:00000000-0000-0000-0000-000000000000"
|
||||||
timeout: "_env:LDAPTIMEOUT:5"
|
scopes: "_env:AZURESCOPES:[ID,Profile]"
|
||||||
search-timeout: "_env:LDAPSEARCHTIME:5"
|
# protocol: "ldap"
|
||||||
pool:
|
# config:
|
||||||
stripes: "_env:LDAPSTRIPES:1"
|
# host: "_env:LDAPHOST:"
|
||||||
timeout: "_env:LDAPTIMEOUT:20"
|
# tls: "_env:LDAPTLS:"
|
||||||
limit: "_env:LDAPLIMIT:10"
|
# 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:
|
lms-direct:
|
||||||
upload-header: "_env:LMSUPLOADHEADER:true"
|
upload-header: "_env:LMSUPLOADHEADER:true"
|
||||||
|
|||||||
@ -301,7 +301,7 @@ export class ExamCorrect {
|
|||||||
users: [user],
|
users: [user],
|
||||||
status: STATUS.LOADING,
|
status: STATUS.LOADING,
|
||||||
};
|
};
|
||||||
if (results && results != {}) rowInfo.results = results;
|
if (results && Object.keys(results).length > 0) rowInfo.results = results;
|
||||||
if (result !== undefined) rowInfo.result = result;
|
if (result !== undefined) rowInfo.result = result;
|
||||||
this._addRow(rowInfo);
|
this._addRow(rowInfo);
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# SPDX-FileCopyrightText: 2022-25 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>,Steffen Jost <s.jost@fraport.de>
|
# SPDX-FileCopyrightText: 2022-2025 Sarah Vaupel <sarah.vaupel@uniworx.de>,Gregor Kleen <gregor.kleen@ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>,Steffen Jost <s.jost@fraport.de>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -150,6 +150,8 @@ InterfaceName: Schnittstelle
|
|||||||
InterfaceLastSynch: Zuletzt
|
InterfaceLastSynch: Zuletzt
|
||||||
InterfaceSubtype: Betreffend
|
InterfaceSubtype: Betreffend
|
||||||
InterfaceWrite: Schreibend
|
InterfaceWrite: Schreibend
|
||||||
|
|
||||||
|
AdminUserPassword: Passwort
|
||||||
InterfaceSuccess: Rückmeldung
|
InterfaceSuccess: Rückmeldung
|
||||||
InterfaceInfo: Nachricht
|
InterfaceInfo: Nachricht
|
||||||
InterfaceFreshness: Maximale Zugriffsfrist
|
InterfaceFreshness: Maximale Zugriffsfrist
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# SPDX-FileCopyrightText: 2022-25 Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>,Steffen Jost <s.jost@fraport.de>
|
# SPDX-FileCopyrightText: 2022-2025 Sarah Vaupel <sarah.vaupel@uniworx.de>,Winnie Ros <winnie.ros@campus.lmu.de>,Steffen Jost <s.jost@fraport.de>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -150,6 +150,8 @@ InterfaceName: Interface
|
|||||||
InterfaceLastSynch: Last
|
InterfaceLastSynch: Last
|
||||||
InterfaceSubtype: Affecting
|
InterfaceSubtype: Affecting
|
||||||
InterfaceWrite: Write
|
InterfaceWrite: Write
|
||||||
|
|
||||||
|
AdminUserPassword: Password
|
||||||
InterfaceSuccess: Returned
|
InterfaceSuccess: Returned
|
||||||
InterfaceInfo: Message
|
InterfaceInfo: Message
|
||||||
InterfaceFreshness: Maximum usage period
|
InterfaceFreshness: Maximum usage period
|
||||||
|
|||||||
@ -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
|
# 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.
|
UnauthorizedCourseTutor: Sie sind nicht Ausbilder:in für diese Kursart.
|
||||||
UnauthorizedTutor: Sie sind nicht Ausbilder:in.
|
UnauthorizedTutor: Sie sind nicht Ausbilder:in.
|
||||||
UnauthorizedTutorialRegisterGroup: Sie sind bereits in einem Kurs mit derselben Registrierungs-Gruppe eingetragen.
|
UnauthorizedTutorialRegisterGroup: Sie sind bereits in einem Kurs mit derselben Registrierungs-Gruppe eingetragen.
|
||||||
UnauthorizedLDAP: Angegebener Nutzer/Angegebene Nutzerin meldet sich nicht mit Fraport Login an.
|
UnauthorizedExternal: Angegebene:r Benuzter:in meldet sich nicht über einen aktuell unterstützten externen Login an.
|
||||||
UnauthorizedPWHash: Angegebener Nutzer/Angegebene Nutzerin meldet sich nicht mit FRADrive-Kennung an.
|
UnauthorizedInternal: Angegebene:r Benutzer:in meldet sich nicht mit FRADrive-Kennung an.
|
||||||
UnauthorizedExternalExamListNotEmpty: Liste von externen Prüfungen ist nicht leer
|
UnauthorizedExternalExamListNotEmpty: Liste von externen Prüfungen ist nicht leer
|
||||||
UnauthorizedExternalExamLecturer: Sie sind nicht als Prüfer:in für diese externe Prüfung eingetragen
|
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
|
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
|
PWHashLoginTitle: Spezieller Funktionsnutzer Login
|
||||||
PWHashLoginNote: Verwenden Sie dieses Formular nur, wenn Sie explizit dazu aufgefordert wurden. Alle anderen sollten das andere Login Formular verwenden!
|
PWHashLoginNote: Verwenden Sie dieses Formular nur, wenn Sie explizit dazu aufgefordert wurden. Alle anderen sollten das andere Login Formular verwenden!
|
||||||
DummyLoginTitle: Development-Login
|
DummyLoginTitle: Development-Login
|
||||||
InternalLdapError: Interner Fehler beim Fraport Büko-Login
|
InternalLoginError: Interner Fehler beim Login
|
||||||
CampusUserInvalidIdent: Konnte anhand des Fraport Büko-Logins keine eindeutige Identifikation ermitteln
|
DecodeUserInvalidIdent: Konnte anhand des Fraport Büko-Logins keine eindeutige Identifikation ermitteln
|
||||||
CampusUserInvalidEmail: Konnte anhand des Fraport Büko-Logins keine E-Mail-Addresse ermitteln
|
DecodeUserInvalidEmail: Konnte anhand des Fraport Büko-Logins keine E-Mail-Addresse ermitteln
|
||||||
CampusUserInvalidDisplayName: Konnte anhand des Fraport Büko-Logins keinen vollen Namen ermitteln
|
DecodeUserInvalidDisplayName: Konnte anhand des Fraport Büko-Logins keinen vollen Namen ermitteln
|
||||||
CampusUserInvalidGivenName: Konnte anhand des Fraport Büko-Logins keinen Vornamen ermitteln
|
DecodeUserInvalidGivenName: Konnte anhand des Fraport Büko-Logins keinen Vornamen ermitteln
|
||||||
CampusUserInvalidSurname: Konnte anhand des Fraport Büko-Logins keinen Nachname ermitteln
|
DecodeUserInvalidSurname: Konnte anhand des Fraport Büko-Logins keinen Nachname ermitteln
|
||||||
CampusUserInvalidTitle: Konnte anhand des Fraport Büko-Logins keinen akademischen Titel ermitteln
|
DecodeUserInvalidTitle: 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
|
DecodeUserInvalidFeaturesOfStudy 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
|
DecodeUserInvalidAssociatedSchools parseErr@Text: Konnte anhand des Fraport Büko-Logins keine Bereiche ermitteln
|
||||||
InvalidCredentialsADNoSuchObject: Benutzereintrag existiert nicht
|
InvalidCredentialsADNoSuchObject: Benutzereintrag existiert nicht
|
||||||
InvalidCredentialsADLogonFailure: Ungültiges Passwort
|
InvalidCredentialsADLogonFailure: Ungültiges Passwort
|
||||||
InvalidCredentialsADAccountRestriction: Beschränkungen des Fraport Accounts verhindern Login
|
InvalidCredentialsADAccountRestriction: Beschränkungen des Fraport Accounts verhindern Login
|
||||||
@ -139,3 +139,6 @@ FormHoneypotNamePlaceholder: Name
|
|||||||
FormHoneypotComment: Kommentar
|
FormHoneypotComment: Kommentar
|
||||||
FormHoneypotCommentPlaceholder: Kommentar
|
FormHoneypotCommentPlaceholder: Kommentar
|
||||||
FormHoneypotFilled: Bitte füllen Sie keines der verstecken Felder aus
|
FormHoneypotFilled: Bitte füllen Sie keines der verstecken Felder aus
|
||||||
|
|
||||||
|
Logout: Abmeldung
|
||||||
|
SingleSignOut: Abmeldung bei Azure
|
||||||
|
|||||||
@ -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
|
# 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.
|
UnauthorizedCourseTutor: You are no instructor for this course.
|
||||||
UnauthorizedTutor: You are no instructor.
|
UnauthorizedTutor: You are no instructor.
|
||||||
UnauthorizedTutorialRegisterGroup: You are already registered for a course with the same registration group.
|
UnauthorizedTutorialRegisterGroup: You are already registered for a course with the same registration group.
|
||||||
UnauthorizedLDAP: Specified user does not log in with their Fraport password.
|
UnauthorizedExternal: Specified user does not log in with any currently supported external login.
|
||||||
UnauthorizedPWHash: Specified user does not log in with an FRADrive-account.
|
UnauthorizedInternal: Specified user does not log in with a FRADrive-account.
|
||||||
UnauthorizedExternalExamListNotEmpty: List of external exams is not empty
|
UnauthorizedExternalExamListNotEmpty: List of external exams is not empty
|
||||||
UnauthorizedExternalExamLecturer: You are not an associated person for this external exam
|
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
|
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
|
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.
|
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
|
DummyLoginTitle: Development login
|
||||||
InternalLdapError: Internal error during Fraport Büko login
|
InternalLoginError: Internal error during login
|
||||||
CampusUserInvalidIdent: Could not determine unique identification during Fraport Büko login
|
DecodeUserInvalidIdent: Could not determine unique identification during Fraport Büko login
|
||||||
CampusUserInvalidEmail: Could not determine email address during Fraport Büko login
|
DecodeUserInvalidEmail: Could not determine email address during Fraport Büko login
|
||||||
CampusUserInvalidDisplayName: Could not determine display name during Fraport Büko login
|
DecodeUserInvalidDisplayName: Could not determine display name during Fraport Büko login
|
||||||
CampusUserInvalidGivenName: Could not determine given name during Fraport Büko login
|
DecodeUserInvalidGivenName: Could not determine given name during Fraport Büko login
|
||||||
CampusUserInvalidSurname: Could not determine surname during Fraport Büko login
|
DecodeUserInvalidSurname: Could not determine surname during Fraport Büko login
|
||||||
CampusUserInvalidTitle: Could not determine title during Fraport Büko login
|
DecodeUserInvalidTitle: Could not determine title during Fraport Büko login
|
||||||
CampusUserInvalidFeaturesOfStudy parseErr: Could not determine features of study during Fraport Büko login
|
DecodeUserInvalidFeaturesOfStudy parseErr: Could not determine features of study during Fraport Büko login
|
||||||
CampusUserInvalidAssociatedSchools parseErr: Could not determine associated departments during Fraport Büko login
|
DecodeUserInvalidAssociatedSchools parseErr: Could not determine associated departments during Fraport Büko login
|
||||||
InvalidCredentialsADNoSuchObject: User entry does not exist
|
InvalidCredentialsADNoSuchObject: User entry does not exist
|
||||||
InvalidCredentialsADLogonFailure: Invalid password
|
InvalidCredentialsADLogonFailure: Invalid password
|
||||||
InvalidCredentialsADAccountRestriction: Restrictions on your Fraport account prevent a login
|
InvalidCredentialsADAccountRestriction: Restrictions on your Fraport account prevent a login
|
||||||
@ -140,3 +140,6 @@ FormHoneypotNamePlaceholder !ident-ok: Name
|
|||||||
FormHoneypotComment: Comment
|
FormHoneypotComment: Comment
|
||||||
FormHoneypotCommentPlaceholder: Comment
|
FormHoneypotCommentPlaceholder: Comment
|
||||||
FormHoneypotFilled: Please do not fill in any of the hidden fields
|
FormHoneypotFilled: Please do not fill in any of the hidden fields
|
||||||
|
|
||||||
|
Logout: Logout
|
||||||
|
SingleSignOut: Azure logout
|
||||||
|
|||||||
@ -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
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -75,11 +75,10 @@ NotPassed: Nicht bestanden
|
|||||||
|
|
||||||
#userAuthModeUpdate.hs + templates
|
#userAuthModeUpdate.hs + templates
|
||||||
MailSubjectUserAuthModeUpdate: Ihr FRADrive-Login
|
MailSubjectUserAuthModeUpdate: Ihr FRADrive-Login
|
||||||
UserAuthModeChangedToLDAP: Sie können sich nun mit Ihrer Fraport AG Kennung (Büko) in FRADrive einloggen.
|
UserAuthPasswordEnabled: Sie können sich nun mit einer FRADrive-internen Kennung einloggen.
|
||||||
UserAuthModeChangedToPWHash: 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.
|
||||||
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.
|
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.
|
||||||
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.
|
||||||
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
|
MailFradrive !ident-ok: FRADrive
|
||||||
MailBodyFradrive: ist die Führerscheinverwaltungsapp der Fraport AG.
|
MailBodyFradrive: ist die Führerscheinverwaltungsapp der Fraport AG.
|
||||||
|
|
||||||
|
|||||||
@ -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
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -75,10 +75,9 @@ NotPassed: Failed
|
|||||||
|
|
||||||
#userAuthModeUpdate.hs + templates
|
#userAuthModeUpdate.hs + templates
|
||||||
MailSubjectUserAuthModeUpdate: Your FRADrive login
|
MailSubjectUserAuthModeUpdate: Your FRADrive login
|
||||||
UserAuthModeChangedToLDAP: You can now log in to FRADrive using your Fraport AG account (Büko)
|
UserAuthPasswordEnabled: You can now log in using your FRADrive-internal account credentials.
|
||||||
UserAuthModeChangedToPWHash: You can now log in using your FRADrive-internal account
|
UserAuthPasswordDisabled: You can no longer log in using your FRADrive-internal account credentials.
|
||||||
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.
|
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.
|
||||||
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.
|
|
||||||
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.
|
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
|
MailFradrive: FRADrive
|
||||||
MailBodyFradrive: is the apron driver's licence management app of Fraport AG.
|
MailBodyFradrive: is the apron driver's licence management app of Fraport AG.
|
||||||
|
|||||||
@ -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
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -45,8 +45,8 @@ AuthTagUserSubmissions: Abgaben erfolgen durch Kursartteilnehmer:innen
|
|||||||
AuthTagCorrectorSubmissions: Abgaben erfolgen durch Korrektor:innen
|
AuthTagCorrectorSubmissions: Abgaben erfolgen durch Korrektor:innen
|
||||||
AuthTagCorrectionAnonymous: Korrektur ist anonymisiert
|
AuthTagCorrectionAnonymous: Korrektur ist anonymisiert
|
||||||
AuthTagSelf: Nutzer:in greift nur auf eigene Daten zu
|
AuthTagSelf: Nutzer:in greift nur auf eigene Daten zu
|
||||||
AuthTagIsLDAP: Nutzer:in meldet sich mit Fraport AG Kennung an
|
AuthTagIsExternal: Nutzer:in meldet sich mit extern verwalteten Logindaten an
|
||||||
AuthTagIsPWHash: Nutzer:in meldet sich mit FRADrive spezifischer Kennung an
|
AuthTagIsInternal: Nutzer:in meldet sich mit FRADrive-internen Logindaten an
|
||||||
AuthTagAuthentication: Nutzer:in ist angemeldet, falls erforderlich
|
AuthTagAuthentication: Nutzer:in ist angemeldet, falls erforderlich
|
||||||
AuthTagRead: Zugriff ist nur lesend
|
AuthTagRead: Zugriff ist nur lesend
|
||||||
AuthTagWrite: Zugriff ist i.A. schreibend
|
AuthTagWrite: Zugriff ist i.A. schreibend
|
||||||
|
|||||||
@ -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
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -45,8 +45,8 @@ AuthTagUserSubmissions: Submissions are made by course category participants
|
|||||||
AuthTagCorrectorSubmissions: Submissions are registered by correctors
|
AuthTagCorrectorSubmissions: Submissions are registered by correctors
|
||||||
AuthTagCorrectionAnonymous: Correction is anonymised
|
AuthTagCorrectionAnonymous: Correction is anonymised
|
||||||
AuthTagSelf: User is only accessing their only data
|
AuthTagSelf: User is only accessing their only data
|
||||||
AuthTagIsLDAP: User logs in using their Fraport AG account
|
AuthTagIsExternal: User logs in using externally managed credentials
|
||||||
AuthTagIsPWHash: User logs in using their FRADrive specific account
|
AuthTagIsInternal: User logs in using FRADrive-internal credentials
|
||||||
AuthTagAuthentication: User is authenticated
|
AuthTagAuthentication: User is authenticated
|
||||||
AuthTagRead: Access is read only
|
AuthTagRead: Access is read only
|
||||||
AuthTagWrite: Access might write
|
AuthTagWrite: Access might write
|
||||||
|
|||||||
@ -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
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -40,8 +40,8 @@ UsersCourseSchool: Bereich
|
|||||||
ActionNoUsersSelected: Keine Benutzer:innen ausgewählt
|
ActionNoUsersSelected: Keine Benutzer:innen ausgewählt
|
||||||
SynchroniseAvsUserQueued n@Int: AVS-Synchronisation von #{n} #{pluralDE n "Benutzer:in" "Benutzer:innen"} zwingend angestoßen, die Ausführung wird mehrere Minuten benötigen!
|
SynchroniseAvsUserQueued n@Int: AVS-Synchronisation von #{n} #{pluralDE n "Benutzer:in" "Benutzer:innen"} zwingend angestoßen, die Ausführung wird mehrere Minuten benötigen!
|
||||||
SynchroniseAvsAllUsersQueued n@Int64: AVS-Synchronisation von allen #{n} #{pluralDE n "Benutzer:in" "Benutzer:innen"} angestoßen, welche heute noch nicht synchronisiert wurden, die Ausführung wird eine Weile brauchen!
|
SynchroniseAvsAllUsersQueued n@Int64: AVS-Synchronisation von allen #{n} #{pluralDE n "Benutzer:in" "Benutzer:innen"} angestoßen, welche heute noch nicht synchronisiert wurden, die Ausführung wird eine Weile brauchen!
|
||||||
SynchroniseLdapUserQueued n@Int: LDAP-Synchronisation von #{n} #{pluralDE n "Benutzer:in" "Benutzer:innen"} angestoßen, die Ausführung wird mehrere Minuten benötigen!
|
SynchroniseUserdbUserQueued n@Int: Benutzerdatenbank-Synchronisation von #{n} #{pluralDE n "Benutzer:in" "Benutzer:innen"} angestoßen, die Ausführung wird mehrere Minuten benötigen!
|
||||||
SynchroniseLdapAllUsersQueued: LDAP-Synchronisation von allen Benutzer:innen angestoßen, die Ausführung kann eine Weile brauchen!
|
SynchroniseUserdbAllUsersQueued: Benutzerdatenbank-Synchronisation von allen Benutzer:innen angestoßen, die Ausführung kann eine Weile brauchen!
|
||||||
UserListTitle: Komprehensive Benutzerliste
|
UserListTitle: Komprehensive Benutzerliste
|
||||||
UserRecipientsTitle name@Text: Benachrichtigungsempfänger für #{name}
|
UserRecipientsTitle name@Text: Benachrichtigungsempfänger für #{name}
|
||||||
AccessRightsSaved: Berechtigungen erfolgreich verändert
|
AccessRightsSaved: Berechtigungen erfolgreich verändert
|
||||||
@ -51,6 +51,7 @@ AuthLDAPInvalidLookup: Bestehender Nutzer/Bestehende Nutzerin konnte nicht einde
|
|||||||
AuthLDAPAlreadyConfigured: Nutzer:in meldet sich bereits per Fraport AG Kennung in FRADrive an
|
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
|
AuthLDAPConfigured: Nutzer:in meldet sich nun per Fraport AG Kennung in FRADrive an
|
||||||
AuthLDAP !ident-ok: Fraport AG Kennung
|
AuthLDAP !ident-ok: Fraport AG Kennung
|
||||||
|
AuthAzure: Azure-Account
|
||||||
AuthNoLogin: Kein Login erlaubt.
|
AuthNoLogin: Kein Login erlaubt.
|
||||||
PasswordResetQueued: Link zum Passwort-Zurücksetzen versandt
|
PasswordResetQueued: Link zum Passwort-Zurücksetzen versandt
|
||||||
UserAssimilateUser: Benutzer:in
|
UserAssimilateUser: Benutzer:in
|
||||||
@ -105,9 +106,6 @@ AllUsersLdapSync: Alle LDAP-Synchronisieren
|
|||||||
AllUsersAvsSync: Alle AVS-Synchronisieren
|
AllUsersAvsSync: Alle AVS-Synchronisieren
|
||||||
ThisUserLdapSync: LDAP Synchronisation
|
ThisUserLdapSync: LDAP Synchronisation
|
||||||
ThisUserAvsSync: AVS Synchronisation
|
ThisUserAvsSync: AVS Synchronisation
|
||||||
AuthKindLDAP: Fraport AG Kennung
|
|
||||||
AuthKindPWHash: FRADrive Kennung
|
|
||||||
AuthKindNoLogin: Kein Login möglich
|
|
||||||
Name !ident-ok: Name
|
Name !ident-ok: Name
|
||||||
UsersChangeSupervisorsSuccess usr@Int spr@Int: #{tshow spr} Ansprechpartner für #{tshow usr} Benutzer gesetzt.
|
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!
|
UsersChangeSupervisorsWarning usr@Int spr@Int bad@Int: Nur _{MsgUsersChangeSupervisorsSuccess usr spr} #{tshow bad} Ansprechpartner #{pluralDE bad "wurde" "wurden"} nicht gefunden!
|
||||||
@ -119,3 +117,9 @@ UserSupervisorReason: Begründung Ansprechpartner
|
|||||||
UserSupervisorReasonTooltip: Optionale Notiz für besondere Fälle. Kann ggf. autmatische Entfernung bei AVS Firmenwechsel verhindern.
|
UserSupervisorReasonTooltip: Optionale Notiz für besondere Fälle. Kann ggf. autmatische Entfernung bei AVS Firmenwechsel verhindern.
|
||||||
UserSupervisorCompany: Ansprechpartner wegen Firma
|
UserSupervisorCompany: Ansprechpartner wegen Firma
|
||||||
AdminUserAllNotifications: Alle Benachrichtigungen and diesen Benutzer
|
AdminUserAllNotifications: Alle Benachrichtigungen and diesen Benutzer
|
||||||
|
AdminUserAuthentication: Authentification
|
||||||
|
AdminUserAuthLastSync: Zuletzt synchronisiert
|
||||||
|
AuthKindLDAP: Fraport-AG-Kennung (LDAP)
|
||||||
|
AuthKindAzure: Azure-Login
|
||||||
|
AuthKindPWHash: Interne FRADrive-Kennung
|
||||||
|
AuthKindNoLogin: Kein Login möglich
|
||||||
|
|||||||
@ -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
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -40,8 +40,8 @@ UsersCourseSchool: Department
|
|||||||
ActionNoUsersSelected: No users selected
|
ActionNoUsersSelected: No users selected
|
||||||
SynchroniseAvsUserQueued n: Triggered forced AVS synchronisation of #{n} #{pluralEN n "user" "users"}, which may take several minutes to complete.
|
SynchroniseAvsUserQueued n: Triggered forced AVS synchronisation of #{n} #{pluralEN n "user" "users"}, which may take several minutes to complete.
|
||||||
SynchroniseAvsAllUsersQueued n: Triggered AVS synchronisation of all #{n} #{pluralEN n "user" "users"} that were not already synchronised today, which may take quite a while to complete.
|
SynchroniseAvsAllUsersQueued n: Triggered AVS synchronisation of all #{n} #{pluralEN n "user" "users"} that were not already synchronised today, which may take quite a while to complete.
|
||||||
SynchroniseLdapUserQueued n: Triggered LDAP synchronisation of #{n} #{pluralEN n "user" "users"}, which may take several minutes to complete.
|
SynchroniseUserdbUserQueued n: Triggered user database synchronisation of #{n} #{pluralEN n "user" "users"}, which may take several minutes to complete.
|
||||||
SynchroniseLdapAllUsersQueued: Triggered LDAP synchronisation of all users, which may take quite a while to complete.
|
SynchroniseUserdbAllUsersQueued: Triggered user database synchronisation of all users, which may take quite a while to complete.
|
||||||
UserListTitle: Comprehensive list of users
|
UserListTitle: Comprehensive list of users
|
||||||
UserRecipientsTitle name: Notificationrecipients for #{name}
|
UserRecipientsTitle name: Notificationrecipients for #{name}
|
||||||
AccessRightsSaved: Successfully updated permissions
|
AccessRightsSaved: Successfully updated permissions
|
||||||
@ -51,6 +51,7 @@ AuthLDAPInvalidLookup: Existing user could not be uniquely matched with a LDAP e
|
|||||||
AuthLDAPAlreadyConfigured: User already logs in using their Fraport AG account
|
AuthLDAPAlreadyConfigured: User already logs in using their Fraport AG account
|
||||||
AuthLDAPConfigured: User now logs in using their Fraport AG account
|
AuthLDAPConfigured: User now logs in using their Fraport AG account
|
||||||
AuthLDAP: Fraport AG account
|
AuthLDAP: Fraport AG account
|
||||||
|
AuthAzure: Azure account
|
||||||
AuthNoLogin: No login allowed.
|
AuthNoLogin: No login allowed.
|
||||||
PasswordResetQueued: Sent link to reset password
|
PasswordResetQueued: Sent link to reset password
|
||||||
UserAssimilateUser: User
|
UserAssimilateUser: User
|
||||||
@ -105,9 +106,6 @@ AllUsersLdapSync: Synchronise all with LDAP
|
|||||||
AllUsersAvsSync: Synchronise all with AVS
|
AllUsersAvsSync: Synchronise all with AVS
|
||||||
ThisUserLdapSync: Synchronise user with LDAP
|
ThisUserLdapSync: Synchronise user with LDAP
|
||||||
ThisUserAvsSync: Synchronise user with AVS
|
ThisUserAvsSync: Synchronise user with AVS
|
||||||
AuthKindLDAP: Fraport AG account
|
|
||||||
AuthKindPWHash: FRADrive account
|
|
||||||
AuthKindNoLogin: No login
|
|
||||||
Name: Name
|
Name: Name
|
||||||
UsersChangeSupervisorsSuccess usr spr: #{pluralENsN spr "supervisor"} for #{pluralENsN usr "user"} set.
|
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!
|
UsersChangeSupervisorsWarning usr spr bad: Only _{MsgUsersChangeSupervisorsSuccess usr spr} #{pluralENsN bad "supervisors"} could not be identified!
|
||||||
@ -119,3 +117,10 @@ UserSupervisorReason: Reason for supervision
|
|||||||
UserSupervisorReasonTooltip: Optional note for special cases. In some case this may prevent automatic removel upon AVS user company changes.
|
UserSupervisorReasonTooltip: Optional note for special cases. In some case this may prevent automatic removel upon AVS user company changes.
|
||||||
UserSupervisorCompany: Supervisor for company
|
UserSupervisorCompany: Supervisor for company
|
||||||
AdminUserAllNotifications: All notification sent to this user
|
AdminUserAllNotifications: All notification sent to this user
|
||||||
|
|
||||||
|
AdminUserAuthentication: Authentifizierung
|
||||||
|
AdminUserAuthLastSync: Last synchronised
|
||||||
|
AuthKindLDAP: Fraport AG account (LDAP)
|
||||||
|
AuthKindAzure: Azure account
|
||||||
|
AuthKindPWHash: Internal FRADrive login
|
||||||
|
AuthKindNoLogin: No login
|
||||||
@ -78,6 +78,7 @@ BreadcrumbAdminFeaturesHeading: Studiengänge
|
|||||||
BreadcrumbAdminTest: Admin-Demo
|
BreadcrumbAdminTest: Admin-Demo
|
||||||
BreadcrumbAdminErrMsg: Fehlermeldung entschlüsseln
|
BreadcrumbAdminErrMsg: Fehlermeldung entschlüsseln
|
||||||
BreadcrumbAdminTokens: Tokens ausstellen
|
BreadcrumbAdminTokens: Tokens ausstellen
|
||||||
|
BreadcrumbAdminLdap !ident-ok: LDAP
|
||||||
BreadcrumbSchoolList: Bereiche
|
BreadcrumbSchoolList: Bereiche
|
||||||
BreadcrumbSchoolNew: Neuen Bereich anlegen
|
BreadcrumbSchoolNew: Neuen Bereich anlegen
|
||||||
BreadcrumbExamOfficeExams: Prüfungen
|
BreadcrumbExamOfficeExams: Prüfungen
|
||||||
|
|||||||
@ -78,6 +78,7 @@ BreadcrumbAdminFeaturesHeading: Features of study
|
|||||||
BreadcrumbAdminTest: Admin-demo
|
BreadcrumbAdminTest: Admin-demo
|
||||||
BreadcrumbAdminErrMsg: Decrypt error message
|
BreadcrumbAdminErrMsg: Decrypt error message
|
||||||
BreadcrumbAdminTokens: Issue tokens
|
BreadcrumbAdminTokens: Issue tokens
|
||||||
|
BreadcrumbAdminLdap: LDAP
|
||||||
BreadcrumbSchoolList: Departments
|
BreadcrumbSchoolList: Departments
|
||||||
BreadcrumbSchoolNew: Create new department
|
BreadcrumbSchoolNew: Create new department
|
||||||
BreadcrumbExamOfficeExams: Exams
|
BreadcrumbExamOfficeExams: Exams
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# SPDX-FileCopyrightText: 2022-25 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@cip.ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>,Steffen Jost <s.jost@fraport.de>
|
# SPDX-FileCopyrightText: 2022-25-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, David Mosbach <david.mosbach@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@cip.ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>,Steffen Jost <s.jost@fraport.de>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -25,6 +25,7 @@ MenuInstance: Instanz-Identifikation
|
|||||||
MenuHealth: Instanz-Zustand
|
MenuHealth: Instanz-Zustand
|
||||||
MenuHealthInterface: Schnittstellen Zustand
|
MenuHealthInterface: Schnittstellen Zustand
|
||||||
MenuHelp: Hilfe
|
MenuHelp: Hilfe
|
||||||
|
MenuAccount: Konto
|
||||||
MenuProfile: Anpassen
|
MenuProfile: Anpassen
|
||||||
MenuLogin !ident-ok: Login
|
MenuLogin !ident-ok: Login
|
||||||
MenuLogout !ident-ok: Logout
|
MenuLogout !ident-ok: Logout
|
||||||
@ -148,7 +149,7 @@ MenuSap: SAP Schnittstelle
|
|||||||
|
|
||||||
MenuAvs: AVS Schnittstelle
|
MenuAvs: AVS Schnittstelle
|
||||||
MenuAvsSynchError: AVS Problemübersicht
|
MenuAvsSynchError: AVS Problemübersicht
|
||||||
MenuLdap: LDAP Schnittstelle
|
MenuExternalUser: Externe Benutzer
|
||||||
MenuApc: Druck
|
MenuApc: Druck
|
||||||
MenuPrintSend: Manueller Briefversand
|
MenuPrintSend: Manueller Briefversand
|
||||||
MenuPrintDownload: Brief herunterladen
|
MenuPrintDownload: Brief herunterladen
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# SPDX-FileCopyrightText: 2022-25 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>,Steffen Jost <s.jost@fraport.de>
|
# SPDX-FileCopyrightText: 2022-25-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, David Mosbach <david.mosbach@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>,Steffen Jost <s.jost@fraport.de>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -25,6 +25,7 @@ MenuInstance: Instance identification
|
|||||||
MenuHealth: Instance health
|
MenuHealth: Instance health
|
||||||
MenuHealthInterface: Interface health
|
MenuHealthInterface: Interface health
|
||||||
MenuHelp: Support
|
MenuHelp: Support
|
||||||
|
MenuAccount: Account
|
||||||
MenuProfile: Settings
|
MenuProfile: Settings
|
||||||
MenuLogin: Login
|
MenuLogin: Login
|
||||||
MenuLogout: Logout
|
MenuLogout: Logout
|
||||||
@ -148,7 +149,7 @@ MenuSap: SAP Interface
|
|||||||
|
|
||||||
MenuAvs: AVS Interface
|
MenuAvs: AVS Interface
|
||||||
MenuAvsSynchError: AVS Problem Overview
|
MenuAvsSynchError: AVS Problem Overview
|
||||||
MenuLdap: LDAP Interface
|
MenuExternalUser: External users
|
||||||
MenuApc: Print
|
MenuApc: Print
|
||||||
MenuPrintSend: Send Letter
|
MenuPrintSend: Send Letter
|
||||||
MenuPrintDownload: Download Letter
|
MenuPrintDownload: Download Letter
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# SPDX-FileCopyrightText: 2023-25 Steffen Jost <jost@tcs.ifi.lmu.de>,Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>,Steffen Jost <s.jost@fraport.de>
|
# SPDX-FileCopyrightText: 2023-25-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Steffen Jost <jost@tcs.ifi.lmu.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>, Sarah Vaupel <sarah.vaupel@ifi.lmu.de>, Winnie Ros <winnie.ros@campus.lmu.de>,Steffen Jost <s.jost@fraport.de>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -165,3 +165,5 @@ SheetTypeNormal !ident-ok: Normal
|
|||||||
SheetTypeBonus !ident-ok: Bonus
|
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!
|
||||||
@ -1,4 +1,4 @@
|
|||||||
# SPDX-FileCopyrightText: 2023-25 Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>
|
# SPDX-FileCopyrightText: 2023-25-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Sarah Vaupel <sarah.vaupel@ifi.lmu.de>, Winnie Ros <winnie.ros@campus.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -165,3 +165,5 @@ SheetTypeNormal: Normal
|
|||||||
SheetTypeBonus: Bonus
|
SheetTypeBonus: Bonus
|
||||||
|
|
||||||
InvalidFormAction: No action taken due to invalid form data
|
InvalidFormAction: No action taken due to invalid form data
|
||||||
|
|
||||||
|
InvalidUuid: Invalid UUID!
|
||||||
@ -42,11 +42,11 @@ Qualification
|
|||||||
-- - PinReset==1 mit bestehendem Passwort kann problemlos erneut gesendet werden
|
-- - PinReset==1 mit bestehendem Passwort kann problemlos erneut gesendet werden
|
||||||
-- - Flag "interner Mitarbeiter" wird von Know-How ignoriert / nicht ausgewertet (legacy)
|
-- - 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
|
-- -- 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
|
-- -- qualification QualificationId OnDeleteCascade OnUpdateCascade -- AND: not unique, ie. qualification can have multiple required preconditions
|
||||||
-- required [QualificationId] -- OR : alternatives, any one will suffice -- we don't want array, since we have recursive CTEs
|
-- -- required [QualificationId] -- OR : alternatives, any one will suffice -- we don't want array, since we have recursive CTEs
|
||||||
-- continuous Bool -- expiring precondition blocks qualification
|
---- continuous Bool -- expiring precondition blocks qualification
|
||||||
-- deriving Generic Show
|
-- -- deriving Generic Show
|
||||||
|
|
||||||
-- Maybe an alternative for online qualification validity checking, transitivity through recursive CTEs? (already available in our version)
|
-- Maybe an alternative for online qualification validity checking, transitivity through recursive CTEs? (already available in our version)
|
||||||
QualificationRequirement
|
QualificationRequirement
|
||||||
@ -57,7 +57,7 @@ QualificationRequirement
|
|||||||
UniqueQualificationRequirement qualification requirement
|
UniqueQualificationRequirement qualification requirement
|
||||||
deriving Generic Show
|
deriving Generic Show
|
||||||
|
|
||||||
-- TODO: connect Qualification with Exams!
|
-- TODO: connect Qualifications with Exams!?
|
||||||
|
|
||||||
QualificationEdit
|
QualificationEdit
|
||||||
user UserId
|
user UserId
|
||||||
@ -84,6 +84,7 @@ QualificationUserBlock
|
|||||||
from UTCTime
|
from UTCTime
|
||||||
reason Text
|
reason Text
|
||||||
blocker UserId Maybe
|
blocker UserId Maybe
|
||||||
|
-- precondition Bool default=false -- if true, this was due to a precondition
|
||||||
deriving Eq Ord Read Show Generic
|
deriving Eq Ord Read Show Generic
|
||||||
|
|
||||||
-- LMS Interface Tables, need regular processing by background jobs, per QualificationId:
|
-- LMS Interface Tables, need regular processing by background jobs, per QualificationId:
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
-- SPDX-FileCopyrightText: 2022-24 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>
|
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>, Sarah Vaupel <sarah.vaupel@ifi.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>, Steffen Jost <s.jost@fraport.de>
|
||||||
--
|
--
|
||||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
-- 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.
|
-- 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
|
-- 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
|
-- Indendent lower-case lines describe the SQL-columns of the table with name, type and options
|
||||||
@ -11,21 +11,22 @@
|
|||||||
-- Indendent upper-case lines usually impose Uniqueness constraints for rows by some columns.
|
-- 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
|
-- 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.
|
User json -- Each Uni2work user has a corresponding row in this table; created upon first login.
|
||||||
surname UserSurname -- Display user names always through 'nameWidget displayName surname'
|
|
||||||
displayName UserDisplayName
|
|
||||||
displayEmail UserEmail -- Case-insensitive eMail address, used for sending; leave empty for using auto-update CompanyEmail via UserCompany
|
|
||||||
email UserEmail -- Case-insensitive eMail address, used for identification and fallback for sending. Defaults to "AVSNO:dddddddd" if unknown
|
|
||||||
ident UserIdent -- Case-insensitive user-identifier. Defaults to "AVSID:dddddddd" if unknown
|
ident UserIdent -- Case-insensitive user-identifier. Defaults to "AVSID:dddddddd" if unknown
|
||||||
authentication AuthenticationMode -- 'AuthLDAP' or ('AuthPWHash'+password-hash)
|
|
||||||
lastAuthentication UTCTime Maybe -- last login date
|
|
||||||
created UTCTime default=now()
|
created UTCTime default=now()
|
||||||
|
passwordHash Text Maybe -- If specified, allows the user to login with credentials independently of external authentication
|
||||||
|
lastAuthentication UTCTime Maybe -- When did the user last authenticate?
|
||||||
lastLdapSynchronisation UTCTime Maybe
|
lastLdapSynchronisation UTCTime Maybe
|
||||||
ldapPrimaryKey UserEduPersonPrincipalName Maybe -- Fraport Personnel Number or Email-Prefix for @fraport.de work here
|
ldapPrimaryKey UserEduPersonPrincipalName Maybe -- Fraport Personnel Number or Email-Prefix for @fraport.de work here
|
||||||
tokensIssuedAfter UTCTime Maybe -- do not accept bearer tokens issued before this time (accept all tokens if null)
|
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!
|
matrikelnummer UserMatriculation Maybe -- usually a number; AVS Personalnummer; nicht Fraport Personalnummer!
|
||||||
|
surname UserSurname -- Display user names always through 'nameWidget displayName surname'
|
||||||
firstName Text -- For export in tables, pre-split firstName from displayName
|
firstName Text -- For export in tables, pre-split firstName from displayName
|
||||||
title Text Maybe -- For upcoming name customisation
|
title Text Maybe -- For upcoming name customisation
|
||||||
|
displayName UserDisplayName
|
||||||
|
displayEmail UserEmail -- Case-insensitive eMail address, used for sending; leave empty for using auto-update CompanyEmail via UserCompany
|
||||||
|
email UserEmail -- Case-insensitive eMail address, used for identification and fallback for sending. Defaults to "AVSNO:dddddddd" if unknown
|
||||||
maxFavourites Int default=12 -- max number of non-manual entries in favourites bar (pruned only if below a set importance threshold)
|
maxFavourites Int default=12 -- max number of non-manual entries in favourites bar (pruned only if below a set importance threshold)
|
||||||
maxFavouriteTerms Int default=2 -- max number of term-sections in favourites bar
|
maxFavouriteTerms Int default=2 -- max number of term-sections in favourites bar
|
||||||
theme Theme default='ThemeDefault' -- Color-theme of the frontend; user-defined
|
theme Theme default='ThemeDefault' -- Color-theme of the frontend; user-defined
|
||||||
@ -50,11 +51,20 @@ User json -- Each Uni2work user has a corresponding row in this table; create
|
|||||||
prefersPostal Bool default=false -- user prefers letters by post instead of email
|
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
|
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
|
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
|
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
|
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, ...)
|
UserFunction -- Administratively assigned functions (lecturer, admin, evaluation, ...)
|
||||||
user UserId
|
user UserId
|
||||||
school SchoolId
|
school SchoolId
|
||||||
|
|||||||
8181
package-lock.json
generated
8181
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "fradrive",
|
"name": "fradrive",
|
||||||
"version": "27.4.59",
|
"version": "27.4.79",
|
||||||
"description": "",
|
"description": "",
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
|
|||||||
10
package.yaml
10
package.yaml
@ -3,13 +3,14 @@
|
|||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
name: uniworx
|
name: uniworx
|
||||||
version: 27.4.79
|
version: 28.1.1
|
||||||
dependencies:
|
dependencies:
|
||||||
- base
|
- base
|
||||||
- yesod
|
- yesod
|
||||||
- yesod-core
|
- yesod-core
|
||||||
- yesod-persistent
|
- yesod-persistent
|
||||||
- yesod-auth
|
- yesod-auth
|
||||||
|
- yesod-auth-oauth2
|
||||||
- yesod-static
|
- yesod-static
|
||||||
- yesod-form
|
- yesod-form
|
||||||
- yesod-persistent
|
- yesod-persistent
|
||||||
@ -24,7 +25,6 @@ dependencies:
|
|||||||
- template-haskell
|
- template-haskell
|
||||||
- shakespeare
|
- shakespeare
|
||||||
- monad-control
|
- monad-control
|
||||||
- wai-extra
|
|
||||||
- yaml
|
- yaml
|
||||||
- http-conduit
|
- http-conduit
|
||||||
- directory
|
- directory
|
||||||
@ -34,7 +34,6 @@ dependencies:
|
|||||||
- conduit
|
- conduit
|
||||||
- monad-logger
|
- monad-logger
|
||||||
- fast-logger
|
- fast-logger
|
||||||
- wai-logger
|
|
||||||
- foreign-store
|
- foreign-store
|
||||||
- file-embed
|
- file-embed
|
||||||
- unordered-containers
|
- unordered-containers
|
||||||
@ -43,6 +42,10 @@ dependencies:
|
|||||||
- time
|
- time
|
||||||
- case-insensitive
|
- case-insensitive
|
||||||
- wai
|
- wai
|
||||||
|
- wai-cors
|
||||||
|
- wai-extra
|
||||||
|
- wai-logger
|
||||||
|
- wai-middleware-prometheus
|
||||||
- cryptonite
|
- cryptonite
|
||||||
- cryptonite-conduit
|
- cryptonite-conduit
|
||||||
- saltine
|
- saltine
|
||||||
@ -147,7 +150,6 @@ dependencies:
|
|||||||
- cookie
|
- cookie
|
||||||
- prometheus-client
|
- prometheus-client
|
||||||
- prometheus-metrics-ghc
|
- prometheus-metrics-ghc
|
||||||
- wai-middleware-prometheus
|
|
||||||
- extended-reals
|
- extended-reals
|
||||||
- rfc5051
|
- rfc5051
|
||||||
- unidecode
|
- unidecode
|
||||||
|
|||||||
11
release.sh
11
release.sh
@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/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
|
# 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
|
standard-version -a -t t
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "Current branch not supported for release!"
|
if echo $@ | grep -xqe '--dev';
|
||||||
exit 1
|
then
|
||||||
|
standard-version -a -t d
|
||||||
|
else
|
||||||
|
echo "Current branch not supported for release!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
8
routes
8
routes
@ -30,8 +30,8 @@
|
|||||||
-- !capacity -- course this route is associated with has at least one unit of participant capacity
|
-- !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
|
-- !empty -- course this route is associated with has no participants whatsoever
|
||||||
--
|
--
|
||||||
-- !is-ldap -- user has authentication mode set to LDAP
|
-- !is-external -- user can login using external sources
|
||||||
-- !is-pw-hash -- user has authentication mode set to PWHash
|
-- !is-internal -- user can login using internal credentials
|
||||||
--
|
--
|
||||||
-- !materials -- only if course allows all materials to be free (no meaning outside of courses)
|
-- !materials -- only if course allows all materials to be free (no meaning outside of courses)
|
||||||
-- !time -- access depends on time somehow
|
-- !time -- access depends on time somehow
|
||||||
@ -47,6 +47,9 @@
|
|||||||
|
|
||||||
/auth AuthR Auth getAuth !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
|
/metrics MetricsR GET !free -- verify if this can be free
|
||||||
|
|
||||||
/err ErrorR GET !free
|
/err ErrorR GET !free
|
||||||
@ -71,6 +74,7 @@
|
|||||||
/admin/crontab/jobs AdminJobsR GET POST
|
/admin/crontab/jobs AdminJobsR GET POST
|
||||||
/admin/avs AdminAvsR GET POST
|
/admin/avs AdminAvsR GET POST
|
||||||
/admin/avs/#CryptoUUIDUser AdminAvsUserR GET POST
|
/admin/avs/#CryptoUUIDUser AdminAvsUserR GET POST
|
||||||
|
/admin/external-user AdminExternalUserR GET POST
|
||||||
/admin/ldap AdminLdapR GET POST
|
/admin/ldap AdminLdapR GET POST
|
||||||
/admin/problems AdminProblemsR GET POST
|
/admin/problems AdminProblemsR GET POST
|
||||||
/admin/problems/no-contact ProblemUnreachableR GET POST
|
/admin/problems/no-contact ProblemUnreachableR GET POST
|
||||||
|
|||||||
35
shell.nix
35
shell.nix
@ -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
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -9,6 +9,13 @@ let
|
|||||||
|
|
||||||
haskellPackages = pkgs.haskellPackages;
|
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" ''
|
postgresSchema = pkgs.writeText "schema.sql" ''
|
||||||
CREATE USER uniworx WITH SUPERUSER;
|
CREATE USER uniworx WITH SUPERUSER;
|
||||||
CREATE DATABASE uniworx_test;
|
CREATE DATABASE uniworx_test;
|
||||||
@ -21,6 +28,17 @@ let
|
|||||||
local all all trust
|
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" ''
|
develop = pkgs.writeScriptBin "develop" ''
|
||||||
#!${pkgs.zsh}/bin/zsh -e
|
#!${pkgs.zsh}/bin/zsh -e
|
||||||
|
|
||||||
@ -44,6 +62,9 @@ let
|
|||||||
type cleanup_cache_memcached &>/dev/null && cleanup_cache_memcached
|
type cleanup_cache_memcached &>/dev/null && cleanup_cache_memcached
|
||||||
type cleanup_minio &>/dev/null && cleanup_minio
|
type cleanup_minio &>/dev/null && cleanup_minio
|
||||||
type cleanup_maildev &>/dev/null && cleanup_maildev
|
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"
|
[ -f "''${basePath}/.develop.env" ] && rm -vf "''${basePath}/.develop.env"
|
||||||
set +x
|
set +x
|
||||||
@ -51,7 +72,17 @@ let
|
|||||||
|
|
||||||
trap cleanup EXIT
|
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
|
if [[ -z "$PGHOST" ]]; then
|
||||||
set -xe
|
set -xe
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
-- SPDX-FileCopyrightText: 2022-2024 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Sarah Vaupel <vaupel.sarah@campus.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>
|
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>,-2024 Gregor Kleen <gregor.kleen@ifi.lmu.de>, Sarah Vaupel <sarah.vaupel@ifi.lmu.de>, Sarah Vaupel <vaupel.sarah@campus.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>, David Mosbach <david.mosbach@uniworx.de>,Steffen Jost <s.jost@fraport.de>
|
||||||
--
|
--
|
||||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -6,10 +6,8 @@
|
|||||||
|
|
||||||
module Application
|
module Application
|
||||||
( getAppSettings, getAppDevSettings
|
( getAppSettings, getAppDevSettings
|
||||||
, appMain
|
, appMain, develMain
|
||||||
, develMain
|
|
||||||
, makeFoundation
|
, makeFoundation
|
||||||
, makeMiddleware
|
|
||||||
-- * for DevelMain
|
-- * for DevelMain
|
||||||
, foundationStoreNum
|
, foundationStoreNum
|
||||||
, getApplicationRepl
|
, getApplicationRepl
|
||||||
@ -20,96 +18,97 @@ module Application
|
|||||||
, addPWEntry
|
, addPWEntry
|
||||||
) where
|
) 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
|
import Database.Persist.Postgresql ( openSimpleConn, pgConnStr, pgPoolIdleTimeout
|
||||||
, pgPoolSize
|
, pgPoolSize
|
||||||
)
|
)
|
||||||
import Database.Persist.SqlBackend.Internal ( connClose )
|
import Database.Persist.SqlBackend.Internal ( connClose )
|
||||||
import qualified Database.PostgreSQL.Simple as PG
|
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 Foreign.Store
|
||||||
|
|
||||||
import Web.Cookie
|
import GHC.RTS.Flags (getRTSFlags)
|
||||||
import Network.HTTP.Types.Header (hSetCookie)
|
|
||||||
|
|
||||||
import qualified Data.UUID as UUID
|
import Language.Haskell.TH.Syntax (qLocation)
|
||||||
import qualified Data.UUID.V4 as UUID
|
|
||||||
|
|
||||||
import System.Directory
|
import qualified Ldap.Client as Ldap (Host(Plain,Tls))
|
||||||
|
|
||||||
import Jobs
|
|
||||||
|
|
||||||
import qualified Data.Text.Encoding as Text
|
|
||||||
|
|
||||||
import Yesod.Auth.Util.PasswordStore
|
|
||||||
|
|
||||||
import qualified Data.ByteString.Lazy as LBS
|
|
||||||
|
|
||||||
|
import Network.Connection (settingDisableCertificateValidation)
|
||||||
import Network.HaskellNet.SSL hiding (Settings)
|
import Network.HaskellNet.SSL hiding (Settings)
|
||||||
import Network.HaskellNet.SMTP.SSL as SMTP 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.Concurrent
|
||||||
import UnliftIO.Pool
|
import UnliftIO.Pool
|
||||||
|
|
||||||
import Control.Monad.Trans.Resource
|
import qualified Web.ServerSession.Backend.Acid as Acid
|
||||||
|
import Web.ServerSession.Core (StorageException(..))
|
||||||
|
|
||||||
import System.Log.FastLogger.Date
|
import Yesod.Auth.OAuth2.AzureAD (oauth2AzureADScoped)
|
||||||
|
import Yesod.Auth.Util.PasswordStore
|
||||||
import qualified Yesod.Core.Types as Yesod (Logger(..))
|
import qualified Yesod.Core.Types as Yesod (Logger(..))
|
||||||
|
|
||||||
import qualified Data.HashMap.Strict as HashMap
|
#ifdef DEVELOPMENT
|
||||||
|
import Data.Maybe (fromJust)
|
||||||
import qualified Data.Aeson as Aeson
|
import Auth.OAuth2 (azureMockServer)
|
||||||
|
#endif
|
||||||
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 GHC.RTS.Flags (getRTSFlags)
|
||||||
|
|
||||||
@ -159,11 +158,11 @@ import Handler.PrintCenter
|
|||||||
import Handler.ApiDocs
|
import Handler.ApiDocs
|
||||||
import Handler.Swagger
|
import Handler.Swagger
|
||||||
import Handler.Firm
|
import Handler.Firm
|
||||||
|
import Handler.SingleSignOut
|
||||||
|
|
||||||
import ServantApi () -- YesodSubDispatch instances
|
import ServantApi () -- YesodSubDispatch instances
|
||||||
import Servant.API
|
import Servant.API
|
||||||
import Servant.Client
|
import Servant.Client
|
||||||
import Network.HTTP.Client.TLS (mkManagerSettings)
|
|
||||||
|
|
||||||
|
|
||||||
-- This line actually creates our YesodDispatch instance. It is the second half
|
-- This line actually creates our YesodDispatch instance. It is the second half
|
||||||
@ -222,7 +221,7 @@ makeFoundation appSettings''@AppSettings{..} = do
|
|||||||
-- from there, and then create the real foundation.
|
-- from there, and then create the real foundation.
|
||||||
let
|
let
|
||||||
mkFoundation :: _ -> (forall m. MonadIO m => Custom.Pool' m DBConnLabel DBConnUseState SqlBackend) -> _
|
mkFoundation :: _ -> (forall m. MonadIO m => Custom.Pool' m DBConnLabel DBConnUseState SqlBackend) -> _
|
||||||
mkFoundation appSettings' appConnPool appSmtpPool appLdapPool appCryptoIDKey appSessionStore appSecretBoxKey appWidgetMemcached appJSONWebKeySet appClusterID appMemcached appUploadCache appVerpSecret appAuthKey appPersonalisedSheetFilesSeedKey appVolatileClusterSettingsCache appAvsQuery = UniWorX{..}
|
mkFoundation appSettings' appConnPool appSmtpPool appLdapPool appCryptoIDKey appSessionStore appSecretBoxKey appWidgetMemcached appJSONWebKeySet appClusterID appMemcached appUploadCache appVerpSecret appAuthKey appAuthPlugins appPersonalisedSheetFilesSeedKey appVolatileClusterSettingsCache appAvsQuery = UniWorX{..}
|
||||||
tempFoundation = mkFoundation
|
tempFoundation = mkFoundation
|
||||||
(error "appSettings' forced in tempFoundation")
|
(error "appSettings' forced in tempFoundation")
|
||||||
(error "connPool forced in tempFoundation")
|
(error "connPool forced in tempFoundation")
|
||||||
@ -238,10 +237,12 @@ makeFoundation appSettings''@AppSettings{..} = do
|
|||||||
(error "MinioConn forced in tempFoundation")
|
(error "MinioConn forced in tempFoundation")
|
||||||
(error "VerpSecret forced in tempFoundation")
|
(error "VerpSecret forced in tempFoundation")
|
||||||
(error "AuthKey forced in tempFoundation")
|
(error "AuthKey forced in tempFoundation")
|
||||||
|
(error "AuthPlugins forced in tempFoundation")
|
||||||
(error "PersonalisedSheetFilesSeedKey forced in tempFoundation")
|
(error "PersonalisedSheetFilesSeedKey forced in tempFoundation")
|
||||||
(error "VolatileClusterSettingsCache forced in tempFoundation")
|
(error "VolatileClusterSettingsCache forced in tempFoundation")
|
||||||
(error "AvsQuery forced in tempFoundation")
|
(error "AvsQuery forced in tempFoundation")
|
||||||
|
|
||||||
|
|
||||||
runAppLoggingT tempFoundation $ do
|
runAppLoggingT tempFoundation $ do
|
||||||
$logInfoS "InstanceID" $ UUID.toText appInstanceID
|
$logInfoS "InstanceID" $ UUID.toText appInstanceID
|
||||||
$logInfoS "Configuration" $ tshowCrop appSettings''
|
$logInfoS "Configuration" $ tshowCrop appSettings''
|
||||||
@ -276,13 +277,32 @@ makeFoundation appSettings''@AppSettings{..} = do
|
|||||||
sqlPool = Custom.hoistPool (liftIO . flip runLoggingT logFunc) sqlPool'
|
sqlPool = Custom.hoistPool (liftIO . flip runLoggingT logFunc) sqlPool'
|
||||||
void . Prometheus.register . poolMetrics PoolDatabaseConnections $ sqlPool @IO
|
void . Prometheus.register . poolMetrics PoolDatabaseConnections $ sqlPool @IO
|
||||||
|
|
||||||
ldapPool <- traverse mkFailoverLabeled <=< forOf (traverse . traverse) appLdapConf $ \conf@LdapConf{..} -> do
|
-- ldapPool <- traverse mkFailoverLabeled <=< forOf (traverse . traverse) appUserDbConf $ \conf -> if
|
||||||
let ldapLabel = case ldapHost of
|
-- | UserDbSingleSource{..} <- conf
|
||||||
Ldap.Plain str -> pack str <> ":" <> tshow ldapPort
|
-- , UserDbLdap LdapConf{..} <- userdbSingleSource
|
||||||
Ldap.Tls str _ -> pack str <> ":" <> tshow ldapPort
|
-- , Just ResourcePoolConf{..} <- userdbPoolConf
|
||||||
$logDebugS "setup" $ "LDAP-Pool " <> ldapLabel
|
-- -> do
|
||||||
(ldapLabel,) . (conf,) <$> createLdapPool ldapHost ldapPort (poolStripes ldapPool) (poolTimeout ldapPool) ldapTimeout (poolLimit ldapPool)
|
-- let ldapLabel = case ldapHost of
|
||||||
forM_ ldapPool $ registerFailoverMetrics "ldap"
|
-- 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.
|
-- Perform database migration using our application's logging settings.
|
||||||
flip runReaderT tempFoundation $
|
flip runReaderT tempFoundation $
|
||||||
@ -303,6 +323,34 @@ makeFoundation appSettings''@AppSettings{..} = do
|
|||||||
appAuthKey <- clusterSetting (Proxy :: Proxy 'ClusterAuthKey) `customRunSqlPool` sqlPool
|
appAuthKey <- clusterSetting (Proxy :: Proxy 'ClusterAuthKey) `customRunSqlPool` sqlPool
|
||||||
appPersonalisedSheetFilesSeedKey <- clusterSetting (Proxy :: Proxy 'ClusterPersonalisedSheetFilesSeedKey) `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
|
let appVolatileClusterSettingsCacheTime' = Clock.fromNanoSecs ns
|
||||||
where (MkFixed ns :: Nano) = realToFrac appVolatileClusterSettingsCacheTime
|
where (MkFixed ns :: Nano) = realToFrac appVolatileClusterSettingsCacheTime
|
||||||
appVolatileClusterSettingsCache <- newTVarIO $ mkVolatileClusterSettingsCache appVolatileClusterSettingsCacheTime'
|
appVolatileClusterSettingsCache <- newTVarIO $ mkVolatileClusterSettingsCache appVolatileClusterSettingsCacheTime'
|
||||||
@ -356,7 +404,8 @@ makeFoundation appSettings''@AppSettings{..} = do
|
|||||||
|
|
||||||
$logDebugS "Runtime configuration" $ tshowCrop appSettings'
|
$logDebugS "Runtime configuration" $ tshowCrop appSettings'
|
||||||
|
|
||||||
let foundation = mkFoundation appSettings' sqlPool smtpPool ldapPool appCryptoIDKey appSessionStore appSecretBoxKey appWidgetMemcached appJSONWebKeySet appClusterID appMemcached appUploadCache appVerpSecret appAuthKey appPersonalisedSheetFilesSeedKey appVolatileClusterSettingsCache appAvsQuery
|
-- TODO: reimplement user db failover
|
||||||
|
let foundation = mkFoundation appSettings' sqlPool smtpPool ldapPool appCryptoIDKey appSessionStore appSecretBoxKey appWidgetMemcached appJSONWebKeySet appClusterID appMemcached appUploadCache appVerpSecret appAuthKey appAuthPlugins appPersonalisedSheetFilesSeedKey appVolatileClusterSettingsCache appAvsQuery
|
||||||
|
|
||||||
-- Return the foundation
|
-- Return the foundation
|
||||||
$logInfoS "setup" "*** DONE ***"
|
$logInfoS "setup" "*** DONE ***"
|
||||||
@ -455,66 +504,6 @@ createMemcached MemcachedConf{memcachedConnectInfo} = snd <$> allocate (Memcache
|
|||||||
makeApplication :: MonadIO m => UniWorX -> m Application
|
makeApplication :: MonadIO m => UniWorX -> m Application
|
||||||
makeApplication foundation = liftIO $ makeMiddleware foundation <*> toWaiAppPlain foundation
|
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.
|
-- | Warp settings for the given foundation value.
|
||||||
warpSettings :: UniWorX -> Settings
|
warpSettings :: UniWorX -> Settings
|
||||||
warpSettings foundation = defaultSettings
|
warpSettings foundation = defaultSettings
|
||||||
@ -595,6 +584,8 @@ appMain = runResourceT $ do
|
|||||||
foundation <- makeFoundation settings
|
foundation <- makeFoundation settings
|
||||||
|
|
||||||
runAppLoggingT foundation $ do
|
runAppLoggingT foundation $ do
|
||||||
|
$logDebugS "AppSettings" $ tshow settings
|
||||||
|
|
||||||
$logInfoS "setup" "Job-Handling"
|
$logInfoS "setup" "Job-Handling"
|
||||||
handleJobs foundation
|
handleJobs foundation
|
||||||
|
|
||||||
@ -711,7 +702,7 @@ shutdownApp app = do
|
|||||||
liftIO $ do
|
liftIO $ do
|
||||||
Custom.purgePool $ appConnPool app
|
Custom.purgePool $ appConnPool app
|
||||||
for_ (appSmtpPool app) destroyAllResources
|
for_ (appSmtpPool app) destroyAllResources
|
||||||
for_ (appLdapPool app) . mapFailover $ views _2 destroyAllResources
|
for_ (appLdapPool app) $ views _2 destroyAllResources
|
||||||
for_ (appWidgetMemcached app) Memcached.close
|
for_ (appWidgetMemcached app) Memcached.close
|
||||||
for_ (appMemcached app) $ views _memcachedConn Memcached.close
|
for_ (appMemcached app) $ views _memcachedConn Memcached.close
|
||||||
release . fst $ appLogger app
|
release . fst $ appLogger app
|
||||||
@ -736,7 +727,7 @@ db' = handler' . runDB
|
|||||||
addPWEntry :: User
|
addPWEntry :: User
|
||||||
-> Text {-^ Password -}
|
-> Text {-^ Password -}
|
||||||
-> IO ()
|
-> IO ()
|
||||||
addPWEntry User{ userAuthentication = _, ..} (Text.encodeUtf8 -> pw) = db' $ do
|
addPWEntry User{ userPasswordHash = _, ..} (Text.encodeUtf8 -> pw) = db' $ do
|
||||||
PWHashConf{..} <- getsYesod $ view _appAuthPWHash
|
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{..}
|
void $ insert User{..}
|
||||||
|
|||||||
242
src/Auth/LDAP.hs
242
src/Auth/LDAP.hs
@ -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
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -7,11 +7,11 @@
|
|||||||
module Auth.LDAP
|
module Auth.LDAP
|
||||||
( apLdap
|
( apLdap
|
||||||
, ADError(..), ADInvalidCredentials(..)
|
, ADError(..), ADInvalidCredentials(..)
|
||||||
, campusLogin
|
, ldapLogin
|
||||||
, CampusUserException(..)
|
, LdapUserException(..)
|
||||||
, campusUser, campusUser', campusUser''
|
, ldapUser, ldapUser', ldapUser''
|
||||||
, campusUserReTest, campusUserReTest'
|
--, ldapUserReTest, ldapUserReTest'
|
||||||
, campusUserMatr, campusUserMatr'
|
, ldapUserMatr, ldapUserMatr'
|
||||||
, CampusMessage(..)
|
, CampusMessage(..)
|
||||||
, ldapPrimaryKey
|
, ldapPrimaryKey
|
||||||
, ldapUserPrincipalName, ldapUserEmail, ldapUserDisplayName
|
, ldapUserPrincipalName, ldapUserEmail, ldapUserDisplayName
|
||||||
@ -20,32 +20,36 @@ module Auth.LDAP
|
|||||||
, ldapUserMobile, ldapUserTelephone
|
, ldapUserMobile, ldapUserTelephone
|
||||||
, ldapUserFraportPersonalnummer, ldapUserFraportAbteilung
|
, ldapUserFraportPersonalnummer, ldapUserFraportAbteilung
|
||||||
, ldapUserTitle
|
, ldapUserTitle
|
||||||
|
, ldapSearch
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Import.NoFoundation
|
import Import.NoFoundation
|
||||||
|
|
||||||
import qualified Data.CaseInsensitive as CI
|
import Auth.LDAP.AD
|
||||||
|
|
||||||
import Utils.Metrics
|
|
||||||
import Utils.Form
|
|
||||||
|
|
||||||
import qualified Ldap.Client as Ldap
|
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 Data.Text.Encoding as Text
|
||||||
|
|
||||||
import qualified Yesod.Auth.Message as Msg
|
import qualified Yesod.Auth.Message as Msg
|
||||||
|
|
||||||
import Auth.LDAP.AD
|
|
||||||
|
|
||||||
-- allow Ldap.Attr usage as key for Data.Map
|
-- | Plugin name of the LDAP yesod auth plugin
|
||||||
deriving newtype instance Ord Ldap.Attr
|
apLdap :: Text
|
||||||
|
apLdap = "LDAP"
|
||||||
|
|
||||||
|
|
||||||
|
-- TODO: rename
|
||||||
data CampusLogin = CampusLogin
|
data CampusLogin = CampusLogin
|
||||||
{ campusIdent :: CI Text
|
{ campusIdent :: CI Text
|
||||||
, campusPassword :: Text
|
, campusPassword :: Text
|
||||||
} deriving (Generic)
|
} deriving (Generic)
|
||||||
|
|
||||||
|
-- TODO: rename
|
||||||
data CampusMessage = MsgCampusIdentPlaceholder
|
data CampusMessage = MsgCampusIdentPlaceholder
|
||||||
| MsgCampusIdent
|
| MsgCampusIdent
|
||||||
| MsgCampusPassword
|
| MsgCampusPassword
|
||||||
@ -53,8 +57,12 @@ data CampusMessage = MsgCampusIdentPlaceholder
|
|||||||
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic)
|
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic)
|
||||||
|
|
||||||
|
|
||||||
findUser :: LdapConf -> Ldap -> Text -> [Ldap.Attr] -> IO [Ldap.SearchEntry]
|
findUser :: LdapConf
|
||||||
findUser conf@LdapConf{..} ldap ident retAttrs = fromMaybe [] <$> findM (assertM (not . null) . lift . flip (Ldap.search ldap ldapBase $ userSearchSettings conf) retAttrs) userFilters
|
-> 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
|
where
|
||||||
userFilters =
|
userFilters =
|
||||||
[ ldapUserPrincipalName Ldap.:= Text.encodeUtf8 ident
|
[ ldapUserPrincipalName Ldap.:= Text.encodeUtf8 ident
|
||||||
@ -69,21 +77,37 @@ findUser conf@LdapConf{..} ldap ident retAttrs = fromMaybe [] <$> findM (assertM
|
|||||||
[ ldapUserFraportPersonalnummer Ldap.:= Text.encodeUtf8 ident
|
[ ldapUserFraportPersonalnummer Ldap.:= Text.encodeUtf8 ident
|
||||||
]
|
]
|
||||||
|
|
||||||
findUserMatr :: LdapConf -> Ldap -> Text -> [Ldap.Attr] -> IO [Ldap.SearchEntry]
|
findUserMatr :: LdapConf
|
||||||
findUserMatr conf@LdapConf{..} ldap userMatr retAttrs = fromMaybe [] <$> findM (assertM (not . null) . lift . flip (Ldap.search ldap ldapBase $ userSearchSettings conf) retAttrs) userFilters
|
-> 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
|
where
|
||||||
userFilters =
|
userFilters =
|
||||||
[ ldapUserFraportPersonalnummer Ldap.:= Text.encodeUtf8 userMatr
|
[ ldapUserFraportPersonalnummer Ldap.:= Text.encodeUtf8 userMatr
|
||||||
]
|
]
|
||||||
|
|
||||||
userSearchSettings :: LdapConf -> Ldap.Mod Ldap.Search
|
userSearchSettings :: LdapConf
|
||||||
|
-> Ldap.Mod Ldap.Search
|
||||||
userSearchSettings LdapConf{..} = mconcat
|
userSearchSettings LdapConf{..} = mconcat
|
||||||
[ Ldap.scope ldapScope
|
[ Ldap.scope ldapConfScope
|
||||||
, Ldap.size 2
|
, Ldap.size 2
|
||||||
, Ldap.time ldapSearchTimeout
|
, Ldap.time ldapConfSearchTimeout
|
||||||
, Ldap.derefAliases Ldap.DerefAlways
|
, 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, ldapUserPrincipalName, ldapUserDisplayName, ldapUserFirstName, ldapUserSurname, ldapAffiliation, ldapUserTitle, ldapUserTelephone, ldapUserMobile, ldapUserFraportPersonalnummer, ldapUserFraportAbteilung :: Ldap.Attr
|
||||||
ldapPrimaryKey = Ldap.Attr "cn" -- should always be identical to "sAMAccountName"
|
ldapPrimaryKey = Ldap.Attr "cn" -- should always be identical to "sAMAccountName"
|
||||||
ldapUserPrincipalName = Ldap.Attr "userPrincipalName"
|
ldapUserPrincipalName = Ldap.Attr "userPrincipalName"
|
||||||
@ -104,30 +128,35 @@ ldapUserEmail = Ldap.Attr "mail" :|
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
data CampusUserException = CampusUserLdapError LdapPoolError
|
-- TODO: deprecate in favour of FetchUserDataException
|
||||||
| CampusUserNoResult
|
data LdapUserException = LdapUserLdapError LdapPoolError
|
||||||
| CampusUserAmbiguous
|
| LdapUserNoResult
|
||||||
|
| LdapUserAmbiguous
|
||||||
deriving (Show, Eq, Generic)
|
deriving (Show, Eq, Generic)
|
||||||
|
|
||||||
instance Exception CampusUserException
|
instance Exception LdapUserException
|
||||||
|
|
||||||
makePrisms ''CampusUserException
|
makePrisms ''LdapUserException
|
||||||
|
|
||||||
campusUserWith :: ( MonadUnliftIO m
|
|
||||||
, MonadCatch m
|
ldapUserWith :: ( MonadUnliftIO m
|
||||||
)
|
, MonadCatch m
|
||||||
=> ( Lens (LdapConf, LdapPool) (LdapConf, Ldap) LdapPool Ldap
|
--, MonadLogger m
|
||||||
-> Failover (LdapConf, LdapPool)
|
)
|
||||||
-> FailoverMode
|
-- ( Lens (LdapConf, LdapPool) (LdapConf, Ldap) LdapPool Ldap
|
||||||
-> ((LdapConf, Ldap) -> m (Either CampusUserException (Ldap.AttrList [])))
|
-- -> (LdapConf, LdapPool)
|
||||||
-> m (Either LdapPoolError (Either CampusUserException (Ldap.AttrList [])))
|
-- -> ((LdapConf, Ldap) -> m (Either CampusUserException (Ldap.AttrList [])))
|
||||||
)
|
-- -> m (Either LdapPoolError (Either CampusUserException (Ldap.AttrList [])))
|
||||||
-> Failover (LdapConf, LdapPool)
|
-- )
|
||||||
-> FailoverMode
|
=> ( LdapPool
|
||||||
-> Creds site
|
-> (Ldap -> m (Either LdapUserException (Ldap.AttrList [])))
|
||||||
-> m (Either CampusUserException (Ldap.AttrList []))
|
-> m (Either LdapPoolError (Either LdapUserException (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
|
-> (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
|
results <- case lookup "DN" credsExtra of
|
||||||
Just userDN -> do
|
Just userDN -> do
|
||||||
let userFilter = Ldap.Present ldapUserPrincipalName
|
let userFilter = Ldap.Present ldapUserPrincipalName
|
||||||
@ -135,43 +164,91 @@ campusUserWith withLdap' pool mode Creds{..} = either (throwM . CampusUserLdapEr
|
|||||||
Nothing -> do
|
Nothing -> do
|
||||||
lift $ findUser conf ldap credsIdent []
|
lift $ findUser conf ldap credsIdent []
|
||||||
case results of
|
case results of
|
||||||
[] -> throwE CampusUserNoResult
|
[] -> throwE LdapUserNoResult
|
||||||
[Ldap.SearchEntry _ attrs] -> return attrs
|
[Ldap.SearchEntry _ attrs] -> return attrs
|
||||||
_otherwise -> throwE CampusUserAmbiguous
|
_otherwise -> throwE LdapUserAmbiguous
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
campusUser :: (MonadMask m, MonadUnliftIO m, MonadLogger m) => Failover (LdapConf, LdapPool) -> FailoverMode -> Creds site -> m (Ldap.AttrList [])
|
-- TODO: reintroduce once failover has been reimplemented
|
||||||
campusUser pool mode creds = throwLeft =<< campusUserWith withLdapFailover pool mode creds
|
-- 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 []))
|
-- TODO: deprecate in favour of fetchUserData
|
||||||
campusUser'' pool mode ident
|
ldapUser :: ( MonadMask m
|
||||||
= runMaybeT . catchIfMaybeT (is _CampusUserNoResult) $ campusUser pool mode (Creds apLdap ident [])
|
, 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 [])
|
ldapUser' :: ( MonadMask m
|
||||||
campusUserMatr pool mode userMatr = either (throwM . CampusUserLdapError) return <=< withLdapFailover _2 pool mode $ \(conf@LdapConf{..}, ldap) -> liftIO $ do
|
, MonadUnliftIO m
|
||||||
Ldap.bind ldap ldapDn ldapPassword
|
--, 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 []
|
results <- findUserMatr conf ldap userMatr []
|
||||||
case results of
|
case results of
|
||||||
[] -> throwM CampusUserNoResult
|
[] -> throwM LdapUserNoResult
|
||||||
[Ldap.SearchEntry _ attrs] -> return attrs
|
[Ldap.SearchEntry _ attrs] -> return attrs
|
||||||
_otherwise -> throwM CampusUserAmbiguous
|
_otherwise -> throwM LdapUserAmbiguous
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
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
|
newtype ADInvalidCredentials = ADInvalidCredentials ADError
|
||||||
@ -186,25 +263,28 @@ campusForm :: ( RenderMessage (HandlerSite m) FormMessage
|
|||||||
, RenderMessage (HandlerSite m) (ValueRequired (HandlerSite m))
|
, RenderMessage (HandlerSite m) (ValueRequired (HandlerSite m))
|
||||||
, RenderMessage (HandlerSite m) CampusMessage
|
, RenderMessage (HandlerSite m) CampusMessage
|
||||||
, MonadHandler m
|
, MonadHandler m
|
||||||
) => WForm m (FormResult CampusLogin)
|
)
|
||||||
|
=> WForm m (FormResult CampusLogin)
|
||||||
campusForm = do
|
campusForm = do
|
||||||
MsgRenderer mr <- getMsgRenderer
|
MsgRenderer mr <- getMsgRenderer
|
||||||
aFormToWForm $ CampusLogin
|
aFormToWForm $ CampusLogin
|
||||||
<$> areq ciField (fslpI MsgCampusIdent (mr MsgCampusIdentPlaceholder) & addAttr "autofocus" "" & addAttr "autocomplete" "username") Nothing
|
<$> areq ciField (fslpI MsgCampusIdent (mr MsgCampusIdentPlaceholder) & addAttr "autofocus" "" & addAttr "autocomplete" "username") Nothing
|
||||||
<*> areq passwordField (fslpI MsgCampusPassword (mr MsgCampusPasswordPlaceholder) & addAttr "autocomplete" "current-password") Nothing
|
<*> areq passwordField (fslpI MsgCampusPassword (mr MsgCampusPasswordPlaceholder) & addAttr "autocomplete" "current-password") Nothing
|
||||||
|
|
||||||
apLdap :: Text
|
|
||||||
apLdap = "LDAP"
|
|
||||||
|
|
||||||
campusLogin :: forall site.
|
-- TODO: reintroduce Failover
|
||||||
( YesodAuth site
|
ldapLogin :: forall site.
|
||||||
, RenderMessage site CampusMessage
|
( YesodAuth site
|
||||||
, RenderAFormSite site
|
, RenderMessage site CampusMessage
|
||||||
, RenderMessage site (ValueRequired site)
|
, RenderAFormSite site
|
||||||
, RenderMessage site ADInvalidCredentials
|
, RenderMessage site (ValueRequired site)
|
||||||
, Button site ButtonSubmit
|
, RenderMessage site ADInvalidCredentials
|
||||||
) => Failover (LdapConf, LdapPool) -> FailoverMode -> AuthPlugin site
|
, Button site ButtonSubmit
|
||||||
campusLogin pool mode = AuthPlugin{..}
|
)
|
||||||
|
=> LdapConf
|
||||||
|
-> LdapPool
|
||||||
|
-> AuthPlugin site
|
||||||
|
ldapLogin conf@LdapConf{..} pool = AuthPlugin{..}
|
||||||
where
|
where
|
||||||
apName :: Text
|
apName :: Text
|
||||||
apName = apLdap
|
apName = apLdap
|
||||||
@ -215,8 +295,8 @@ campusLogin pool mode = AuthPlugin{..}
|
|||||||
tp <- getRouteToParent
|
tp <- getRouteToParent
|
||||||
|
|
||||||
resp <- formResultMaybe loginRes $ \CampusLogin{ campusIdent = CI.original -> campusIdent, ..} -> Just <$> do
|
resp <- formResultMaybe loginRes $ \CampusLogin{ campusIdent = CI.original -> campusIdent, ..} -> Just <$> do
|
||||||
ldapResult <- withLdapFailover _2 pool mode $ \(conf@LdapConf{..}, ldap) -> liftIO $ do
|
ldapResult <- withLdap pool $ \ldap -> liftIO $ do
|
||||||
Ldap.bind ldap ldapDn ldapPassword
|
Ldap.bind ldap ldapConfDn ldapConfPassword
|
||||||
searchResults <- findUser conf ldap campusIdent [ldapUserPrincipalName]
|
searchResults <- findUser conf ldap campusIdent [ldapUserPrincipalName]
|
||||||
case searchResults of
|
case searchResults of
|
||||||
[Ldap.SearchEntry (Ldap.Dn userDN) userAttrs]
|
[Ldap.SearchEntry (Ldap.Dn userDN) userAttrs]
|
||||||
|
|||||||
248
src/Auth/OAuth2.hs
Normal file
248
src/Auth/OAuth2.hs
Normal 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
|
||||||
|
|
||||||
@ -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
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -68,12 +68,13 @@ hashLogin pwHashAlgo = AuthPlugin{..}
|
|||||||
tp <- getRouteToParent
|
tp <- getRouteToParent
|
||||||
|
|
||||||
resp <- formResultMaybe loginRes $ \HashLogin{..} -> Just <$> do
|
resp <- formResultMaybe loginRes $ \HashLogin{..} -> Just <$> do
|
||||||
user <- liftHandler . runDB . getBy $ UniqueAuthentication hashIdent
|
user :: Maybe (Entity User) <- liftHandler . runDB . getBy $ UniqueAuthentication hashIdent
|
||||||
case user of
|
case user of
|
||||||
Just (Entity _ User{ userAuthentication = AuthPWHash{..}, userIdent = CI.original -> userIdent })
|
Just (Entity _ User{userIdent,userPasswordHash})
|
||||||
| verifyPasswordWith pwHashAlgo (2^) (encodeUtf8 hashPassword) (encodeUtf8 authPWHash) -> do -- (2^) is magic.
|
| Just pwHash <- userPasswordHash
|
||||||
|
, verifyPasswordWith pwHashAlgo (2^) (encodeUtf8 hashPassword) (encodeUtf8 pwHash) -> do -- (2^) is magic.
|
||||||
observeLoginOutcome apName LoginSuccessful
|
observeLoginOutcome apName LoginSuccessful
|
||||||
setCredsRedirect $ Creds apName userIdent []
|
setCredsRedirect $ Creds apName (CI.original userIdent) []
|
||||||
other -> do
|
other -> do
|
||||||
$logDebugS apName $ tshow other
|
$logDebugS apName $ tshow other
|
||||||
observeLoginOutcome apName LoginInvalidCredentials
|
observeLoginOutcome apName LoginInvalidCredentials
|
||||||
|
|||||||
@ -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
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -1523,7 +1523,7 @@ tagAccessPredicate AuthSelf = APDB $ \_ _ mAuthId route _ -> exceptT return retu
|
|||||||
| uid == referencedUser -> return Authorized
|
| uid == referencedUser -> return Authorized
|
||||||
Nothing -> return AuthenticationRequired
|
Nothing -> return AuthenticationRequired
|
||||||
_other -> unauthorizedI MsgUnauthorizedSelf
|
_other -> unauthorizedI MsgUnauthorizedSelf
|
||||||
tagAccessPredicate AuthIsLDAP = APDB $ \_ _ _ route _ -> exceptT return return $ do
|
tagAccessPredicate AuthIsExternal = APDB $ \_ _ _ route _ -> exceptT return return $ do
|
||||||
referencedUser <- case route of
|
referencedUser <- case route of
|
||||||
AdminUserR cID -> return cID
|
AdminUserR cID -> return cID
|
||||||
AdminUserDeleteR cID -> return cID
|
AdminUserDeleteR cID -> return cID
|
||||||
@ -1531,13 +1531,17 @@ tagAccessPredicate AuthIsLDAP = APDB $ \_ _ _ route _ -> exceptT return return $
|
|||||||
UserNotificationR cID -> return cID
|
UserNotificationR cID -> return cID
|
||||||
UserPasswordR cID -> return cID
|
UserPasswordR cID -> return cID
|
||||||
CourseR _ _ _ (CUserR 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
|
referencedUser' <- catchIfMExceptT (const $ unauthorizedI MsgUnauthorizedSelf) (const True :: CryptoIDError -> Bool) $ decrypt referencedUser
|
||||||
maybeTMExceptT (unauthorizedI MsgUnauthorizedLDAP) $ do
|
availableSources <- getsYesod (view _appUserAuthConf) >>= \case
|
||||||
User{..} <- MaybeT $ get referencedUser'
|
UserAuthConfSingleSource{..} -> return . singleton $ case userAuthConfSingleSource of
|
||||||
guard $ userAuthentication == AuthLDAP
|
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
|
return Authorized
|
||||||
tagAccessPredicate AuthIsPWHash = APDB $ \_ _ _ route _ -> exceptT return return $ do
|
tagAccessPredicate AuthIsInternal = APDB $ \_ _ _ route _ -> exceptT return return $ do
|
||||||
referencedUser <- case route of
|
referencedUser <- case route of
|
||||||
AdminUserR cID -> return cID
|
AdminUserR cID -> return cID
|
||||||
AdminUserDeleteR cID -> return cID
|
AdminUserDeleteR cID -> return cID
|
||||||
@ -1545,11 +1549,11 @@ tagAccessPredicate AuthIsPWHash = APDB $ \_ _ _ route _ -> exceptT return return
|
|||||||
UserNotificationR cID -> return cID
|
UserNotificationR cID -> return cID
|
||||||
UserPasswordR cID -> return cID
|
UserPasswordR cID -> return cID
|
||||||
CourseR _ _ _ (CUserR 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
|
referencedUser' <- catchIfMExceptT (const $ unauthorizedI MsgUnauthorizedSelf) (const True :: CryptoIDError -> Bool) $ decrypt referencedUser
|
||||||
maybeTMExceptT (unauthorizedI MsgUnauthorizedPWHash) $ do
|
maybeTMExceptT (unauthorizedI MsgUnauthorizedInternal) $ do
|
||||||
User{..} <- MaybeT $ get referencedUser'
|
User{..} <- MaybeT $ get referencedUser'
|
||||||
guard $ is _AuthPWHash userAuthentication
|
guard $ is _Just userPasswordHash
|
||||||
return Authorized
|
return Authorized
|
||||||
tagAccessPredicate AuthAuthentication = APDB $ \_ _ mAuthId route _ -> case route of
|
tagAccessPredicate AuthAuthentication = APDB $ \_ _ mAuthId route _ -> case route of
|
||||||
MessageR cID -> maybeT (unauthorizedI MsgUnauthorizedSystemMessageAuth) $ do
|
MessageR cID -> maybeT (unauthorizedI MsgUnauthorizedSystemMessageAuth) $ do
|
||||||
|
|||||||
@ -410,8 +410,6 @@ embedRenderMessage ''UniWorX ''ExamRequiredEquipmentPreset id
|
|||||||
embedRenderMessage ''UniWorX ''ChangelogItemKind id
|
embedRenderMessage ''UniWorX ''ChangelogItemKind id
|
||||||
embedRenderMessage ''UniWorX ''RoomReference' $ dropSuffix "'"
|
embedRenderMessage ''UniWorX ''RoomReference' $ dropSuffix "'"
|
||||||
|
|
||||||
embedRenderMessage ''UniWorX ''AuthenticationMode id
|
|
||||||
|
|
||||||
embedRenderMessage ''UniWorX ''RatingValidityException id
|
embedRenderMessage ''UniWorX ''RatingValidityException id
|
||||||
|
|
||||||
embedRenderMessage ''UniWorX ''UrlFieldMessage id
|
embedRenderMessage ''UniWorX ''UrlFieldMessage id
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Wolfgang Witt <Wolfgang.Witt@campus.lmu.de>
|
-- SPDX-FileCopyrightText: 2022-2025 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>, Sarah Vaupel <sarah.vaupel@ifi.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>, Wolfgang Witt <Wolfgang.Witt@campus.lmu.de>, David Mosbach <david.mosbach@uniworx.de>
|
||||||
--
|
--
|
||||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -11,11 +11,14 @@ module Foundation.Instances
|
|||||||
, unsafeHandler
|
, unsafeHandler
|
||||||
) where
|
) where
|
||||||
|
|
||||||
|
import qualified Prelude as P
|
||||||
|
|
||||||
import Import.NoFoundation
|
import Import.NoFoundation
|
||||||
|
|
||||||
import qualified Data.Text as Text
|
import qualified Data.Text as Text
|
||||||
import Data.List (inits)
|
import Data.List (inits)
|
||||||
|
|
||||||
|
import Yesod.Auth.OAuth2
|
||||||
import qualified Yesod.Core.Unsafe as Unsafe
|
import qualified Yesod.Core.Unsafe as Unsafe
|
||||||
import qualified Yesod.Auth.Message as Auth
|
import qualified Yesod.Auth.Message as Auth
|
||||||
|
|
||||||
@ -23,6 +26,7 @@ import Utils.Form
|
|||||||
import Auth.LDAP
|
import Auth.LDAP
|
||||||
import Auth.PWHash
|
import Auth.PWHash
|
||||||
import Auth.Dummy
|
import Auth.Dummy
|
||||||
|
import Auth.OAuth2
|
||||||
|
|
||||||
import qualified Foundation.Yesod.Session as UniWorX
|
import qualified Foundation.Yesod.Session as UniWorX
|
||||||
import qualified Foundation.Yesod.Middleware as UniWorX
|
import qualified Foundation.Yesod.Middleware as UniWorX
|
||||||
@ -41,6 +45,8 @@ import Foundation.DB
|
|||||||
|
|
||||||
import Network.Wai.Parse (lbsBackEnd)
|
import Network.Wai.Parse (lbsBackEnd)
|
||||||
|
|
||||||
|
import System.Environment (lookupEnv)
|
||||||
|
|
||||||
import UnliftIO.Pool (withResource)
|
import UnliftIO.Pool (withResource)
|
||||||
|
|
||||||
import qualified Control.Monad.State.Class as State
|
import qualified Control.Monad.State.Class as State
|
||||||
@ -128,21 +134,30 @@ instance YesodAuth UniWorX where
|
|||||||
-- Where to send a user after logout
|
-- Where to send a user after logout
|
||||||
logoutDest _ = NewsR
|
logoutDest _ = NewsR
|
||||||
-- Override the above two destinations when a Referer: header is present
|
-- Override the above two destinations when a Referer: header is present
|
||||||
redirectToReferer _ = True
|
redirectToReferer _ = False
|
||||||
|
|
||||||
loginHandler = do
|
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
|
toParent <- getRouteToParent
|
||||||
liftHandler . defaultLayout $ do
|
liftHandler . defaultLayout $ do
|
||||||
plugins <- getsYesod authPlugins
|
|
||||||
$logDebugS "Auth" $ "Enabled plugins: " <> Text.intercalate ", " (map apName plugins)
|
$logDebugS "Auth" $ "Enabled plugins: " <> Text.intercalate ", " (map apName plugins)
|
||||||
|
mPort <- liftIO $ lookupEnv "OAUTH2_SERVER_PORT"
|
||||||
setTitleI MsgLoginTitle
|
setTitleI MsgLoginTitle
|
||||||
$(widgetFile "login")
|
$(widgetFile "login")
|
||||||
|
|
||||||
authenticate = UniWorX.authenticate
|
authenticate = UniWorX.authenticate
|
||||||
|
|
||||||
authPlugins UniWorX{ appSettings' = AppSettings{..}, appLdapPool } = catMaybes
|
authPlugins UniWorX{ appSettings' = AppSettings{..}, appLdapPool, appAuthPlugins } = appAuthPlugins ++ catMaybes
|
||||||
[ flip campusLogin campusUserFailoverMode <$> appLdapPool
|
[ uncurry ldapLogin <$> appLdapPool
|
||||||
, Just . hashLogin $ pwHashAlgorithm appAuthPWHash
|
, Just . hashLogin $ pwHashAlgorithm appAuthPWHash
|
||||||
, dummyLogin <$ guard appAuthDummyLogin
|
, dummyLogin <$ guard appAuthDummyLogin
|
||||||
]
|
]
|
||||||
@ -157,6 +172,11 @@ instance YesodAuth UniWorX where
|
|||||||
|
|
||||||
addMessage Success . toHtml $ mr Auth.NowLoggedIn
|
addMessage Success . toHtml $ mr Auth.NowLoggedIn
|
||||||
|
|
||||||
|
-- onLogout = do
|
||||||
|
-- AppSettings{..} <- getsYesod appSettings'
|
||||||
|
-- when appSingleSignOn $ singleSignOut @UniWorX Nothing
|
||||||
|
|
||||||
|
|
||||||
onErrorHtml dest msg = do
|
onErrorHtml dest msg = do
|
||||||
addMessage Error $ toHtml msg
|
addMessage Error $ toHtml msg
|
||||||
redirect dest
|
redirect dest
|
||||||
|
|||||||
@ -75,6 +75,8 @@ breadcrumb :: ( BearerAuthSite UniWorX
|
|||||||
=> Route UniWorX
|
=> Route UniWorX
|
||||||
-> m Breadcrumb
|
-> m Breadcrumb
|
||||||
breadcrumb (AuthR _) = i18nCrumb MsgMenuLogin $ Just NewsR
|
breadcrumb (AuthR _) = i18nCrumb MsgMenuLogin $ Just NewsR
|
||||||
|
breadcrumb SOutR = i18nCrumb MsgLogout Nothing
|
||||||
|
breadcrumb SSOutR = i18nCrumb MsgSingleSignOut Nothing
|
||||||
breadcrumb (StaticR _) = i18nCrumb MsgBreadcrumbStatic Nothing
|
breadcrumb (StaticR _) = i18nCrumb MsgBreadcrumbStatic Nothing
|
||||||
breadcrumb (WellKnownR _) = i18nCrumb MsgBreadcrumbWellKnown Nothing
|
breadcrumb (WellKnownR _) = i18nCrumb MsgBreadcrumbWellKnown Nothing
|
||||||
breadcrumb MetricsR = i18nCrumb MsgBreadcrumbMetrics Nothing
|
breadcrumb MetricsR = i18nCrumb MsgBreadcrumbMetrics Nothing
|
||||||
@ -116,9 +118,10 @@ breadcrumb AdminErrMsgR = i18nCrumb MsgMenuAdminErrMsg $ Just
|
|||||||
breadcrumb AdminTokensR = i18nCrumb MsgMenuAdminTokens $ Just AdminR
|
breadcrumb AdminTokensR = i18nCrumb MsgMenuAdminTokens $ Just AdminR
|
||||||
breadcrumb AdminCrontabR = i18nCrumb MsgBreadcrumbAdminCrontab $ Just AdminR
|
breadcrumb AdminCrontabR = i18nCrumb MsgBreadcrumbAdminCrontab $ Just AdminR
|
||||||
breadcrumb AdminJobsR = i18nCrumb MsgBreadcrumbAdminJobs $ Just AdminCrontabR
|
breadcrumb AdminJobsR = i18nCrumb MsgBreadcrumbAdminJobs $ Just AdminCrontabR
|
||||||
|
breadcrumb AdminLdapR = i18nCrumb MsgBreadcrumbAdminLdap $ Just AdminR
|
||||||
breadcrumb AdminAvsR = i18nCrumb MsgMenuAvs $ Just AdminR
|
breadcrumb AdminAvsR = i18nCrumb MsgMenuAvs $ Just AdminR
|
||||||
breadcrumb AdminAvsUserR{} = i18nCrumb MsgAvsPersonInfo $ Just AdminAvsR
|
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 AdminProblemsR = i18nCrumb MsgProblemsHeading $ Just AdminR
|
||||||
breadcrumb ProblemUnreachableR = i18nCrumb MsgProblemsUnreachableHeading $ Just AdminProblemsR
|
breadcrumb ProblemUnreachableR = i18nCrumb MsgProblemsUnreachableHeading $ Just AdminProblemsR
|
||||||
breadcrumb ProblemWithoutAvsId = i18nCrumb MsgProblemsNoAvsIdHeading $ Just AdminProblemsR
|
breadcrumb ProblemWithoutAvsId = i18nCrumb MsgProblemsNoAvsIdHeading $ Just AdminProblemsR
|
||||||
@ -562,42 +565,37 @@ defaultLinks :: ( MonadHandler m
|
|||||||
, BearerAuthSite UniWorX
|
, BearerAuthSite UniWorX
|
||||||
) => m [Nav]
|
) => m [Nav]
|
||||||
defaultLinks = fmap catMaybes . mapM runMaybeT $ -- Define the menu items of the header.
|
defaultLinks = fmap catMaybes . mapM runMaybeT $ -- Define the menu items of the header.
|
||||||
[ return NavHeader
|
[ return NavHeaderContainer
|
||||||
{ navHeaderRole = NavHeaderSecondary
|
{ navHeaderRole = NavHeaderSecondary
|
||||||
, navIcon = IconMenuLogout
|
, navLabel = SomeMessage MsgMenuAccount
|
||||||
, navLink = NavLink
|
, navIcon = IconMenuAccount
|
||||||
{ navLabel = MsgMenuLogout
|
, navChildren =
|
||||||
, navRoute = AuthR LogoutR
|
[ NavLink
|
||||||
, navAccess' = NavAccessHandler $ is _Just <$> maybeAuthId
|
{ navLabel = MsgMenuLogout
|
||||||
, navType = NavTypeLink { navModal = False }
|
, navRoute = SSOutR -- AuthR LogoutR
|
||||||
, navQuick' = mempty
|
, navAccess' = NavAccessHandler $ is _Just <$> maybeAuthId
|
||||||
, navForceActive = False
|
, navType = NavTypeLink { navModal = False }
|
||||||
}
|
, navQuick' = mempty
|
||||||
}
|
, navForceActive = False
|
||||||
, return NavHeader
|
}
|
||||||
{ navHeaderRole = NavHeaderSecondary
|
, NavLink
|
||||||
, navIcon = IconMenuLogin
|
{ navLabel = MsgMenuLogin
|
||||||
, navLink = NavLink
|
, navRoute = AuthR LoginR
|
||||||
{ navLabel = MsgMenuLogin
|
, navAccess' = NavAccessHandler $ is _Nothing <$> maybeAuthId
|
||||||
, navRoute = AuthR LoginR
|
, navType = NavTypeLink { navModal = False }
|
||||||
, navAccess' = NavAccessHandler $ is _Nothing <$> maybeAuthId
|
, navQuick' = mempty
|
||||||
, navType = NavTypeLink { navModal = True }
|
, navForceActive = False
|
||||||
, navQuick' = mempty
|
}
|
||||||
, navForceActive = False
|
, NavLink
|
||||||
}
|
{ navLabel = MsgMenuProfile
|
||||||
}
|
, navRoute = ProfileR
|
||||||
, return NavHeader
|
, navAccess' = NavAccessHandler $ is _Just <$> maybeAuthId
|
||||||
{ navHeaderRole = NavHeaderSecondary
|
, navType = NavTypeLink { navModal = False }
|
||||||
, navIcon = IconMenuProfile
|
, navQuick' = mempty
|
||||||
, navLink = NavLink
|
, navForceActive = False
|
||||||
{ navLabel = MsgMenuProfile
|
}
|
||||||
, navRoute = ProfileR
|
]
|
||||||
, navAccess' = NavAccessHandler $ is _Just <$> maybeAuthId
|
}
|
||||||
, navType = NavTypeLink { navModal = False }
|
|
||||||
, navQuick' = mempty
|
|
||||||
, navForceActive = False
|
|
||||||
}
|
|
||||||
}
|
|
||||||
, do
|
, do
|
||||||
mCurrentRoute <- getCurrentRoute
|
mCurrentRoute <- getCurrentRoute
|
||||||
|
|
||||||
@ -876,8 +874,8 @@ defaultLinks = fmap catMaybes . mapM runMaybeT $ -- Define the menu items of the
|
|||||||
, navForceActive = False
|
, navForceActive = False
|
||||||
}
|
}
|
||||||
, NavLink
|
, NavLink
|
||||||
{ navLabel = MsgMenuLdap
|
{ navLabel = MsgMenuExternalUser
|
||||||
, navRoute = AdminLdapR
|
, navRoute = AdminExternalUserR
|
||||||
, navAccess' = NavAccessTrue
|
, navAccess' = NavAccessTrue
|
||||||
, navType = NavTypeLink { navModal = False }
|
, navType = NavTypeLink { navModal = False }
|
||||||
, navQuick' = mempty
|
, navQuick' = mempty
|
||||||
@ -1264,8 +1262,8 @@ pageActions (AdminUserR cID) = return
|
|||||||
, navRoute = UserPasswordR cID
|
, navRoute = UserPasswordR cID
|
||||||
, navAccess' = NavAccessDB $ do
|
, navAccess' = NavAccessDB $ do
|
||||||
uid <- decrypt cID
|
uid <- decrypt cID
|
||||||
User{userAuthentication} <- get404 uid
|
User{userPasswordHash} <- get404 uid
|
||||||
return $ is _AuthPWHash userAuthentication
|
return $ is _Just userPasswordHash
|
||||||
, navType = NavTypeLink { navModal = True }
|
, navType = NavTypeLink { navModal = True }
|
||||||
, navQuick' = mempty
|
, navQuick' = mempty
|
||||||
, navForceActive = False
|
, navForceActive = False
|
||||||
|
|||||||
@ -157,6 +157,10 @@ siteLayout' overrideHeading widget = do
|
|||||||
|
|
||||||
isAuth <- isJust <$> maybeAuthId
|
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
|
now <- liftIO getCurrentTime
|
||||||
|
|
||||||
muid <- maybeAuthPair
|
muid <- maybeAuthPair
|
||||||
|
|||||||
@ -67,7 +67,7 @@ data UniWorX = UniWorX
|
|||||||
, appStatic :: EmbeddedStatic -- ^ Settings for static file serving.
|
, appStatic :: EmbeddedStatic -- ^ Settings for static file serving.
|
||||||
, appConnPool :: forall m. MonadIO m => Custom.Pool' m DBConnLabel DBConnUseState SqlBackend -- ^ Database connection pool.
|
, appConnPool :: forall m. MonadIO m => Custom.Pool' m DBConnLabel DBConnUseState SqlBackend -- ^ Database connection pool.
|
||||||
, appSmtpPool :: Maybe SMTPPool
|
, appSmtpPool :: Maybe SMTPPool
|
||||||
, appLdapPool :: Maybe (Failover (LdapConf, LdapPool))
|
, appLdapPool :: Maybe (LdapConf, LdapPool) -- TODO: reintroduce Failover
|
||||||
, appWidgetMemcached :: Maybe Memcached.Connection -- ^ Actually a proper pool
|
, appWidgetMemcached :: Maybe Memcached.Connection -- ^ Actually a proper pool
|
||||||
, appHttpManager :: Manager
|
, appHttpManager :: Manager
|
||||||
, appLogger :: (ReleaseKey, TVar Logger)
|
, appLogger :: (ReleaseKey, TVar Logger)
|
||||||
@ -84,6 +84,7 @@ data UniWorX = UniWorX
|
|||||||
, appUploadCache :: Maybe MinioConn
|
, appUploadCache :: Maybe MinioConn
|
||||||
, appVerpSecret :: VerpSecret
|
, appVerpSecret :: VerpSecret
|
||||||
, appAuthKey :: Auth.Key
|
, appAuthKey :: Auth.Key
|
||||||
|
, appAuthPlugins :: [AuthPlugin UniWorX]
|
||||||
, appPersonalisedSheetFilesSeedKey :: PersonalisedSheetFilesSeedKey
|
, appPersonalisedSheetFilesSeedKey :: PersonalisedSheetFilesSeedKey
|
||||||
, appVolatileClusterSettingsCache :: TVar VolatileClusterSettingsCache
|
, appVolatileClusterSettingsCache :: TVar VolatileClusterSettingsCache
|
||||||
, appStartTime :: UTCTime -- for Status Page
|
, appStartTime :: UTCTime -- for Status Page
|
||||||
|
|||||||
@ -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
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
module Foundation.Types
|
module Foundation.Types
|
||||||
( UpsertCampusUserMode(..)
|
( UpsertUserMode(..)
|
||||||
, _UpsertCampusUserLoginLdap, _UpsertCampusUserLoginDummy, _UpsertCampusUserLoginOther, _UpsertCampusUserLdapSync, _UpsertCampusUserGuessUser
|
, _UpsertUserLogin, _UpsertUserLoginDummy, _UpsertUserLoginOther, _UpsertUserSync, _UpsertUserGuessUser
|
||||||
, _upsertCampusUserIdent
|
, _upsertUserSource, _upsertUserIdent
|
||||||
|
, UpsertUserData(..)
|
||||||
|
, _UpsertUserDataAzure, _UpsertUserDataLdap
|
||||||
|
, _upsertUserAzureTenantId, _upsertUserAzureData, _upsertUserLdapHost, _upsertUserLdapData
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Import.NoFoundation
|
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
|
-- TODO: rename?
|
||||||
makePrisms ''UpsertCampusUserMode
|
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
|
||||||
|
|||||||
@ -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
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
module Foundation.Yesod.Auth
|
module Foundation.Yesod.Auth
|
||||||
( authenticate
|
( authenticate
|
||||||
, ldapLookupAndUpsert
|
, userLookupAndUpsert
|
||||||
, upsertCampusUser
|
, upsertUser, maybeUpsertUser
|
||||||
, decodeUserTest
|
, decodeUserTest
|
||||||
, CampusUserConversionException(..)
|
, DecodeUserException(..)
|
||||||
, campusUserFailoverMode, updateUserLanguage
|
, updateUserLanguage
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Import.NoFoundation hiding (authenticate)
|
import Import.NoFoundation hiding (authenticate)
|
||||||
|
|
||||||
|
import Auth.Dummy (apDummy)
|
||||||
|
import Auth.LDAP
|
||||||
|
import Auth.OAuth2
|
||||||
|
import Auth.PWHash (apHash)
|
||||||
|
|
||||||
import Foundation.Type
|
import Foundation.Type
|
||||||
import Foundation.Types
|
import Foundation.Types
|
||||||
import Foundation.I18n
|
import Foundation.I18n
|
||||||
|
|
||||||
import Handler.Utils.Profile
|
-- import Handler.Utils.Profile
|
||||||
import Handler.Utils.LdapSystemFunctions
|
import Handler.Utils.LdapSystemFunctions
|
||||||
import Handler.Utils.Memcached
|
import Handler.Utils.Memcached
|
||||||
import Foundation.Authorization (AuthorizationCacheKey(..))
|
import Foundation.Authorization (AuthorizationCacheKey(..))
|
||||||
|
|
||||||
import Yesod.Auth.Message
|
import Yesod.Auth.Message
|
||||||
import Auth.LDAP
|
import Yesod.Auth.OAuth2 (getAccessToken, getRefreshToken)
|
||||||
import Auth.PWHash (apHash)
|
|
||||||
import Auth.Dummy (apDummy)
|
|
||||||
|
|
||||||
import qualified Data.CaseInsensitive as CI
|
|
||||||
import qualified Control.Monad.Catch as C (Handler(..))
|
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 as Text
|
||||||
import qualified Data.Text.Encoding 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 Ldap.Client as Ldap
|
||||||
|
|
||||||
-- 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)
|
|
||||||
|
|
||||||
|
|
||||||
authenticate :: ( MonadHandler m, HandlerSite m ~ UniWorX
|
authenticate :: ( MonadHandler m, HandlerSite m ~ UniWorX
|
||||||
, YesodPersist UniWorX, BackendCompatible SqlBackend (YesodPersistBackend UniWorX)
|
, YesodPersist UniWorX, BackendCompatible SqlBackend (YesodPersistBackend UniWorX)
|
||||||
, YesodAuth UniWorX, UserId ~ AuthId UniWorX
|
, YesodAuth UniWorX, UserId ~ AuthId UniWorX
|
||||||
)
|
)
|
||||||
=> Creds UniWorX -> m (AuthenticationResult UniWorX)
|
=> Creds UniWorX
|
||||||
|
-> m (AuthenticationResult UniWorX)
|
||||||
authenticate creds@Creds{..} = liftHandler . runDB . withReaderT projectBackend $ do
|
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
|
now <- liftIO getCurrentTime
|
||||||
|
|
||||||
let
|
let
|
||||||
uAuth = UniqueAuthentication $ CI.mk credsIdent
|
uAuth = UniqueAuthentication $ CI.mk credsIdent
|
||||||
upsertMode = creds ^? _upsertCampusUserMode
|
upsertMode = creds ^? _upsertUserMode
|
||||||
|
|
||||||
isDummy = is (_Just . _UpsertCampusUserLoginDummy) upsertMode
|
isDummy = is (_Just . _UpsertUserLoginDummy) upsertMode
|
||||||
isOther = is (_Just . _UpsertCampusUserLoginOther) upsertMode
|
isOther = is (_Just . _UpsertUserLoginOther) upsertMode
|
||||||
|
|
||||||
excRecovery res
|
excRecovery res
|
||||||
| isDummy || isOther
|
| isDummy || isOther
|
||||||
@ -77,21 +74,21 @@ authenticate creds@Creds{..} = liftHandler . runDB . withReaderT projectBackend
|
|||||||
= return res
|
= return res
|
||||||
|
|
||||||
excHandlers =
|
excHandlers =
|
||||||
[ C.Handler $ \case
|
[ C.Handler $ \(fExc :: FetchUserDataException) -> case fExc of
|
||||||
CampusUserNoResult -> do
|
FetchUserDataNoResult -> do
|
||||||
$logWarnS "LDAP" $ "User lookup failed after successful login for " <> credsIdent
|
$logWarnS "FetchUserException" $ "User lookup failed after successful login for " <> credsIdent
|
||||||
excRecovery . UserError $ IdentifierNotFound credsIdent
|
excRecovery . UserError $ IdentifierNotFound credsIdent
|
||||||
CampusUserAmbiguous -> do
|
FetchUserDataAmbiguous -> do
|
||||||
$logWarnS "LDAP" $ "Multiple LDAP results for " <> credsIdent
|
$logWarnS "FetchUserException" $ "Multiple User results for " <> credsIdent
|
||||||
excRecovery . UserError $ IdentifierNotFound credsIdent
|
excRecovery . UserError $ IdentifierNotFound credsIdent
|
||||||
err -> do
|
err -> do
|
||||||
$logErrorS "LDAP" $ tshow err
|
$logErrorS "FetchUserException" $ tshow err
|
||||||
mr <- getMessageRender
|
mr <- getMessageRender
|
||||||
excRecovery . ServerError $ mr MsgInternalLdapError
|
excRecovery . ServerError $ mr MsgInternalLoginError
|
||||||
, C.Handler $ \(cExc :: CampusUserConversionException) -> do
|
, C.Handler $ \(dExc :: DecodeUserException) -> do
|
||||||
$logErrorS "LDAP" $ tshow cExc
|
$logErrorS "Auth" $ tshow dExc
|
||||||
mr <- getMessageRender
|
mr <- getMessageRender
|
||||||
excRecovery . ServerError $ mr cExc
|
excRecovery . ServerError $ mr dExc
|
||||||
]
|
]
|
||||||
|
|
||||||
acceptExisting :: SqlPersistT (HandlerFor UniWorX) (AuthenticationResult UniWorX)
|
acceptExisting :: SqlPersistT (HandlerFor UniWorX) (AuthenticationResult UniWorX)
|
||||||
@ -107,83 +104,140 @@ authenticate creds@Creds{..} = liftHandler . runDB . withReaderT projectBackend
|
|||||||
| not isDummy -> res <$ update uid [ UserLastAuthentication =. Just now ]
|
| not isDummy -> res <$ update uid [ UserLastAuthentication =. Just now ]
|
||||||
_other -> return res
|
_other -> return res
|
||||||
|
|
||||||
$logDebugS "auth" $ tshow Creds{..}
|
$logDebugS "Auth" $ tshow Creds{..}
|
||||||
ldapPool' <- getsYesod $ view _appLdapPool
|
|
||||||
|
|
||||||
flip catches excHandlers $ case ldapPool' of
|
flip catches excHandlers $ if
|
||||||
Just ldapPool
|
| not isDummy, not isOther
|
||||||
| Just upsertMode' <- upsertMode -> do
|
, Just upsertMode' <- upsertMode -> fetchUserData Creds{..} >>= \case
|
||||||
ldapData <- campusUser ldapPool campusUserFailoverMode Creds{..}
|
Just userData -> do
|
||||||
$logDebugS "LDAP" $ "Successful LDAP lookup: " <> tshow ldapData
|
$logDebugS "Auth" $ "Successful user data lookup: " <> tshow userData
|
||||||
Authenticated . entityKey <$> upsertCampusUser upsertMode' ldapData
|
Authenticated . entityKey <$> upsertUser upsertMode' userData
|
||||||
_other
|
Nothing
|
||||||
|
-> throwM FetchUserDataNoResult
|
||||||
|
| otherwise
|
||||||
-> acceptExisting
|
-> acceptExisting
|
||||||
|
|
||||||
|
|
||||||
data CampusUserConversionException
|
data DecodeUserException
|
||||||
= CampusUserInvalidIdent
|
= DecodeUserInvalidIdent
|
||||||
| CampusUserInvalidEmail
|
| DecodeUserInvalidEmail
|
||||||
| CampusUserInvalidDisplayName
|
| DecodeUserInvalidDisplayName
|
||||||
| CampusUserInvalidGivenName
|
| DecodeUserInvalidGivenName
|
||||||
| CampusUserInvalidSurname
|
| DecodeUserInvalidSurname
|
||||||
| CampusUserInvalidTitle
|
| DecodeUserInvalidTitle
|
||||||
-- | CampusUserInvalidMatriculation
|
| DecodeUserInvalidFeaturesOfStudy Text
|
||||||
| CampusUserInvalidFeaturesOfStudy Text
|
| DecodeUserInvalidAssociatedSchools Text
|
||||||
| CampusUserInvalidAssociatedSchools Text
|
|
||||||
deriving (Eq, Ord, Read, Show, Generic)
|
deriving (Eq, Ord, Read, Show, Generic)
|
||||||
deriving anyclass (Exception)
|
deriving anyclass (Exception)
|
||||||
|
|
||||||
_upsertCampusUserMode :: Traversal' (Creds UniWorX) UpsertCampusUserMode
|
|
||||||
_upsertCampusUserMode mMode cs@Creds{..}
|
_upsertUserMode :: Traversal' (Creds UniWorX) UpsertUserMode
|
||||||
| credsPlugin == apDummy = setMode <$> mMode (UpsertCampusUserLoginDummy $ CI.mk credsIdent)
|
_upsertUserMode mMode cs@Creds{..}
|
||||||
| credsPlugin == apLdap = setMode <$> mMode UpsertCampusUserLoginLdap
|
| credsPlugin == apDummy = setMode <$> mMode (UpsertUserLoginDummy $ CI.mk credsIdent)
|
||||||
| otherwise = setMode <$> mMode (UpsertCampusUserLoginOther $ CI.mk credsIdent)
|
| credsPlugin `elem` loginAPs
|
||||||
|
= setMode <$> mMode (UpsertUserLogin credsPlugin)
|
||||||
|
| otherwise = setMode <$> mMode (UpsertUserLoginOther $ CI.mk credsIdent)
|
||||||
where
|
where
|
||||||
setMode UpsertCampusUserLoginLdap
|
setMode UpsertUserLogin{..} | upsertUserSource `elem` loginAPs
|
||||||
= cs{ credsPlugin = apLdap }
|
= cs { credsPlugin = upsertUserSource }
|
||||||
setMode (UpsertCampusUserLoginDummy ident)
|
setMode UpsertUserLoginDummy{..}
|
||||||
= cs{ credsPlugin = apDummy
|
= cs { credsPlugin = apDummy
|
||||||
, credsIdent = CI.original ident
|
, credsIdent = CI.original upsertUserIdent
|
||||||
}
|
}
|
||||||
setMode (UpsertCampusUserLoginOther ident)
|
setMode UpsertUserLoginOther{..}
|
||||||
= cs{ credsPlugin = bool defaultOther credsPlugin (credsPlugin /= apDummy && credsPlugin /= apLdap)
|
= cs { credsPlugin = bool defaultOther credsPlugin (credsPlugin `notElem` [apDummy, apLdap, apAzure])
|
||||||
, credsIdent = CI.original ident
|
, credsIdent = CI.original upsertUserIdent
|
||||||
}
|
}
|
||||||
setMode _ = cs
|
setMode _ = cs
|
||||||
|
|
||||||
|
loginAPs = [ apAzure, apLdap ]
|
||||||
defaultOther = apHash
|
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!
|
userLookupAndUpsert :: forall m.
|
||||||
upsertCampusUserByCn :: forall m.
|
( MonadHandler m
|
||||||
( MonadHandler m, HandlerSite m ~ UniWorX
|
, HandlerSite m ~ UniWorX
|
||||||
, MonadThrow m
|
, MonadMask m
|
||||||
)
|
, MonadUnliftIO m
|
||||||
=> Text -> SqlPersistT m (Entity User)
|
)
|
||||||
upsertCampusUserByCn persNo = upsertCampusUser UpsertCampusUserGuessUser [(ldapPrimaryKey,[Text.encodeUtf8 persNo])]
|
=> 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.
|
data FetchUserDataException
|
||||||
( MonadHandler m, HandlerSite m ~ UniWorX
|
= FetchUserDataNoResult
|
||||||
, MonadCatch m
|
| FetchUserDataAmbiguous
|
||||||
)
|
| FetchUserDataException
|
||||||
=> UpsertCampusUserMode -> Ldap.AttrList [] -> SqlPersistT m (Entity User)
|
deriving (Eq, Ord, Read, Show, Generic)
|
||||||
upsertCampusUser upsertMode ldapData = do
|
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
|
now <- liftIO getCurrentTime
|
||||||
userDefaultConf <- getsYesod $ view _appUserDefaults
|
userDefaultConf <- getsYesod $ view _appUserDefaults
|
||||||
|
|
||||||
(newUser,userUpdate) <- decodeUser now userDefaultConf upsertMode ldapData
|
(newUser,userUpdate) <- decodeUser now userDefaultConf upsertMode ldapData
|
||||||
|
--TODO: newUser should be associated with a company and company supervisor through Handler.Utils.Company.upsertUserCompany, but this is called by upsertAvsUser already - conflict?
|
||||||
|
|
||||||
oldUsers <- for (userLdapPrimaryKey newUser) $ \pKey -> selectKeysList [ UserLdapPrimaryKey ==. Just pKey ] []
|
oldUsers <- selectKeysList [ UserIdent ==. userIdent newUser ] []
|
||||||
|
|
||||||
user@(Entity userId userRec) <- case oldUsers of
|
user@(Entity userId userRec) <- case oldUsers of
|
||||||
Just [oldUserId] -> updateGetEntity oldUserId userUpdate
|
Just [oldUserId] -> updateGetEntity oldUserId userUpdate
|
||||||
@ -192,30 +246,36 @@ upsertCampusUser upsertMode ldapData = do
|
|||||||
(newUser ^. _userFirstName)
|
(newUser ^. _userFirstName)
|
||||||
(newUser ^. _userSurname)
|
(newUser ^. _userSurname)
|
||||||
(userRec ^. _userDisplayName)) $
|
(userRec ^. _userDisplayName)) $
|
||||||
update userId [ UserDisplayName =. (newUser ^. _userDisplayName) ] -- update invalid display names only
|
update userId [ UserDisplayName =. (newUser ^. _userDisplayName) ]
|
||||||
when (validEmail' (userRec ^. _userEmail)) $ do -- RECALL: userRec already contains basic updates
|
when (validEmail' (userRec ^. _userEmail)) $ do
|
||||||
let emUps = [ UserDisplayEmail =. (newUser ^. _userEmail) | not (validEmail' (userRec ^. _userDisplayEmail)) ]
|
let emUps = [ UserDisplayEmail =. (newUser ^. _userEmail) | not (validEmail' (userRec ^. _userDisplayEmail)) ]
|
||||||
++ [ UserAuthentication =. AuthLDAP | is _AuthNoLogin (userRec ^. _userAuthentication) ]
|
++ [ UserAuthentication =. AuthLDAP | is _AuthNoLogin (userRec ^. _userAuthentication) ]
|
||||||
update userId emUps -- update already checks whether list is empty
|
unless (null emUps) $ update userId emUps
|
||||||
-- Attempt to update ident, too:
|
-- Attempt to update ident, too:
|
||||||
unless (validEmail' (userRec ^. _userIdent)) $
|
unless (validEmail' (userRec ^. _userIdent)) $
|
||||||
void $ maybeCatchAll (update userId [ UserIdent =. (newUser ^. _userEmail) ] >> return (Just ()))
|
void $ maybeCatchAll (update userId [ UserIdent =. (newUser ^. _userEmail) ] >> return (Just ()))
|
||||||
|
|
||||||
let
|
let
|
||||||
userSystemFunctions = determineSystemFunctions . Set.fromList $ map CI.mk userSystemFunctions'
|
userSystemFunctions = determineSystemFunctions . Set.fromList $ map CI.mk userSystemFunctions'
|
||||||
userSystemFunctions' = do
|
userSystemFunctions' = concat $ upsertData <&> \case
|
||||||
(k, v) <- ldapData
|
UpsertUserDataAzure{..} -> do
|
||||||
guard $ k == ldapAffiliation
|
(_k, v) <- upsertUserAzureData
|
||||||
v' <- v
|
v' <- v
|
||||||
Right str <- return $ Text.decodeUtf8' v'
|
Right str <- return $ Text.decodeUtf8' v'
|
||||||
assertM' (not . Text.null) $ Text.strip str
|
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
|
iforM_ userSystemFunctions $ \func preset -> do
|
||||||
memcachedByInvalidate (AuthCacheSystemFunctionList func) $ Proxy @(Set UserId)
|
memcachedByInvalidate (AuthCacheSystemFunctionList func) $ Proxy @(Set UserId)
|
||||||
if | preset -> void $ upsert (UserSystemFunction userId func False False) []
|
if | preset -> void $ upsert (UserSystemFunction userId func False False) []
|
||||||
| otherwise -> deleteWhere [UserSystemFunctionUser ==. userId, UserSystemFunctionFunction ==. func, UserSystemFunctionIsOptOut ==. False, UserSystemFunctionManual ==. False]
|
| otherwise -> deleteWhere [UserSystemFunctionUser ==. userId, UserSystemFunctionFunction ==. func, UserSystemFunctionIsOptOut ==. False, UserSystemFunctionManual ==. False]
|
||||||
|
|
||||||
return user
|
return $ Just user
|
||||||
|
|
||||||
decodeUserTest :: (MonadHandler m, HandlerSite m ~ UniWorX, MonadCatch m)
|
decodeUserTest :: (MonadHandler m, HandlerSite m ~ UniWorX, MonadCatch m)
|
||||||
=> Maybe UserIdent -> Ldap.AttrList [] -> m (Either CampusUserConversionException (User, [Update User]))
|
=> Maybe UserIdent -> Ldap.AttrList [] -> m (Either CampusUserConversionException (User, [Update User]))
|
||||||
@ -229,8 +289,8 @@ decodeUserTest mbIdent ldapData = do
|
|||||||
decodeUser :: (MonadThrow m) => UTCTime -> UserDefaultConf -> UpsertCampusUserMode -> Ldap.AttrList [] -> m (User,_)
|
decodeUser :: (MonadThrow m) => UTCTime -> UserDefaultConf -> UpsertCampusUserMode -> Ldap.AttrList [] -> m (User,_)
|
||||||
decodeUser now UserDefaultConf{..} upsertMode ldapData = do
|
decodeUser now UserDefaultConf{..} upsertMode ldapData = do
|
||||||
let
|
let
|
||||||
userTelephone = decodeLdap ldapUserTelephone <&> canonicalPhone
|
userTelephone = decodeLdap ldapUserTelephone
|
||||||
userMobile = decodeLdap ldapUserMobile <&> canonicalPhone
|
userMobile = decodeLdap ldapUserMobile
|
||||||
userCompanyPersonalNumber = decodeLdap ldapUserFraportPersonalnummer
|
userCompanyPersonalNumber = decodeLdap ldapUserFraportPersonalnummer
|
||||||
userCompanyDepartment = decodeLdap ldapUserFraportAbteilung
|
userCompanyDepartment = decodeLdap ldapUserFraportAbteilung
|
||||||
|
|
||||||
@ -250,12 +310,14 @@ decodeUser now UserDefaultConf{..} upsertMode ldapData = do
|
|||||||
-- (maybeThrow CampusUserInvalidDisplayName . checkDisplayName userTitle userFirstName userSurname)
|
-- (maybeThrow CampusUserInvalidDisplayName . checkDisplayName userTitle userFirstName userSurname)
|
||||||
|
|
||||||
userIdent <- if
|
userIdent <- if
|
||||||
| [bs] <- ldapMap !!! ldapUserPrincipalName
|
| Just azureData <- mbAzureData
|
||||||
, Right userIdent' <- CI.mk <$> Text.decodeUtf8' bs
|
, [Text.decodeUtf8' -> Right azureUserPrincipalName'] <- azureData !!! azureUserPrincipalName
|
||||||
, hasn't _upsertCampusUserIdent upsertMode || has (_upsertCampusUserIdent . only userIdent') upsertMode
|
, Just azureUserPrincipalName'' <- assertM' (not . Text.null) $ Text.strip azureUserPrincipalName'
|
||||||
-> return userIdent'
|
-> return $ CI.mk azureUserPrincipalName''
|
||||||
| Just userIdent' <- upsertMode ^? _upsertCampusUserIdent
|
| Just ldapData <- mbLdapData
|
||||||
-> return userIdent'
|
, [Text.decodeUtf8' -> Right ldapPrimaryKey'] <- ldapData !!! ldapPrimaryKey
|
||||||
|
, Just ldapPrimaryKey'' <- assertM' (not . Text.null) $ Text.strip ldapPrimaryKey'
|
||||||
|
-> return $ CI.mk ldapPrimaryKey''
|
||||||
| otherwise
|
| otherwise
|
||||||
-> throwM CampusUserInvalidIdent
|
-> throwM CampusUserInvalidIdent
|
||||||
|
|
||||||
@ -277,39 +339,40 @@ decodeUser now UserDefaultConf{..} upsertMode ldapData = do
|
|||||||
|
|
||||||
let
|
let
|
||||||
newUser = User
|
newUser = User
|
||||||
{ userMaxFavourites = userDefaultMaxFavourites
|
{ userMaxFavourites = userDefaultMaxFavourites
|
||||||
, userMaxFavouriteTerms = userDefaultMaxFavouriteTerms
|
, userMaxFavouriteTerms = userDefaultMaxFavouriteTerms
|
||||||
, userTheme = userDefaultTheme
|
, userTheme = userDefaultTheme
|
||||||
, userDateTimeFormat = userDefaultDateTimeFormat
|
, userDateTimeFormat = userDefaultDateTimeFormat
|
||||||
, userDateFormat = userDefaultDateFormat
|
, userDateFormat = userDefaultDateFormat
|
||||||
, userTimeFormat = userDefaultTimeFormat
|
, userTimeFormat = userDefaultTimeFormat
|
||||||
, userDownloadFiles = userDefaultDownloadFiles
|
, userDownloadFiles = userDefaultDownloadFiles
|
||||||
, userWarningDays = userDefaultWarningDays
|
, userWarningDays = userDefaultWarningDays
|
||||||
, userShowSex = userDefaultShowSex
|
, userShowSex = userDefaultShowSex
|
||||||
, userSex = Nothing
|
, userSex = Nothing
|
||||||
, userBirthday = Nothing
|
, userBirthday = Nothing
|
||||||
, userExamOfficeGetSynced = userDefaultExamOfficeGetSynced
|
, userTitle = Nothing
|
||||||
, userExamOfficeGetLabels = userDefaultExamOfficeGetLabels
|
, userExamOfficeGetSynced = userDefaultExamOfficeGetSynced
|
||||||
, userNotificationSettings = def
|
, userExamOfficeGetLabels = userDefaultExamOfficeGetLabels
|
||||||
, userLanguages = Nothing
|
, userNotificationSettings = def
|
||||||
, userCsvOptions = def
|
, userCsvOptions = def
|
||||||
, userTokensIssuedAfter = Nothing
|
, userTokensIssuedAfter = Nothing
|
||||||
, userCreated = now
|
, userDisplayEmail = userEmail
|
||||||
, userLastLdapSynchronisation = Just now
|
, userMatrikelnummer = Nothing -- TODO: not known from Azure/LDAP, must be derived from REST interface to AVS
|
||||||
, userDisplayName = userDisplayName
|
, userPostAddress = Nothing -- TODO: not known from Azure/LDAP, must be derived from REST interface to AVS
|
||||||
, userDisplayEmail = userEmail
|
, userPostLastUpdate = Nothing
|
||||||
, userMatrikelnummer = Nothing -- not known from LDAP, must be derived from REST interface to AVS TODO
|
, userPinPassword = Nothing -- must be derived via AVS
|
||||||
, userPostAddress = Nothing -- not known from LDAP, must be derived from REST interface to AVS TODO
|
, userPrefersPostal = userDefaultPrefersPostal
|
||||||
, userPostLastUpdate = Nothing
|
, userPasswordHash = Nothing
|
||||||
, userPinPassword = Nothing -- must be derived via AVS
|
, userLastAuthentication = Nothing
|
||||||
, userPrefersPostal = userDefaultPrefersPostal
|
, userCreated = now
|
||||||
|
, userLastSync = Just now
|
||||||
, ..
|
, ..
|
||||||
}
|
}
|
||||||
userUpdate =
|
userUpdate =
|
||||||
[ UserLastAuthentication =. Just now | isLogin ] ++
|
[ UserLastAuthentication =. Just now | isLogin ] ++
|
||||||
[ UserEmail =. userEmail | validEmail' userEmail ] ++
|
[ UserEmail =. userEmail | validEmail' userEmail ] ++
|
||||||
[
|
[
|
||||||
-- UserDisplayName =. userDisplayName -- not updated here, since users are allowed to change their DisplayName; see line 191
|
-- UserDisplayName =. userDisplayName -- not updated here, since users are allowed to change their DisplayName; see line 272
|
||||||
UserFirstName =. userFirstName
|
UserFirstName =. userFirstName
|
||||||
, UserSurname =. userSurname
|
, UserSurname =. userSurname
|
||||||
, UserLastLdapSynchronisation =. Just now
|
, UserLastLdapSynchronisation =. Just now
|
||||||
@ -322,15 +385,21 @@ decodeUser now UserDefaultConf{..} upsertMode ldapData = do
|
|||||||
return (newUser, userUpdate)
|
return (newUser, userUpdate)
|
||||||
|
|
||||||
where
|
where
|
||||||
ldapMap :: Map.Map Ldap.Attr [Ldap.AttrValue] -- Recall: Ldap.AttrValue == ByteString
|
mbAzureData :: Maybe (Map Text [ByteString])
|
||||||
ldapMap = Map.fromListWith (++) $ ldapData <&> second (filter (not . ByteString.null))
|
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
|
-- just returns Nothing on error, pure
|
||||||
decodeLdap :: Ldap.Attr -> Maybe Text
|
decodeAzure :: Map Text [ByteString] -> Text -> Maybe Text
|
||||||
decodeLdap attr = listToMaybe . rights $ Text.decodeUtf8' <$> ldapMap !!! attr
|
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
|
-- decodeAzure' :: Map Text [ByteString] -> Text -> Text
|
||||||
decodeLdap' = fromMaybe "" . decodeLdap
|
-- 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
|
-- 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' :: (Exception e) => Ldap.Attr -> e -> m (Maybe Text)
|
||||||
-- decodeLdap' attr err
|
-- decodeLdap' attr err
|
||||||
@ -342,11 +411,11 @@ decodeUser now UserDefaultConf{..} upsertMode ldapData = do
|
|||||||
|
|
||||||
-- only accepts the first successful decoding, ignoring all others, but failing if there is none
|
-- 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 :: (MonadThrow m, Exception e) => Ldap.Attr -> e -> m Text
|
||||||
decodeLdap1 attr err
|
-- decodeLdap1 ldapData attr err
|
||||||
| (h:_) <- rights vs = return h
|
-- | (h:_) <- rights vs = return h
|
||||||
| otherwise = throwM err
|
-- | otherwise = throwM err
|
||||||
where
|
-- where
|
||||||
vs = Text.decodeUtf8' <$> (ldapMap !!! attr)
|
-- vs = Text.decodeUtf8' <$> (ldapData !!! attr)
|
||||||
|
|
||||||
-- accept and merge one or more successful decodings, ignoring all others
|
-- accept and merge one or more successful decodings, ignoring all others
|
||||||
-- decodeLdapN attr err
|
-- decodeLdapN attr err
|
||||||
@ -356,6 +425,17 @@ decodeUser now UserDefaultConf{..} upsertMode ldapData = do
|
|||||||
-- where
|
-- where
|
||||||
-- vs = Text.decodeUtf8' <$> (ldapMap !!! attr)
|
-- 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 :: MonadIO m => UserId -> SqlPersistT m ()
|
||||||
associateUserSchoolsByTerms uid = do
|
associateUserSchoolsByTerms uid = do
|
||||||
@ -370,11 +450,14 @@ associateUserSchoolsByTerms uid = do
|
|||||||
, userSchoolIsOptOut = False
|
, userSchoolIsOptOut = False
|
||||||
}
|
}
|
||||||
|
|
||||||
updateUserLanguage :: ( MonadHandler m, HandlerSite m ~ UniWorX
|
|
||||||
|
updateUserLanguage :: ( MonadHandler m
|
||||||
|
, HandlerSite m ~ UniWorX
|
||||||
, YesodAuth UniWorX
|
, YesodAuth UniWorX
|
||||||
, UserId ~ AuthId UniWorX
|
, UserId ~ AuthId UniWorX
|
||||||
)
|
)
|
||||||
=> Maybe Lang -> SqlPersistT m (Maybe Lang)
|
=> Maybe Lang
|
||||||
|
-> SqlPersistT m (Maybe Lang)
|
||||||
updateUserLanguage (Just lang) = do
|
updateUserLanguage (Just lang) = do
|
||||||
unless (lang `elem` appLanguages) $
|
unless (lang `elem` appLanguages) $
|
||||||
invalidArgs ["Unsupported language"]
|
invalidArgs ["Unsupported language"]
|
||||||
@ -405,7 +488,4 @@ updateUserLanguage Nothing = runMaybeT $ do
|
|||||||
setRegisteredCookie CookieLang lang
|
setRegisteredCookie CookieLang lang
|
||||||
return lang
|
return lang
|
||||||
|
|
||||||
campusUserFailoverMode :: FailoverMode
|
embedRenderMessage ''UniWorX ''DecodeUserException id
|
||||||
campusUserFailoverMode = FailoverUnlimited
|
|
||||||
|
|
||||||
embedRenderMessage ''UniWorX ''CampusUserConversionException id
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
-- SPDX-FileCopyrightText: 2022-2025 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>
|
-- SPDX-FileCopyrightText: 2022-202025 Sarah Vaupel <sarah.vaupel@uniworx.de>,Gregor Kleen <gregor.kleen@ifi.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>
|
||||||
--
|
--
|
||||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -38,7 +38,11 @@ import Handler.Admin.ErrorMessage as Handler.Admin
|
|||||||
import Handler.Admin.Tokens as Handler.Admin
|
import Handler.Admin.Tokens as Handler.Admin
|
||||||
import Handler.Admin.Crontab as Handler.Admin
|
import Handler.Admin.Crontab as Handler.Admin
|
||||||
import Handler.Admin.Avs as Handler.Admin
|
import Handler.Admin.Avs as Handler.Admin
|
||||||
import Handler.Admin.Ldap as Handler.Admin
|
import Handler.Admin.ExternalUser as Handler.Admin
|
||||||
|
|
||||||
|
-- avoids repetition of local definitions
|
||||||
|
single :: (k,a) -> Map k a
|
||||||
|
single = uncurry Map.singleton
|
||||||
|
|
||||||
|
|
||||||
-- Types and Template Haskell
|
-- Types and Template Haskell
|
||||||
|
|||||||
74
src/Handler/Admin/ExternalUser.hs
Normal file
74
src/Handler/Admin/ExternalUser.hs
Normal 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")
|
||||||
@ -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")
|
|
||||||
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
-- SPDX-FileCopyrightText: 2022-2925 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>
|
-- SPDX-FileCopyrightText: 2022-2025 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>
|
||||||
--
|
--
|
||||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -331,8 +331,8 @@ postAdminTestR = do
|
|||||||
<dl .deflist>
|
<dl .deflist>
|
||||||
<dt .deflist__dt> appJobCronInterval
|
<dt .deflist__dt> appJobCronInterval
|
||||||
<dd .deflist__dd>#{tshow appJobCronInterval}
|
<dd .deflist__dd>#{tshow appJobCronInterval}
|
||||||
<dt .deflist__dt> appSynchroniseLdapUsersWithin
|
<dt .deflist__dt> appUserSyncWithin
|
||||||
<dd .deflist__dd>#{tshow appSynchroniseLdapUsersWithin}
|
<dd .deflist__dd>#{tshow appUserSyncWithin}
|
||||||
<dt .deflist__dt> appSynchroniseAvsUsersWithin
|
<dt .deflist__dt> appSynchroniseAvsUsersWithin
|
||||||
<dd .deflist__dd>#{tshow appSynchroniseAvsUsersWithin}
|
<dd .deflist__dd>#{tshow appSynchroniseAvsUsersWithin}
|
||||||
|]
|
|]
|
||||||
|
|||||||
@ -266,7 +266,6 @@ data UserTableCsv = UserTableCsv
|
|||||||
, csvUserSex :: Maybe Sex
|
, csvUserSex :: Maybe Sex
|
||||||
, csvUserBirthday :: Maybe Day
|
, csvUserBirthday :: Maybe Day
|
||||||
, csvUserMatriculation :: Maybe UserMatriculation
|
, csvUserMatriculation :: Maybe UserMatriculation
|
||||||
, csvUserEPPN :: Maybe UserEduPersonPrincipalName
|
|
||||||
, csvUserEmail :: UserEmail
|
, csvUserEmail :: UserEmail
|
||||||
, csvUserQualifications :: [QualificationName]
|
, csvUserQualifications :: [QualificationName]
|
||||||
, csvUserSubmissionGroup :: Maybe SubmissionGroupName
|
, csvUserSubmissionGroup :: Maybe SubmissionGroupName
|
||||||
@ -286,7 +285,6 @@ instance Csv.ToNamedRecord UserTableCsv where
|
|||||||
, "sex" Csv..= csvUserSex
|
, "sex" Csv..= csvUserSex
|
||||||
, "birthday" Csv..= csvUserBirthday
|
, "birthday" Csv..= csvUserBirthday
|
||||||
, "matriculation" Csv..= csvUserMatriculation
|
, "matriculation" Csv..= csvUserMatriculation
|
||||||
, "eduPersonPrincipalName" Csv..= csvUserEPPN
|
|
||||||
, "email" Csv..= csvUserEmail
|
, "email" Csv..= csvUserEmail
|
||||||
, "qualifications" Csv..= CsvSemicolonList csvUserQualifications
|
, "qualifications" Csv..= CsvSemicolonList csvUserQualifications
|
||||||
, "submission-group" Csv..= csvUserSubmissionGroup
|
, "submission-group" Csv..= csvUserSubmissionGroup
|
||||||
@ -348,7 +346,6 @@ data UserTableJson = UserTableJson
|
|||||||
, jsonUserName :: UserDisplayName
|
, jsonUserName :: UserDisplayName
|
||||||
, jsonUserSex :: Maybe (Maybe Sex)
|
, jsonUserSex :: Maybe (Maybe Sex)
|
||||||
, jsonUserMatriculation :: Maybe UserMatriculation
|
, jsonUserMatriculation :: Maybe UserMatriculation
|
||||||
, jsonUserEPPN :: Maybe UserEduPersonPrincipalName
|
|
||||||
, jsonUserEmail :: UserEmail
|
, jsonUserEmail :: UserEmail
|
||||||
, jsonUserQualifications :: Set QualificationName
|
, jsonUserQualifications :: Set QualificationName
|
||||||
, jsonUserSubmissionGroup :: Maybe SubmissionGroupName
|
, jsonUserSubmissionGroup :: Maybe SubmissionGroupName
|
||||||
@ -385,7 +382,6 @@ instance ToJSON UserTableJson where
|
|||||||
, pure $ "name" JSON..= jsonUserName
|
, pure $ "name" JSON..= jsonUserName
|
||||||
, ("sex" JSON..=) <$> jsonUserSex
|
, ("sex" JSON..=) <$> jsonUserSex
|
||||||
, ("matriculation" JSON..=) <$> jsonUserMatriculation
|
, ("matriculation" JSON..=) <$> jsonUserMatriculation
|
||||||
, ("eduPersonPrincipalName" JSON..=) <$> jsonUserEPPN
|
|
||||||
, pure $ "email" JSON..= jsonUserEmail
|
, pure $ "email" JSON..= jsonUserEmail
|
||||||
, ("qualifications" JSON..=) <$> assertM' (not . onull) jsonUserQualifications
|
, ("qualifications" JSON..=) <$> assertM' (not . onull) jsonUserQualifications
|
||||||
, ("submission-group" JSON..=) <$> jsonUserSubmissionGroup
|
, ("submission-group" JSON..=) <$> jsonUserSubmissionGroup
|
||||||
@ -650,7 +646,6 @@ makeCourseUserTable cid acts restrict colChoices psValidator csvColumns = do
|
|||||||
<*> view (hasUser . _userSex)
|
<*> view (hasUser . _userSex)
|
||||||
<*> view (hasUser . _userBirthday)
|
<*> view (hasUser . _userBirthday)
|
||||||
<*> view (hasUser . _userMatrikelnummer)
|
<*> view (hasUser . _userMatrikelnummer)
|
||||||
<*> view (hasUser . _userLdapPrimaryKey)
|
|
||||||
<*> view (hasUser . _userEmail)
|
<*> view (hasUser . _userEmail)
|
||||||
<*> (over traverse (qualificationName . entityVal) <$> view _userQualifications)
|
<*> (over traverse (qualificationName . entityVal) <$> view _userQualifications)
|
||||||
<*> preview (_userSubmissionGroup . _entityVal . _submissionGroupName)
|
<*> preview (_userSubmissionGroup . _entityVal . _submissionGroupName)
|
||||||
@ -682,7 +677,6 @@ makeCourseUserTable cid acts restrict colChoices psValidator csvColumns = do
|
|||||||
<*> view (hasUser . _userDisplayName)
|
<*> view (hasUser . _userDisplayName)
|
||||||
<*> views (hasUser . _userSex) (guardOn showSex)
|
<*> views (hasUser . _userSex) (guardOn showSex)
|
||||||
<*> view (hasUser . _userMatrikelnummer)
|
<*> view (hasUser . _userMatrikelnummer)
|
||||||
<*> view (hasUser . _userLdapPrimaryKey)
|
|
||||||
<*> view (hasUser . _userEmail)
|
<*> view (hasUser . _userEmail)
|
||||||
<*> view (_userQualifications . folded . to (Set.singleton . qualificationName . entityVal))
|
<*> view (_userQualifications . folded . to (Set.singleton . qualificationName . entityVal))
|
||||||
<*> preview (_userSubmissionGroup . _entityVal . _submissionGroupName)
|
<*> preview (_userSubmissionGroup . _entityVal . _submissionGroupName)
|
||||||
|
|||||||
@ -190,7 +190,6 @@ data ExamUserTableCsv = ExamUserTableCsv
|
|||||||
, csvEUserFirstName :: Maybe Text
|
, csvEUserFirstName :: Maybe Text
|
||||||
, csvEUserName :: Maybe Text
|
, csvEUserName :: Maybe Text
|
||||||
, csvEUserMatriculation :: Maybe Text
|
, csvEUserMatriculation :: Maybe Text
|
||||||
, csvEUserEPPN :: Maybe UserEduPersonPrincipalName
|
|
||||||
, csvEUserStudyFeatures :: UserTableStudyFeatures
|
, csvEUserStudyFeatures :: UserTableStudyFeatures
|
||||||
, csvEUserOccurrence :: Maybe (CI Text)
|
, csvEUserOccurrence :: Maybe (CI Text)
|
||||||
, csvEUserExercisePoints :: Maybe (Maybe Points)
|
, csvEUserExercisePoints :: Maybe (Maybe Points)
|
||||||
@ -211,7 +210,6 @@ instance ToNamedRecord ExamUserTableCsv where
|
|||||||
, "first-name" Csv..= csvEUserFirstName
|
, "first-name" Csv..= csvEUserFirstName
|
||||||
, "name" Csv..= csvEUserName
|
, "name" Csv..= csvEUserName
|
||||||
, "matriculation" Csv..= csvEUserMatriculation
|
, "matriculation" Csv..= csvEUserMatriculation
|
||||||
, "eduPersonPrincipalName" Csv..= csvEUserEPPN
|
|
||||||
, "study-features" Csv..= csvEUserStudyFeatures
|
, "study-features" Csv..= csvEUserStudyFeatures
|
||||||
, "occurrence" Csv..= csvEUserOccurrence
|
, "occurrence" Csv..= csvEUserOccurrence
|
||||||
] ++ catMaybes
|
] ++ catMaybes
|
||||||
@ -237,7 +235,6 @@ instance FromNamedRecord ExamUserTableCsv where
|
|||||||
<*> csv .:?? "first-name"
|
<*> csv .:?? "first-name"
|
||||||
<*> csv .:?? "name"
|
<*> csv .:?? "name"
|
||||||
<*> csv .:?? "matriculation"
|
<*> csv .:?? "matriculation"
|
||||||
<*> csv .:?? "eduPersonPrincipalName"
|
|
||||||
<*> pure mempty
|
<*> pure mempty
|
||||||
<*> csv .:?? "occurrence"
|
<*> csv .:?? "occurrence"
|
||||||
<*> fmap Just (csv .:?? "exercise-points")
|
<*> fmap Just (csv .:?? "exercise-points")
|
||||||
@ -280,7 +277,7 @@ examUserTableCsvHeader :: ( MonoFoldable mono
|
|||||||
=> SheetGradeSummary -> Bool -> mono -> Csv.Header
|
=> SheetGradeSummary -> Bool -> mono -> Csv.Header
|
||||||
examUserTableCsvHeader allBoni doBonus pNames = Csv.header $
|
examUserTableCsvHeader allBoni doBonus pNames = Csv.header $
|
||||||
[ "surname", "first-name", "name"
|
[ "surname", "first-name", "name"
|
||||||
, "matriculation", "eduPersonPrincipalName"
|
, "matriculation"
|
||||||
, "study-features"
|
, "study-features"
|
||||||
, "course-note"
|
, "course-note"
|
||||||
, "occurrence"
|
, "occurrence"
|
||||||
@ -626,7 +623,6 @@ postEUsersR tid ssh csh examn = do
|
|||||||
<*> view (resultUser . _entityVal . _userFirstName . to Just)
|
<*> view (resultUser . _entityVal . _userFirstName . to Just)
|
||||||
<*> view (resultUser . _entityVal . _userDisplayName . to Just)
|
<*> view (resultUser . _entityVal . _userDisplayName . to Just)
|
||||||
<*> view (resultUser . _entityVal . _userMatrikelnummer)
|
<*> view (resultUser . _entityVal . _userMatrikelnummer)
|
||||||
<*> view (resultUser . _entityVal . _userLdapPrimaryKey)
|
|
||||||
<*> view resultStudyFeatures
|
<*> view resultStudyFeatures
|
||||||
<*> preview (resultExamOccurrence . _entityVal . _examOccurrenceName)
|
<*> preview (resultExamOccurrence . _entityVal . _examOccurrenceName)
|
||||||
<*> fmap (bool (const Nothing) Just showPoints) (preview $ resultUser . _entityKey . to (examBonusAchieved ?? bonus) . _achievedPoints . _Wrapped)
|
<*> fmap (bool (const Nothing) Just showPoints) (preview $ resultUser . _entityKey . to (examBonusAchieved ?? bonus) . _achievedPoints . _Wrapped)
|
||||||
@ -950,7 +946,6 @@ postEUsersR tid ssh csh examn = do
|
|||||||
guessUser' ExamUserTableCsv{..} = do
|
guessUser' ExamUserTableCsv{..} = do
|
||||||
let criteria = PredDNF . maybe Set.empty Set.singleton . fromNullable . Set.fromList . fmap PLVariable $ catMaybes
|
let criteria = PredDNF . maybe Set.empty Set.singleton . fromNullable . Set.fromList . fmap PLVariable $ catMaybes
|
||||||
[ GuessUserMatrikelnummer <$> csvEUserMatriculation
|
[ GuessUserMatrikelnummer <$> csvEUserMatriculation
|
||||||
, GuessUserEduPersonPrincipalName <$> csvEUserEPPN
|
|
||||||
, GuessUserDisplayName <$> csvEUserName
|
, GuessUserDisplayName <$> csvEUserName
|
||||||
, GuessUserSurname <$> csvEUserSurname
|
, GuessUserSurname <$> csvEUserSurname
|
||||||
, GuessUserFirstName <$> csvEUserFirstName
|
, GuessUserFirstName <$> csvEUserFirstName
|
||||||
|
|||||||
@ -70,7 +70,7 @@ fakeQualificationUsers (Entity qid Qualification{qualificationRefreshWithin}) (u
|
|||||||
let pw = "123.456"
|
let pw = "123.456"
|
||||||
PWHashConf{..} <- getsYesod $ view _appAuthPWHash
|
PWHashConf{..} <- getsYesod $ view _appAuthPWHash
|
||||||
pwHash <- liftIO $ PWStore.makePasswordWith pwHashAlgorithm pw pwHashStrength
|
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]
|
theSupervisor <- selectKeysList [UserSurname ==. "Jost", UserFirstName ==. "Steffen"] [Asc UserCreated, LimitTo 1]
|
||||||
let addSupervisor = case theSupervisor of
|
let addSupervisor = case theSupervisor of
|
||||||
[s] -> \suid k -> case k of
|
[s] -> \suid k -> case k of
|
||||||
@ -86,15 +86,14 @@ fakeQualificationUsers (Entity qid Qualification{qualificationRefreshWithin}) (u
|
|||||||
fakeUser :: ([Text], UserSurname, (Maybe Languages, DateTimeFormat, DateTimeFormat, DateTimeFormat), Bool, Int) -> User
|
fakeUser :: ([Text], UserSurname, (Maybe Languages, DateTimeFormat, DateTimeFormat, DateTimeFormat), Bool, Int) -> User
|
||||||
fakeUser (firstNames, userSurname, (userLanguages, userDateTimeFormat, userDateFormat, userTimeFormat), userPrefersPostal, _isSupervised) =
|
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"
|
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
|
userEmail = userIdent
|
||||||
userDisplayEmail = userIdent
|
userDisplayEmail = userIdent
|
||||||
userDisplayName = Text.unwords $ firstNames <> [userSurname]
|
userDisplayName = Text.unwords $ firstNames <> [userSurname]
|
||||||
userMatrikelnummer = Just "TESTUSER"
|
userMatrikelnummer = Just "TESTUSER"
|
||||||
userAuthentication = pwSimple
|
|
||||||
userLastAuthentication = Nothing
|
|
||||||
userCreated = now
|
userCreated = now
|
||||||
userLastLdapSynchronisation = Nothing
|
userLastSync = Just now
|
||||||
userLdapPrimaryKey = Nothing
|
|
||||||
userTokensIssuedAfter = Nothing
|
userTokensIssuedAfter = Nothing
|
||||||
userFirstName = Text.unwords firstNames
|
userFirstName = Text.unwords firstNames
|
||||||
userTitle = Nothing
|
userTitle = Nothing
|
||||||
|
|||||||
@ -616,6 +616,8 @@ makeProfileData :: Entity User -> DB Widget
|
|||||||
makeProfileData usrEnt@(Entity uid usrVal@User{..}) = do
|
makeProfileData usrEnt@(Entity uid usrVal@User{..}) = do
|
||||||
now <- liftIO getCurrentTime
|
now <- liftIO getCurrentTime
|
||||||
avsId <- entityVal <<$>> getBy (UniqueUserAvsUser uid)
|
avsId <- entityVal <<$>> getBy (UniqueUserAvsUser uid)
|
||||||
|
externalUsers <- (\(Entity _ ExternalUser{..}) -> (externalUserUser, externalUserSource, externalUserLastSync)) <<$>> selectList [ ExternalUserUser ==. userIdent ] []
|
||||||
|
|
||||||
let usrAutomatic :: CU_UserAvs_User -> Widget
|
let usrAutomatic :: CU_UserAvs_User -> Widget
|
||||||
usrAutomatic = updateAutomatic . mayUpdate usrVal avsId . mkCheckUpdate
|
usrAutomatic = updateAutomatic . mayUpdate usrVal avsId . mkCheckUpdate
|
||||||
addressLinkdIcon <- messageTooltip <$> messageIconI Info IconLink MsgAddressIsLinkedTip
|
addressLinkdIcon <- messageTooltip <$> messageIconI Info IconLink MsgAddressIsLinkedTip
|
||||||
|
|||||||
@ -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
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -17,11 +17,9 @@ import Handler.Utils.Csv
|
|||||||
import Handler.Utils.Profile
|
import Handler.Utils.Profile
|
||||||
|
|
||||||
import qualified Data.Text as Text (intercalate)
|
import qualified Data.Text as Text (intercalate)
|
||||||
-- import qualified Data.CaseInsensitive as CI
|
|
||||||
import qualified Data.Csv as Csv
|
import qualified Data.Csv as Csv
|
||||||
import Database.Esqueleto.Experimental ((:&)(..))
|
import Database.Esqueleto.Experimental ((:&)(..))
|
||||||
import qualified Database.Esqueleto.Experimental as E -- needs TypeApplications Lang-Pragma
|
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.PostgreSQL as E
|
||||||
import qualified Database.Esqueleto.Utils as E
|
import qualified Database.Esqueleto.Utils as E
|
||||||
|
|
||||||
@ -97,7 +95,15 @@ getQualificationSAPDirectR :: Handler TypedContent
|
|||||||
getQualificationSAPDirectR = do
|
getQualificationSAPDirectR = do
|
||||||
now <- liftIO getCurrentTime
|
now <- liftIO getCurrentTime
|
||||||
fdate <- formatTime' "%Y%m%d_%H-%M" now
|
fdate <- formatTime' "%Y%m%d_%H-%M" now
|
||||||
let ldap_cutoff = addDiffDaysRollOver (fromMonths $ -3) now
|
userAuthConf <- getsYesod $ view _appUserAuthConf
|
||||||
|
|
||||||
|
let
|
||||||
|
ldapSources = case userAuthConf of
|
||||||
|
UserAuthConfSingleSource (AuthSourceConfLdap LdapConf{..})
|
||||||
|
-> singleton $ AuthSourceIdLdap ldapConfSourceId
|
||||||
|
_other -> mempty
|
||||||
|
ldapCutoff = addDiffDaysRollOver (fromMonths $ -3) now
|
||||||
|
|
||||||
qualUsers <- runDBRead $ E.select $ do
|
qualUsers <- runDBRead $ E.select $ do
|
||||||
(qual :& qualUser :& user :& qualBlock) <-
|
(qual :& qualUser :& user :& qualBlock) <-
|
||||||
E.from $ E.table @Qualification
|
E.from $ E.table @Qualification
|
||||||
@ -111,9 +117,12 @@ getQualificationSAPDirectR = do
|
|||||||
E.&&. E.val now E.>~. qualBlock E.?. QualificationUserBlockFrom
|
E.&&. E.val now E.>~. qualBlock E.?. QualificationUserBlockFrom
|
||||||
)
|
)
|
||||||
E.where_ $ E.isJust (qual E.^. QualificationSapId)
|
E.where_ $ E.isJust (qual E.^. QualificationSapId)
|
||||||
E.&&. E.isJust (user E.^. UserCompanyPersonalNumber)
|
E.&&. E.isJust (user E.^. UserCompanyPersonalNumber)
|
||||||
E.&&. E.isJust (user E.^. UserLastLdapSynchronisation)
|
E.where_ . E.exists $ do
|
||||||
E.&&. (E.justVal ldap_cutoff E.<=. user E.^. UserLastLdapSynchronisation)
|
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
|
E.groupBy ( user E.^. UserCompanyPersonalNumber
|
||||||
, qualUser E.^. QualificationUserFirstHeld
|
, qualUser E.^. QualificationUserFirstHeld
|
||||||
, qualUser E.^. QualificationUserValidUntil
|
, qualUser E.^. QualificationUserValidUntil
|
||||||
|
|||||||
31
src/Handler/SingleSignOut.hs
Normal file
31
src/Handler/SingleSignOut.hs
Normal 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)
|
||||||
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
-- SPDX-FileCopyrightText: 2022-2023 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Sarah Vaupel <vaupel.sarah@campus.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>
|
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Sarah Vaupel <vaupel.sarah@campus.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>
|
||||||
--
|
--
|
||||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -12,7 +12,6 @@ module Handler.Users
|
|||||||
import Import
|
import Import
|
||||||
|
|
||||||
import Jobs
|
import Jobs
|
||||||
-- import Data.Text
|
|
||||||
import Handler.Utils
|
import Handler.Utils
|
||||||
import Handler.Utils.Users
|
import Handler.Utils.Users
|
||||||
import Handler.Utils.Invitations
|
import Handler.Utils.Invitations
|
||||||
@ -42,9 +41,7 @@ import qualified Data.ByteString.Base64 as Base64
|
|||||||
|
|
||||||
import Data.Aeson hiding (Result(..))
|
import Data.Aeson hiding (Result(..))
|
||||||
|
|
||||||
-- import Handler.Users.Add as Handler.Users
|
import qualified Data.Conduit.List as C
|
||||||
|
|
||||||
-- import qualified Data.Conduit.List as C
|
|
||||||
|
|
||||||
import qualified Data.HashSet as HashSet
|
import qualified Data.HashSet as HashSet
|
||||||
|
|
||||||
@ -130,10 +127,10 @@ postUsersR = do
|
|||||||
icnReroute = text2widget " " <> toWgt (icon IconReroute)
|
icnReroute = text2widget " " <> toWgt (icon IconReroute)
|
||||||
pure $ mconcat supervisors
|
pure $ mconcat supervisors
|
||||||
, sortable (Just "last-login") (i18nCell MsgLastLogin) $ \DBRow{ dbrOutput = Entity _ User{..} } -> maybe mempty dateTimeCell userLastAuthentication
|
, sortable (Just "last-login") (i18nCell MsgLastLogin) $ \DBRow{ dbrOutput = Entity _ User{..} } -> maybe mempty dateTimeCell userLastAuthentication
|
||||||
, sortable (Just "auth-ldap") (i18nCell MsgAuthMode) $ \DBRow{ dbrOutput = Entity _ User{..} } -> i18nCell userAuthentication
|
-- , sortable (Just "auth-ldap") (i18nCell MsgAuthMode) $ \DBRow{ dbrOutput = Entity _ User{..} } -> i18nCell userAuthentication -- TODO: reintroduce via ExternalUser
|
||||||
|
-- , sortable (Just "ldap-sync") (i18nCell MsgLdapSynced) $ \DBRow{ dbrOutput = Entity _ User{..} } -> maybe mempty dateTimeCell userLastLdapSynchronisation -- TODO: reintroduce via ExternalUser
|
||||||
, colUserEmail
|
, colUserEmail
|
||||||
, colUserLetterEmailPin
|
, colUserLetterEmailPin
|
||||||
, sortable (Just "ldap-sync") (i18nCell MsgLdapSynced) $ \DBRow{ dbrOutput = Entity _ User{..} } -> maybe mempty dateTimeCell userLastLdapSynchronisation
|
|
||||||
, flip foldMap universeF $ \function ->
|
, flip foldMap universeF $ \function ->
|
||||||
sortable (Just $ SortingKey $ CI.mk $ toPathPiece function) (i18nCell function) $ \DBRow{ dbrOutput = Entity uid _ } -> flip (set' cellContents) mempty $ do
|
sortable (Just $ SortingKey $ CI.mk $ toPathPiece function) (i18nCell function) $ \DBRow{ dbrOutput = Entity uid _ } -> flip (set' cellContents) mempty $ do
|
||||||
schools <- liftHandler . runDBRead . E.select . E.from $ \(school `E.InnerJoin` userFunction) -> do
|
schools <- liftHandler . runDBRead . E.select . E.from $ \(school `E.InnerJoin` userFunction) -> do
|
||||||
@ -240,15 +237,15 @@ postUsersR = do
|
|||||||
, ( "company-department"
|
, ( "company-department"
|
||||||
, SortColumn $ \user -> user E.^. UserCompanyDepartment
|
, SortColumn $ \user -> user E.^. UserCompanyDepartment
|
||||||
)
|
)
|
||||||
, ( "auth-ldap"
|
-- , ( "auth-ldap"
|
||||||
, SortColumn $ \user -> user E.^. UserAuthentication E.!=. E.val AuthLDAP
|
-- , SortColumn $ \user -> user E.^. UserAuthentication E.!=. E.val AuthLDAP
|
||||||
)
|
-- ) -- TODO: reintroduce via ExternalUser
|
||||||
, ( "last-login"
|
, ( "last-login"
|
||||||
, SortColumn $ \user -> user E.^. UserLastAuthentication
|
, SortColumn $ \user -> user E.^. UserLastAuthentication
|
||||||
)
|
)
|
||||||
, ( "ldap-sync"
|
-- , ( "ldap-sync"
|
||||||
, SortColumn $ \user -> user E.^. UserLastLdapSynchronisation
|
-- , SortColumn $ \user -> user E.^. UserLastLdapSynchronisation
|
||||||
)
|
-- ) -- TODO: reintroduce via ExternalUser
|
||||||
, ( "user-company"
|
, ( "user-company"
|
||||||
, SortColumn $ \user -> E.subSelect $ E.from $ \(usrComp `E.InnerJoin` comp) -> do
|
, SortColumn $ \user -> E.subSelect $ E.from $ \(usrComp `E.InnerJoin` comp) -> do
|
||||||
E.on $ usrComp E.^. UserCompanyCompany E.==. comp E.^. CompanyId
|
E.on $ usrComp E.^. UserCompanyCompany E.==. comp E.^. CompanyId
|
||||||
@ -291,24 +288,24 @@ postUsersR = do
|
|||||||
| Set.null criteria -> E.true -- TODO: why can this be eFalse and work still?
|
| 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
|
| otherwise -> E.any (\c -> user E.^. UserCompanyDepartment `E.hasInfix` E.val c) criteria
|
||||||
)
|
)
|
||||||
, ( "auth-ldap", FilterColumn $ \user (criterion :: Last Bool) -> if
|
-- , ( "auth-ldap", FilterColumn $ \user (criterion :: Last Bool) -> if
|
||||||
| Just crit <- getLast criterion
|
-- | Just crit <- getLast criterion
|
||||||
-> (user E.^. UserAuthentication E.==. E.val AuthLDAP) E.==. E.val crit
|
-- -> (user E.^. UserAuthentication E.==. E.val AuthLDAP) E.==. E.val crit
|
||||||
| otherwise
|
-- | otherwise
|
||||||
-> E.true
|
-- -> E.true
|
||||||
)
|
-- ) -- TODO: reintroduce via ExternalUser
|
||||||
, ( "school", FilterColumn $ \user criterion -> if
|
, ( "school", FilterColumn $ \user criterion -> if
|
||||||
| Set.null criterion -> E.val True :: E.SqlExpr (E.Value Bool)
|
| Set.null criterion -> E.val True :: E.SqlExpr (E.Value Bool)
|
||||||
| otherwise -> let schools = E.valList (Set.toList criterion) in
|
| otherwise -> let schools = E.valList (Set.toList criterion) in
|
||||||
E.exists . E.from $ \ufunc -> E.where_ $ ufunc E.^. UserFunctionUser E.==. user E.^. UserId
|
E.exists . E.from $ \ufunc -> E.where_ $ ufunc E.^. UserFunctionUser E.==. user E.^. UserId
|
||||||
E.&&. ufunc E.^. UserFunctionFunction `E.in_` schools
|
E.&&. ufunc E.^. UserFunctionFunction `E.in_` schools
|
||||||
)
|
)
|
||||||
, ( "ldap-sync", FilterColumn $ \user criteria -> if
|
-- , ( "ldap-sync", FilterColumn $ \user criteria -> if
|
||||||
| Just criteria' <- fromNullable criteria
|
-- | Just criteria' <- fromNullable criteria
|
||||||
-> let minTime = minimum (criteria' :: NonNull (Set UTCTime))
|
-- -> let minTime = minimum (criteria' :: NonNull (Set UTCTime))
|
||||||
in E.maybe E.true (E.<=. E.val minTime) $ user E.^. UserLastLdapSynchronisation
|
-- in E.maybe E.true (E.<=. E.val minTime) $ user E.^. UserLastLdapSynchronisation
|
||||||
| otherwise -> E.val True :: E.SqlExpr (E.Value Bool)
|
-- | otherwise -> E.val True :: E.SqlExpr (E.Value Bool)
|
||||||
)
|
-- ) -- TODO: reintroduce via ExternalUser
|
||||||
, ( "avs-sync", FilterColumn . E.mkExistsFilter $ \user criterion ->
|
, ( "avs-sync", FilterColumn . E.mkExistsFilter $ \user criterion ->
|
||||||
E.from $ \usrAvs -> do
|
E.from $ \usrAvs -> do
|
||||||
let minTime = (E.val criterion :: E.SqlExpr (E.Value UTCTime))
|
let minTime = (E.val criterion :: E.SqlExpr (E.Value UTCTime))
|
||||||
@ -357,8 +354,8 @@ postUsersR = do
|
|||||||
, prismAForm (singletonFilter "user-supervisor") mPrev $ aopt textField (fslI MsgTableSupervisor)
|
, prismAForm (singletonFilter "user-supervisor") mPrev $ aopt textField (fslI MsgTableSupervisor)
|
||||||
, prismAForm (singletonFilter "school") mPrev $ aopt (lift `hoistField` selectFieldList schoolOptions) (fslI MsgCourseSchool)
|
, 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 "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 "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)
|
-- , prismAForm (singletonFilter "ldap-sync" . maybePrism _PathPiece) mPrev $ aopt utcTimeField (fslI MsgLdapSyncedBefore) -- TODO: reintroduce via ExternalUser
|
||||||
, prismAForm (singletonFilter "avs-sync" . maybePrism _PathPiece) mPrev $ aopt utcTimeField (fslI MsgLastAvsSyncedBefore)
|
, prismAForm (singletonFilter "avs-sync" . maybePrism _PathPiece) mPrev $ aopt utcTimeField (fslI MsgLastAvsSyncedBefore)
|
||||||
]
|
]
|
||||||
, dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout }
|
, dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout }
|
||||||
@ -388,7 +385,7 @@ postUsersR = do
|
|||||||
addMessageI Info MsgActionNoUsersSelected
|
addMessageI Info MsgActionNoUsersSelected
|
||||||
(UserLdapSyncData, userSet) -> do
|
(UserLdapSyncData, userSet) -> do
|
||||||
forM_ userSet $ \uid -> void . queueJob $ JobSynchroniseLdapUser uid
|
forM_ userSet $ \uid -> void . queueJob $ JobSynchroniseLdapUser uid
|
||||||
addMessageI Success . MsgSynchroniseLdapUserQueued $ Set.size userSet
|
addMessageI Success . MsgSynchroniseUserdbUserQueued $ Set.size userSet
|
||||||
redirectKeepGetParams UsersR
|
redirectKeepGetParams UsersR
|
||||||
(UserAvsSyncData, userSet) -> do
|
(UserAvsSyncData, userSet) -> do
|
||||||
n <- runDB $ queueAvsUpdateByUID userSet Nothing
|
n <- runDB $ queueAvsUpdateByUID userSet Nothing
|
||||||
@ -427,9 +424,9 @@ postUsersR = do
|
|||||||
|
|
||||||
formResult allUsersRes $ \case
|
formResult allUsersRes $ \case
|
||||||
AllUsersLdapSync -> do
|
AllUsersLdapSync -> do
|
||||||
-- runDBJobs . runConduit $ selectSource [] [] .| C.mapM_ (queueDBJob . JobSynchroniseLdapUser . entityKey) -- to slow to execute directly
|
-- runDBJobs . runConduit $ selectSource [] [] .| C.mapM_ (queueDBJob . JobSynchroniseUser . entityKey) -- to slow to execute directly
|
||||||
queueJob' JobSynchroniseLdapAll
|
queueJob' JobSynchroniseUserdbAll
|
||||||
addMessageI Success MsgSynchroniseLdapAllUsersQueued
|
addMessageI Success MsgSynchroniseUserdbAllUsersQueued
|
||||||
redirect UsersR
|
redirect UsersR
|
||||||
AllUsersAvsSync -> do
|
AllUsersAvsSync -> do
|
||||||
now <- liftIO getCurrentTime
|
now <- liftIO getCurrentTime
|
||||||
@ -583,7 +580,7 @@ postAdminUserR uuid = do
|
|||||||
return (result, $(widgetFile "widgets/user-rights-form/user-rights-form"))
|
return (result, $(widgetFile "widgets/user-rights-form/user-rights-form"))
|
||||||
userAuthenticationForm :: Form ButtonAuthMode
|
userAuthenticationForm :: Form ButtonAuthMode
|
||||||
userAuthenticationForm = buttonForm' $ if
|
userAuthenticationForm = buttonForm' $ if
|
||||||
| userAuthentication == AuthLDAP -> [BtnAuthPWHash]
|
| is _Nothing userPasswordHash -> [BtnAuthPWHash]
|
||||||
| otherwise -> [BtnAuthLDAP, BtnPasswordReset]
|
| otherwise -> [BtnAuthLDAP, BtnPasswordReset]
|
||||||
systemFunctionsForm' = funcForm systemFuncForm (fslI MsgUserSystemFunctions) False
|
systemFunctionsForm' = funcForm systemFuncForm (fslI MsgUserSystemFunctions) False
|
||||||
where systemFuncForm func = apopt checkBoxField (fslI func) . Just $ systemFunctions func
|
where systemFuncForm func = apopt checkBoxField (fslI func) . Just $ systemFunctions func
|
||||||
@ -609,33 +606,41 @@ postAdminUserR uuid = do
|
|||||||
redirect $ AdminUserR uuid
|
redirect $ AdminUserR uuid
|
||||||
|
|
||||||
userAuthenticationAction = \case
|
userAuthenticationAction = \case
|
||||||
BtnAuthLDAP -> do
|
BtnAuthLDAP -> do -- TODO: Reformulate messages and constructors to "remove pw hash" or "external login only"
|
||||||
let
|
-- let
|
||||||
campusHandler :: MonadPlus m => Auth.CampusUserException -> m a
|
-- ldapHandler :: MonadPlus m => Auth.LdapUserException -> m a
|
||||||
campusHandler _ = mzero
|
-- ldapHandler _ = mzero
|
||||||
campusResult <- runMaybeT . handle campusHandler $ do
|
-- ldapResult <- runMaybeT . handle ldapHandler $ do
|
||||||
Just pool <- getsYesod $ view _appLdapPool
|
-- Just pool <- getsYesod $ view _appLdapPool
|
||||||
void . lift . Auth.campusUser pool FailoverUnlimited $ Creds Auth.apLdap (CI.original userIdent) []
|
-- void . lift . Auth.ldapUser pool $ Creds Auth.apLdap (CI.original userIdent) []
|
||||||
case campusResult of
|
-- case ldapResult of
|
||||||
Nothing -> addMessageI Error MsgAuthLDAPInvalidLookup
|
-- Nothing -> addMessageI Error MsgAuthLDAPInvalidLookup
|
||||||
_other
|
-- _other
|
||||||
| is _AuthLDAP userAuthentication
|
-- | is _AuthLDAP userAuthentication
|
||||||
-> addMessageI Info MsgAuthLDAPAlreadyConfigured
|
-- -> addMessageI Info MsgAuthLDAPAlreadyConfigured
|
||||||
Just () -> do
|
-- Just () -> do
|
||||||
runDBJobs $ do
|
-- runDBJobs $ do
|
||||||
update uid [ UserAuthentication =. AuthLDAP ]
|
-- update uid [ UserAuthentication =. AuthLDAP ]
|
||||||
queueDBJob . JobQueueNotification $ NotificationUserAuthModeUpdate uid
|
-- 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
|
addMessageI Success MsgAuthLDAPConfigured
|
||||||
redirect $ AdminUserR uuid
|
redirect $ AdminUserR uuid
|
||||||
BtnAuthPWHash -> do
|
BtnAuthPWHash -> do
|
||||||
if
|
if
|
||||||
| is _AuthPWHash userAuthentication
|
| is _Just userPasswordHash
|
||||||
-> addMessageI Info MsgAuthPWHashAlreadyConfigured
|
-> addMessageI Info MsgAuthPWHashAlreadyConfigured
|
||||||
| otherwise
|
| otherwise
|
||||||
-> do
|
-> do
|
||||||
runDBJobs $ do
|
runDBJobs $ do
|
||||||
update uid [ UserAuthentication =. AuthPWHash "" ]
|
update uid [ UserPasswordHash =. Just "" ]
|
||||||
queueDBJob . JobQueueNotification $ NotificationUserAuthModeUpdate uid
|
queueDBJob . JobQueueNotification $ NotificationUserAuthModeUpdate uid
|
||||||
queueDBJob $ JobSendPasswordReset uid
|
queueDBJob $ JobSendPasswordReset uid
|
||||||
|
|
||||||
@ -795,18 +800,18 @@ postUserPasswordR cID = do
|
|||||||
isAdmin <- hasWriteAccessTo $ AdminUserR cID
|
isAdmin <- hasWriteAccessTo $ AdminUserR cID
|
||||||
|
|
||||||
requireCurrent <- maybeT (return True) $ asum
|
requireCurrent <- maybeT (return True) $ asum
|
||||||
[ False <$ guard (isn't _AuthPWHash userAuthentication)
|
[ False <$ guard (isn't _Just userPasswordHash)
|
||||||
, False <$ guard isAdmin
|
, False <$ guard isAdmin
|
||||||
, do
|
, do
|
||||||
authMode <- Base64.decodeLenient . encodeUtf8 <$> MaybeT maybeCurrentBearerRestrictions
|
authMode <- Base64.decodeLenient . encodeUtf8 <$> MaybeT maybeCurrentBearerRestrictions
|
||||||
unless (authMode `constEq` computeUserAuthenticationDigest userAuthentication) . lift $
|
unless (authMode `constEq` computeUserAuthenticationDigest userPasswordHash) . lift $
|
||||||
invalidArgsI [MsgUnauthorizedPasswordResetToken]
|
invalidArgsI [MsgUnauthorizedPasswordResetToken]
|
||||||
return False
|
return False
|
||||||
]
|
]
|
||||||
|
|
||||||
((passResult, passFormWidget), passEnctype) <- runFormPost . formEmbedBearerPost . renderAForm FormStandard . wFormToAForm $ do
|
((passResult, passFormWidget), passEnctype) <- runFormPost . formEmbedBearerPost . renderAForm FormStandard . wFormToAForm $ do
|
||||||
currentResult <- if
|
currentResult <- if
|
||||||
| AuthPWHash (encodeUtf8 -> pwHash) <- userAuthentication
|
| Just (encodeUtf8 -> pwHash) <- userPasswordHash
|
||||||
, requireCurrent
|
, requireCurrent
|
||||||
-> wreq
|
-> wreq
|
||||||
(checkMap (bool (Left MsgCurrentPasswordInvalid) (Right ()) . flip (PWStore.verifyPasswordWith pwHashAlgorithm (2^)) pwHash . encodeUtf8) (const "") passwordField)
|
(checkMap (bool (Left MsgCurrentPasswordInvalid) (Right ()) . flip (PWStore.verifyPasswordWith pwHashAlgorithm (2^)) pwHash . encodeUtf8) (const "") passwordField)
|
||||||
@ -823,7 +828,7 @@ postUserPasswordR cID = do
|
|||||||
|
|
||||||
formResultModal passResult (bool ProfileR (UserPasswordR cID) isAdmin) $ \newPass -> do
|
formResultModal passResult (bool ProfileR (UserPasswordR cID) isAdmin) $ \newPass -> do
|
||||||
newHash <- fmap decodeUtf8 . liftIO $ PWStore.makePasswordWith pwHashAlgorithm newPass pwHashStrength
|
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
|
tell . pure =<< messageI Success MsgPasswordChangedSuccess
|
||||||
|
|
||||||
siteLayout [whamlet|_{MsgUserPasswordHeadingFor} ^{userEmailWidget usr}|] $
|
siteLayout [whamlet|_{MsgUserPasswordHeadingFor} ^{userEmailWidget usr}|] $
|
||||||
|
|||||||
@ -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
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -35,20 +35,18 @@ adminUserForm template = renderAForm FormStandard
|
|||||||
<*> aopt (textField & cfStrip) (fslI MsgAdminUserPinPassword) (audPinPassword <$> template)
|
<*> aopt (textField & cfStrip) (fslI MsgAdminUserPinPassword) (audPinPassword <$> template)
|
||||||
<*> areq (emailField & cfCI) (fslI MsgAdminUserEmail) (audEmail <$> template)
|
<*> areq (emailField & cfCI) (fslI MsgAdminUserEmail) (audEmail <$> template)
|
||||||
<*> areq (textField & cfStrip & cfCI) (fslI MsgAdminUserIdent) (audIdent <$> 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 :: AddUserData -> Handler (Maybe UserId)
|
||||||
addNewUserNotify aud = do
|
addNewUserNotify aud = do
|
||||||
mbUid <- addNewUser aud
|
mbUid <- addNewUser aud
|
||||||
case mbUid of
|
case mbUid of
|
||||||
Nothing -> return Nothing
|
Nothing -> return Nothing
|
||||||
Just uid -> runDBJobs $ do
|
Just uid -> runDBJobs $ do
|
||||||
queueDBJob $ JobSynchroniseLdapUser uid
|
queueDBJob $ JobSynchroniseUser uid
|
||||||
let authKind = audAuth aud
|
when (is _Just $ audPassword aud) $ do
|
||||||
when (authKind /= AuthKindNoLogin) $
|
|
||||||
queueDBJob . JobQueueNotification $ NotificationUserAuthModeUpdate uid
|
queueDBJob . JobQueueNotification $ NotificationUserAuthModeUpdate uid
|
||||||
when (authKind == AuthKindPWHash) $
|
|
||||||
queueDBJob $ JobSendPasswordReset uid
|
queueDBJob $ JobSendPasswordReset uid
|
||||||
return $ Just uid
|
return $ Just uid
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
-- SPDX-FileCopyrightText: 2022-2025 Steffen Jost <s.jost@fraport.de>
|
-- SPDX-FileCopyrightText: 2022-2025 Sarah Vaupel <sarah.vaupel@uniworx.de>, Steffen Jost <s.jost@fraport.de>
|
||||||
--
|
--
|
||||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -46,8 +46,7 @@ import qualified Data.Text as Text
|
|||||||
|
|
||||||
import qualified Control.Monad.Catch as Catch
|
import qualified Control.Monad.Catch as Catch
|
||||||
|
|
||||||
-- import Auth.LDAP (ldapUserPrincipalName)
|
import Foundation.Yesod.Auth (userLookupAndUpsert)
|
||||||
import Foundation.Yesod.Auth (ldapLookupAndUpsert) -- , CampusUserConversionException())
|
|
||||||
|
|
||||||
import Jobs.Queue
|
import Jobs.Queue
|
||||||
|
|
||||||
@ -1030,3 +1029,249 @@ getDifferingLicences (AvsResponseGetLicences licences) = do
|
|||||||
set to 1: vorfeld-set && nicht in rollfeld-set || rollfeld-unset && nicht in vorfeld-unset
|
set to 1: vorfeld-set && nicht in rollfeld-set || rollfeld-unset && nicht in vorfeld-unset
|
||||||
set to 2: rollfeld-set && nicht in vorfeld-unset && (in vorfeld-set || AVS_Licence>0 == vorORrollfeld)
|
set to 2: rollfeld-set && nicht in vorfeld-unset && (in vorfeld-set || AVS_Licence>0 == vorORrollfeld)
|
||||||
-}
|
-}
|
||||||
|
|
||||||
|
-- | Find or upsert User by AvsCardId (with dot), Fraport PersonalNumber, Fraport Email-Address or by prefixed AvsId or prefixed AvsNo; fail-safe, may or may not update existing users, may insert new users
|
||||||
|
-- If an existing User with internal number is found, an AVS query is executed
|
||||||
|
guessAvsUser :: Text -> Handler (Maybe UserId)
|
||||||
|
guessAvsUser (Text.splitAt 6 -> ("AVSID:", avsidTxt)) = ifMaybeM (readMay avsidTxt) Nothing $ \avsidNr ->
|
||||||
|
let avsid = AvsPersonId avsidNr
|
||||||
|
maybeAvsUpsert = maybeCatchAll $ upsertAvsUserById avsid
|
||||||
|
extractUid (Entity _ UserAvs{userAvsUser=uid}) = return $ Just uid
|
||||||
|
in maybeM maybeAvsUpsert extractUid $ runDB $ getBy $ UniqueUserAvsId avsid
|
||||||
|
guessAvsUser (Text.splitAt 6 -> ("AVSNO:", avsnoTxt)) = ifMaybeM (readMay avsnoTxt) Nothing $ \avsno ->
|
||||||
|
runDB (selectList [UserAvsNoPerson ==. avsno] []) <&> \case
|
||||||
|
[Entity _ UserAvs{userAvsUser=uid}] -> Just uid
|
||||||
|
_ -> Nothing
|
||||||
|
guessAvsUser someid = do
|
||||||
|
let maybeUpsertAvsUserByCard = maybeCatchAll . upsertAvsUserByCard
|
||||||
|
case discernAvsCardPersonalNo someid of
|
||||||
|
Just cid@(Left _cardNo) -> maybeUpsertAvsUserByCard cid
|
||||||
|
-- NOTE: card validity might be outdated, so we must always check with avs
|
||||||
|
-- maybeM (maybeUpsertAvsUserByCard cid) extractUid $ runDB $ do
|
||||||
|
-- let extractUid (Entity _ UserAvs{userAvsUser=uid}) = return $ Just uid
|
||||||
|
-- extractUidCard UserAvsCard{userAvsCardPersonId=avid} = getBy $ UniqueUserAvsId avid
|
||||||
|
-- cards <- selectList [UserAvsCardCardNo ==. cardNo] []
|
||||||
|
-- case [c | cent <- cards, let c = entityVal cent, avsDataValid (userAvsCardCard c)] of
|
||||||
|
-- [justOneCard] -> maybeM (return Nothing) extractUidCard (return $ Just justOneCard)
|
||||||
|
-- _ -> return Nothing
|
||||||
|
Just cid@(Right _wholeNumber) ->
|
||||||
|
maybeUpsertAvsUserByCard cid >>= \case
|
||||||
|
Nothing ->
|
||||||
|
runDB (selectList [UserCompanyPersonalNumber ==. Just someid] []) >>= \case
|
||||||
|
[Entity uid _] -> return $ Just uid
|
||||||
|
_ -> return Nothing
|
||||||
|
uid -> return uid
|
||||||
|
Nothing -> try (runDB $ userLookupAndUpsert someid UpsertUserGuessUser) >>= \case
|
||||||
|
Right (Just Entity{entityKey=uid, entityVal=User{userCompanyPersonalNumber=Just persNo}}) ->
|
||||||
|
maybeM (return $ Just uid) (return . Just) (maybeUpsertAvsUserByCard (Right $ mkAvsInternalPersonalNo persNo))
|
||||||
|
Right (Just Entity{entityKey=uid}) -> return $ Just uid
|
||||||
|
other -> do -- attempt to recover by trying other ids
|
||||||
|
whenIsLeft other (\(err::SomeException) -> $logInfoS "AVS" $ "upsertAvsUser external error " <> tshow err) -- this line primarily forces exception type to catch-all
|
||||||
|
runDB . runMaybeT $
|
||||||
|
let someIdent = stripCI someid
|
||||||
|
in MaybeT (getKeyBy $ UniqueEmail someIdent)
|
||||||
|
<|> MaybeT (getKeyBy $ UniqueAuthentication someIdent)
|
||||||
|
|
||||||
|
-- | Always update AVS Data, accepts AvsCardId (with dot), Fraport PersonalNumber or Fraport Email-Address
|
||||||
|
upsertAvsUser :: Text -> Handler (Maybe UserId) -- TODO: change to Entity
|
||||||
|
upsertAvsUser (discernAvsCardPersonalNo -> Just someid) = maybeCatchAll $ upsertAvsUserByCard someid -- Note: Right case is any number; it could be AvsCardNumber or AvsInternalPersonalNumber; we cannot know, but the latter is much more likely and useful to users!
|
||||||
|
upsertAvsUser otherId = -- attempt LDAP lookup to find by eMail
|
||||||
|
try (runDB $ userLookupAndUpsert otherId UpsertUserGuessUser) >>= \case
|
||||||
|
Right (Just Entity{entityVal=User{userCompanyPersonalNumber=Just persNo}}) -> maybeCatchAll $ upsertAvsUserByCard (Right $ mkAvsInternalPersonalNo persNo)
|
||||||
|
other -> do -- attempt to recover by trying other ids
|
||||||
|
whenIsLeft other (\(err::SomeException) -> $logInfoS "AVS" $ "upsertAvsUser LDAP error " <> tshow err) -- this line primarily forces exception type to catch-all
|
||||||
|
apid <- runDB . runMaybeT $ do
|
||||||
|
let someIdent = stripCI otherId
|
||||||
|
uid <- MaybeT (getKeyBy $ UniqueEmail someIdent)
|
||||||
|
<|> MaybeT (getKeyBy $ UniqueAuthentication someIdent)
|
||||||
|
MaybeT $ view (_entityVal . _userAvsPersonId) <<$>> getBy (UniqueUserAvsUser uid)
|
||||||
|
ifMaybeM apid Nothing upsertAvsUserById
|
||||||
|
|
||||||
|
|
||||||
|
-- | Given CardNo or internal Number, retrieve UserId. Create non-existing users, if possible. Always update.
|
||||||
|
-- Throws errors if the avsInterface in unavailable or the user is non-unique within external AVS DB.
|
||||||
|
upsertAvsUserByCard :: Either AvsFullCardNo AvsInternalPersonalNo -> Handler (Maybe UserId) -- Idee: Eingabe ohne Punkt is AvsInternalPersonalNo mit Punkt is Ausweisnummer?!
|
||||||
|
upsertAvsUserByCard persNo = do
|
||||||
|
let qry = case persNo of
|
||||||
|
Left AvsFullCardNo{..} -> def{ avsPersonQueryCardNo = Just avsFullCardNo, avsPersonQueryVersionNo = Just avsFullCardVersion }
|
||||||
|
Right fpn -> def{ avsPersonQueryInternalPersonalNo = Just fpn }
|
||||||
|
AvsQuery{..} <- maybeThrowM AvsInterfaceUnavailable $ getsYesod $ view _appAvsQuery
|
||||||
|
AvsResponsePerson adps <- throwLeftM $ avsQueryPerson qry
|
||||||
|
case Set.elems adps of
|
||||||
|
[] -> throwM AvsPersonSearchEmpty
|
||||||
|
(_:_:_) -> throwM AvsPersonSearchAmbiguous
|
||||||
|
[AvsDataPerson{avsPersonPersonID=api}] -> upsertAvsUserById api -- always trigger an update
|
||||||
|
-- do
|
||||||
|
-- mbuid <- runDB $ getBy $ UniqueUserAvsId api
|
||||||
|
-- case mbuid of
|
||||||
|
-- (Just (Entity _ UserAvs{userAvsUser=uau})) -> return $ Just uau
|
||||||
|
-- Nothing -> upsertAvsUserById api
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- | Retrieve and _always_ update user by AvsPersonId. Non-existing users are created. Ignore AVS Licence status! Updates Company, Address, PinPassword
|
||||||
|
-- Throws errors if the avsInterface in unavailable or the user is non-unique within external AVS DB (should never happen).
|
||||||
|
upsertAvsUserById :: AvsPersonId -> Handler (Maybe UserId)
|
||||||
|
upsertAvsUserById api = do
|
||||||
|
mbapd <- lookupAvsUser api
|
||||||
|
now <- liftIO getCurrentTime
|
||||||
|
mbuid <- runDB $ do
|
||||||
|
mbuid <- getBy (UniqueUserAvsId api)
|
||||||
|
case (mbuid, mbapd) of
|
||||||
|
(Nothing, Just AvsDataPerson{..}) -- FRADriver User does not exist yet, but found in AVS and has Internal Personal Number
|
||||||
|
| Just (avsInternalPersonalNo -> persNo) <- canonical avsPersonInternalPersonalNo -> do
|
||||||
|
$logInfoS "AVS" $ "Creating new user with avsInternalPersonalNo " <> tshow persNo
|
||||||
|
candidates <- selectKeysList [UserCompanyPersonalNumber ==. Just persNo] []
|
||||||
|
case candidates of
|
||||||
|
[uid] -> $logInfoS "AVS" "Matching user found, linking." >> insertUniqueEntity (UserAvs api uid avsPersonPersonNo now Nothing)
|
||||||
|
(_:_) -> throwM $ AvsUserAmbiguous api
|
||||||
|
[] -> do
|
||||||
|
upsRes :: Either SomeException (Maybe (Entity User))
|
||||||
|
<- try $ userLookupAndUpsert persNo UpsertUserGuessUser -- TODO: do azure lookup and upsert if appropriate
|
||||||
|
$logInfoS "AVS" $ "No matching existing user found. Attempted LDAP upsert returned: " <> tshow upsRes
|
||||||
|
case upsRes of
|
||||||
|
Right (Just Entity{entityKey=uid}) -> insertUniqueEntity $ UserAvs api uid avsPersonPersonNo now Nothing -- pin/addr are updated in next step anyway
|
||||||
|
Right Nothing -> do
|
||||||
|
$logWarnS "AVS" $ "AVS user with avsInternalPersonalNo " <> tshow persNo <> " not found in external databases"
|
||||||
|
return mbuid -- == Nothing -- user could not be created somehow
|
||||||
|
Left err -> do
|
||||||
|
$logWarnS "AVS" $ "AVS user with avsInternalPersonalNo " <> tshow persNo <> " not found in external databases: " <> tshow err
|
||||||
|
return mbuid -- == Nothing -- user could not be created somehow
|
||||||
|
(Just Entity{ entityKey = uaid }, _) -> do
|
||||||
|
update uaid [ UserAvsLastSynch =. now, UserAvsLastSynchError =. Nothing ] -- mark as updated early, to prevent failed users to clog the synch
|
||||||
|
return mbuid
|
||||||
|
_other -> return mbuid
|
||||||
|
$logInfoS "AVS" $ "upsert prestep result: " <> tshow mbuid <> " --- " <> tshow mbapd
|
||||||
|
case (mbuid, mbapd) of
|
||||||
|
( _ , Nothing ) -> throwM $ AvsUserUnknownByAvs api -- User not found in AVS at all, i.e. no valid card exists yet
|
||||||
|
(Nothing, Just AvsDataPerson{avsPersonFirstName= Text.strip -> avsFirstName, avsPersonLastName= Text.strip -> avsSurname, ..}) -> do -- No LDAP User, but found in AVS; create new user
|
||||||
|
let (mbCompany, mbCoFirmAddr, _) = guessLicenceAddress avsPersonPersonCards
|
||||||
|
userFirmAddr= plaintextToStoredMarkup <$> mbCoFirmAddr
|
||||||
|
pinCard = Set.lookupMax avsPersonPersonCards
|
||||||
|
userPin = personCard2pin <$> pinCard
|
||||||
|
fakeIdent = CI.mk $ "AVSID:" <> tshow api
|
||||||
|
fakeNo = CI.mk $ "AVSNO:" <> tshow avsPersonPersonNo
|
||||||
|
newUsr = AddUserData
|
||||||
|
{ audTitle = Nothing
|
||||||
|
, audFirstName = avsFirstName
|
||||||
|
, audSurname = avsSurname
|
||||||
|
, audDisplayName = avsFirstName <> Text.cons ' ' avsSurname
|
||||||
|
, audDisplayEmail = "" -- Email is unknown in this version of the avs query, to be updated later (FUTURE TODO)
|
||||||
|
, audMatriculation = Just $ tshow avsPersonPersonNo
|
||||||
|
, audSex = Nothing
|
||||||
|
, audBirthday = Nothing
|
||||||
|
, audMobile = Nothing
|
||||||
|
, audTelephone = Nothing
|
||||||
|
, audFPersonalNumber = avsInternalPersonalNo <$> canonical avsPersonInternalPersonalNo
|
||||||
|
, audFDepartment = Nothing
|
||||||
|
, audPostAddress = userFirmAddr
|
||||||
|
, audPrefersPostal = True
|
||||||
|
, audPinPassword = userPin
|
||||||
|
, audEmail = fakeNo -- Email is unknown in this version of the avs query, to be updated later (FUTURE TODO)
|
||||||
|
, audIdent = fakeIdent -- use AvsPersonId instead
|
||||||
|
, audPassword = Nothing
|
||||||
|
--, audAuth = maybe AuthKindNoLogin (const AuthKindAzure) avsPersonInternalPersonalNo -- FUTURE TODO: if email is known, use AuthKinfPWHash for email invite, if no internal personnel number is known
|
||||||
|
}
|
||||||
|
mbUid <- addNewUser newUsr -- triggers JobSynchroniseUserdbUser, JobSendPasswordReset and NotificationUserAutoModeUpdate -- TODO: check if these are failsafe
|
||||||
|
whenIsJust mbUid $ \uid -> runDB $ do
|
||||||
|
insert_ $ UserAvs avsPersonPersonID uid avsPersonPersonNo now Nothing
|
||||||
|
forM_ avsPersonPersonCards $ -- save all cards for later comparisons whether an update occurred
|
||||||
|
-- let cs :: Set AvsDataPersonCard = Set.fromList $ catMaybes [pinCard, addrCard]
|
||||||
|
-- forM_ cs $ -- only save used cards for the postal address update detection
|
||||||
|
\avsCard -> insert_ $ UserAvsCard avsPersonPersonID (getFullCardNo avsCard) avsCard now
|
||||||
|
upsertUserCompany uid mbCompany userFirmAddr
|
||||||
|
return mbUid
|
||||||
|
|
||||||
|
(Just (Entity _ UserAvs{userAvsUser=uid})
|
||||||
|
, Just AvsDataPerson{avsPersonPersonCards, avsPersonInternalPersonalNo, avsPersonPersonNo, avsPersonFirstName= Text.strip -> avsFirstName, avsPersonLastName= Text.strip -> avsSurname}) -> do -- known user, update address and pinPassword
|
||||||
|
let (mbCompany, mbCoFirmAddr, _) = guessLicenceAddress avsPersonPersonCards
|
||||||
|
userFirmAddr = plaintextToStoredMarkup <$> mbCoFirmAddr
|
||||||
|
pinCard = Set.lookupMax avsPersonPersonCards
|
||||||
|
userPin = personCard2pin <$> pinCard
|
||||||
|
runDB $ do
|
||||||
|
update uid [ UserFirstName =. avsFirstName -- update in case of name changes via AVS; might be changed again through LDAP
|
||||||
|
, UserSurname =. avsSurname
|
||||||
|
, UserDisplayName =. avsFirstName <> Text.cons ' ' avsSurname
|
||||||
|
, UserMatrikelnummer =. Just (tshow avsPersonPersonNo) -- TODO: Deactivate this update after Q2/2023; this is only needed since UserMatrikelnummer was used for AVSNO later
|
||||||
|
, UserCompanyPersonalNumber =. avsInternalPersonalNo <$> canonical avsPersonInternalPersonalNo
|
||||||
|
]
|
||||||
|
oldCards <- selectList [UserAvsCardPersonId ==. api] []
|
||||||
|
let oldAddrs = Set.fromList $ mapMaybe (snd3 . getCompanyAddress . userAvsCardCard . entityVal) oldCards
|
||||||
|
unless (maybe True (`Set.member` oldAddrs) mbCoFirmAddr) $ do -- update postal address, unless the exact address had been seen before
|
||||||
|
encRecipient :: CryptoUUIDUser <- encrypt uid
|
||||||
|
$logInfoS "AVS" $ "Postal address updated for" <> tshow encRecipient
|
||||||
|
updateWhere [UserId ==. uid] [UserPostAddress =. userFirmAddr, UserPostLastUpdate =. Just now]
|
||||||
|
whenIsJust pinCard $ \pCard -> -- update pin, but only if it was unset or set to the value of an old card
|
||||||
|
unlessM (exists [UserAvsCardCardNo ==. getFullCardNo pCard]) $ do
|
||||||
|
let oldPins = Just . personCard2pin . userAvsCardCard . entityVal <$> oldCards
|
||||||
|
updateWhere [UserId ==. uid, UserPinPassword !=. userPin, UserPinPassword <-. oldPins] -- check for old pin ensures that unset/manually set passwords remain unchanged
|
||||||
|
[UserPinPassword =. userPin]
|
||||||
|
insert_ $ UserAvsCard api (getFullCardNo pCard) pCard now
|
||||||
|
upsertUserCompany uid mbCompany userFirmAddr
|
||||||
|
forM_ avsPersonPersonCards $ \aCard -> do
|
||||||
|
let fcn = getFullCardNo aCard
|
||||||
|
-- probably not efficient, but fixes the problem that AvsCardNo is not unique as assumed before and may get reused
|
||||||
|
deleteWhere [UserAvsCardCardNo ==. fcn]
|
||||||
|
insert_ $ UserAvsCard
|
||||||
|
{ userAvsCardPersonId = api
|
||||||
|
, userAvsCardCardNo = fcn
|
||||||
|
, userAvsCardCard = aCard
|
||||||
|
, userAvsCardLastSynch = now
|
||||||
|
}
|
||||||
|
return $ Just uid
|
||||||
|
|
||||||
|
|
||||||
|
lookupAvsUser :: ( MonadThrow m, MonadHandler m, HandlerSite m ~ UniWorX ) =>
|
||||||
|
AvsPersonId -> m (Maybe AvsDataPerson)
|
||||||
|
lookupAvsUser api = Map.lookup api <$> lookupAvsUsers (Set.singleton api)
|
||||||
|
|
||||||
|
-- | retrieves complete avs user records for given AvsPersonIds.
|
||||||
|
-- Note that this requires several AVS-API queries, since
|
||||||
|
-- - avsQueryPerson does not support querying an AvsPersonId directly
|
||||||
|
-- - avsQueryStatus only provides limited information
|
||||||
|
-- avsQuery is used to obtain all card numbers, which are then queried separately an merged
|
||||||
|
-- May throw Servant.ClientError or AvsExceptions
|
||||||
|
-- Does not write to our own DB!
|
||||||
|
lookupAvsUsers :: ( MonadThrow m, MonadHandler m, HandlerSite m ~ UniWorX ) =>
|
||||||
|
Set AvsPersonId -> m (Map AvsPersonId AvsDataPerson)
|
||||||
|
lookupAvsUsers apis = do
|
||||||
|
AvsQuery{..} <- maybeThrowM AvsInterfaceUnavailable $ getsYesod $ view _appAvsQuery
|
||||||
|
AvsResponseStatus statuses <- throwLeftM . avsQueryStatus $ AvsQueryStatus apis
|
||||||
|
let forFoldlM = $(permuteFun [3,2,1]) foldlM
|
||||||
|
forFoldlM statuses mempty $ \acc1 AvsStatusPerson{avsStatusPersonCardStatus=cards} ->
|
||||||
|
forFoldlM cards acc1 $ \acc2 AvsDataPersonCard{avsDataCardNo, avsDataVersionNo} -> do
|
||||||
|
AvsResponsePerson adps <- throwLeftM . avsQueryPerson $ def{avsPersonQueryCardNo = Just avsDataCardNo, avsPersonQueryVersionNo = Just avsDataVersionNo}
|
||||||
|
return $ mergeByPersonId adps acc2
|
||||||
|
|
||||||
|
|
||||||
|
-- | Like `Handler.Utils.getReceivers`, but calls upsertAvsUserById on each user to ensure that postal address is up-to-date
|
||||||
|
updateReceivers :: UserId -> Handler (Entity User, [Entity User], Bool)
|
||||||
|
updateReceivers uid = do
|
||||||
|
-- First perform AVS update for receiver
|
||||||
|
runDB (getBy (UniqueUserAvsUser uid)) >>= \case
|
||||||
|
Just Entity{entityVal=UserAvs{userAvsPersonId = apid}} -> void . maybeCatchAll $ upsertAvsUserById apid
|
||||||
|
Nothing -> return ()
|
||||||
|
-- Retrieve updated user and supervisors now
|
||||||
|
(underling :: Entity User, avsSupers :: [(E.Value UserId, E.Value (Maybe AvsPersonId))]) <- runDB $ (,)
|
||||||
|
<$> getJustEntity uid
|
||||||
|
<*> (E.select $ do
|
||||||
|
(usrSuper :& usrAvs) <-
|
||||||
|
E.from $ E.table @UserSupervisor
|
||||||
|
`E.leftJoin` E.table @UserAvs
|
||||||
|
`E.on` (\(usrSuper :& userAvs) -> usrSuper E.^. UserSupervisorSupervisor E.=?. userAvs E.?. UserAvsUser)
|
||||||
|
E.where_ $ (usrSuper E.^. UserSupervisorUser E.==. E.val uid)
|
||||||
|
E.&&. (usrSuper E.^. UserSupervisorRerouteNotifications)
|
||||||
|
pure (usrSuper E.^. UserSupervisorSupervisor, usrAvs E.?. UserAvsPersonId)
|
||||||
|
)
|
||||||
|
let (superVs, avsIds) = unzip avsSupers
|
||||||
|
receiverIDs :: [UserId] = E.unValue <$> superVs
|
||||||
|
toUpdate = Set.fromList $ mapMaybe E.unValue avsIds
|
||||||
|
directResult = return (underling, pure underling, True) -- already contains updated address
|
||||||
|
forM_ toUpdate (void . maybeCatchAll . upsertAvsUserById) -- attempt to update postaddress from AVS
|
||||||
|
if null receiverIDs
|
||||||
|
then directResult
|
||||||
|
else do
|
||||||
|
receivers <- runDB $ selectList [UserId <-. receiverIDs] [] -- due to possible address updates, we must runDB once more and cannot join above
|
||||||
|
if null receivers
|
||||||
|
then directResult
|
||||||
|
else return (underling, receivers, uid `elem` (entityKey <$> receivers))
|
||||||
|
|||||||
@ -112,7 +112,7 @@ csvFilenameLmsReport = makeLmsFilename "report"
|
|||||||
makeLmsFilename :: MonadHandler m => Text -> QualificationShorthand -> m Text
|
makeLmsFilename :: MonadHandler m => Text -> QualificationShorthand -> m Text
|
||||||
makeLmsFilename ftag (citext2lower -> qsh) = do
|
makeLmsFilename ftag (citext2lower -> qsh) = do
|
||||||
ymth <- getYMTH
|
ymth <- getYMTH
|
||||||
return $ "fradrive_" <> qsh <> "_" <> ftag <> "_" <> ymth <> ".csv"
|
return $ "fradrive_" <> "test" <> "_" <> qsh <> "_" <> ftag <> "_" <> ymth <> ".csv"
|
||||||
|
|
||||||
-- | Return current datetime in YYYYMMDDHH format
|
-- | Return current datetime in YYYYMMDDHH format
|
||||||
getYMTH :: MonadHandler m => m Text
|
getYMTH :: MonadHandler m => m Text
|
||||||
@ -188,8 +188,8 @@ maxLmsUserIdentRetries = 27
|
|||||||
-- eopt = Elo.genOptions -- { genCapitals = False, genSpecials = False, genDigitis = True }
|
-- eopt = Elo.genOptions -- { genCapitals = False, genSpecials = False, genDigitis = True }
|
||||||
|
|
||||||
randomLMSIdent :: MonadIO m => Maybe Char -> m LmsIdent
|
randomLMSIdent :: MonadIO m => Maybe Char -> m LmsIdent
|
||||||
randomLMSIdent Nothing = LmsIdent . Text.cons 'j' <$> randomText [] (pred lengthIdent) -- idents must not contain '_' nor '-'
|
randomLMSIdent Nothing = LmsIdent . Text.cons 't' . Text.cons 'j' <$> randomText [] (pred $ pred lengthIdent) -- idents must not contain '_' nor '-'
|
||||||
randomLMSIdent (Just c) = LmsIdent . Text.cons c <$> randomText [] (pred lengthIdent)
|
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 :: MonadIO m => Maybe Char -> Set LmsIdent -> m (Maybe LmsIdent)
|
||||||
randomLMSIdentBut prefix banList = untilJustMaxM maxLmsUserIdentRetries getIdentOk
|
randomLMSIdentBut prefix banList = untilJustMaxM maxLmsUserIdentRetries getIdentOk
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
-- SPDX-FileCopyrightText: 2022-26 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Sarah Vaupel <vaupel.sarah@campus.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>
|
-- SPDX-FileCopyrightText: 2022-2025 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Sarah Vaupel <vaupel.sarah@campus.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>
|
||||||
--
|
--
|
||||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -29,8 +29,7 @@ module Handler.Utils.Users
|
|||||||
) where
|
) where
|
||||||
|
|
||||||
import Import
|
import Import
|
||||||
import Auth.LDAP (campusUserMatr')
|
import Foundation.Yesod.Auth (userLookupAndUpsert)
|
||||||
import Foundation.Yesod.Auth (upsertCampusUser)
|
|
||||||
|
|
||||||
import Crypto.Hash (hashlazy)
|
import Crypto.Hash (hashlazy)
|
||||||
|
|
||||||
@ -42,7 +41,6 @@ import qualified Data.Aeson as JSON
|
|||||||
import qualified Data.Aeson.Types as JSON
|
import qualified Data.Aeson.Types as JSON
|
||||||
|
|
||||||
import qualified Data.Set as Set
|
import qualified Data.Set as Set
|
||||||
-- import qualified Data.List as List
|
|
||||||
import qualified Data.CaseInsensitive as CI
|
import qualified Data.CaseInsensitive as CI
|
||||||
|
|
||||||
import Database.Esqueleto.Experimental ((:&)(..))
|
import Database.Esqueleto.Experimental ((:&)(..))
|
||||||
@ -227,7 +225,7 @@ getSupervisees forceLogin = do
|
|||||||
return $ Set.insert uid $ Set.fromAscList svs
|
return $ Set.insert uid $ Set.fromAscList svs
|
||||||
|
|
||||||
|
|
||||||
computeUserAuthenticationDigest :: AuthenticationMode -> Digest SHA3_256
|
computeUserAuthenticationDigest :: Maybe Text -> Digest SHA3_256
|
||||||
computeUserAuthenticationDigest = hashlazy . JSON.encode
|
computeUserAuthenticationDigest = hashlazy . JSON.encode
|
||||||
|
|
||||||
-- guessUserByCompanyPersonalNumber :: Text -> Text -> DB (Maybe UserId)
|
-- guessUserByCompanyPersonalNumber :: Text -> Text -> DB (Maybe UserId)
|
||||||
@ -245,8 +243,6 @@ guessUserByEmail eml = firstJustM $
|
|||||||
data GuessUserInfo
|
data GuessUserInfo
|
||||||
= GuessUserMatrikelnummer
|
= GuessUserMatrikelnummer
|
||||||
{ guessUserMatrikelnummer :: UserMatriculation }
|
{ guessUserMatrikelnummer :: UserMatriculation }
|
||||||
| GuessUserEduPersonPrincipalName
|
|
||||||
{ guessUserEduPersonPrincipalName :: UserEduPersonPrincipalName }
|
|
||||||
| GuessUserDisplayName
|
| GuessUserDisplayName
|
||||||
{ guessUserDisplayName :: UserDisplayName }
|
{ guessUserDisplayName :: UserDisplayName }
|
||||||
| GuessUserSurname
|
| GuessUserSurname
|
||||||
@ -298,12 +294,11 @@ guessUser (((Set.toList . toNullable) <$>) . Set.toList . dnfTerms -> criteria)
|
|||||||
|
|
||||||
toSql user pl = bool id E.not__ (is _PLNegated pl) $ case pl ^. _plVar of
|
toSql user pl = bool id E.not__ (is _PLNegated pl) $ case pl ^. _plVar of
|
||||||
GuessUserMatrikelnummer userMatriculation' -> user E.^. UserMatrikelnummer E.==. E.val (Just userMatriculation')
|
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'
|
GuessUserDisplayName userDisplayName' -> user E.^. UserDisplayName `containsAsSet` userDisplayName'
|
||||||
GuessUserSurname userSurname' -> user E.^. UserSurname `containsAsSet` userSurname'
|
GuessUserSurname userSurname' -> user E.^. UserSurname `containsAsSet` userSurname'
|
||||||
GuessUserFirstName userFirstName' -> user E.^. UserFirstName `containsAsSet` userFirstName'
|
GuessUserFirstName userFirstName' -> user E.^. UserFirstName `containsAsSet` userFirstName'
|
||||||
|
|
||||||
go didLdap = do
|
go didUpsert = do
|
||||||
let retrieveUsers = E.select . EL.from $ \user -> do
|
let retrieveUsers = E.select . EL.from $ \user -> do
|
||||||
E.where_ . E.or $ map (E.and . map (toSql user)) criteria
|
E.where_ . E.or $ map (E.and . map (toSql user)) criteria
|
||||||
when (is _Just mQueryLimit) $ (E.limit . fromJust) mQueryLimit
|
when (is _Just mQueryLimit) $ (E.limit . fromJust) mQueryLimit
|
||||||
@ -345,11 +340,7 @@ guessUser (((Set.toList . toNullable) <$>) . Set.toList . dnfTerms -> criteria)
|
|||||||
| EQ <- x `closeness` x' = x : takeClosest (x':xs)
|
| EQ <- x `closeness` x' = x : takeClosest (x':xs)
|
||||||
| otherwise = [x]
|
| otherwise = [x]
|
||||||
|
|
||||||
doLdap userMatr = do
|
doUpsert = flip userLookupAndUpsert UpsertUserGuessUser
|
||||||
ldapPool' <- getsYesod $ view _appLdapPool
|
|
||||||
fmap join . for ldapPool' $ \ldapPool -> do
|
|
||||||
ldapData <- campusUserMatr' ldapPool FailoverUnlimited userMatr
|
|
||||||
for ldapData $ upsertCampusUser UpsertCampusUserGuessUser
|
|
||||||
|
|
||||||
let
|
let
|
||||||
getTermMatr :: [PredLiteral GuessUserInfo] -> Maybe UserMatriculation
|
getTermMatr :: [PredLiteral GuessUserInfo] -> Maybe UserMatriculation
|
||||||
@ -365,25 +356,25 @@ guessUser (((Set.toList . toNullable) <$>) . Set.toList . dnfTerms -> criteria)
|
|||||||
| otherwise = Nothing
|
| otherwise = Nothing
|
||||||
getTermMatrAux acc (_:xs) = getTermMatrAux acc xs
|
getTermMatrAux acc (_:xs) = getTermMatrAux acc xs
|
||||||
|
|
||||||
convertLdapResults :: [Entity User] -> Maybe (Either (NonEmpty (Entity User)) (Entity User))
|
convertUpsertResults :: [Entity User] -> Maybe (Either (NonEmpty (Entity User)) (Entity User))
|
||||||
convertLdapResults [] = Nothing
|
convertUpsertResults [] = Nothing
|
||||||
convertLdapResults [x] = Just $ Right x
|
convertUpsertResults [x] = Just $ Right x
|
||||||
convertLdapResults xs = Just $ Left $ NonEmpty.fromList xs
|
convertUpsertResults xs = Just $ Left $ NonEmpty.fromList xs
|
||||||
|
|
||||||
if
|
if
|
||||||
| [x] <- users'
|
| [x] <- users'
|
||||||
, Just True == matchesMatriculation x || didLdap
|
, Just True == matchesMatriculation x || didUpsert
|
||||||
-> return $ Just $ Right x
|
-> return $ Just $ Right x
|
||||||
| x : x' : _ <- users'
|
| x : x' : _ <- users'
|
||||||
, Just True == matchesMatriculation x || didLdap
|
, Just True == matchesMatriculation x || didUpsert
|
||||||
, GT <- x `closeness` x'
|
, GT <- x `closeness` x'
|
||||||
-> return $ Just $ Right x
|
-> return $ Just $ Right x
|
||||||
| xs@(x:_:_) <- takeClosest users'
|
| xs@(x:_:_) <- takeClosest users'
|
||||||
, Just True == matchesMatriculation x || didLdap
|
, Just True == matchesMatriculation x || didUpsert
|
||||||
-> return $ Just $ Left $ NonEmpty.fromList xs
|
-> return $ Just $ Left $ NonEmpty.fromList xs
|
||||||
| not didLdap
|
| not didUpsert
|
||||||
, userMatrs <- ((Set.toList . Set.fromList) (mapMaybe getTermMatr criteria))
|
, userMatrs <- ((Set.toList . Set.fromList) (mapMaybe getTermMatr criteria))
|
||||||
-> mapM doLdap userMatrs >>= maybe (go True) (return . Just) . convertLdapResults . catMaybes
|
-> mapM doUpsert userMatrs >>= maybe (go True) (return . Just) . convertUpsertResults . catMaybes
|
||||||
| otherwise
|
| otherwise
|
||||||
-> return Nothing
|
-> return Nothing
|
||||||
|
|
||||||
@ -1076,8 +1067,7 @@ assimilateUser newUserId oldUserId = mapReaderT execWriterT $ do
|
|||||||
mergeMaybe = mergeBy (\oldV newV -> isNothing newV && isJust oldV)
|
mergeMaybe = mergeBy (\oldV newV -> isNothing newV && isJust oldV)
|
||||||
|
|
||||||
update newUserId $ catMaybes -- NOTE: persist does shortcircuit null updates as expected
|
update newUserId $ catMaybes -- NOTE: persist does shortcircuit null updates as expected
|
||||||
[ mergeMaybe UserLdapPrimaryKey
|
[ mergeMaybe UserPasswordHash
|
||||||
, mergeBy (<) UserAuthentication
|
|
||||||
, mergeBy (>) UserLastAuthentication
|
, mergeBy (>) UserLastAuthentication
|
||||||
, mergeBy (<) UserCreated
|
, mergeBy (<) UserCreated
|
||||||
, toMaybe (not (validEmail' (newUser ^. _userEmail )) && validEmail' (oldUser ^. _userEmail))
|
, toMaybe (not (validEmail' (newUser ^. _userEmail )) && validEmail' (oldUser ^. _userEmail))
|
||||||
|
|||||||
@ -254,6 +254,7 @@ import Data.Encoding.UTF8 as Import (UTF8(UTF8))
|
|||||||
|
|
||||||
import GHC.TypeLits as Import (KnownSymbol)
|
import GHC.TypeLits as Import (KnownSymbol)
|
||||||
|
|
||||||
|
import Data.Word as Import (Word16)
|
||||||
import Data.Word.Word24 as Import
|
import Data.Word.Word24 as Import
|
||||||
|
|
||||||
import Data.Kind as Import (Type, Constraint)
|
import Data.Kind as Import (Type, Constraint)
|
||||||
|
|||||||
@ -61,7 +61,7 @@ import Jobs.Handler.SendCourseCommunication
|
|||||||
import Jobs.Handler.Invitation
|
import Jobs.Handler.Invitation
|
||||||
import Jobs.Handler.SendPasswordReset
|
import Jobs.Handler.SendPasswordReset
|
||||||
import Jobs.Handler.TransactionLog
|
import Jobs.Handler.TransactionLog
|
||||||
import Jobs.Handler.SynchroniseLdap
|
import Jobs.Handler.SynchroniseUser
|
||||||
import Jobs.Handler.SynchroniseAvs
|
import Jobs.Handler.SynchroniseAvs
|
||||||
import Jobs.Handler.PruneInvitations
|
import Jobs.Handler.PruneInvitations
|
||||||
import Jobs.Handler.ChangeUserDisplayEmail
|
import Jobs.Handler.ChangeUserDisplayEmail
|
||||||
@ -483,7 +483,7 @@ handleJobs' wNum = C.mapM_ $ \jctl -> hoist delimitInternalState . withJobWorker
|
|||||||
, Exc.Handler $ \case
|
, Exc.Handler $ \case
|
||||||
MailNotAvailable -> return $ Right ()
|
MailNotAvailable -> return $ Right ()
|
||||||
e -> return . Left $ SomeException e
|
e -> return . Left $ SomeException e
|
||||||
, Exc.Handler $ \SynchroniseLdapNoLdap -> return $ Right ()
|
, Exc.Handler $ \SynchroniseUserNoSource -> return $ Right ()
|
||||||
#endif
|
#endif
|
||||||
, Exc.Handler $ \(e :: SomeException) -> return $ Left e
|
, Exc.Handler $ \(e :: SomeException) -> return $ Left e
|
||||||
] . fmap Right
|
] . fmap Right
|
||||||
|
|||||||
@ -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
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -253,15 +253,14 @@ determineCrontab = execWriterT $ do
|
|||||||
return (nextEpoch, nextInterval, nextIntervalTime, numIntervals)
|
return (nextEpoch, nextInterval, nextIntervalTime, numIntervals)
|
||||||
|
|
||||||
if
|
if
|
||||||
| is _Just appLdapConf
|
| Just syncWithin <- appUserSyncWithin
|
||||||
, Just syncWithin <- appSynchroniseLdapUsersWithin
|
, Just cInterval <- appJobCronInterval
|
||||||
, Just cInterval <- appJobCronInterval
|
|
||||||
-> do
|
-> do
|
||||||
nextIntervals <- getNextIntervals syncWithin appSynchroniseLdapUsersInterval cInterval
|
nextIntervals <- getNextIntervals syncWithin appUserSyncInterval cInterval
|
||||||
|
|
||||||
forM_ nextIntervals $ \(nextEpoch, nextInterval, nextIntervalTime, numIntervals) -> do
|
forM_ nextIntervals $ \(nextEpoch, nextInterval, nextIntervalTime, numIntervals) -> do
|
||||||
tell $ HashMap.singleton
|
tell $ HashMap.singleton
|
||||||
(JobCtlQueue JobSynchroniseLdap
|
(JobCtlQueue JobSynchroniseUsers
|
||||||
{ jEpoch = fromInteger nextEpoch
|
{ jEpoch = fromInteger nextEpoch
|
||||||
, jNumIterations = fromInteger numIntervals
|
, jNumIterations = fromInteger numIntervals
|
||||||
, jIteration = fromInteger nextInterval
|
, jIteration = fromInteger nextInterval
|
||||||
@ -269,8 +268,8 @@ determineCrontab = execWriterT $ do
|
|||||||
Cron
|
Cron
|
||||||
{ cronInitial = CronTimestamp $ utcToLocalTimeTZ appTZ $ toTimeOfDay 23 30 0 $ utctDay nextIntervalTime
|
{ cronInitial = CronTimestamp $ utcToLocalTimeTZ appTZ $ toTimeOfDay 23 30 0 $ utctDay nextIntervalTime
|
||||||
, cronRepeat = CronRepeatNever
|
, cronRepeat = CronRepeatNever
|
||||||
, cronRateLimit = appSynchroniseLdapUsersInterval
|
, cronRateLimit = appUserSyncInterval
|
||||||
, cronNotAfter = Right . CronTimestamp . utcToLocalTimeTZ appTZ $ addUTCTime appSynchroniseLdapUsersInterval nextIntervalTime
|
, cronNotAfter = Right . CronTimestamp . utcToLocalTimeTZ appTZ $ addUTCTime appUserSyncInterval nextIntervalTime
|
||||||
}
|
}
|
||||||
| otherwise
|
| otherwise
|
||||||
-> return ()
|
-> return ()
|
||||||
|
|||||||
@ -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
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
|||||||
@ -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
|
-- 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
|
resetBearer' <- bearerToken (HashSet.singleton $ Right jRecipient) Nothing (HashMap.singleton BearerTokenRouteEval . HashSet.singleton $ UserPasswordR cID) Nothing (Just $ Just tomorrowEndOfDay) Nothing
|
||||||
let resetBearer = resetBearer'
|
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
|
encodedBearer <- encodeBearer resetBearer
|
||||||
|
|
||||||
resetUrl <- toTextUrl (UserPasswordR cID, [(toPathPiece GetBearer, toPathPiece encodedBearer)])
|
resetUrl <- toTextUrl (UserPasswordR cID, [(toPathPiece GetBearer, toPathPiece encodedBearer)])
|
||||||
|
|||||||
@ -1,69 +0,0 @@
|
|||||||
-- SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>
|
|
||||||
--
|
|
||||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
module Jobs.Handler.SynchroniseLdap
|
|
||||||
( dispatchJobSynchroniseLdap
|
|
||||||
, dispatchJobSynchroniseLdapUser
|
|
||||||
, dispatchJobSynchroniseLdapAll
|
|
||||||
, SynchroniseLdapException(..)
|
|
||||||
) where
|
|
||||||
|
|
||||||
import Import
|
|
||||||
|
|
||||||
import qualified Data.CaseInsensitive as CI
|
|
||||||
import qualified Data.Conduit.List as C
|
|
||||||
|
|
||||||
import Auth.LDAP
|
|
||||||
import Foundation.Yesod.Auth (CampusUserConversionException, upsertCampusUser)
|
|
||||||
|
|
||||||
import Jobs.Queue
|
|
||||||
|
|
||||||
|
|
||||||
data SynchroniseLdapException
|
|
||||||
= SynchroniseLdapNoLdap
|
|
||||||
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic)
|
|
||||||
instance Exception SynchroniseLdapException
|
|
||||||
|
|
||||||
dispatchJobSynchroniseLdap :: Natural -> Natural -> Natural -> JobHandler UniWorX
|
|
||||||
dispatchJobSynchroniseLdap numIterations epoch iteration
|
|
||||||
= JobHandlerAtomic . runConduit $
|
|
||||||
readUsers .| filterIteration .| sinkDBJobs
|
|
||||||
where
|
|
||||||
readUsers :: ConduitT () UserId (YesodJobDB UniWorX) ()
|
|
||||||
readUsers = selectKeys [] []
|
|
||||||
|
|
||||||
filterIteration :: ConduitT UserId Job (YesodJobDB UniWorX) ()
|
|
||||||
filterIteration = C.mapMaybeM $ \userId -> runMaybeT $ do
|
|
||||||
let
|
|
||||||
userIteration, currentIteration :: Integer
|
|
||||||
userIteration = toInteger (hash epoch `hashWithSalt` userId) `mod` toInteger numIterations
|
|
||||||
currentIteration = toInteger iteration `mod` toInteger numIterations
|
|
||||||
$logDebugS "SynchroniseLdap" [st|User ##{tshow (fromSqlKey userId)}: LDAP sync on #{tshow userIteration}/#{tshow numIterations}, now #{tshow currentIteration}|]
|
|
||||||
guard $ userIteration == currentIteration
|
|
||||||
|
|
||||||
return $ JobSynchroniseLdapUser userId
|
|
||||||
|
|
||||||
dispatchJobSynchroniseLdapUser :: UserId -> JobHandler UniWorX
|
|
||||||
dispatchJobSynchroniseLdapUser jUser = JobHandlerException $ do
|
|
||||||
UniWorX{..} <- getYesod
|
|
||||||
case appLdapPool of
|
|
||||||
Just ldapPool ->
|
|
||||||
runDB . void . runMaybeT . handleExc $ do
|
|
||||||
user@User{userIdent,userLdapPrimaryKey} <- MaybeT $ get jUser
|
|
||||||
let upsertIdent = maybe userIdent CI.mk userLdapPrimaryKey
|
|
||||||
$logInfoS "SynchroniseLdap" [st|Synchronising #{upsertIdent}|]
|
|
||||||
|
|
||||||
reTestAfter <- getsYesod $ view _appLdapReTestFailover
|
|
||||||
ldapAttrs <- MaybeT $ campusUserReTest' ldapPool ((>= reTestAfter) . realToFrac) FailoverUnlimited user
|
|
||||||
void . lift $ upsertCampusUser (UpsertCampusUserLdapSync upsertIdent) ldapAttrs
|
|
||||||
Nothing ->
|
|
||||||
throwM SynchroniseLdapNoLdap
|
|
||||||
where
|
|
||||||
handleExc :: MaybeT DB a -> MaybeT DB a
|
|
||||||
handleExc
|
|
||||||
= catchMPlus (Proxy @CampusUserException)
|
|
||||||
. catchMPlus (Proxy @CampusUserConversionException)
|
|
||||||
|
|
||||||
dispatchJobSynchroniseLdapAll :: JobHandler UniWorX
|
|
||||||
dispatchJobSynchroniseLdapAll = JobHandlerAtomic . runConduit $ selectSource [] [] .| C.mapM_ (queueDBJob . JobSynchroniseLdapUser . entityKey)
|
|
||||||
48
src/Jobs/Handler/SynchroniseUser.hs
Normal file
48
src/Jobs/Handler/SynchroniseUser.hs
Normal 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{..}
|
||||||
@ -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
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -107,23 +107,31 @@ dispatchHealthCheckHTTPReachable = fmap HealthHTTPReachable . yesodTimeout (^. _
|
|||||||
getsYesod $ (== clusterId) . appClusterID
|
getsYesod $ (== clusterId) . appClusterID
|
||||||
|
|
||||||
|
|
||||||
|
-- TODO: generalize health check
|
||||||
dispatchHealthCheckLDAPAdmins :: Handler HealthReport
|
dispatchHealthCheckLDAPAdmins :: Handler HealthReport
|
||||||
dispatchHealthCheckLDAPAdmins = fmap HealthLDAPAdmins . yesodTimeout (^. _appHealthCheckLDAPAdminsTimeout) (Just 0) $ do
|
dispatchHealthCheckLDAPAdmins = fmap HealthLDAPAdmins . yesodTimeout (^. _appHealthCheckLDAPAdminsTimeout) (Just 0) $ do
|
||||||
ldapPool' <- getsYesod appLdapPool
|
ldapPool' <- getsYesod appLdapPool
|
||||||
reTestAfter <- getsYesod $ view _appLdapReTestFailover
|
userAuthConf <- getsYesod $ view _appUserAuthConf
|
||||||
case ldapPool' of
|
case ldapPool' of
|
||||||
Just ldapPool -> do
|
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
|
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.on $ user E.^. UserId E.==. userFunction E.^. UserFunctionUser
|
||||||
E.where_ $ userFunction E.^. UserFunctionFunction E.==. E.val SchoolAdmin
|
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
|
return $ user E.^. UserIdent
|
||||||
for (assertM' (not . null) ldapAdminUsers') $ \ldapAdminUsers -> do
|
for (assertM' (not . null) ldapAdminUsers') $ \ldapAdminUsers -> do
|
||||||
let numAdmins = genericLength ldapAdminUsers
|
let numAdmins = genericLength ldapAdminUsers
|
||||||
Sum numResolved <- fmap fold . forM ldapAdminUsers $ \(CI.original -> adminIdent) ->
|
Sum numResolved <- fmap fold . forM ldapAdminUsers $ \(CI.original -> adminIdent) ->
|
||||||
let hCampusExc :: CampusUserException -> Handler (Sum Integer)
|
let hLdapExc :: LdapUserException -> Handler (Sum Integer)
|
||||||
hCampusExc err = mempty <$ $logErrorS "healthCheckLDAPAdmins" (adminIdent <> ": " <> tshow err)
|
hLdapExc err = mempty <$ $logErrorS "healthCheckLDAPAdmins" (adminIdent <> ": " <> tshow err)
|
||||||
in handle hCampusExc $ Sum 1 <$ campusUserReTest ldapPool ((>= reTestAfter) . realToFrac) FailoverUnlimited (Creds apLdap adminIdent [])
|
in handle hLdapExc $ Sum 1 <$ ldapUser ldapPool (Creds apLdap adminIdent [])
|
||||||
|
--in handle hLdapExc $ Sum 1 <$ ldapUserReTest ldapPool (const True) FailoverUnlimited (Creds apLdap adminIdent [])
|
||||||
if
|
if
|
||||||
| numAdmins >= 1 -> return $ numResolved % numAdmins
|
| numAdmins >= 1 -> return $ numResolved % numAdmins
|
||||||
| otherwise -> return 0
|
| otherwise -> return 0
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
-- SPDX-FileCopyrightText: 2022-2024 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>
|
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>
|
||||||
--
|
--
|
||||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -91,12 +91,12 @@ data Job
|
|||||||
| JobTruncateTransactionLog
|
| JobTruncateTransactionLog
|
||||||
| JobPruneInvitations
|
| JobPruneInvitations
|
||||||
| JobDeleteTransactionLogIPs
|
| JobDeleteTransactionLogIPs
|
||||||
| JobSynchroniseLdap { jNumIterations
|
| JobSynchroniseUsers { jNumIterations
|
||||||
, jEpoch
|
, jEpoch
|
||||||
, jIteration :: Natural
|
, jIteration :: Natural
|
||||||
}
|
}
|
||||||
| JobSynchroniseLdapUser { jUser :: UserId }
|
| JobSynchroniseUser { jUser :: UserId }
|
||||||
| JobSynchroniseLdapAll
|
| JobSynchroniseUserAll
|
||||||
| JobSynchroniseAvs { jNumIterations
|
| JobSynchroniseAvs { jNumIterations
|
||||||
, jEpoch
|
, jEpoch
|
||||||
, jIteration :: Natural
|
, jIteration :: Natural
|
||||||
@ -326,9 +326,9 @@ jobNoQueueSame = \case
|
|||||||
JobTruncateTransactionLog{} -> Just JobNoQueueSame
|
JobTruncateTransactionLog{} -> Just JobNoQueueSame
|
||||||
JobPruneInvitations{} -> Just JobNoQueueSame
|
JobPruneInvitations{} -> Just JobNoQueueSame
|
||||||
JobDeleteTransactionLogIPs{} -> Just JobNoQueueSame
|
JobDeleteTransactionLogIPs{} -> Just JobNoQueueSame
|
||||||
JobSynchroniseLdap{} -> Just JobNoQueueSame
|
JobSynchroniseUsers{} -> Just JobNoQueueSame
|
||||||
JobSynchroniseLdapUser{} -> Just JobNoQueueSame
|
JobSynchroniseUser{} -> Just JobNoQueueSame
|
||||||
JobSynchroniseLdapAll{} -> Just JobNoQueueSameTag
|
JobSynchroniseUserAll{} -> Just JobNoQueueSameTag
|
||||||
JobSynchroniseAvs{} -> Just JobNoQueueSame
|
JobSynchroniseAvs{} -> Just JobNoQueueSame
|
||||||
-- JobSynchroniseAvsUser{} -> Just JobNoQueueSame
|
-- JobSynchroniseAvsUser{} -> Just JobNoQueueSame
|
||||||
-- JobSynchroniseAvsId{} -> Just JobNoQueueSame
|
-- JobSynchroniseAvsId{} -> Just JobNoQueueSame
|
||||||
|
|||||||
@ -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
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -9,7 +9,53 @@ module Ldap.Client.Instances
|
|||||||
) where
|
) where
|
||||||
|
|
||||||
import ClassyPrelude
|
import ClassyPrelude
|
||||||
|
|
||||||
|
import Data.Aeson.TH
|
||||||
|
import Data.Data (Data)
|
||||||
|
|
||||||
|
import Database.Persist.TH (derivePersistField)
|
||||||
|
|
||||||
|
import Utils.PathPiece (derivePathPiece)
|
||||||
|
|
||||||
import Ldap.Client
|
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 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
|
||||||
|
|||||||
@ -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
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
|||||||
100
src/Middleware.hs
Normal file
100
src/Middleware.hs
Normal 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
|
||||||
|
}
|
||||||
@ -51,7 +51,8 @@ data ManualMigration
|
|||||||
| Migration20230703LmsUserStatus
|
| Migration20230703LmsUserStatus
|
||||||
| Migration20240212InitInterfaceHealth -- create table interface_health and fill with default values
|
| Migration20240212InitInterfaceHealth -- create table interface_health and fill with default values
|
||||||
| Migration20240224UniquenessCompanyAvsNr
|
| Migration20240224UniquenessCompanyAvsNr
|
||||||
| Migration20240930RoomOccurrences -- rooms become a part of occurrences
|
| Migration20240312OAuth2
|
||||||
|
| Migration20240930RoomOccurrences
|
||||||
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic)
|
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic)
|
||||||
deriving anyclass (Universe, Finite)
|
deriving anyclass (Universe, Finite)
|
||||||
|
|
||||||
@ -85,7 +86,7 @@ migrateManual = do
|
|||||||
, ("user_matrikelnummer", "CREATE INDEX user_matrikelnummer ON \"user\" (matrikelnummer)" )
|
, ("user_matrikelnummer", "CREATE INDEX user_matrikelnummer ON \"user\" (matrikelnummer)" )
|
||||||
, ("submission_sheet", "CREATE INDEX submission_sheet ON submission (sheet)" )
|
, ("submission_sheet", "CREATE INDEX submission_sheet ON submission (sheet)" )
|
||||||
, ("submission_edit_submission", "CREATE INDEX submission_edit_submission ON submission_edit (submission)" )
|
, ("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)" )
|
, ("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_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")
|
, ("sent_mail_recipient", "CREATE INDEX sent_mail_recipient ON \"sent_mail\" (recipient) WHERE recipient IS NOT NULL")
|
||||||
@ -210,6 +211,23 @@ customMigrations = mapF $ \case
|
|||||||
ALTER TABLE "company" DROP CONSTRAINT IF EXISTS "unique_company_shorthand";
|
ALTER TABLE "company" DROP CONSTRAINT IF EXISTS "unique_company_shorthand";
|
||||||
|]
|
|]
|
||||||
|
|
||||||
|
Migration20240312OAuth2 -> whenM (andM [ columnNotExists "user" "password_hash", columnExists "user" "authentication", columnExists "user" "last_ldap_synchronisation", columnNotExists "user" "last_sync", columnExists "user" "ldap_primary_key" ]) $ do
|
||||||
|
[executeQQ|
|
||||||
|
ALTER TABLE "user" ADD COLUMN "password_hash" VARCHAR NULL;
|
||||||
|
|]
|
||||||
|
let getPWHashes = [queryQQ| SELECT "id", "authentication"->'pw-hash' FROM "user" WHERE "authentication"->'pw-hash' IS NOT NULL; |]
|
||||||
|
migratePWHash [ fromPersistValue -> Right (uid :: UserId), fromPersistValue -> Right (pwHash :: Text) ] = [executeQQ| UPDATE "user" SET "password_hash" = #{pwHash} WHERE "id" = #{uid}; |]
|
||||||
|
migratePWHash _ = return ()
|
||||||
|
in runConduit $ getPWHashes .| C.mapM_ migratePWHash
|
||||||
|
[executeQQ|
|
||||||
|
ALTER TABLE "user" DROP COLUMN "authentication";
|
||||||
|
|]
|
||||||
|
|
||||||
|
[executeQQ|
|
||||||
|
ALTER TABLE "user" RENAME COLUMN "last_ldap_synchronisation" TO "last_sync";
|
||||||
|
ALTER TABLE "user" DROP COLUMN "ldap_primary_key";
|
||||||
|
|]
|
||||||
|
|
||||||
Migration20240930RoomOccurrences -> do
|
Migration20240930RoomOccurrences -> do
|
||||||
whenM (tableColumnExists "tutorial" "room")
|
whenM (tableColumnExists "tutorial" "room")
|
||||||
[executeQQ|
|
[executeQQ|
|
||||||
|
|||||||
@ -6,6 +6,7 @@ module Model.Types
|
|||||||
( module Types
|
( module Types
|
||||||
) where
|
) where
|
||||||
|
|
||||||
|
import Model.Types.Auth as Types
|
||||||
import Model.Types.Common as Types
|
import Model.Types.Common as Types
|
||||||
import Model.Types.Course as Types
|
import Model.Types.Course as Types
|
||||||
import Model.Types.DateTime 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.ExamOffice as Types
|
||||||
import Model.Types.Health as Types
|
import Model.Types.Health as Types
|
||||||
import Model.Types.Mail as Types
|
import Model.Types.Mail as Types
|
||||||
import Model.Types.Security as Types
|
|
||||||
import Model.Types.Sheet as Types
|
import Model.Types.Sheet as Types
|
||||||
import Model.Types.Submission as Types
|
import Model.Types.Submission as Types
|
||||||
import Model.Types.Misc as Types
|
import Model.Types.Misc as Types
|
||||||
|
|||||||
@ -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
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||||
|
|
||||||
{-|
|
{-|
|
||||||
Module: Model.Types.Security
|
Module: Model.Types.Auth
|
||||||
Description: Types for authentication and authorisation
|
Description: Types for authentication and authorisation
|
||||||
-}
|
-}
|
||||||
|
|
||||||
module Model.Types.Security
|
module Model.Types.Auth
|
||||||
( module Model.Types.Security
|
( module Model.Types.Auth
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import ClassyPrelude.Yesod hiding (derivePersistFieldJSON, Proxy(..))
|
import ClassyPrelude.Yesod hiding (derivePersistFieldJSON, Proxy(..))
|
||||||
|
|
||||||
import Utils
|
|
||||||
|
|
||||||
import Data.Aeson
|
|
||||||
import Data.Aeson.TH
|
|
||||||
import Model.Types.TH.JSON
|
import Model.Types.TH.JSON
|
||||||
import Data.Universe
|
import Model.Types.TH.PathPiece
|
||||||
import Data.Universe.Instances.Reverse ()
|
|
||||||
import Data.Proxy
|
import Utils
|
||||||
import Data.Data (Data)
|
import Utils.Lens.TH
|
||||||
|
|
||||||
import Control.Lens
|
import Control.Lens
|
||||||
|
|
||||||
import qualified Data.Set as Set
|
import Data.Aeson
|
||||||
|
import Data.Aeson.TH
|
||||||
import qualified Data.Text as Text
|
|
||||||
|
|
||||||
import qualified Data.HashMap.Strict as HashMap
|
|
||||||
|
|
||||||
import qualified Data.Aeson.Types as Aeson
|
import qualified Data.Aeson.Types as Aeson
|
||||||
|
import qualified Data.Binary as Binary
|
||||||
import Data.CaseInsensitive (CI)
|
import Data.Binary (Binary)
|
||||||
|
import Data.Binary.Instances.UnorderedContainers ()
|
||||||
import qualified Data.CaseInsensitive as CI
|
import qualified Data.CaseInsensitive as CI
|
||||||
|
import Data.CaseInsensitive (CI)
|
||||||
import Data.CaseInsensitive.Instances ()
|
import Data.CaseInsensitive.Instances ()
|
||||||
|
import Data.Data (Data)
|
||||||
import Data.Set.Instances ()
|
import qualified Data.HashMap.Strict as HashMap
|
||||||
import Data.NonNull.Instances ()
|
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.Universe.Instances.Reverse.MonoTraversable ()
|
||||||
|
import Data.UUID (UUID)
|
||||||
|
|
||||||
import Model.Types.TH.PathPiece
|
|
||||||
import Database.Persist.Sql
|
import Database.Persist.Sql
|
||||||
|
|
||||||
import Servant.Docs (ToSample(..), samples)
|
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 }
|
----- Authentication Sources -----
|
||||||
| AuthNoLogin
|
----------------------------------
|
||||||
deriving (Eq, Ord, Read, Show, Generic)
|
|
||||||
|
|
||||||
instance Hashable AuthenticationMode
|
type AzureScopes = Set Text
|
||||||
instance NFData AuthenticationMode
|
|
||||||
|
-- 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
|
deriveJSON defaultOptions
|
||||||
{ constructorTagModifier = camelToPathPiece' 1
|
{ fieldLabelModifier = camelToPathPiece' 3
|
||||||
, fieldLabelModifier = camelToPathPiece' 1
|
, constructorTagModifier = camelToPathPiece' 3
|
||||||
, sumEncoding = UntaggedValue
|
, sumEncoding = UntaggedValue
|
||||||
} ''AuthenticationMode
|
} ''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
|
data AuthTag -- sortiert nach gewünschter Reihenfolge auf /authpreds, d.h. Prädikate sind sortier nach Relevanz für Benutzer
|
||||||
= AuthAdmin
|
= AuthAdmin
|
||||||
@ -105,8 +133,8 @@ data AuthTag -- sortiert nach gewünschter Reihenfolge auf /authpreds, d.h. Prä
|
|||||||
| AuthRegisterGroup
|
| AuthRegisterGroup
|
||||||
| AuthEmpty
|
| AuthEmpty
|
||||||
| AuthSelf
|
| AuthSelf
|
||||||
| AuthIsLDAP
|
| AuthIsExternal -- TODO: maybe distinguish between AuthenticationProtocols
|
||||||
| AuthIsPWHash
|
| AuthIsInternal
|
||||||
| AuthAuthentication
|
| AuthAuthentication
|
||||||
| AuthNoEscalation
|
| AuthNoEscalation
|
||||||
| AuthRead
|
| AuthRead
|
||||||
@ -179,6 +207,11 @@ _ReducedActiveAuthTags = iso toReducedActiveAuthTags fromReducedActiveAuthTags
|
|||||||
fromReducedActiveAuthTags (ReducedActiveAuthTags hm) = AuthTagActive $ \n -> fromMaybe (authTagIsActive def n) $ HashMap.lookup n hm
|
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 }
|
data PredLiteral a = PLVariable { plVar :: a } | PLNegated { plVar :: a }
|
||||||
deriving (Eq, Ord, Read, Show, Data, Generic)
|
deriving (Eq, Ord, Read, Show, Data, Generic)
|
||||||
deriving anyclass (Hashable, Binary, NFData)
|
deriving anyclass (Hashable, Binary, NFData)
|
||||||
@ -220,7 +253,6 @@ parsePredDNF start = fmap (PredDNF . Set.mapMonotonic impureNonNull) . ofoldM pa
|
|||||||
| otherwise
|
| otherwise
|
||||||
= Left t
|
= Left t
|
||||||
|
|
||||||
|
|
||||||
$(return [])
|
$(return [])
|
||||||
|
|
||||||
instance ToJSON a => ToJSON (PredDNF a) where
|
instance ToJSON a => ToJSON (PredDNF a) where
|
||||||
@ -73,7 +73,7 @@ import qualified Data.Foldable
|
|||||||
|
|
||||||
import Data.Aeson (genericToJSON, genericParseJSON)
|
import Data.Aeson (genericToJSON, genericParseJSON)
|
||||||
|
|
||||||
import Model.Types.Security
|
import Model.Types.Auth
|
||||||
|
|
||||||
{-# ANN module ("HLint: ignore Use newtype instead of data" :: String) #-}
|
{-# ANN module ("HLint: ignore Use newtype instead of data" :: String) #-}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
-- SPDX-FileCopyrightText: 2022-2024 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>
|
-- SPDX-FileCopyrightText: 2022-2025 Sarah Vaupel <sarah.vaupel@uniworx.de>,-2024 Gregor Kleen <gregor.kleen@ifi.lmu.de>, Sarah Vaupel <sarah.vaupel@ifi.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>
|
||||||
--
|
--
|
||||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -8,9 +8,6 @@ import Import.NoModel
|
|||||||
import Model.Types.TH.PathPiece
|
import Model.Types.TH.PathPiece
|
||||||
|
|
||||||
|
|
||||||
type UserEduPersonPrincipalName = Text
|
|
||||||
|
|
||||||
|
|
||||||
data SystemFunction
|
data SystemFunction
|
||||||
= SystemExamOffice
|
= SystemExamOffice
|
||||||
| SystemFaculty
|
| SystemFaculty
|
||||||
|
|||||||
512
src/Settings.hs
512
src/Settings.hs
@ -1,4 +1,4 @@
|
|||||||
-- SPDX-FileCopyrightText: 2022-2025 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>
|
-- SPDX-FileCopyrightText: 2022-2025 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>,David Mosbach <david.mosbach@uniworx.de>
|
||||||
--
|
--
|
||||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -13,10 +13,13 @@
|
|||||||
module Settings
|
module Settings
|
||||||
( module Settings
|
( module Settings
|
||||||
, module Settings.Cluster
|
, module Settings.Cluster
|
||||||
, module Settings.Mime
|
|
||||||
, module Settings.Cookies
|
, module Settings.Cookies
|
||||||
|
, module Settings.Ldap
|
||||||
, module Settings.Log
|
, module Settings.Log
|
||||||
, module Settings.Locale
|
, module Settings.Locale
|
||||||
|
, module Settings.Mime
|
||||||
|
, module Settings.OAuth2
|
||||||
|
, module Settings.ResourcePool
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Import.NoModel
|
import Import.NoModel
|
||||||
@ -41,12 +44,8 @@ import Language.Haskell.TH.Syntax (Exp, Q)
|
|||||||
import qualified Yesod.Auth.Util.PasswordStore as PWStore
|
import qualified Yesod.Auth.Util.PasswordStore as PWStore
|
||||||
|
|
||||||
import qualified Data.Scientific as Scientific
|
import qualified Data.Scientific as Scientific
|
||||||
import Data.Word (Word16)
|
|
||||||
|
|
||||||
import qualified Data.Text as Text
|
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.HaskellNet.Auth as HaskellNet (UserName, Password, AuthType(..))
|
||||||
import qualified Network.Socket as HaskellNet
|
import qualified Network.Socket as HaskellNet
|
||||||
@ -56,11 +55,15 @@ import Network.Mail.Mime.Instances ()
|
|||||||
import qualified Database.Memcached.Binary.Types as Memcached
|
import qualified Database.Memcached.Binary.Types as Memcached
|
||||||
|
|
||||||
import Model
|
import Model
|
||||||
|
|
||||||
import Settings.Cluster
|
import Settings.Cluster
|
||||||
import Settings.Mime
|
|
||||||
import Settings.Cookies
|
import Settings.Cookies
|
||||||
|
import Settings.Ldap
|
||||||
import Settings.Log
|
import Settings.Log
|
||||||
import Settings.Locale
|
import Settings.Locale
|
||||||
|
import Settings.Mime
|
||||||
|
import Settings.OAuth2
|
||||||
|
import Settings.ResourcePool
|
||||||
|
|
||||||
import qualified System.FilePath as FilePath
|
import qualified System.FilePath as FilePath
|
||||||
|
|
||||||
@ -73,8 +76,6 @@ import qualified Web.ServerSession.Core as ServerSession
|
|||||||
|
|
||||||
import Text.Show (showParen, showString)
|
import Text.Show (showParen, showString)
|
||||||
|
|
||||||
import qualified Data.List.PointedList as P
|
|
||||||
|
|
||||||
import qualified Network.Minio as Minio
|
import qualified Network.Minio as Minio
|
||||||
|
|
||||||
import Data.Conduit.Algorithms.FastCDC
|
import Data.Conduit.Algorithms.FastCDC
|
||||||
@ -84,176 +85,6 @@ import Utils.Lens.TH
|
|||||||
import qualified Data.Set as Set
|
import qualified Data.Set as Set
|
||||||
|
|
||||||
|
|
||||||
-- | Runtime settings to configure this application. These settings can be
|
|
||||||
-- loaded from various sources: defaults, environment variables, config files,
|
|
||||||
-- theoretically even a database.
|
|
||||||
data AppSettings = AppSettings
|
|
||||||
{ appStaticDir :: FilePath
|
|
||||||
-- ^ Directory from which to serve static files.
|
|
||||||
, appBundlerEntrypoints :: FilePath
|
|
||||||
, appWellKnownDir :: FilePath
|
|
||||||
, appWellKnownLinkFile :: FilePath
|
|
||||||
, appDatabaseConf :: PostgresConf
|
|
||||||
-- ^ Configuration settings for accessing the database.
|
|
||||||
, appAutoDbMigrate :: Bool
|
|
||||||
, appLdapConf :: Maybe (PointedList LdapConf)
|
|
||||||
-- ^ Configuration settings for CSV export/import to LMS (= Learn Management System)
|
|
||||||
, appLmsConf :: LmsConf
|
|
||||||
-- ^ Configuration settings for accessing the LDAP-directory
|
|
||||||
, appAvsConf :: Maybe AvsConf
|
|
||||||
-- ^ Configuration settings for accessing AVS Server (= Ausweis Verwaltungs System)
|
|
||||||
, appAvsLicenceSynchConf :: AvsLicenceSynchConf
|
|
||||||
-- ^ Configuration settings for automatically synching driving licences with AVS
|
|
||||||
, appLprConf :: LprConf
|
|
||||||
-- ^ Configuration settings for accessing a printer queue via lpr for letter mailing
|
|
||||||
, appSmtpConf :: Maybe SmtpConf
|
|
||||||
-- ^ Configuration settings for accessing a SMTP Mailserver
|
|
||||||
, appWidgetMemcachedConf :: Maybe WidgetMemcachedConf
|
|
||||||
-- ^ Configuration settings for accessing a Memcached instance for use with `addStaticContent`
|
|
||||||
, appRoot :: ApprootScope -> Maybe Text
|
|
||||||
-- ^ Base for all generated URLs. If @Nothing@, determined
|
|
||||||
-- from the request headers.
|
|
||||||
, appHost :: HostPreference
|
|
||||||
-- ^ Host/interface the server should bind to.
|
|
||||||
, appPort :: Int
|
|
||||||
-- ^ Port to listen on
|
|
||||||
, appIpFromHeader :: Bool
|
|
||||||
-- ^ Get the IP address from the header when logging. Useful when sitting
|
|
||||||
-- behind a reverse proxy.
|
|
||||||
|
|
||||||
, appServerSessionConfig :: ServerSessionSettings
|
|
||||||
, appServerSessionAcidFallback :: Bool
|
|
||||||
, appSessionMemcachedConf :: Maybe MemcachedConf
|
|
||||||
, appSessionTokenStart
|
|
||||||
, appSessionTokenExpiration :: Maybe NominalDiffTime
|
|
||||||
, appSessionTokenEncoding :: JwtEncoding
|
|
||||||
, appSessionTokenClockLeniencyStart, appSessionTokenClockLeniencyEnd
|
|
||||||
, appBearerTokenClockLeniencyStart, appBearerTokenClockLeniencyEnd
|
|
||||||
, appUploadTokenClockLeniencyStart, appUploadTokenClockLeniencyEnd :: Maybe NominalDiffTime
|
|
||||||
|
|
||||||
, appMailObjectDomain :: Text
|
|
||||||
, appMailVerp :: VerpMode
|
|
||||||
, appMailRetainSent :: Maybe NominalDiffTime
|
|
||||||
, appMailEnvelopeFrom :: Text
|
|
||||||
, appMailFrom
|
|
||||||
, appMailSender
|
|
||||||
, appMailSupport :: Address
|
|
||||||
, appMailRerouteTo :: Maybe Address
|
|
||||||
, appMailUseReplyToInstead :: Bool
|
|
||||||
, appJobWorkers :: Natural
|
|
||||||
, appJobFlushInterval :: Maybe NominalDiffTime
|
|
||||||
, appJobCronInterval :: Maybe NominalDiffTime
|
|
||||||
, appJobStaleThreshold :: NominalDiffTime
|
|
||||||
, appJobMoveThreshold :: Maybe DiffTime
|
|
||||||
, appNotificationRateLimit :: NominalDiffTime
|
|
||||||
, appNotificationCollateDelay :: NominalDiffTime
|
|
||||||
, appNotificationExpiration :: NominalDiffTime
|
|
||||||
, appSessionTimeout :: NominalDiffTime
|
|
||||||
, appMaximumContentLength :: Maybe Word64
|
|
||||||
, appBearerExpiration :: Maybe NominalDiffTime
|
|
||||||
, appBearerEncoding :: JwtEncoding
|
|
||||||
|
|
||||||
, appHealthCheckInterval :: HealthCheck -> Maybe NominalDiffTime
|
|
||||||
, appHealthCheckDelayNotify :: Bool
|
|
||||||
, appHealthCheckHTTP :: Bool
|
|
||||||
|
|
||||||
, appHealthCheckActiveJobExecutorsTimeout :: NominalDiffTime
|
|
||||||
, appHealthCheckActiveWidgetMemcachedTimeout :: NominalDiffTime
|
|
||||||
, appHealthCheckSMTPConnectTimeout :: NominalDiffTime
|
|
||||||
, appHealthCheckLDAPAdminsTimeout :: NominalDiffTime
|
|
||||||
, appHealthCheckHTTPReachableTimeout :: NominalDiffTime
|
|
||||||
, appHealthCheckMatchingClusterConfigTimeout :: NominalDiffTime
|
|
||||||
|
|
||||||
, appSynchroniseLdapUsersWithin :: Maybe NominalDiffTime
|
|
||||||
, appSynchroniseLdapUsersInterval :: NominalDiffTime
|
|
||||||
, appSynchroniseLdapUsersExpire :: Maybe NominalDiffTime
|
|
||||||
|
|
||||||
, appSynchroniseAvsUsersWithin :: Maybe NominalDiffTime
|
|
||||||
, appSynchroniseAvsUsersInterval :: NominalDiffTime
|
|
||||||
|
|
||||||
, appLdapReTestFailover :: DiffTime
|
|
||||||
|
|
||||||
, appSessionFilesExpire :: NominalDiffTime
|
|
||||||
, appKeepUnreferencedFiles :: NominalDiffTime
|
|
||||||
|
|
||||||
, appPruneUnreferencedFilesWithin :: Maybe NominalDiffTime
|
|
||||||
, appPruneUnreferencedFilesInterval :: NominalDiffTime
|
|
||||||
|
|
||||||
, appInitialLogSettings :: LogSettings
|
|
||||||
|
|
||||||
, appTransactionLogIPRetentionTime :: NominalDiffTime
|
|
||||||
|
|
||||||
, appReloadTemplates :: Bool
|
|
||||||
-- ^ Use the reload version of templates
|
|
||||||
, appMutableStatic :: Bool
|
|
||||||
-- ^ Assume that files in the static dir may change after compilation
|
|
||||||
, appSkipCombining :: Bool
|
|
||||||
-- ^ Perform no stylesheet/script combining
|
|
||||||
, appAuthDummyLogin :: Bool
|
|
||||||
-- ^ Indicate if auth dummy login should be enabled.
|
|
||||||
, appAllowDeprecated :: Bool
|
|
||||||
-- ^ Indicate if deprecated routes are accessible for everyone
|
|
||||||
, appEncryptErrors :: Bool
|
|
||||||
, appClearCache :: Bool
|
|
||||||
|
|
||||||
, appUserDefaults :: UserDefaultConf
|
|
||||||
, appAuthPWHash :: PWHashConf
|
|
||||||
|
|
||||||
, appExternalApisPingInterval
|
|
||||||
, appExternalApisPongTimeout
|
|
||||||
, appExternalApisExpiry :: NominalDiffTime
|
|
||||||
|
|
||||||
, appCookieSettings :: RegisteredCookie -> CookieSettings
|
|
||||||
|
|
||||||
, appMemcachedConf :: Maybe MemcachedConf
|
|
||||||
, appMemcacheAuth :: Bool
|
|
||||||
|
|
||||||
, appUploadCacheConf :: Maybe Minio.ConnectInfo
|
|
||||||
, appUploadCacheBucket, appUploadTmpBucket :: Minio.Bucket
|
|
||||||
, appInjectFiles :: Maybe NominalDiffTime
|
|
||||||
, appRechunkFiles :: Maybe NominalDiffTime
|
|
||||||
, appCheckMissingFiles :: Maybe NominalDiffTime
|
|
||||||
, appFileUploadDBChunksize :: Int
|
|
||||||
|
|
||||||
|
|
||||||
, appFavouritesQuickActionsBurstsize
|
|
||||||
, appFavouritesQuickActionsAvgInverseRate :: Word64
|
|
||||||
, appFavouritesQuickActionsTimeout :: DiffTime
|
|
||||||
, appFavouritesQuickActionsCacheTTL :: Maybe DiffTime
|
|
||||||
|
|
||||||
, appPersistentTokenBuckets :: TokenBucketIdent -> TokenBucketConf
|
|
||||||
|
|
||||||
, appFallbackPersonalisedSheetFilesKeysExpire :: NominalDiffTime
|
|
||||||
|
|
||||||
, appDownloadTokenExpire :: NominalDiffTime
|
|
||||||
|
|
||||||
, appInitialInstanceID :: Maybe (Either FilePath UUID)
|
|
||||||
, appRibbon :: Maybe Text
|
|
||||||
|
|
||||||
, appJobMode :: JobMode
|
|
||||||
|
|
||||||
, appStudyFeaturesRecacheRelevanceWithin :: Maybe NominalDiffTime
|
|
||||||
, appStudyFeaturesRecacheRelevanceInterval :: NominalDiffTime
|
|
||||||
|
|
||||||
, appJobLmsQualificationsEnqueueHour :: Maybe Natural
|
|
||||||
, appJobLmsQualificationsDequeueHour :: Maybe Natural
|
|
||||||
|
|
||||||
, appBotMitigations :: Set SettingBotMitigation
|
|
||||||
|
|
||||||
, appVolatileClusterSettingsCacheTime :: DiffTime
|
|
||||||
|
|
||||||
, appJobMaxFlush :: Maybe Natural
|
|
||||||
|
|
||||||
, appCommunicationAttachmentsMaxSize :: Maybe Natural
|
|
||||||
, appCommunicationGlobalCC :: Maybe UserEmail
|
|
||||||
|
|
||||||
, appFileChunkingParams :: FastCDCParameters
|
|
||||||
|
|
||||||
, appLegalExternal :: Set LegalExternal
|
|
||||||
|
|
||||||
} deriving Show
|
|
||||||
|
|
||||||
|
|
||||||
data JobMode = JobsLocal { jobsAcceptOffload :: Bool }
|
data JobMode = JobsLocal { jobsAcceptOffload :: Bool }
|
||||||
| JobsOffload
|
| JobsOffload
|
||||||
| JobsDrop
|
| JobsDrop
|
||||||
@ -305,15 +136,29 @@ instance FromJSON PWHashConf where
|
|||||||
|
|
||||||
return PWHashConf{..}
|
return PWHashConf{..}
|
||||||
|
|
||||||
data LdapConf = LdapConf
|
|
||||||
{ ldapHost :: Ldap.Host, ldapPort :: Ldap.PortNumber
|
data AuthSourceConf = AuthSourceConfLdap LdapConf | AuthSourceConfAzureAdV2 AzureConf
|
||||||
, ldapDn :: Ldap.Dn, ldapPassword :: Ldap.Password
|
deriving (Show)
|
||||||
, ldapBase :: Ldap.Dn
|
|
||||||
, ldapScope :: Ldap.Scope
|
newtype UserAuthConf =
|
||||||
, ldapTimeout :: NominalDiffTime
|
UserAuthConfSingleSource -- ^ use only one specific source
|
||||||
, ldapSearchTimeout :: Int32
|
{ userAuthConfSingleSource :: AuthSourceConf
|
||||||
, ldapPool :: ResourcePoolConf
|
}
|
||||||
} deriving (Show)
|
-- 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
|
data LmsConf = LmsConf
|
||||||
{ lmsUploadHeader :: Bool
|
{ lmsUploadHeader :: Bool
|
||||||
@ -394,12 +239,6 @@ instance FromJSON WidgetMemcachedConf where
|
|||||||
widgetMemcachedBaseUrl <- o .:? "base-url" .!= ""
|
widgetMemcachedBaseUrl <- o .:? "base-url" .!= ""
|
||||||
return WidgetMemcachedConf{..}
|
return WidgetMemcachedConf{..}
|
||||||
|
|
||||||
data ResourcePoolConf = ResourcePoolConf
|
|
||||||
{ poolStripes :: Int
|
|
||||||
, poolTimeout :: NominalDiffTime
|
|
||||||
, poolLimit :: Int
|
|
||||||
} deriving (Show)
|
|
||||||
|
|
||||||
data SmtpSslMode = SmtpSslNone | SmtpSslSmtps | SmtpSslStarttls
|
data SmtpSslMode = SmtpSslNone | SmtpSslSmtps | SmtpSslStarttls
|
||||||
deriving (Show)
|
deriving (Show)
|
||||||
|
|
||||||
@ -452,7 +291,6 @@ deriveJSON defaultOptions
|
|||||||
{ fieldLabelModifier = camelToPathPiece' 2
|
{ fieldLabelModifier = camelToPathPiece' 2
|
||||||
} ''TokenBucketConf
|
} ''TokenBucketConf
|
||||||
|
|
||||||
deriveFromJSON defaultOptions ''Ldap.Scope
|
|
||||||
deriveFromJSON defaultOptions
|
deriveFromJSON defaultOptions
|
||||||
{ fieldLabelModifier = camelToPathPiece' 2
|
{ fieldLabelModifier = camelToPathPiece' 2
|
||||||
} ''UserDefaultConf
|
} ''UserDefaultConf
|
||||||
@ -469,47 +307,31 @@ pathPieceJSONKey ''SettingBotMitigation
|
|||||||
makePrisms ''JobMode
|
makePrisms ''JobMode
|
||||||
makeLenses_ ''JobMode
|
makeLenses_ ''JobMode
|
||||||
|
|
||||||
|
makePrisms ''AuthSourceConf
|
||||||
|
makeLenses_ ''UserAuthConf
|
||||||
|
makePrisms ''UserAuthConf
|
||||||
|
|
||||||
instance FromJSON LdapConf where
|
deriveFromJSON defaultOptions
|
||||||
parseJSON = withObject "LdapConf" $ \o -> do
|
{ constructorTagModifier = toLower . dropPrefix "AuthSourceConf"
|
||||||
ldapTls <- o .:? "tls"
|
, sumEncoding = TaggedObject "protocol" "config"
|
||||||
tlsSettings <- case ldapTls :: Maybe String of
|
} ''AuthSourceConf
|
||||||
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
|
deriveFromJSON defaultOptions
|
||||||
defaultOptions
|
{ constructorTagModifier = camelToPathPiece' 3
|
||||||
{ fieldLabelModifier = intercalate "-" . map toLower . drop 1 . splitCamel
|
, fieldLabelModifier = camelToPathPiece' 3
|
||||||
}
|
, sumEncoding = UntaggedValue -- TaggedObject "mode" "config"
|
||||||
''ResourcePoolConf
|
, unwrapUnaryRecords = True
|
||||||
|
} ''UserAuthConf
|
||||||
|
|
||||||
instance FromJSON HaskellNet.PortNumber where
|
instance FromJSON HaskellNet.PortNumber where
|
||||||
parseJSON = withScientific "PortNumber" $ \sciNum -> case Scientific.toBoundedInteger sciNum of
|
parseJSON = withScientific "PortNumber" $ \sciNum -> case Scientific.toBoundedInteger sciNum of
|
||||||
Just int -> return $ fromIntegral (int :: Word16)
|
Just int -> return $ fromIntegral (int :: Word16)
|
||||||
Nothing -> fail "Expected whole number of plausible size to denote port"
|
Nothing -> fail "Expected whole number of plausible size to denote port"
|
||||||
|
|
||||||
deriveFromJSON
|
deriveFromJSON defaultOptions
|
||||||
defaultOptions
|
{ constructorTagModifier = unpack . intercalate "-" . Text.splitOn "_" . toLower . pack
|
||||||
{ constructorTagModifier = unpack . intercalate "-" . Text.splitOn "_" . toLower . pack
|
, allNullaryToStringTag = True
|
||||||
, allNullaryToStringTag = True
|
} ''HaskellNet.AuthType
|
||||||
}
|
|
||||||
''HaskellNet.AuthType
|
|
||||||
|
|
||||||
instance FromJSON LmsConf where
|
instance FromJSON LmsConf where
|
||||||
parseJSON = withObject "LmsConf" $ \o -> do
|
parseJSON = withObject "LmsConf" $ \o -> do
|
||||||
@ -602,7 +424,6 @@ instance FromJSON Minio.ConnectInfo where
|
|||||||
connectDisableTLSCertValidation <- o .:? "disable-cert-validation" .!= False
|
connectDisableTLSCertValidation <- o .:? "disable-cert-validation" .!= False
|
||||||
return Minio.ConnectInfo{..}
|
return Minio.ConnectInfo{..}
|
||||||
|
|
||||||
|
|
||||||
instance FromJSON ServerSessionSettings where
|
instance FromJSON ServerSessionSettings where
|
||||||
parseJSON = withObject "ServerSession.State" $ \o -> do
|
parseJSON = withObject "ServerSession.State" $ \o -> do
|
||||||
idleTimeout <- o .:? "idle-timeout"
|
idleTimeout <- o .:? "idle-timeout"
|
||||||
@ -625,6 +446,188 @@ instance FromJSON LegalExternal where
|
|||||||
externalPayments <- o .: "payments"
|
externalPayments <- o .: "payments"
|
||||||
return LegalExternal{..}
|
return LegalExternal{..}
|
||||||
|
|
||||||
|
submissionBlacklist :: [Pattern]
|
||||||
|
submissionBlacklist = $$(patternFile compDefault "config/submission-blacklist")
|
||||||
|
|
||||||
|
personalisedSheetFilesCollatable :: Map Text Pattern
|
||||||
|
personalisedSheetFilesCollatable = $$(patternFile' compDefault "config/personalised-sheet-files-collate")
|
||||||
|
|
||||||
|
|
||||||
|
-- | Runtime settings to configure this application. These settings can be
|
||||||
|
-- loaded from various sources: defaults, environment variables, config files,
|
||||||
|
-- theoretically even a database.
|
||||||
|
data AppSettings = AppSettings
|
||||||
|
{ appStaticDir :: FilePath
|
||||||
|
-- ^ Directory from which to serve static files.
|
||||||
|
, appBundlerEntrypoints :: FilePath
|
||||||
|
, appWellKnownDir :: FilePath
|
||||||
|
, appWellKnownLinkFile :: FilePath
|
||||||
|
, appDatabaseConf :: PostgresConf
|
||||||
|
-- ^ Configuration settings for accessing the database.
|
||||||
|
, appAutoDbMigrate :: Bool
|
||||||
|
, appUserAuthConf :: UserAuthConf
|
||||||
|
, appSingleSignOn :: Bool
|
||||||
|
-- ^ Enable OIDC single sign-on
|
||||||
|
, appAutoSignOn :: Bool
|
||||||
|
-- ^ Automatically redirect to SSO route when not signed on
|
||||||
|
-- ^ Note: This will force authentication, thus the site will be inaccessible without external credentials. Only use this option when it is ensured that every user that should be able to access the site has valid external credentials!
|
||||||
|
, appLmsConf :: LmsConf
|
||||||
|
-- ^ Configuration settings for CSV export/import to LMS (= Learn Management System) -- TODO, TODISCUSS: reimplement as user-auth source?
|
||||||
|
, appAvsConf :: Maybe AvsConf
|
||||||
|
-- ^ Configuration settings for accessing AVS Server (= Ausweis Verwaltungs System) -- TODO, TODISCUSS: reimplement as user-auth source?
|
||||||
|
, appAvsLicenceSynchConf :: Maybe AvsLicenceSynchConf
|
||||||
|
, appLprConf :: LprConf
|
||||||
|
-- ^ Configuration settings for accessing a printer queue via lpr for letter mailing
|
||||||
|
, appSmtpConf :: Maybe SmtpConf
|
||||||
|
-- ^ Configuration settings for accessing a SMTP Mailserver
|
||||||
|
, appWidgetMemcachedConf :: Maybe WidgetMemcachedConf
|
||||||
|
-- ^ Configuration settings for accessing a Memcached instance for use with `addStaticContent`
|
||||||
|
, appRoot :: ApprootScope -> Maybe Text
|
||||||
|
-- ^ Base for all generated URLs. If @Nothing@, determined from the request headers.
|
||||||
|
, appHost :: HostPreference
|
||||||
|
-- ^ Host/interface the server should bind to.
|
||||||
|
, appPort :: Int
|
||||||
|
-- ^ Port to listen on
|
||||||
|
, appIpFromHeader :: Bool
|
||||||
|
-- ^ Get the IP address from the header when logging. Useful when sitting behind a reverse proxy.
|
||||||
|
|
||||||
|
, appServerSessionConfig :: ServerSessionSettings
|
||||||
|
, appServerSessionAcidFallback :: Bool
|
||||||
|
, appSessionMemcachedConf :: Maybe MemcachedConf
|
||||||
|
, appSessionTokenStart
|
||||||
|
, appSessionTokenExpiration :: Maybe NominalDiffTime
|
||||||
|
, appSessionTokenEncoding :: JwtEncoding
|
||||||
|
, appSessionTokenClockLeniencyStart, appSessionTokenClockLeniencyEnd
|
||||||
|
, appBearerTokenClockLeniencyStart, appBearerTokenClockLeniencyEnd
|
||||||
|
, appUploadTokenClockLeniencyStart, appUploadTokenClockLeniencyEnd :: Maybe NominalDiffTime
|
||||||
|
|
||||||
|
, appMailObjectDomain :: Text
|
||||||
|
, appMailVerp :: VerpMode
|
||||||
|
, appMailRetainSent :: Maybe NominalDiffTime
|
||||||
|
, appMailEnvelopeFrom :: Text
|
||||||
|
, appMailFrom
|
||||||
|
, appMailSender
|
||||||
|
, appMailSupport :: Address
|
||||||
|
, appMailRerouteTo :: Maybe Address
|
||||||
|
, appMailUseReplyToInstead :: Bool
|
||||||
|
, appJobWorkers :: Natural
|
||||||
|
, appJobFlushInterval :: Maybe NominalDiffTime
|
||||||
|
, appJobCronInterval :: Maybe NominalDiffTime
|
||||||
|
, appJobStaleThreshold :: NominalDiffTime
|
||||||
|
, appJobMoveThreshold :: Maybe DiffTime
|
||||||
|
, appNotificationRateLimit :: NominalDiffTime
|
||||||
|
, appNotificationCollateDelay :: NominalDiffTime
|
||||||
|
, appNotificationExpiration :: NominalDiffTime
|
||||||
|
, appSessionTimeout :: NominalDiffTime
|
||||||
|
, appMaximumContentLength :: Maybe Word64
|
||||||
|
, appBearerExpiration :: Maybe NominalDiffTime
|
||||||
|
, appBearerEncoding :: JwtEncoding
|
||||||
|
|
||||||
|
, appHealthCheckInterval :: HealthCheck -> Maybe NominalDiffTime
|
||||||
|
, appHealthCheckDelayNotify :: Bool
|
||||||
|
, appHealthCheckHTTP :: Bool
|
||||||
|
|
||||||
|
, appHealthCheckActiveJobExecutorsTimeout :: NominalDiffTime
|
||||||
|
, appHealthCheckActiveWidgetMemcachedTimeout :: NominalDiffTime
|
||||||
|
, appHealthCheckSMTPConnectTimeout :: NominalDiffTime
|
||||||
|
, appHealthCheckLDAPAdminsTimeout :: NominalDiffTime -- TODO: either generalize over every external auth sources, or otherwise reimplement for different semantics
|
||||||
|
, appHealthCheckHTTPReachableTimeout :: NominalDiffTime
|
||||||
|
, appHealthCheckMatchingClusterConfigTimeout :: NominalDiffTime
|
||||||
|
|
||||||
|
-- , appUserRetestFailover :: DiffTime -- TODO: reintroduce and move into failover settings once failover mode has been reimplemented
|
||||||
|
-- TODO; maybe implement syncWithin and syncInterval per auth source
|
||||||
|
, appUserSyncWithin :: Maybe NominalDiffTime
|
||||||
|
, appUserSyncInterval :: NominalDiffTime
|
||||||
|
-- , appSynchroniseLdapUsersExpire :: Maybe NominalDiffTime -- TODO: migrate
|
||||||
|
|
||||||
|
, appLdapPoolConf :: Maybe ResourcePoolConf -- TODO: generalize for arbitrary auth protocols
|
||||||
|
-- TODO: maybe use separate pools for external databases?
|
||||||
|
|
||||||
|
, appSynchroniseAvsUsersWithin :: Maybe NominalDiffTime
|
||||||
|
, appSynchroniseAvsUsersInterval :: NominalDiffTime
|
||||||
|
|
||||||
|
, appSynchroniseLdapUsersExpire :: Maybe NominalDiffTime
|
||||||
|
|
||||||
|
, appSessionFilesExpire :: NominalDiffTime
|
||||||
|
, appKeepUnreferencedFiles :: NominalDiffTime
|
||||||
|
|
||||||
|
, appPruneUnreferencedFilesWithin :: Maybe NominalDiffTime
|
||||||
|
, appPruneUnreferencedFilesInterval :: NominalDiffTime
|
||||||
|
|
||||||
|
, appInitialLogSettings :: LogSettings
|
||||||
|
|
||||||
|
, appTransactionLogIPRetentionTime :: NominalDiffTime
|
||||||
|
|
||||||
|
, appReloadTemplates :: Bool
|
||||||
|
-- ^ Use the reload version of templates
|
||||||
|
, appMutableStatic :: Bool
|
||||||
|
-- ^ Assume that files in the static dir may change after compilation
|
||||||
|
, appSkipCombining :: Bool
|
||||||
|
-- ^ Perform no stylesheet/script combining
|
||||||
|
, appAuthDummyLogin :: Bool
|
||||||
|
-- ^ Indicate if auth dummy login should be enabled.
|
||||||
|
, appAllowDeprecated :: Bool
|
||||||
|
-- ^ Indicate if deprecated routes are accessible for everyone
|
||||||
|
, appEncryptErrors :: Bool
|
||||||
|
, appClearCache :: Bool
|
||||||
|
|
||||||
|
, appUserDefaults :: UserDefaultConf
|
||||||
|
, appAuthPWHash :: PWHashConf
|
||||||
|
|
||||||
|
, appExternalApisPingInterval
|
||||||
|
, appExternalApisPongTimeout
|
||||||
|
, appExternalApisExpiry :: NominalDiffTime
|
||||||
|
|
||||||
|
, appCookieSettings :: RegisteredCookie -> CookieSettings
|
||||||
|
|
||||||
|
, appMemcachedConf :: Maybe MemcachedConf
|
||||||
|
, appMemcacheAuth :: Bool
|
||||||
|
|
||||||
|
, appUploadCacheConf :: Maybe Minio.ConnectInfo
|
||||||
|
, appUploadCacheBucket, appUploadTmpBucket :: Minio.Bucket
|
||||||
|
, appInjectFiles :: Maybe NominalDiffTime
|
||||||
|
, appRechunkFiles :: Maybe NominalDiffTime
|
||||||
|
, appCheckMissingFiles :: Maybe NominalDiffTime
|
||||||
|
, appFileUploadDBChunksize :: Int
|
||||||
|
|
||||||
|
|
||||||
|
, appFavouritesQuickActionsBurstsize
|
||||||
|
, appFavouritesQuickActionsAvgInverseRate :: Word64
|
||||||
|
, appFavouritesQuickActionsTimeout :: DiffTime
|
||||||
|
, appFavouritesQuickActionsCacheTTL :: Maybe DiffTime
|
||||||
|
|
||||||
|
, appPersistentTokenBuckets :: TokenBucketIdent -> TokenBucketConf
|
||||||
|
|
||||||
|
, appFallbackPersonalisedSheetFilesKeysExpire :: NominalDiffTime
|
||||||
|
|
||||||
|
, appDownloadTokenExpire :: NominalDiffTime
|
||||||
|
|
||||||
|
, appInitialInstanceID :: Maybe (Either FilePath UUID)
|
||||||
|
, appRibbon :: Maybe Text
|
||||||
|
|
||||||
|
, appJobMode :: JobMode
|
||||||
|
|
||||||
|
, appStudyFeaturesRecacheRelevanceWithin :: Maybe NominalDiffTime
|
||||||
|
, appStudyFeaturesRecacheRelevanceInterval :: NominalDiffTime
|
||||||
|
|
||||||
|
, appJobLmsQualificationsEnqueueHour :: Maybe Natural
|
||||||
|
, appJobLmsQualificationsDequeueHour :: Maybe Natural
|
||||||
|
|
||||||
|
, appBotMitigations :: Set SettingBotMitigation
|
||||||
|
|
||||||
|
, appVolatileClusterSettingsCacheTime :: DiffTime
|
||||||
|
|
||||||
|
, appJobMaxFlush :: Maybe Natural
|
||||||
|
|
||||||
|
, appCommunicationAttachmentsMaxSize :: Maybe Natural
|
||||||
|
, appCommunicationGlobalCC :: Maybe UserEmail
|
||||||
|
|
||||||
|
, appFileChunkingParams :: FastCDCParameters
|
||||||
|
|
||||||
|
, appLegalExternal :: Set LegalExternal
|
||||||
|
|
||||||
|
} deriving Show
|
||||||
|
|
||||||
instance FromJSON AppSettings where
|
instance FromJSON AppSettings where
|
||||||
parseJSON = withObject "AppSettings" $ \o -> do
|
parseJSON = withObject "AppSettings" $ \o -> do
|
||||||
let defaultDev =
|
let defaultDev =
|
||||||
@ -639,10 +642,16 @@ instance FromJSON AppSettings where
|
|||||||
appBundlerEntrypoints <- o .: "bundler-manifest"
|
appBundlerEntrypoints <- o .: "bundler-manifest"
|
||||||
appDatabaseConf <- o .: "database"
|
appDatabaseConf <- o .: "database"
|
||||||
appAutoDbMigrate <- o .: "auto-db-migrate"
|
appAutoDbMigrate <- o .: "auto-db-migrate"
|
||||||
let nonEmptyHost LdapConf{..} = case ldapHost of
|
-- TODO: reintroduce non-emptyness check for ldap hosts
|
||||||
Ldap.Tls host _ -> not $ null host
|
-- let nonEmptyHost (UserDbLdap LdapConf{..}) = case ldapHost of
|
||||||
Ldap.Plain host -> not $ null host
|
-- Ldap.Tls host _ -> not $ null host
|
||||||
appLdapConf <- P.fromList . mapMaybe (assertM nonEmptyHost) <$> o .:? "ldap" .!= []
|
-- 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"
|
appLmsConf <- o .: "lms-direct"
|
||||||
appAvsConf <- assertM (not . null . avsPass) <$> o .:? "avs"
|
appAvsConf <- assertM (not . null . avsPass) <$> o .:? "avs"
|
||||||
appAvsLicenceSynchConf <- o .:? "avs-licence-synch" .!= def
|
appAvsLicenceSynchConf <- o .:? "avs-licence-synch" .!= def
|
||||||
@ -707,15 +716,14 @@ instance FromJSON AppSettings where
|
|||||||
|
|
||||||
appSessionTimeout <- o .: "session-timeout"
|
appSessionTimeout <- o .: "session-timeout"
|
||||||
|
|
||||||
appSynchroniseLdapUsersWithin <- o .:? "synchronise-ldap-users-within"
|
-- appUserRetestFailover <- o .: "userdb-retest-failover"
|
||||||
appSynchroniseLdapUsersInterval <- o .: "synchronise-ldap-users-interval"
|
appUserSyncWithin <- o .:? "user-sync-within"
|
||||||
|
appUserSyncInterval <- o .: "user-sync-interval"
|
||||||
appSynchroniseLdapUsersExpire <- o .:? "synrchonise-ldap-users-expire" -- time after last synch to delete LDAP sepcific data
|
appSynchroniseLdapUsersExpire <- o .:? "synrchonise-ldap-users-expire" -- time after last synch to delete LDAP sepcific data
|
||||||
|
|
||||||
appSynchroniseAvsUsersWithin <- o .:? "synchronise-avs-users-within"
|
appSynchroniseAvsUsersWithin <- o .:? "synchronise-avs-users-within"
|
||||||
appSynchroniseAvsUsersInterval <- o .: "synchronise-avs-users-interval"
|
appSynchroniseAvsUsersInterval <- o .: "synchronise-avs-users-interval"
|
||||||
|
|
||||||
appLdapReTestFailover <- o .: "ldap-re-test-failover"
|
|
||||||
|
|
||||||
appSessionFilesExpire <- o .: "session-files-expire"
|
appSessionFilesExpire <- o .: "session-files-expire"
|
||||||
appKeepUnreferencedFiles <- o .:? "keep-unreferenced-files" .!= 0
|
appKeepUnreferencedFiles <- o .:? "keep-unreferenced-files" .!= 0
|
||||||
appInjectFiles <- o .:? "inject-files"
|
appInjectFiles <- o .:? "inject-files"
|
||||||
@ -817,6 +825,26 @@ instance FromJSON AppSettings where
|
|||||||
|
|
||||||
makeClassy_ ''AppSettings
|
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
|
-- | Settings for 'widgetFile', such as which template languages to support and
|
||||||
-- default Hamlet settings.
|
-- default Hamlet settings.
|
||||||
--
|
--
|
||||||
@ -826,16 +854,6 @@ makeClassy_ ''AppSettings
|
|||||||
widgetFileSettings :: WidgetFileSettings
|
widgetFileSettings :: WidgetFileSettings
|
||||||
widgetFileSettings = def
|
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
|
widgetFile :: String -> Q Exp
|
||||||
#ifdef DEVELOPMENT
|
#ifdef DEVELOPMENT
|
||||||
widgetFile nameBase = do
|
widgetFile nameBase = do
|
||||||
|
|||||||
63
src/Settings/Ldap.hs
Normal file
63
src/Settings/Ldap.hs
Normal 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
32
src/Settings/OAuth2.hs
Normal 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
|
||||||
30
src/Settings/ResourcePool.hs
Normal file
30
src/Settings/ResourcePool.hs
Normal 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
|
||||||
@ -1,4 +1,4 @@
|
|||||||
-- SPDX-FileCopyrightText: 2023-2025 Felix Hamann <felix.hamann@campus.lmu.de>,Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Sarah Vaupel <vaupel.sarah@campus.lmu.de>,Steffen Jost <jost@cip.ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Wolfgang Witt <Wolfgang.Witt@campus.lmu.de>,Steffen Jost <s.jost@fraport.de>
|
-- SPDX-FileCopyrightText: 2023-2025 Sarah Vaupel <sarah.vaupel@uniworx.de>, Felix Hamann <felix.hamann@campus.lmu.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>, Sarah Vaupel <sarah.vaupel@ifi.lmu.de>, Sarah Vaupel <vaupel.sarah@campus.lmu.de>, Steffen Jost <jost@cip.ifi.lmu.de>, Steffen Jost <jost@tcs.ifi.lmu.de>, Wolfgang Witt <Wolfgang.Witt@campus.lmu.de>,Steffen Jost <s.jost@fraport.de>
|
||||||
--
|
--
|
||||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -19,7 +19,7 @@ import Settings
|
|||||||
import Utils.Parameters
|
import Utils.Parameters
|
||||||
import Utils.Lens
|
import Utils.Lens
|
||||||
|
|
||||||
import Text.Blaze (Markup)
|
import Text.Blaze (Markup, toMarkup)
|
||||||
import qualified Text.Blaze.Internal as Blaze (null)
|
import qualified Text.Blaze.Internal as Blaze (null)
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import qualified Data.Char as C
|
import qualified Data.Char as C
|
||||||
@ -27,6 +27,7 @@ import qualified Data.Char as C
|
|||||||
import Data.CaseInsensitive (CI)
|
import Data.CaseInsensitive (CI)
|
||||||
import qualified Data.CaseInsensitive as CI
|
import qualified Data.CaseInsensitive as CI
|
||||||
import Data.Universe
|
import Data.Universe
|
||||||
|
import qualified Data.UUID as UUID
|
||||||
|
|
||||||
import Data.List (nub, (!!))
|
import Data.List (nub, (!!))
|
||||||
import Data.Map.Lazy ((!))
|
import Data.Map.Lazy ((!))
|
||||||
@ -81,6 +82,9 @@ import qualified Data.ByteString.Base64.URL as Base64 (encodeUnpadded)
|
|||||||
import qualified Data.ByteString as BS
|
import qualified Data.ByteString as BS
|
||||||
|
|
||||||
|
|
||||||
|
fvWidget :: FieldView site -> WidgetFor site ()
|
||||||
|
fvWidget FieldView{..} = $(widgetFile "widgets/field-view/field-view")
|
||||||
|
|
||||||
------------
|
------------
|
||||||
-- Fields --
|
-- Fields --
|
||||||
------------
|
------------
|
||||||
@ -116,6 +120,17 @@ commentField msg = Field {..}
|
|||||||
fieldView _ _ _ _ _ = msg2widget msg
|
fieldView _ _ _ _ _ = msg2widget msg
|
||||||
fieldEnctype = UrlEncoded
|
fieldEnctype = UrlEncoded
|
||||||
|
|
||||||
|
uuidField :: Monad m => Field m UUID
|
||||||
|
uuidField = Field{..}
|
||||||
|
where
|
||||||
|
fieldParse = parseHelperGen $ maybe (Left $ tshow "Invalid UUID!") Right . UUID.fromText
|
||||||
|
fieldView fvId (toMarkup -> fvLabel) fvAttrs fvInput' fvRequired = fvWidget FieldView{..}
|
||||||
|
where fvTooltip = Nothing
|
||||||
|
fvErrors = either (Just . toMarkup) (const Nothing) fvInput'
|
||||||
|
fvInput = [whamlet|<input type="text" *{fvAttrs} name=#{fvLabel} :fvRequired:required value=#{fvValue}>|]
|
||||||
|
fvValue = either id UUID.toText fvInput'
|
||||||
|
fieldEnctype = UrlEncoded
|
||||||
|
|
||||||
-- | Fields of sets are only allowed indirectly
|
-- | Fields of sets are only allowed indirectly
|
||||||
mkSetField :: (Ord a, Functor m) => Field m [a] -> Field m (Set a)
|
mkSetField :: (Ord a, Functor m) => Field m [a] -> Field m (Set a)
|
||||||
mkSetField = convertField Set.fromList Set.toList
|
mkSetField = convertField Set.fromList Set.toList
|
||||||
@ -1273,10 +1288,6 @@ formSection formSectionTitle = do
|
|||||||
, fvInput = mempty
|
, fvInput = mempty
|
||||||
})
|
})
|
||||||
|
|
||||||
fvWidget :: FieldView site -> WidgetFor site ()
|
|
||||||
fvWidget FieldView{..} = $(widgetFile "widgets/field-view/field-view")
|
|
||||||
|
|
||||||
|
|
||||||
doFormHoneypots :: ( MonadHandler m
|
doFormHoneypots :: ( MonadHandler m
|
||||||
, HasAppSettings (HandlerSite m)
|
, HasAppSettings (HandlerSite m)
|
||||||
, YesodAuth (HandlerSite m)
|
, YesodAuth (HandlerSite m)
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
-- SPDX-FileCopyrightText: 2022-2024 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Sarah Vaupel <vaupel.sarah@campus.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Wolfgang Witt <Wolfgang.Witt@campus.lmu.de>,David Mosbach <david.mosbach@uniworx.de>
|
-- SPDX-FileCopyrightText: 2022-2024 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Sarah Vaupel <vaupel.sarah@campus.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Wolfgang Witt <Wolfgang.Witt@campus.lmu.de>,David Mosbach <david.mosbach@uniworx.de>,Steffen Jost <s.jost@fraport.de>
|
||||||
--
|
--
|
||||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -87,6 +87,7 @@ data Icon
|
|||||||
| IconNavContainerClose | IconPageActionChildrenClose
|
| IconNavContainerClose | IconPageActionChildrenClose
|
||||||
| IconMenuNews
|
| IconMenuNews
|
||||||
| IconMenuHelp
|
| IconMenuHelp
|
||||||
|
| IconMenuAccount
|
||||||
| IconMenuProfile
|
| IconMenuProfile
|
||||||
| IconMenuLogin | IconMenuLogout
|
| IconMenuLogin | IconMenuLogout
|
||||||
| IconBreadcrumbsHome
|
| IconBreadcrumbsHome
|
||||||
|
|||||||
@ -269,8 +269,6 @@ makeLenses_ ''ExamOccurrence
|
|||||||
|
|
||||||
makeLenses_ ''ExamOfficeLabel
|
makeLenses_ ''ExamOfficeLabel
|
||||||
|
|
||||||
makePrisms ''AuthenticationMode
|
|
||||||
|
|
||||||
makeLenses_ ''CourseUserNote
|
makeLenses_ ''CourseUserNote
|
||||||
makeLenses_ ''CourseParticipant
|
makeLenses_ ''CourseParticipant
|
||||||
|
|
||||||
|
|||||||
@ -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
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -20,6 +20,7 @@ data SessionKey = SessionActiveAuthTags | SessionInactiveAuthTags
|
|||||||
| SessionLang
|
| SessionLang
|
||||||
| SessionError
|
| SessionError
|
||||||
| SessionFiles
|
| SessionFiles
|
||||||
|
| SessionOAuth2Token
|
||||||
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic)
|
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic)
|
||||||
deriving anyclass (Universe, Finite)
|
deriving anyclass (Universe, Finite)
|
||||||
|
|
||||||
|
|||||||
@ -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
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -12,24 +12,6 @@ module Utils.Users
|
|||||||
|
|
||||||
import Import
|
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
|
data AddUserData = AddUserData
|
||||||
{ audTitle :: Maybe Text
|
{ audTitle :: Maybe Text
|
||||||
@ -70,44 +52,46 @@ addNewUserDB aud = do
|
|||||||
makeUser :: MonadIO m => UserDefaultConf -> AddUserData -> m User
|
makeUser :: MonadIO m => UserDefaultConf -> AddUserData -> m User
|
||||||
makeUser UserDefaultConf{..} AddUserData{..} = do
|
makeUser UserDefaultConf{..} AddUserData{..} = do
|
||||||
now <- liftIO getCurrentTime
|
now <- liftIO getCurrentTime
|
||||||
return User
|
UserDefaultConf{..} <- getsYesod $ view _appUserDefaults
|
||||||
{ userIdent = audIdent
|
let
|
||||||
, userMaxFavourites = userDefaultMaxFavourites
|
newUser = User
|
||||||
, userMaxFavouriteTerms = userDefaultMaxFavouriteTerms
|
{ userIdent = audIdent
|
||||||
, userTheme = userDefaultTheme
|
, userMaxFavourites = userDefaultMaxFavourites
|
||||||
, userDateTimeFormat = userDefaultDateTimeFormat
|
, userMaxFavouriteTerms = userDefaultMaxFavouriteTerms
|
||||||
, userDateFormat = userDefaultDateFormat
|
, userTheme = userDefaultTheme
|
||||||
, userTimeFormat = userDefaultTimeFormat
|
, userDateTimeFormat = userDefaultDateTimeFormat
|
||||||
, userDownloadFiles = userDefaultDownloadFiles
|
, userDateFormat = userDefaultDateFormat
|
||||||
, userWarningDays = userDefaultWarningDays
|
, userTimeFormat = userDefaultTimeFormat
|
||||||
, userShowSex = userDefaultShowSex
|
, userDownloadFiles = userDefaultDownloadFiles
|
||||||
, userExamOfficeGetSynced = userDefaultExamOfficeGetSynced
|
, userWarningDays = userDefaultWarningDays
|
||||||
, userExamOfficeGetLabels = userDefaultExamOfficeGetLabels
|
, userShowSex = userDefaultShowSex
|
||||||
, userNotificationSettings = def
|
, userExamOfficeGetSynced = userDefaultExamOfficeGetSynced
|
||||||
, userLanguages = Nothing
|
, userExamOfficeGetLabels = userDefaultExamOfficeGetLabels
|
||||||
, userCsvOptions = def { csvFormat = review csvPreset CsvPresetXlsx }
|
, userNotificationSettings = def
|
||||||
, userTokensIssuedAfter = Nothing
|
, userLanguages = Nothing
|
||||||
, userCreated = now
|
, userCsvOptions = def { csvFormat = review csvPreset CsvPresetXlsx }
|
||||||
, userLastLdapSynchronisation = Nothing
|
, userTokensIssuedAfter = Nothing
|
||||||
, userLdapPrimaryKey = audFPersonalNumber
|
, userCreated = now
|
||||||
, userLastAuthentication = Nothing
|
, userLastLdapSynchronisation = Nothing
|
||||||
, userEmail = audEmail
|
, userLdapPrimaryKey = audFPersonalNumber
|
||||||
, userDisplayName = audDisplayName
|
, userLastAuthentication = Nothing
|
||||||
, userDisplayEmail = audDisplayEmail
|
, userEmail = audEmail
|
||||||
, userFirstName = audFirstName
|
, userDisplayName = audDisplayName
|
||||||
, userSurname = audSurname
|
, userDisplayEmail = audDisplayEmail
|
||||||
, userTitle = audTitle
|
, userFirstName = audFirstName
|
||||||
, userSex = audSex
|
, userSurname = audSurname
|
||||||
, userBirthday = audBirthday
|
, userTitle = audTitle
|
||||||
, userMobile = audMobile
|
, userSex = audSex
|
||||||
, userTelephone = audTelephone
|
, userBirthday = audBirthday
|
||||||
, userCompanyPersonalNumber = audFPersonalNumber
|
, userMobile = audMobile
|
||||||
, userCompanyDepartment = audFDepartment
|
, userTelephone = audTelephone
|
||||||
, userPostAddress = audPostAddress
|
, userCompanyPersonalNumber = audFPersonalNumber
|
||||||
, userPostLastUpdate = Nothing
|
, userCompanyDepartment = audFDepartment
|
||||||
, userPrefersPostal = audPrefersPostal
|
, userPostAddress = audPostAddress
|
||||||
, userPinPassword = audPinPassword
|
, userPostLastUpdate = Nothing
|
||||||
, userMatrikelnummer = audMatriculation
|
, userPrefersPostal = audPrefersPostal
|
||||||
, userAuthentication = mkAuthMode audAuth
|
, userPinPassword = audPinPassword
|
||||||
}
|
, userMatrikelnummer = audMatriculation
|
||||||
|
, userLastSync = Nothing -- TODO: combine add user with external sync?
|
||||||
|
}
|
||||||
|
runDB $ insertUnique newUser
|
||||||
@ -30,7 +30,7 @@ import Control.Lens.Extras
|
|||||||
import Foundation.Servant.Types
|
import Foundation.Servant.Types
|
||||||
|
|
||||||
import Utils hiding (HasRoute)
|
import Utils hiding (HasRoute)
|
||||||
import Model.Types.Security
|
import Model.Types.Auth
|
||||||
|
|
||||||
import Yesod.Core ( Yesod
|
import Yesod.Core ( Yesod
|
||||||
, RenderRoute(..), ParseRoute(..)
|
, RenderRoute(..), ParseRoute(..)
|
||||||
|
|||||||
@ -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
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
|||||||
46
templates/admin/external-user.hamlet
Normal file
46
templates/admin/external-user.hamlet
Normal 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}
|
||||||
|
$# —
|
||||||
|
$# Latin: #{vLatin1}
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<p>
|
||||||
|
Upsert user from external database:
|
||||||
|
^{upsertForm}
|
||||||
|
$maybe response <- mbUpsert
|
||||||
|
<h1>
|
||||||
|
Response: #
|
||||||
|
<p>
|
||||||
|
#{tshow response}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
$newline never
|
$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
|
$# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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}
|
|
||||||
—
|
|
||||||
Latin: #{presentLatin1 lv}
|
|
||||||
<section>
|
|
||||||
<p>
|
|
||||||
LDAP Upsert user in DB:
|
|
||||||
^{upsertForm}
|
|
||||||
$maybe answer <- mbLdapUpsert
|
|
||||||
<h1>
|
|
||||||
Antwort: #
|
|
||||||
<p>
|
|
||||||
#{tshow answer}
|
|
||||||
@ -1,20 +1,33 @@
|
|||||||
$newline never
|
$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
|
$# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
$forall AuthPlugin{apName, apLogin} <- plugins
|
$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>
|
<section>
|
||||||
<h2>_{MsgLDAPLoginTitle}
|
<h2>_{MsgLDAPLoginTitle}
|
||||||
^{apLogin toParent}
|
^{apLogin toParent}
|
||||||
$elseif apName == "PWHash"
|
$elseif apName == apHash
|
||||||
<section>
|
<section>
|
||||||
<h2>_{MsgPWHashLoginTitle}
|
<h2>_{MsgPWHashLoginTitle}
|
||||||
<p>_{MsgPWHashLoginNote}
|
<p>_{MsgPWHashLoginNote}
|
||||||
^{apLogin toParent}
|
^{apLogin toParent}
|
||||||
$elseif apName == "dummy"
|
$elseif apName == apDummy
|
||||||
<section>
|
<section>
|
||||||
<h2>_{MsgDummyLoginTitle}
|
<h2>_{MsgDummyLoginTitle}
|
||||||
^{apLogin toParent}
|
^{apLogin toParent}
|
||||||
|
$maybe port <- mPort
|
||||||
|
<section>
|
||||||
|
<h2>SSO Dev Test
|
||||||
|
<a href=http://localhost:#{port}/test-sso>Test login via single sign-on
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
$newline never
|
$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
|
$# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -19,21 +19,16 @@ $# SPDX-License-Identifier: AGPL-3.0-or-later
|
|||||||
}
|
}
|
||||||
<body>
|
<body>
|
||||||
<h1>
|
<h1>
|
||||||
$case userAuthentication
|
$if is _Just userPasswordHash
|
||||||
$of AuthLDAP
|
_{SomeMessage MsgUserAuthPasswordEnabled}
|
||||||
_{SomeMessage MsgUserAuthModeChangedToLDAP}
|
$else
|
||||||
$of AuthPWHash _
|
_{SomeMessage MsgUserAuthPasswordDisabled}
|
||||||
_{SomeMessage MsgUserAuthModeChangedToPWHash}
|
|
||||||
$of AuthNoLogin
|
|
||||||
_{SomeMessage MsgUserAuthModeChangedToNoLogin}
|
|
||||||
<p>
|
<p>
|
||||||
<a href=@{NewsR}>
|
<a href=@{NewsR}>
|
||||||
_{SomeMessage MsgMailFradrive} #
|
_{SomeMessage MsgMailFradrive} #
|
||||||
_{SomeMessage MsgMailBodyFradrive}
|
_{SomeMessage MsgMailBodyFradrive}
|
||||||
|
|
||||||
$if is _AuthPWHash userAuthentication
|
$if is _Just userPasswordHash
|
||||||
<p>
|
|
||||||
_{SomeMessage MsgAuthPWHashTip}
|
|
||||||
<dl>
|
<dl>
|
||||||
<dt>
|
<dt>
|
||||||
_{SomeMessage MsgPWHashIdent}
|
_{SomeMessage MsgPWHashIdent}
|
||||||
@ -42,6 +37,9 @@ $# SPDX-License-Identifier: AGPL-3.0-or-later
|
|||||||
<dt>_{SomeMessage MsgPWHashPassword}
|
<dt>_{SomeMessage MsgPWHashPassword}
|
||||||
<dd>
|
<dd>
|
||||||
_{SomeMessage MsgPasswordResetEmailIncoming}
|
_{SomeMessage MsgPasswordResetEmailIncoming}
|
||||||
|
$else
|
||||||
|
<p>
|
||||||
|
_{SomeMessage MsgAuthExternalLoginTip}
|
||||||
|
|
||||||
$if is _Just userLastAuthentication
|
$if is _Just userLastAuthentication
|
||||||
^{editNotifications}
|
^{editNotifications}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
$newline never
|
$newline never
|
||||||
|
|
||||||
$# SPDX-FileCopyrightText: 2022-25 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <vaupel.sarah@campus.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>,Steffen Jost <s.jost@fraport.de>
|
$# SPDX-FileCopyrightText: 2022-2025 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <vaupel.sarah@campus.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Winnie Ros <winnie.ros@campus.lmu.de>,Steffen Jost <s.jost@fraport.de>
|
||||||
$#
|
$#
|
||||||
$# SPDX-License-Identifier: AGPL-3.0-or-later
|
$# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -135,6 +135,25 @@ $# SPDX-License-Identifier: AGPL-3.0-or-later
|
|||||||
_{MsgUserCreated}
|
_{MsgUserCreated}
|
||||||
<dd .deflist__dd>
|
<dd .deflist__dd>
|
||||||
^{formatTimeW SelFormatDateTime userCreated}
|
^{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>
|
<dt .deflist__dt>
|
||||||
_{MsgLastLogin}
|
_{MsgLastLogin}
|
||||||
<dd .deflist__dd>
|
<dd .deflist__dd>
|
||||||
@ -142,18 +161,6 @@ $# SPDX-License-Identifier: AGPL-3.0-or-later
|
|||||||
^{formatTimeW SelFormatDateTime llogin}
|
^{formatTimeW SelFormatDateTime llogin}
|
||||||
$nothing
|
$nothing
|
||||||
_{MsgNeverSet}
|
_{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>
|
<dt .deflist__dt>
|
||||||
_{MsgTokensLastReset}
|
_{MsgTokensLastReset}
|
||||||
<dd .deflist__dd>
|
<dd .deflist__dd>
|
||||||
|
|||||||
@ -83,8 +83,9 @@ fillDb = do
|
|||||||
|
|
||||||
gkleen <- insert User
|
gkleen <- insert User
|
||||||
{ userIdent = "G.Kleen@campus.lmu.de"
|
{ userIdent = "G.Kleen@campus.lmu.de"
|
||||||
, userAuthentication = AuthLDAP
|
, userPasswordHash = Nothing
|
||||||
, userLastAuthentication = Just now
|
, userLastAuthentication = Just now
|
||||||
|
, userLastSync = Just now
|
||||||
, userTokensIssuedAfter = Just now
|
, userTokensIssuedAfter = Just now
|
||||||
, userMatrikelnummer = Just "99"
|
, userMatrikelnummer = Just "99"
|
||||||
, userEmail = "G.Kleen@campus.lmu.de"
|
, userEmail = "G.Kleen@campus.lmu.de"
|
||||||
@ -104,8 +105,6 @@ fillDb = do
|
|||||||
, userLanguages = Just $ Languages ["en"]
|
, userLanguages = Just $ Languages ["en"]
|
||||||
, userNotificationSettings = def
|
, userNotificationSettings = def
|
||||||
, userCreated = now
|
, userCreated = now
|
||||||
, userLastLdapSynchronisation = Nothing
|
|
||||||
, userLdapPrimaryKey = Nothing
|
|
||||||
, userCsvOptions = def { csvFormat = csvPreset # CsvPresetRFC }
|
, userCsvOptions = def { csvFormat = csvPreset # CsvPresetRFC }
|
||||||
, userSex = Just SexMale
|
, userSex = Just SexMale
|
||||||
, userBirthday = Nothing
|
, userBirthday = Nothing
|
||||||
@ -123,8 +122,9 @@ fillDb = do
|
|||||||
}
|
}
|
||||||
fhamann <- insert User
|
fhamann <- insert User
|
||||||
{ userIdent = "felix.hamann@campus.lmu.de"
|
{ userIdent = "felix.hamann@campus.lmu.de"
|
||||||
, userAuthentication = AuthLDAP
|
, userPasswordHash = Nothing
|
||||||
, userLastAuthentication = Nothing
|
, userLastAuthentication = Nothing
|
||||||
|
, userLastSync = Nothing
|
||||||
, userTokensIssuedAfter = Nothing
|
, userTokensIssuedAfter = Nothing
|
||||||
, userMatrikelnummer = Nothing
|
, userMatrikelnummer = Nothing
|
||||||
, userEmail = "AVSNO:123456"
|
, userEmail = "AVSNO:123456"
|
||||||
@ -144,8 +144,6 @@ fillDb = do
|
|||||||
, userLanguages = Nothing
|
, userLanguages = Nothing
|
||||||
, userNotificationSettings = def
|
, userNotificationSettings = def
|
||||||
, userCreated = now
|
, userCreated = now
|
||||||
, userLastLdapSynchronisation = Nothing
|
|
||||||
, userLdapPrimaryKey = Nothing
|
|
||||||
, userCsvOptions = def { csvFormat = csvPreset # CsvPresetExcel }
|
, userCsvOptions = def { csvFormat = csvPreset # CsvPresetExcel }
|
||||||
, userSex = Just SexMale
|
, userSex = Just SexMale
|
||||||
, userShowSex = userDefaultShowSex
|
, userShowSex = userDefaultShowSex
|
||||||
@ -165,12 +163,12 @@ fillDb = do
|
|||||||
let pw = "123.456"
|
let pw = "123.456"
|
||||||
PWHashConf{..} <- getsYesod $ view _appAuthPWHash
|
PWHashConf{..} <- getsYesod $ view _appAuthPWHash
|
||||||
pwHash <- liftIO $ PWStore.makePasswordWith pwHashAlgorithm pw pwHashStrength
|
pwHash <- liftIO $ PWStore.makePasswordWith pwHashAlgorithm pw pwHashStrength
|
||||||
return $ AuthPWHash $ TEnc.decodeUtf8 pwHash
|
return $ TEnc.decodeUtf8 pwHash
|
||||||
jost <- insert User
|
jost <- insert User
|
||||||
{ userIdent = "jost@tcs.ifi.lmu.de"
|
{ userIdent = "jost@tcs.ifi.lmu.de"
|
||||||
-- , userAuthentication = AuthLDAP
|
, userPasswordHash = Just pwSimple
|
||||||
, userAuthentication = pwSimple
|
|
||||||
, userLastAuthentication = Nothing
|
, userLastAuthentication = Nothing
|
||||||
|
, userLastSync = Nothing
|
||||||
, userTokensIssuedAfter = Nothing
|
, userTokensIssuedAfter = Nothing
|
||||||
, userMatrikelnummer = Just "12345678"
|
, userMatrikelnummer = Just "12345678"
|
||||||
, userEmail = "S.Jost@Fraport.de"
|
, userEmail = "S.Jost@Fraport.de"
|
||||||
@ -190,8 +188,6 @@ fillDb = do
|
|||||||
, userLanguages = Nothing
|
, userLanguages = Nothing
|
||||||
, userNotificationSettings = def
|
, userNotificationSettings = def
|
||||||
, userCreated = now
|
, userCreated = now
|
||||||
, userLastLdapSynchronisation = Nothing
|
|
||||||
, userLdapPrimaryKey = Nothing
|
|
||||||
, userSex = Just SexMale
|
, userSex = Just SexMale
|
||||||
, userBirthday = Just $ n_day $ 35 * (-365)
|
, userBirthday = Just $ n_day $ 35 * (-365)
|
||||||
, userCsvOptions = def
|
, userCsvOptions = def
|
||||||
@ -209,8 +205,9 @@ fillDb = do
|
|||||||
}
|
}
|
||||||
maxMuster <- insert User
|
maxMuster <- insert User
|
||||||
{ userIdent = "max@campus.lmu.de"
|
{ userIdent = "max@campus.lmu.de"
|
||||||
, userAuthentication = AuthLDAP
|
, userPasswordHash = Nothing
|
||||||
, userLastAuthentication = Just now
|
, userLastAuthentication = Just now
|
||||||
|
, userLastSync = Just now
|
||||||
, userTokensIssuedAfter = Nothing
|
, userTokensIssuedAfter = Nothing
|
||||||
, userMatrikelnummer = Just "1299"
|
, userMatrikelnummer = Just "1299"
|
||||||
, userEmail = "max@campus.lmu.de"
|
, userEmail = "max@campus.lmu.de"
|
||||||
@ -230,8 +227,6 @@ fillDb = do
|
|||||||
, userLanguages = Just $ Languages ["de"]
|
, userLanguages = Just $ Languages ["de"]
|
||||||
, userNotificationSettings = def
|
, userNotificationSettings = def
|
||||||
, userCreated = now
|
, userCreated = now
|
||||||
, userLastLdapSynchronisation = Nothing
|
|
||||||
, userLdapPrimaryKey = Nothing
|
|
||||||
, userCsvOptions = def
|
, userCsvOptions = def
|
||||||
, userSex = Just SexMale
|
, userSex = Just SexMale
|
||||||
, userBirthday = Just $ n_day $ 27 * (-365)
|
, userBirthday = Just $ n_day $ 27 * (-365)
|
||||||
@ -249,8 +244,9 @@ fillDb = do
|
|||||||
}
|
}
|
||||||
tinaTester <- insert $ User
|
tinaTester <- insert $ User
|
||||||
{ userIdent = "tester@campus.lmu.de"
|
{ userIdent = "tester@campus.lmu.de"
|
||||||
, userAuthentication = AuthNoLogin
|
, userPasswordHash = Nothing
|
||||||
, userLastAuthentication = Nothing
|
, userLastAuthentication = Nothing
|
||||||
|
, userLastSync = Nothing
|
||||||
, userTokensIssuedAfter = Nothing
|
, userTokensIssuedAfter = Nothing
|
||||||
, userMatrikelnummer = Just "999"
|
, userMatrikelnummer = Just "999"
|
||||||
, userEmail = "tester@campus.lmu.de"
|
, userEmail = "tester@campus.lmu.de"
|
||||||
@ -270,8 +266,6 @@ fillDb = do
|
|||||||
, userLanguages = Just $ Languages ["sn"]
|
, userLanguages = Just $ Languages ["sn"]
|
||||||
, userNotificationSettings = def
|
, userNotificationSettings = def
|
||||||
, userCreated = now
|
, userCreated = now
|
||||||
, userLastLdapSynchronisation = Nothing
|
|
||||||
, userLdapPrimaryKey = Nothing
|
|
||||||
, userCsvOptions = def
|
, userCsvOptions = def
|
||||||
, userSex = Just SexNotApplicable
|
, userSex = Just SexNotApplicable
|
||||||
, userBirthday = Just $ n_day 3
|
, userBirthday = Just $ n_day 3
|
||||||
@ -289,8 +283,9 @@ fillDb = do
|
|||||||
}
|
}
|
||||||
svaupel <- insert User
|
svaupel <- insert User
|
||||||
{ userIdent = "vaupel.sarah@campus.lmu.de"
|
{ userIdent = "vaupel.sarah@campus.lmu.de"
|
||||||
, userAuthentication = AuthLDAP
|
, userPasswordHash = Nothing
|
||||||
, userLastAuthentication = Nothing
|
, userLastAuthentication = Nothing
|
||||||
|
, userLastSync = Nothing
|
||||||
, userTokensIssuedAfter = Nothing
|
, userTokensIssuedAfter = Nothing
|
||||||
, userMatrikelnummer = Just "365"
|
, userMatrikelnummer = Just "365"
|
||||||
, userEmail = "vaupel.sarah@campus.lmu.de"
|
, userEmail = "vaupel.sarah@campus.lmu.de"
|
||||||
@ -310,8 +305,6 @@ fillDb = do
|
|||||||
, userLanguages = Nothing
|
, userLanguages = Nothing
|
||||||
, userNotificationSettings = def
|
, userNotificationSettings = def
|
||||||
, userCreated = now
|
, userCreated = now
|
||||||
, userLastLdapSynchronisation = Nothing
|
|
||||||
, userLdapPrimaryKey = Nothing
|
|
||||||
, userCsvOptions = def
|
, userCsvOptions = def
|
||||||
, userSex = Just SexFemale
|
, userSex = Just SexFemale
|
||||||
, userBirthday = Nothing
|
, userBirthday = Nothing
|
||||||
@ -329,8 +322,9 @@ fillDb = do
|
|||||||
}
|
}
|
||||||
sbarth <- insert User
|
sbarth <- insert User
|
||||||
{ userIdent = "Stephan.Barth@campus.lmu.de"
|
{ userIdent = "Stephan.Barth@campus.lmu.de"
|
||||||
, userAuthentication = AuthLDAP
|
, userPasswordHash = Nothing
|
||||||
, userLastAuthentication = Nothing
|
, userLastAuthentication = Nothing
|
||||||
|
, userLastSync = Nothing
|
||||||
, userTokensIssuedAfter = Nothing
|
, userTokensIssuedAfter = Nothing
|
||||||
, userMatrikelnummer = Nothing
|
, userMatrikelnummer = Nothing
|
||||||
, userEmail = "Stephan.Barth@lmu.de"
|
, userEmail = "Stephan.Barth@lmu.de"
|
||||||
@ -350,8 +344,6 @@ fillDb = do
|
|||||||
, userLanguages = Nothing
|
, userLanguages = Nothing
|
||||||
, userNotificationSettings = def
|
, userNotificationSettings = def
|
||||||
, userCreated = now
|
, userCreated = now
|
||||||
, userLastLdapSynchronisation = Nothing
|
|
||||||
, userLdapPrimaryKey = Nothing
|
|
||||||
, userCsvOptions = def
|
, userCsvOptions = def
|
||||||
, userSex = Just SexMale
|
, userSex = Just SexMale
|
||||||
, userBirthday = Nothing
|
, userBirthday = Nothing
|
||||||
@ -369,8 +361,9 @@ fillDb = do
|
|||||||
}
|
}
|
||||||
stranger1 <- insert User
|
stranger1 <- insert User
|
||||||
{ userIdent = "AVSID:996699"
|
{ userIdent = "AVSID:996699"
|
||||||
, userAuthentication = AuthLDAP
|
, userPasswordHash = Nothing
|
||||||
, userLastAuthentication = Nothing
|
, userLastAuthentication = Nothing
|
||||||
|
, userLastSync = Nothing
|
||||||
, userTokensIssuedAfter = Nothing
|
, userTokensIssuedAfter = Nothing
|
||||||
, userMatrikelnummer = Nothing
|
, userMatrikelnummer = Nothing
|
||||||
, userEmail = "E996699@fraport.de"
|
, userEmail = "E996699@fraport.de"
|
||||||
@ -390,8 +383,6 @@ fillDb = do
|
|||||||
, userLanguages = Nothing
|
, userLanguages = Nothing
|
||||||
, userNotificationSettings = def
|
, userNotificationSettings = def
|
||||||
, userCreated = now
|
, userCreated = now
|
||||||
, userLastLdapSynchronisation = Nothing
|
|
||||||
, userLdapPrimaryKey = Nothing
|
|
||||||
, userCsvOptions = def
|
, userCsvOptions = def
|
||||||
, userSex = Just SexMale
|
, userSex = Just SexMale
|
||||||
, userBirthday = Nothing
|
, userBirthday = Nothing
|
||||||
@ -409,8 +400,9 @@ fillDb = do
|
|||||||
}
|
}
|
||||||
stranger2 <- insert User
|
stranger2 <- insert User
|
||||||
{ userIdent = "AVSID:669966"
|
{ userIdent = "AVSID:669966"
|
||||||
, userAuthentication = AuthLDAP
|
, userPasswordHash = Nothing
|
||||||
, userLastAuthentication = Nothing
|
, userLastAuthentication = Nothing
|
||||||
|
, userLastSync = Nothing
|
||||||
, userTokensIssuedAfter = Nothing
|
, userTokensIssuedAfter = Nothing
|
||||||
, userMatrikelnummer = Nothing
|
, userMatrikelnummer = Nothing
|
||||||
, userEmail = "E669966@fraport.de"
|
, userEmail = "E669966@fraport.de"
|
||||||
@ -430,8 +422,6 @@ fillDb = do
|
|||||||
, userLanguages = Nothing
|
, userLanguages = Nothing
|
||||||
, userNotificationSettings = def
|
, userNotificationSettings = def
|
||||||
, userCreated = now
|
, userCreated = now
|
||||||
, userLastLdapSynchronisation = Nothing
|
|
||||||
, userLdapPrimaryKey = Nothing
|
|
||||||
, userCsvOptions = def
|
, userCsvOptions = def
|
||||||
, userSex = Just SexMale
|
, userSex = Just SexMale
|
||||||
, userBirthday = Nothing
|
, userBirthday = Nothing
|
||||||
@ -449,8 +439,9 @@ fillDb = do
|
|||||||
}
|
}
|
||||||
stranger3 <- insert User
|
stranger3 <- insert User
|
||||||
{ userIdent = "AVSID:6969"
|
{ userIdent = "AVSID:6969"
|
||||||
, userAuthentication = AuthLDAP
|
, userPasswordHash = Nothing
|
||||||
, userLastAuthentication = Nothing
|
, userLastAuthentication = Nothing
|
||||||
|
, userLastSync = Nothing
|
||||||
, userTokensIssuedAfter = Nothing
|
, userTokensIssuedAfter = Nothing
|
||||||
, userMatrikelnummer = Nothing
|
, userMatrikelnummer = Nothing
|
||||||
, userEmail = "E6969@fraport.de"
|
, userEmail = "E6969@fraport.de"
|
||||||
@ -470,8 +461,6 @@ fillDb = do
|
|||||||
, userLanguages = Nothing
|
, userLanguages = Nothing
|
||||||
, userNotificationSettings = def
|
, userNotificationSettings = def
|
||||||
, userCreated = now
|
, userCreated = now
|
||||||
, userLastLdapSynchronisation = Nothing
|
|
||||||
, userLdapPrimaryKey = Nothing
|
|
||||||
, userCsvOptions = def
|
, userCsvOptions = def
|
||||||
, userSex = Just SexMale
|
, userSex = Just SexMale
|
||||||
, userBirthday = Nothing
|
, userBirthday = Nothing
|
||||||
@ -528,8 +517,9 @@ fillDb = do
|
|||||||
middlenames = [ Nothing, Just "Jamesson", Just "Theresa", Just "Ally", Just "Tiberius", Just "Maria" ]
|
middlenames = [ Nothing, Just "Jamesson", Just "Theresa", Just "Ally", Just "Tiberius", Just "Maria" ]
|
||||||
manyUser (firstName, middleName, userSurname) userMatrikelnummer' = User
|
manyUser (firstName, middleName, userSurname) userMatrikelnummer' = User
|
||||||
{ userIdent
|
{ userIdent
|
||||||
, userAuthentication = AuthLDAP
|
, userPasswordHash = Nothing
|
||||||
, userLastAuthentication = Nothing
|
, userLastAuthentication = Nothing
|
||||||
|
, userLastSync = Nothing
|
||||||
, userTokensIssuedAfter = Nothing
|
, userTokensIssuedAfter = Nothing
|
||||||
, userMatrikelnummer = Just userMatrikelnummer'
|
, userMatrikelnummer = Just userMatrikelnummer'
|
||||||
, userEmail = userEmail'
|
, userEmail = userEmail'
|
||||||
@ -551,8 +541,6 @@ fillDb = do
|
|||||||
, userLanguages = Nothing
|
, userLanguages = Nothing
|
||||||
, userNotificationSettings = def
|
, userNotificationSettings = def
|
||||||
, userCreated = now
|
, userCreated = now
|
||||||
, userLastLdapSynchronisation = Nothing
|
|
||||||
, userLdapPrimaryKey = Nothing
|
|
||||||
, userCsvOptions = def
|
, userCsvOptions = def
|
||||||
, userSex = Nothing
|
, userSex = Nothing
|
||||||
, userBirthday = Nothing
|
, userBirthday = Nothing
|
||||||
|
|||||||
231
test/Database/test-users.yaml
Normal file
231
test/Database/test-users.yaml
Normal 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" ]
|
||||||
|
|
||||||
@ -51,7 +51,6 @@ makeUsers (fromIntegral -> n) = do
|
|||||||
let baseid = "user." <> tshow i
|
let baseid = "user." <> tshow i
|
||||||
u' = u { userIdent = CI.mk baseid
|
u' = u { userIdent = CI.mk baseid
|
||||||
, userEmail = CI.mk $ baseid <> "@example.com"
|
, userEmail = CI.mk $ baseid <> "@example.com"
|
||||||
, userLdapPrimaryKey = Just $ baseid <> ".ldap"
|
|
||||||
}
|
}
|
||||||
return u'
|
return u'
|
||||||
uids <- insertMany users
|
uids <- insertMany users
|
||||||
|
|||||||
@ -10,7 +10,6 @@ module Model.TypesSpec
|
|||||||
|
|
||||||
import TestImport
|
import TestImport
|
||||||
import TestInstances ()
|
import TestInstances ()
|
||||||
import Settings
|
|
||||||
|
|
||||||
import Utils (guardOn)
|
import Utils (guardOn)
|
||||||
|
|
||||||
@ -21,7 +20,6 @@ import qualified Data.Aeson.Types as Aeson
|
|||||||
import Model.Types.LanguagesSpec ()
|
import Model.Types.LanguagesSpec ()
|
||||||
|
|
||||||
import System.IO.Unsafe
|
import System.IO.Unsafe
|
||||||
import Yesod.Auth.Util.PasswordStore
|
|
||||||
|
|
||||||
import Database.Persist.Sql (SqlBackend, fromSqlKey, toSqlKey)
|
import Database.Persist.Sql (SqlBackend, fromSqlKey, toSqlKey)
|
||||||
|
|
||||||
@ -218,21 +216,6 @@ instance Arbitrary Value where
|
|||||||
arbitrary' = scale (`div` 2) arbitrary
|
arbitrary' = scale (`div` 2) arbitrary
|
||||||
shrink = genericShrink
|
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
|
instance Arbitrary LecturerType where
|
||||||
arbitrary = genericArbitrary
|
arbitrary = genericArbitrary
|
||||||
shrink = genericShrink
|
shrink = genericShrink
|
||||||
@ -469,8 +452,6 @@ spec = do
|
|||||||
[ eqLaws, ordLaws, boundedEnumLaws, showReadLaws, jsonLaws, finiteLaws, pathPieceLaws, persistFieldLaws ]
|
[ eqLaws, ordLaws, boundedEnumLaws, showReadLaws, jsonLaws, finiteLaws, pathPieceLaws, persistFieldLaws ]
|
||||||
lawsCheckHspec (Proxy @CorrectorState)
|
lawsCheckHspec (Proxy @CorrectorState)
|
||||||
[ eqLaws, ordLaws, showReadLaws, boundedEnumLaws, jsonLaws, finiteLaws, pathPieceLaws, persistFieldLaws ]
|
[ eqLaws, ordLaws, showReadLaws, boundedEnumLaws, jsonLaws, finiteLaws, pathPieceLaws, persistFieldLaws ]
|
||||||
lawsCheckHspec (Proxy @AuthenticationMode)
|
|
||||||
[ eqLaws, ordLaws, showReadLaws, jsonLaws, persistFieldLaws ]
|
|
||||||
lawsCheckHspec (Proxy @Value)
|
lawsCheckHspec (Proxy @Value)
|
||||||
[ persistFieldLaws ]
|
[ persistFieldLaws ]
|
||||||
lawsCheckHspec (Proxy @Scientific)
|
lawsCheckHspec (Proxy @Scientific)
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
-- SPDX-FileCopyrightText: 2022-2024 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@cip.ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>
|
-- SPDX-FileCopyrightText: 2022-2024 Sarah Vaupel <sarah.vaupel@uniworx.de>, Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Steffen Jost <jost@cip.ifi.lmu.de>,Steffen Jost <jost@tcs.ifi.lmu.de>,Steffen Jost <s.jost@fraport.de>
|
||||||
--
|
--
|
||||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -9,7 +9,7 @@ module ModelSpec where
|
|||||||
|
|
||||||
import TestImport
|
import TestImport
|
||||||
|
|
||||||
import Settings (getTimeLocale', VerpMode(..))
|
import Settings
|
||||||
|
|
||||||
import Model.TypesSpec ()
|
import Model.TypesSpec ()
|
||||||
import MailSpec ()
|
import MailSpec ()
|
||||||
@ -34,9 +34,10 @@ import qualified Data.CryptoID.Class.ImplicitNamespace as Implicit
|
|||||||
import qualified Data.CryptoID.Class as Explicit
|
import qualified Data.CryptoID.Class as Explicit
|
||||||
import Data.Binary.SerializationLength
|
import Data.Binary.SerializationLength
|
||||||
|
|
||||||
import Control.Monad.Catch.Pure (Catch, runCatch)
|
import System.IO.Unsafe
|
||||||
|
import Yesod.Auth.Util.PasswordStore
|
||||||
|
|
||||||
import System.IO.Unsafe (unsafePerformIO)
|
import Control.Monad.Catch.Pure (Catch, runCatch)
|
||||||
|
|
||||||
import Data.Universe
|
import Data.Universe
|
||||||
|
|
||||||
@ -103,7 +104,12 @@ instance Arbitrary User where
|
|||||||
[ getPrintableString <$> arbitrary
|
[ getPrintableString <$> arbitrary
|
||||||
, on (\l d -> l <> "@" <> d) getPrintableString <$> arbitrary <*> arbitrary
|
, on (\l d -> l <> "@" <> d) getPrintableString <$> arbitrary <*> arbitrary
|
||||||
]
|
]
|
||||||
userAuthentication <- arbitrary
|
userPasswordHash <-
|
||||||
|
let genPwd = do
|
||||||
|
pw <- encodeUtf8 . pack . getPrintableString <$> arbitrary
|
||||||
|
let PWHashConf{..} = appAuthPWHash compileTimeAppSettings
|
||||||
|
return . unsafePerformIO . fmap decodeUtf8 $ makePasswordWith pwHashAlgorithm pw (pwHashStrength `div` 2)
|
||||||
|
in oneof [ pure Nothing, Just <$> genPwd ]
|
||||||
userLastAuthentication <- arbitrary
|
userLastAuthentication <- arbitrary
|
||||||
userTokensIssuedAfter <- arbitrary
|
userTokensIssuedAfter <- arbitrary
|
||||||
userMatrikelnummer <- fmap pack . assertM' (not . null) <$> listOf (elements ['0'..'9'])
|
userMatrikelnummer <- fmap pack . assertM' (not . null) <$> listOf (elements ['0'..'9'])
|
||||||
@ -147,14 +153,7 @@ instance Arbitrary User where
|
|||||||
userExamOfficeGetLabels <- arbitrary
|
userExamOfficeGetLabels <- arbitrary
|
||||||
|
|
||||||
userCreated <- arbitrary
|
userCreated <- arbitrary
|
||||||
userLastLdapSynchronisation <- arbitrary
|
userLastSync <- arbitrary
|
||||||
userLdapPrimaryKey <- oneof
|
|
||||||
[ pure Nothing
|
|
||||||
, fmap Just $ pack <$> oneof
|
|
||||||
[ getPrintableString <$> arbitrary
|
|
||||||
, on (\l d -> l <> "@" <> d) getPrintableString <$> arbitrary <*> arbitrary
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|
|
||||||
return User{..}
|
return User{..}
|
||||||
shrink = genericShrink
|
shrink = genericShrink
|
||||||
|
|||||||
@ -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
|
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
@ -9,7 +9,8 @@ module TestImport
|
|||||||
, module X
|
, module X
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Application (makeFoundation, makeMiddleware, shutdownApp)
|
import Application (makeFoundation, shutdownApp)
|
||||||
|
import Middleware (makeMiddleware)
|
||||||
import ClassyPrelude as X
|
import ClassyPrelude as X
|
||||||
hiding ( delete, deleteBy
|
hiding ( delete, deleteBy
|
||||||
, Handler, Index
|
, Handler, Index
|
||||||
|
|||||||
@ -21,8 +21,9 @@ fakeUser adjUser = adjUser User{..}
|
|||||||
UserDefaultConf{..} = appUserDefaults compileTimeAppSettings
|
UserDefaultConf{..} = appUserDefaults compileTimeAppSettings
|
||||||
|
|
||||||
userMatrikelnummer = Nothing
|
userMatrikelnummer = Nothing
|
||||||
userAuthentication = AuthLDAP
|
userPasswordHash = Nothing
|
||||||
userLastAuthentication = Nothing
|
userLastAuthentication = Nothing
|
||||||
|
userLastSync = Nothing
|
||||||
userTokensIssuedAfter = Nothing
|
userTokensIssuedAfter = Nothing
|
||||||
userIdent = "dummy@example.invalid"
|
userIdent = "dummy@example.invalid"
|
||||||
userEmail = "dummy@example.invalid"
|
userEmail = "dummy@example.invalid"
|
||||||
@ -48,8 +49,6 @@ fakeUser adjUser = adjUser User{..}
|
|||||||
userShowSex = userDefaultShowSex
|
userShowSex = userDefaultShowSex
|
||||||
userNotificationSettings = def
|
userNotificationSettings = def
|
||||||
userCreated = unsafePerformIO getCurrentTime
|
userCreated = unsafePerformIO getCurrentTime
|
||||||
userLastLdapSynchronisation = Nothing
|
|
||||||
userLdapPrimaryKey = Nothing
|
|
||||||
userMobile = Nothing
|
userMobile = Nothing
|
||||||
userTelephone = Nothing
|
userTelephone = Nothing
|
||||||
userCompanyPersonalNumber = Nothing
|
userCompanyPersonalNumber = Nothing
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user