WebAdmin: allow to pre-select groups on add user page

The admin will still be able to choose different groups

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2022-09-13 18:04:27 +02:00
parent bd585d8e52
commit ea3c1d7a3b
No known key found for this signature in database
GPG key ID: 2F1FB59433D5A8CB
24 changed files with 1320 additions and 203 deletions

View file

@ -278,7 +278,7 @@ Confirm that the database connection works by initializing the data provider.
```shell
$ sudo su - sftpgo -s /bin/bash -c 'sftpgo initprovider -c /etc/sftpgo'
2021-05-19T22:21:54.000 INF Initializing provider: "postgresql" config file: "/etc/sftpgo/sftpgo.json"
2021-05-19T22:21:54.000 INF updating database version: 8 -> 9
2021-05-19T22:21:54.000 INF updating database schema version: 8 -> 9
2021-05-19T22:21:54.000 INF Data provider successfully initialized/updated
```
@ -332,7 +332,7 @@ Confirm that the database connection works by initializing the data provider.
```shell
$ sudo su - sftpgo -s /bin/bash -c 'sftpgo initprovider -c /etc/sftpgo'
2021-05-19T22:29:30.000 INF Initializing provider: "mysql" config file: "/etc/sftpgo/sftpgo.json"
2021-05-19T22:29:30.000 INF updating database version: 8 -> 9
2021-05-19T22:29:30.000 INF updating database schema version: 8 -> 9
2021-05-19T22:29:30.000 INF Data provider successfully initialized/updated
```
@ -428,10 +428,10 @@ Confirm that the database connection works by initializing the data provider.
$ sudo su - sftpgo -s /bin/bash -c 'sftpgo initprovider -c /etc/sftpgo'
2022-06-02T14:54:04.510 INF Initializing provider: "cockroachdb" config file: "/etc/sftpgo/sftpgo.json"
2022-06-02T14:54:04.554 INF creating initial database schema, version 15
2022-06-02T14:54:04.698 INF updating database version: 15 -> 16
2022-06-02T14:54:07.093 INF updating database version: 16 -> 17
2022-06-02T14:54:07.672 INF updating database version: 17 -> 18
2022-06-02T14:54:07.699 INF updating database version: 18 -> 19
2022-06-02T14:54:04.698 INF updating database schema version: 15 -> 16
2022-06-02T14:54:07.093 INF updating database schema version: 16 -> 17
2022-06-02T14:54:07.672 INF updating database schema version: 17 -> 18
2022-06-02T14:54:07.699 INF updating database schema version: 18 -> 19
2022-06-02T14:54:07.721 INF Data provider successfully initialized/updated
```

View file

@ -155,9 +155,9 @@ Next, initialize the data provider with the following command.
```shell
$ sudo su - sftpgo -s /bin/bash -c 'sftpgo initprovider -c /etc/sftpgo'
2020-10-09T21:07:50.000 INF Initializing provider: "postgresql" config file: "/etc/sftpgo/sftpgo.json"
2020-10-09T21:07:50.000 INF updating database version: 1 -> 2
2020-10-09T21:07:50.000 INF updating database version: 2 -> 3
2020-10-09T21:07:50.000 INF updating database version: 3 -> 4
2020-10-09T21:07:50.000 INF updating database schema version: 1 -> 2
2020-10-09T21:07:50.000 INF updating database schema version: 2 -> 3
2020-10-09T21:07:50.000 INF updating database schema version: 3 -> 4
2020-10-09T21:07:50.000 INF Data provider successfully initialized/updated
```

18
go.mod
View file

@ -17,8 +17,8 @@ require (
github.com/aws/aws-sdk-go-v2/service/s3 v1.27.9
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.22
github.com/aws/aws-sdk-go-v2/service/sts v1.16.17
github.com/cockroachdb/cockroach-go/v2 v2.2.15
github.com/coreos/go-oidc/v3 v3.3.0
github.com/cockroachdb/cockroach-go/v2 v2.2.16
github.com/coreos/go-oidc/v3 v3.4.0
github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001
github.com/fclairamb/ftpserverlib v0.19.1
github.com/fclairamb/go-log v0.4.1
@ -51,7 +51,7 @@ require (
github.com/rs/cors v1.8.3-0.20220619195839-da52b0701de5
github.com/rs/xid v1.4.0
github.com/rs/zerolog v1.28.0
github.com/sftpgo/sdk v0.1.2-0.20220828084006-f9e2fffac657
github.com/sftpgo/sdk v0.1.2-0.20220913155952-81743fa5ded5
github.com/shirou/gopsutil/v3 v3.22.8
github.com/spf13/afero v1.9.2
github.com/spf13/cobra v1.5.0
@ -66,9 +66,9 @@ require (
go.uber.org/automaxprocs v1.5.1
gocloud.dev v0.26.0
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90
golang.org/x/net v0.0.0-20220907135653-1e95f45603a7
golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094
golang.org/x/sys v0.0.0-20220907062415-87db552b00fd
golang.org/x/net v0.0.0-20220909164309-bea034e7d591
golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1
golang.org/x/sys v0.0.0-20220913153101-76c7481b5158
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9
google.golang.org/api v0.95.0
gopkg.in/natefinch/lumberjack.v2 v2.0.0
@ -123,7 +123,7 @@ require (
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/option v1.0.0 // indirect
github.com/lufia/plan9stats v0.0.0-20220517141722-cf486979b281 // indirect
github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
@ -156,7 +156,7 @@ require (
golang.org/x/tools v0.1.12 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20220902135211-223410557253 // indirect
google.golang.org/genproto v0.0.0-20220909194730-69f6226f97e5 // indirect
google.golang.org/grpc v1.49.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
@ -168,5 +168,5 @@ require (
replace (
github.com/jlaffaye/ftp => github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9
golang.org/x/crypto => github.com/drakkan/crypto v0.0.0-20220831070132-e3c36f2ab82b
golang.org/x/net => github.com/drakkan/net v0.0.0-20220908074131-65c0cd1ffa8a
golang.org/x/net => github.com/drakkan/net v0.0.0-20220913160159-a08dc61b7895
)

31
go.sum
View file

@ -238,10 +238,10 @@ github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWH
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/cockroachdb/cockroach-go/v2 v2.2.15 h1:6TeTC1JLSlHJWJCswWZ7mQyT16kY5mQSs53C2coQISI=
github.com/cockroachdb/cockroach-go/v2 v2.2.15/go.mod h1:xZ2VHjUEb/cySv0scXBx7YsBnHtLHkR1+w/w73b5i3M=
github.com/coreos/go-oidc/v3 v3.3.0 h1:Y1LV3mP+QT3MEycATZpAiwfyN+uxZLqVbAHJUuOJEe4=
github.com/coreos/go-oidc/v3 v3.3.0/go.mod h1:eHUXhZtXPQLgEaDrOVTgwbgmz1xGOkJNye6h3zkD2Pw=
github.com/cockroachdb/cockroach-go/v2 v2.2.16 h1:t9dmZuC9J2W8IDQDSIGXmP+fBuEJSsrGXxWQz4cYqBY=
github.com/cockroachdb/cockroach-go/v2 v2.2.16/go.mod h1:xZ2VHjUEb/cySv0scXBx7YsBnHtLHkR1+w/w73b5i3M=
github.com/coreos/go-oidc/v3 v3.4.0 h1:xz7elHb/LDwm/ERpwHd+5nb7wFHL32rsr6bBOgaeu6g=
github.com/coreos/go-oidc/v3 v3.4.0/go.mod h1:eHUXhZtXPQLgEaDrOVTgwbgmz1xGOkJNye6h3zkD2Pw=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
@ -267,8 +267,8 @@ github.com/drakkan/crypto v0.0.0-20220831070132-e3c36f2ab82b h1:kCNBtUFKfhiUaE1Z
github.com/drakkan/crypto v0.0.0-20220831070132-e3c36f2ab82b/go.mod h1:SiM6ypd8Xu1xldObYtbDztuUU7xUzMnUULfphXFZmro=
github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9 h1:LPH1dEblAOO/LoG7yHPMtBLXhQmjaga91/DDjWk9jWA=
github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9/go.mod h1:2lmrmq866uF2tnje75wQHzmPXhmSWUt7Gyx2vgK1RCU=
github.com/drakkan/net v0.0.0-20220908074131-65c0cd1ffa8a h1:b2KZbApbkwXCrmoDqOQqfIlIMRxIaUYC+d1CjRHcd4Y=
github.com/drakkan/net v0.0.0-20220908074131-65c0cd1ffa8a/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
github.com/drakkan/net v0.0.0-20220913160159-a08dc61b7895 h1:YZkDIISo8YO7PAOX85GYxGCayjBqAutIAjL+XsdEgkc=
github.com/drakkan/net v0.0.0-20220913160159-a08dc61b7895/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001 h1:/ZshrfQzayqRSBDodmp3rhNCHJCff+utvgBuWRbiqu4=
github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001/go.mod h1:kltMsfRMTHSFdMbK66XdS8mfMW77+FZA1fGY1xYMF84=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@ -596,8 +596,8 @@ github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=
github.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/lufia/plan9stats v0.0.0-20220517141722-cf486979b281 h1:aczX6NMOtt6L4YT0fQvKkDK6LZEtdOso9sUH89V1+P0=
github.com/lufia/plan9stats v0.0.0-20220517141722-cf486979b281/go.mod h1:lc+czkgO/8F7puNki5jk8QyujbfK1LOT7Wl0ON2hxyk=
github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c h1:VtwQ41oftZwlMnOEbMWQtSEUgU64U4s+GHk7hZK+jtY=
github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c/go.mod h1:JKx41uQRwqlTZabZc+kILPrO/3jlKnQ2Z8b7YiVw5cE=
github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=
github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
@ -717,8 +717,8 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4 h1:PT+ElG/UUFMfqy5HrxJxNzj3QBOf7dZwupeVC+mG1Lo=
github.com/sftpgo/sdk v0.1.2-0.20220828084006-f9e2fffac657 h1:UXTpae6d+G/VI3sVITl+58SK0F3ZULn9dlEPMXcyNKY=
github.com/sftpgo/sdk v0.1.2-0.20220828084006-f9e2fffac657/go.mod h1:PTp1TfXa+95wHw9yuZu7BA3vmzLqbRkz3gBmMNnwFQg=
github.com/sftpgo/sdk v0.1.2-0.20220913155952-81743fa5ded5 h1:VrrDnP3PP+UAWcxDgYfedmCBDxlkmugWZx7NdP0Dnng=
github.com/sftpgo/sdk v0.1.2-0.20220913155952-81743fa5ded5/go.mod h1:PTp1TfXa+95wHw9yuZu7BA3vmzLqbRkz3gBmMNnwFQg=
github.com/shirou/gopsutil/v3 v3.22.8 h1:a4s3hXogo5mE2PfdfJIonDbstO/P+9JszdfhAHSzD9Y=
github.com/shirou/gopsutil/v3 v3.22.8/go.mod h1:s648gW4IywYzUfE/KjXxUsqrqx/T2xO5VqOXxONeRfI=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
@ -877,8 +877,9 @@ golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=
golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094 h1:2o1E+E8TpNLklK9nHiPiK1uzIYrIHt+cQx3ynCwq9V8=
golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1 h1:lxqLZaMad/dJHMFZH0NiNpiEZI/nhgWhe4wgzpE+MuA=
golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -979,8 +980,8 @@ golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220907062415-87db552b00fd h1:AZeIEzg+8RCELJYq8w+ODLVxFgLMMigSwO/ffKPEd9U=
golang.org/x/sys v0.0.0-20220907062415-87db552b00fd/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220913153101-76c7481b5158 h1:XQphkCZeKYaMRSo28HqvvNYuLOoM5CIOOvTZfthvTgI=
golang.org/x/sys v0.0.0-20220913153101-76c7481b5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@ -1228,8 +1229,8 @@ google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP
google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
google.golang.org/genproto v0.0.0-20220902135211-223410557253 h1:vXJMM8Shg7TGaYxZsQ++A/FOSlbDmDtWhS/o+3w/hj4=
google.golang.org/genproto v0.0.0-20220902135211-223410557253/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=
google.golang.org/genproto v0.0.0-20220909194730-69f6226f97e5 h1:ngtP8S8JkBWfJACT9cmj5eTkS9tIWPQI5leBz/7Bq/c=
google.golang.org/genproto v0.0.0-20220909194730-69f6226f97e5/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=

View file

@ -54,6 +54,7 @@ const (
var (
actionsConcurrencyGuard = make(chan struct{}, 100)
reservedUsers = []string{ActionExecutorSelf, ActionExecutorSystem}
)
func executeAction(operation, executor, ip, objectType, objectName string, object plugin.Renderer) {

View file

@ -22,9 +22,11 @@ import (
"fmt"
"net"
"os"
"sort"
"strings"
"github.com/alexedwards/argon2id"
"github.com/sftpgo/sdk"
passwordvalidator "github.com/wagslane/go-password-validator"
"golang.org/x/crypto/bcrypt"
@ -57,6 +59,15 @@ const (
PermAdminManageEventRules = "manage_event_rules"
)
const (
// GroupAddToUsersAsMembership defines that the admin's group will be added as membership group for new users
GroupAddToUsersAsMembership = iota
// GroupAddToUsersAsPrimary defines that the admin's group will be added as primary group for new users
GroupAddToUsersAsPrimary
// GroupAddToUsersAsSecondary defines that the admin's group will be added as secondary group for new users
GroupAddToUsersAsSecondary
)
var (
validAdminPerms = []string{PermAdminAny, PermAdminAddUsers, PermAdminChangeUsers, PermAdminDeleteUsers,
PermAdminViewUsers, PermAdminManageGroups, PermAdminViewConnections, PermAdminCloseConnections,
@ -113,6 +124,36 @@ type AdminFilters struct {
RecoveryCodes []RecoveryCode `json:"recovery_codes,omitempty"`
}
// AdminGroupMappingOptions defines the options for admin/group mapping
type AdminGroupMappingOptions struct {
AddToUsersAs int `json:"add_to_users_as,omitempty"`
}
func (o *AdminGroupMappingOptions) validate() error {
if o.AddToUsersAs < GroupAddToUsersAsMembership || o.AddToUsersAs > GroupAddToUsersAsSecondary {
return util.NewValidationError(fmt.Sprintf("Invalid mode to add groups to new users: %d", o.AddToUsersAs))
}
return nil
}
// GetUserGroupType returns the type for the matching user group
func (o *AdminGroupMappingOptions) GetUserGroupType() int {
switch o.AddToUsersAs {
case GroupAddToUsersAsPrimary:
return sdk.GroupTypePrimary
case GroupAddToUsersAsSecondary:
return sdk.GroupTypeSecondary
default:
return sdk.GroupTypeMembership
}
}
// AdminGroupMapping defines the mapping between an SFTPGo admin and a group
type AdminGroupMapping struct {
Name string `json:"name"`
Options AdminGroupMappingOptions `json:"options"`
}
// Admin defines a SFTPGo admin
type Admin struct {
// Database unique identifier
@ -127,6 +168,8 @@ type Admin struct {
Filters AdminFilters `json:"filters,omitempty"`
Description string `json:"description,omitempty"`
AdditionalInfo string `json:"additional_info,omitempty"`
// Groups membership
Groups []AdminGroupMapping `json:"groups,omitempty"`
// Creation time as unix timestamp in milliseconds. It will be 0 for admins created before v2.2.0
CreatedAt int64 `json:"created_at"`
// last update time as unix timestamp in milliseconds
@ -206,11 +249,33 @@ func (a *Admin) validatePermissions() error {
return nil
}
func (a *Admin) validateGroups() error {
hasPrimary := false
for _, g := range a.Groups {
if g.Name == "" {
return util.NewValidationError("group name is mandatory")
}
if err := g.Options.validate(); err != nil {
return err
}
if g.Options.AddToUsersAs == GroupAddToUsersAsPrimary {
if hasPrimary {
return util.NewValidationError("only one primary group is allowed")
}
hasPrimary = true
}
}
return nil
}
func (a *Admin) validate() error {
a.SetEmptySecretsIfNil()
if a.Username == "" {
return util.NewValidationError("username is mandatory")
}
if err := checkReservedUsernames(a.Username); err != nil {
return err
}
if a.Password == "" {
return util.NewValidationError("please set a password")
}
@ -243,7 +308,20 @@ func (a *Admin) validate() error {
}
}
return nil
return a.validateGroups()
}
// GetGroupsAsString returns the user's groups as a string
func (a *Admin) GetGroupsAsString() string {
if len(a.Groups) == 0 {
return ""
}
var groups []string
for _, g := range a.Groups {
groups = append(groups, g.Name)
}
sort.Strings(groups)
return strings.Join(groups, ",")
}
// CheckPassword verifies the admin password
@ -422,6 +500,15 @@ func (a *Admin) getACopy() Admin {
Used: code.Used,
})
}
groups := make([]AdminGroupMapping, 0, len(a.Groups))
for _, g := range a.Groups {
groups = append(groups, AdminGroupMapping{
Name: g.Name,
Options: AdminGroupMappingOptions{
AddToUsersAs: g.Options.AddToUsersAs,
},
})
}
return Admin{
ID: a.ID,
@ -430,6 +517,7 @@ func (a *Admin) getACopy() Admin {
Password: a.Password,
Email: a.Email,
Permissions: permissions,
Groups: groups,
Filters: filters,
AdditionalInfo: a.AdditionalInfo,
Description: a.Description,

View file

@ -35,7 +35,7 @@ import (
)
const (
boltDatabaseVersion = 21
boltDatabaseVersion = 22
)
var (
@ -372,6 +372,10 @@ func (p *BoltProvider) addAdmin(admin *Admin) error {
if err != nil {
return err
}
groupBucket, err := p.getGroupsBucket(tx)
if err != nil {
return err
}
if a := bucket.Get([]byte(admin.Username)); a != nil {
return fmt.Errorf("admin %v already exists", admin.Username)
}
@ -383,6 +387,12 @@ func (p *BoltProvider) addAdmin(admin *Admin) error {
admin.LastLogin = 0
admin.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
admin.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
for idx := range admin.Groups {
err = p.addAdminToGroupMapping(admin.Username, admin.Groups[idx].Name, groupBucket)
if err != nil {
return err
}
}
buf, err := json.Marshal(admin)
if err != nil {
return err
@ -401,8 +411,11 @@ func (p *BoltProvider) updateAdmin(admin *Admin) error {
if err != nil {
return err
}
groupBucket, err := p.getGroupsBucket(tx)
if err != nil {
return err
}
var a []byte
if a = bucket.Get([]byte(admin.Username)); a == nil {
return util.NewRecordNotFoundError(fmt.Sprintf("admin %v does not exist", admin.Username))
}
@ -412,6 +425,18 @@ func (p *BoltProvider) updateAdmin(admin *Admin) error {
return err
}
for idx := range oldAdmin.Groups {
err = p.removeAdminFromGroupMapping(oldAdmin.Username, oldAdmin.Groups[idx].Name, groupBucket)
if err != nil {
return err
}
}
for idx := range admin.Groups {
err = p.addAdminToGroupMapping(admin.Username, admin.Groups[idx].Name, groupBucket)
if err != nil {
return err
}
}
admin.ID = oldAdmin.ID
admin.CreatedAt = oldAdmin.CreatedAt
admin.LastLogin = oldAdmin.LastLogin
@ -431,9 +456,27 @@ func (p *BoltProvider) deleteAdmin(admin Admin) error {
return err
}
if bucket.Get([]byte(admin.Username)) == nil {
var a []byte
if a = bucket.Get([]byte(admin.Username)); a == nil {
return util.NewRecordNotFoundError(fmt.Sprintf("admin %v does not exist", admin.Username))
}
var oldAdmin Admin
err = json.Unmarshal(a, &oldAdmin)
if err != nil {
return err
}
if len(oldAdmin.Groups) > 0 {
groupBucket, err := p.getGroupsBucket(tx)
if err != nil {
return err
}
for idx := range oldAdmin.Groups {
err = p.removeAdminFromGroupMapping(oldAdmin.Username, oldAdmin.Groups[idx].Name, groupBucket)
if err != nil {
return err
}
}
}
if err := p.deleteRelatedAPIKey(tx, admin.Username, APIKeyScopeAdmin); err != nil {
return err
@ -1014,6 +1057,7 @@ func (p *BoltProvider) addFolder(folder *vfs.BaseVirtualFolder) error {
return fmt.Errorf("folder %v already exists", folder.Name)
}
folder.Users = nil
folder.Groups = nil
return p.addFolderInternal(*folder, bucket)
})
}
@ -1044,6 +1088,7 @@ func (p *BoltProvider) updateFolder(folder *vfs.BaseVirtualFolder) error {
folder.UsedQuotaFiles = oldFolder.UsedQuotaFiles
folder.UsedQuotaSize = oldFolder.UsedQuotaSize
folder.Users = oldFolder.Users
folder.Groups = oldFolder.Groups
buf, err := json.Marshal(folder)
if err != nil {
return err
@ -1332,6 +1377,8 @@ func (p *BoltProvider) addGroup(group *Group) error {
group.ID = int64(id)
group.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
group.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
group.Users = nil
group.Admins = nil
for idx := range group.VirtualFolders {
err = p.addRelationToFolderMapping(&group.VirtualFolders[idx].BaseVirtualFolder, nil, group, foldersBucket)
if err != nil {
@ -1383,6 +1430,7 @@ func (p *BoltProvider) updateGroup(group *Group) error {
group.ID = oldGroup.ID
group.CreatedAt = oldGroup.CreatedAt
group.Users = oldGroup.Users
group.Admins = oldGroup.Admins
group.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
buf, err := json.Marshal(group)
if err != nil {
@ -1408,17 +1456,31 @@ func (p *BoltProvider) deleteGroup(group Group) error {
return err
}
if len(oldGroup.Users) > 0 {
return util.NewValidationError(fmt.Sprintf("the group %#v is referenced, it cannot be removed", group.Name))
return util.NewValidationError(fmt.Sprintf("the group %#v is referenced, it cannot be removed", oldGroup.Name))
}
foldersBucket, err := p.getFoldersBucket(tx)
if err != nil {
return err
}
for idx := range group.VirtualFolders {
err = p.removeRelationFromFolderMapping(group.VirtualFolders[idx], "", group.Name, foldersBucket)
if len(oldGroup.VirtualFolders) > 0 {
foldersBucket, err := p.getFoldersBucket(tx)
if err != nil {
return err
}
for idx := range oldGroup.VirtualFolders {
err = p.removeRelationFromFolderMapping(oldGroup.VirtualFolders[idx], "", oldGroup.Name, foldersBucket)
if err != nil {
return err
}
}
}
if len(oldGroup.Admins) > 0 {
adminsBucket, err := p.getAdminsBucket(tx)
if err != nil {
return err
}
for idx := range oldGroup.Admins {
err = p.removeGroupFromAdminMapping(oldGroup.Name, oldGroup.Admins[idx], adminsBucket)
if err != nil {
return err
}
}
}
return bucket.Delete([]byte(group.Name))
@ -2517,23 +2579,23 @@ func (p *BoltProvider) migrateDatabase() error {
providerLog(logger.LevelDebug, "bolt database is up to date, current version: %v", version)
return ErrNoInitRequired
case version < 19:
err = fmt.Errorf("database version %v is too old, please see the upgrading docs", version)
err = fmt.Errorf("database schema version %v is too old, please see the upgrading docs", version)
providerLog(logger.LevelError, "%v", err)
logger.ErrorToConsole("%v", err)
return err
case version == 19, version == 20:
logger.InfoToConsole(fmt.Sprintf("updating database version: %d -> 21", version))
providerLog(logger.LevelInfo, "updating database version: %d -> 21", version)
return updateBoltDatabaseVersion(p.dbHandle, 21)
case version == 19, version == 20, version == 21:
logger.InfoToConsole(fmt.Sprintf("updating database schema version: %d -> 22", version))
providerLog(logger.LevelInfo, "updating database schema version: %d -> 22", version)
return updateBoltDatabaseVersion(p.dbHandle, 22)
default:
if version > boltDatabaseVersion {
providerLog(logger.LevelError, "database version %v is newer than the supported one: %v", version,
providerLog(logger.LevelError, "database schema version %v is newer than the supported one: %v", version,
boltDatabaseVersion)
logger.WarnToConsole("database version %v is newer than the supported one: %v", version,
logger.WarnToConsole("database schema version %v is newer than the supported one: %v", version,
boltDatabaseVersion)
return nil
}
return fmt.Errorf("database version not handled: %v", version)
return fmt.Errorf("database schema version not handled: %v", version)
}
}
@ -2547,8 +2609,8 @@ func (p *BoltProvider) revertDatabase(targetVersion int) error {
}
switch dbVersion.Version {
case 20, 21:
logger.InfoToConsole("downgrading database version: %d -> 19", dbVersion.Version)
providerLog(logger.LevelInfo, "downgrading database version: %d -> 19", dbVersion.Version)
logger.InfoToConsole("downgrading database schema version: %d -> 19", dbVersion.Version)
providerLog(logger.LevelInfo, "downgrading database schema version: %d -> 19", dbVersion.Version)
err := p.dbHandle.Update(func(tx *bolt.Tx) error {
for _, bucketName := range [][]byte{actionsBucket, rulesBucket} {
err := tx.DeleteBucket(bucketName)
@ -2563,7 +2625,7 @@ func (p *BoltProvider) revertDatabase(targetVersion int) error {
}
return updateBoltDatabaseVersion(p.dbHandle, 19)
default:
return fmt.Errorf("database version not handled: %v", dbVersion.Version)
return fmt.Errorf("database schema version not handled: %v", dbVersion.Version)
}
}
@ -2766,14 +2828,32 @@ func (p *BoltProvider) removeUserFromGroupMapping(username, groupname string, bu
if err != nil {
return err
}
if util.Contains(group.Users, username) {
var users []string
for _, u := range group.Users {
if u != username {
users = append(users, u)
}
var users []string
for _, u := range group.Users {
if u != username {
users = append(users, u)
}
group.Users = util.RemoveDuplicates(users, false)
}
group.Users = util.RemoveDuplicates(users, false)
buf, err := json.Marshal(group)
if err != nil {
return err
}
return bucket.Put([]byte(group.Name), buf)
}
func (p *BoltProvider) addAdminToGroupMapping(username, groupname string, bucket *bolt.Bucket) error {
g := bucket.Get([]byte(groupname))
if g == nil {
return util.NewRecordNotFoundError(fmt.Sprintf("group %q does not exist", groupname))
}
var group Group
err := json.Unmarshal(g, &group)
if err != nil {
return err
}
if !util.Contains(group.Admins, username) {
group.Admins = append(group.Admins, username)
buf, err := json.Marshal(group)
if err != nil {
return err
@ -2783,6 +2863,55 @@ func (p *BoltProvider) removeUserFromGroupMapping(username, groupname string, bu
return nil
}
func (p *BoltProvider) removeAdminFromGroupMapping(username, groupname string, bucket *bolt.Bucket) error {
g := bucket.Get([]byte(groupname))
if g == nil {
return util.NewRecordNotFoundError(fmt.Sprintf("group %q does not exist", groupname))
}
var group Group
err := json.Unmarshal(g, &group)
if err != nil {
return err
}
var admins []string
for _, a := range group.Admins {
if a != username {
admins = append(admins, a)
}
}
group.Admins = util.RemoveDuplicates(admins, false)
buf, err := json.Marshal(group)
if err != nil {
return err
}
return bucket.Put([]byte(group.Name), buf)
}
func (p *BoltProvider) removeGroupFromAdminMapping(groupName, adminName string, bucket *bolt.Bucket) error {
var a []byte
if a = bucket.Get([]byte(adminName)); a == nil {
// the admin does not exist so there is no associated group
return nil
}
var admin Admin
err := json.Unmarshal(a, &admin)
if err != nil {
return err
}
var newGroups []AdminGroupMapping
for _, g := range admin.Groups {
if g.Name != groupName {
newGroups = append(newGroups, g)
}
}
admin.Groups = newGroups
buf, err := json.Marshal(admin)
if err != nil {
return err
}
return bucket.Put([]byte(adminName), buf)
}
func (p *BoltProvider) addRelationToFolderMapping(baseFolder *vfs.BaseVirtualFolder, user *User, group *Group, bucket *bolt.Bucket) error {
f := bucket.Get([]byte(baseFolder.Name))
if f == nil {
@ -2836,7 +2965,7 @@ func (p *BoltProvider) removeRelationFromFolderMapping(folder vfs.VirtualFolder,
return err
}
found := false
if username != "" && util.Contains(baseFolder.Users, username) {
if username != "" {
found = true
var newUserMapping []string
for _, u := range baseFolder.Users {
@ -2846,7 +2975,7 @@ func (p *BoltProvider) removeRelationFromFolderMapping(folder vfs.VirtualFolder,
}
baseFolder.Users = newUserMapping
}
if groupname != "" && util.Contains(baseFolder.Groups, groupname) {
if groupname != "" {
found = true
var newGroupMapping []string
for _, g := range baseFolder.Groups {
@ -3066,7 +3195,7 @@ func getBoltDatabaseVersion(dbHandle *bolt.DB) (schemaVersion, error) {
err := dbHandle.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket(dbVersionBucket)
if bucket == nil {
return fmt.Errorf("unable to find database version bucket")
return fmt.Errorf("unable to find database schema version bucket")
}
v := bucket.Get(dbVersionKey)
if v == nil {
@ -3084,7 +3213,7 @@ func updateBoltDatabaseVersion(dbHandle *bolt.DB, version int) error {
err := dbHandle.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket(dbVersionBucket)
if bucket == nil {
return fmt.Errorf("unable to find database version bucket")
return fmt.Errorf("unable to find database schema version bucket")
}
newDbVersion := schemaVersion{
Version: version,

View file

@ -180,6 +180,7 @@ var (
sqlTableActiveTransfers string
sqlTableGroups string
sqlTableUsersGroupsMapping string
sqlTableAdminsGroupsMapping string
sqlTableGroupsFoldersMapping string
sqlTableSharedSessions string
sqlTableEventsActions string
@ -209,6 +210,7 @@ func initSQLTables() {
sqlTableGroups = "groups"
sqlTableUsersGroupsMapping = "users_groups_mapping"
sqlTableGroupsFoldersMapping = "groups_folders_mapping"
sqlTableAdminsGroupsMapping = "admins_groups_mapping"
sqlTableSharedSessions = "shared_sessions"
sqlTableEventsActions = "events_actions"
sqlTableEventsRules = "events_rules"
@ -928,6 +930,7 @@ func validateSQLTablesPrefix() error {
sqlTableActiveTransfers = config.SQLTablesPrefix + sqlTableActiveTransfers
sqlTableGroups = config.SQLTablesPrefix + sqlTableGroups
sqlTableUsersGroupsMapping = config.SQLTablesPrefix + sqlTableUsersGroupsMapping
sqlTableAdminsGroupsMapping = config.SQLTablesPrefix + sqlTableAdminsGroupsMapping
sqlTableGroupsFoldersMapping = config.SQLTablesPrefix + sqlTableGroupsFoldersMapping
sqlTableSharedSessions = config.SQLTablesPrefix + sqlTableSharedSessions
sqlTableEventsActions = config.SQLTablesPrefix + sqlTableEventsActions
@ -937,12 +940,12 @@ func validateSQLTablesPrefix() error {
sqlTableSchemaVersion = config.SQLTablesPrefix + sqlTableSchemaVersion
providerLog(logger.LevelDebug, "sql table for users %q, folders %q users folders mapping %q admins %q "+
"api keys %q shares %q defender hosts %q defender events %q transfers %q groups %q "+
"users groups mapping %q groups folders mapping %q shared sessions %q schema version %q"+
"events actions %q events rules %q rules actions mapping %q tasks %q",
"users groups mapping %q admins groups mapping %q groups folders mapping %q shared sessions %q "+
"schema version %q events actions %q events rules %q rules actions mapping %q tasks %q",
sqlTableUsers, sqlTableFolders, sqlTableUsersFoldersMapping, sqlTableAdmins, sqlTableAPIKeys,
sqlTableShares, sqlTableDefenderHosts, sqlTableDefenderEvents, sqlTableActiveTransfers, sqlTableGroups,
sqlTableUsersGroupsMapping, sqlTableGroupsFoldersMapping, sqlTableSharedSessions, sqlTableSchemaVersion,
sqlTableEventsActions, sqlTableEventsRules, sqlTableRulesActionsMapping, sqlTableTasks)
sqlTableUsersGroupsMapping, sqlTableAdminsGroupsMapping, sqlTableGroupsFoldersMapping, sqlTableSharedSessions,
sqlTableSchemaVersion, sqlTableEventsActions, sqlTableEventsRules, sqlTableRulesActionsMapping, sqlTableTasks)
}
return nil
}
@ -2309,7 +2312,7 @@ func validateUserGroups(user *User) error {
groupNames := make(map[string]bool)
for _, g := range user.Groups {
if g.Type < sdk.GroupTypePrimary && g.Type > sdk.GroupTypeSecondary {
if g.Type < sdk.GroupTypePrimary && g.Type > sdk.GroupTypeMembership {
return util.NewValidationError(fmt.Sprintf("invalid group type: %v", g.Type))
}
if g.Type == sdk.GroupTypePrimary {
@ -2678,6 +2681,9 @@ func validateBaseParams(user *User) error {
if user.Username == "" {
return util.NewValidationError("username is mandatory")
}
if err := checkReservedUsernames(user.Username); err != nil {
return err
}
if user.Email != "" && !util.IsEmailValid(user.Email) {
return util.NewValidationError(fmt.Sprintf("email %#v is not valid", user.Email))
}
@ -3963,6 +3969,13 @@ func getConfigPath(name, configDir string) string {
return name
}
func checkReservedUsernames(username string) error {
if util.Contains(reservedUsers, username) {
return util.NewValidationError("this username is reserved")
}
return nil
}
func providerLog(level logger.LogLevel, format string, v ...any) {
logger.Log(level, logSender, "", format, v...)
}

View file

@ -187,6 +187,8 @@ func (g *Group) validateUserSettings() error {
func (g *Group) getACopy() Group {
users := make([]string, len(g.Users))
copy(users, g.Users)
admins := make([]string, len(g.Admins))
copy(admins, g.Admins)
virtualFolders := make([]vfs.VirtualFolder, 0, len(g.VirtualFolders))
for idx := range g.VirtualFolders {
vfolder := g.VirtualFolders[idx].GetACopy()
@ -207,6 +209,7 @@ func (g *Group) getACopy() Group {
CreatedAt: g.CreatedAt,
UpdatedAt: g.UpdatedAt,
Users: users,
Admins: admins,
},
UserSettings: GroupUserSettings{
BaseGroupUserSettings: sdk.BaseGroupUserSettings{

View file

@ -330,12 +330,18 @@ func (p *MemoryProvider) addUser(user *User) error {
user.FirstDownload = 0
user.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
user.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
user.VirtualFolders = p.joinUserVirtualFoldersFields(user)
var mappedGroups []string
for idx := range user.Groups {
if err = p.addUserFromGroupMapping(user.Username, user.Groups[idx].Name); err != nil {
if err = p.addUserToGroupMapping(user.Username, user.Groups[idx].Name); err != nil {
// try to remove group mapping
for _, g := range mappedGroups {
p.removeUserFromGroupMapping(user.Username, g)
}
return err
}
mappedGroups = append(mappedGroups, user.Groups[idx].Name)
}
user.VirtualFolders = p.joinUserVirtualFoldersFields(user)
p.dbHandle.users[user.Username] = user.getACopy()
p.dbHandle.usernames = append(p.dbHandle.usernames, user.Username)
sort.Strings(p.dbHandle.usernames)
@ -360,20 +366,25 @@ func (p *MemoryProvider) updateUser(user *User) error {
if err != nil {
return err
}
for idx := range u.Groups {
p.removeUserFromGroupMapping(u.Username, u.Groups[idx].Name)
}
for idx := range user.Groups {
if err = p.addUserToGroupMapping(user.Username, user.Groups[idx].Name); err != nil {
// try to add old mapping
for _, g := range u.Groups {
if errRollback := p.addUserToGroupMapping(user.Username, g.Name); errRollback != nil {
providerLog(logger.LevelError, "unable to rollback old group mapping %q for user %q, error: %v",
g.Name, user.Username, errRollback)
}
}
return err
}
}
for _, oldFolder := range u.VirtualFolders {
p.removeRelationFromFolderMapping(oldFolder.Name, u.Username, "")
}
for idx := range u.Groups {
if err = p.removeUserFromGroupMapping(u.Username, u.Groups[idx].Name); err != nil {
return err
}
}
user.VirtualFolders = p.joinUserVirtualFoldersFields(user)
for idx := range user.Groups {
if err = p.addUserFromGroupMapping(user.Username, user.Groups[idx].Name); err != nil {
return err
}
}
user.LastQuotaUpdate = u.LastQuotaUpdate
user.UsedQuotaSize = u.UsedQuotaSize
user.UsedQuotaFiles = u.UsedQuotaFiles
@ -405,9 +416,7 @@ func (p *MemoryProvider) deleteUser(user User, softDelete bool) error {
p.removeRelationFromFolderMapping(oldFolder.Name, u.Username, "")
}
for idx := range u.Groups {
if err = p.removeUserFromGroupMapping(u.Username, u.Groups[idx].Name); err != nil {
return err
}
p.removeUserFromGroupMapping(u.Username, u.Groups[idx].Name)
}
delete(p.dbHandle.users, user.Username)
// this could be more efficient
@ -644,6 +653,17 @@ func (p *MemoryProvider) addAdmin(admin *Admin) error {
admin.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
admin.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
admin.LastLogin = 0
var mappedAdmins []string
for idx := range admin.Groups {
if err = p.addAdminToGroupMapping(admin.Username, admin.Groups[idx].Name); err != nil {
// try to remove group mapping
for _, g := range mappedAdmins {
p.removeAdminFromGroupMapping(admin.Username, g)
}
return err
}
mappedAdmins = append(mappedAdmins, admin.Groups[idx].Name)
}
p.dbHandle.admins[admin.Username] = admin.getACopy()
p.dbHandle.adminsUsernames = append(p.dbHandle.adminsUsernames, admin.Username)
sort.Strings(p.dbHandle.adminsUsernames)
@ -664,6 +684,21 @@ func (p *MemoryProvider) updateAdmin(admin *Admin) error {
if err != nil {
return err
}
for idx := range a.Groups {
p.removeAdminFromGroupMapping(a.Username, a.Groups[idx].Name)
}
for idx := range admin.Groups {
if err = p.addAdminToGroupMapping(admin.Username, admin.Groups[idx].Name); err != nil {
// try to add old mapping
for _, oldGroup := range a.Groups {
if errRollback := p.addAdminToGroupMapping(a.Username, oldGroup.Name); errRollback != nil {
providerLog(logger.LevelError, "unable to rollback old group mapping %q for admin %q, error: %v",
oldGroup.Name, a.Username, errRollback)
}
}
return err
}
}
admin.ID = a.ID
admin.CreatedAt = a.CreatedAt
admin.LastLogin = a.LastLogin
@ -678,10 +713,13 @@ func (p *MemoryProvider) deleteAdmin(admin Admin) error {
if p.dbHandle.isClosed {
return errMemoryProviderClosed
}
_, err := p.adminExistsInternal(admin.Username)
a, err := p.adminExistsInternal(admin.Username)
if err != nil {
return err
}
for idx := range a.Groups {
p.removeAdminFromGroupMapping(a.Username, a.Groups[idx].Name)
}
delete(p.dbHandle.admins, admin.Username)
// this could be more efficient
@ -906,6 +944,8 @@ func (p *MemoryProvider) addGroup(group *Group) error {
group.ID = p.getNextGroupID()
group.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
group.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
group.Users = nil
group.Admins = nil
group.VirtualFolders = p.joinGroupVirtualFoldersFields(group)
p.dbHandle.groups[group.Name] = group.getACopy()
p.dbHandle.groupnames = append(p.dbHandle.groupnames, group.Name)
@ -933,6 +973,8 @@ func (p *MemoryProvider) updateGroup(group *Group) error {
group.CreatedAt = g.CreatedAt
group.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
group.ID = g.ID
group.Users = g.Users
group.Admins = g.Admins
p.dbHandle.groups[group.Name] = group.getACopy()
return nil
}
@ -953,6 +995,9 @@ func (p *MemoryProvider) deleteGroup(group Group) error {
for _, oldFolder := range g.VirtualFolders {
p.removeRelationFromFolderMapping(oldFolder.Name, "", g.Name)
}
for _, a := range g.Admins {
p.removeGroupFromAdminMapping(g.Name, a)
}
delete(p.dbHandle.groups, group.Name)
// this could be more efficient
p.dbHandle.groupnames = make([]string, 0, len(p.dbHandle.groups))
@ -1050,11 +1095,11 @@ func (p *MemoryProvider) addRuleToActionMapping(ruleName, actionName string) err
return nil
}
func (p *MemoryProvider) removeRuleFromActionMapping(ruleName, actionName string) error {
func (p *MemoryProvider) removeRuleFromActionMapping(ruleName, actionName string) {
a, err := p.actionExistsInternal(actionName)
if err != nil {
providerLog(logger.LevelWarn, "action %q does not exist, cannot remove from mapping", actionName)
return nil
return
}
if util.Contains(a.Rules, ruleName) {
var rules []string
@ -1066,10 +1111,52 @@ func (p *MemoryProvider) removeRuleFromActionMapping(ruleName, actionName string
a.Rules = rules
p.dbHandle.actions[actionName] = a
}
}
func (p *MemoryProvider) addAdminToGroupMapping(username, groupname string) error {
g, err := p.groupExistsInternal(groupname)
if err != nil {
return err
}
if !util.Contains(g.Admins, username) {
g.Admins = append(g.Admins, username)
p.dbHandle.groups[groupname] = g
}
return nil
}
func (p *MemoryProvider) addUserFromGroupMapping(username, groupname string) error {
func (p *MemoryProvider) removeAdminFromGroupMapping(username, groupname string) {
g, err := p.groupExistsInternal(groupname)
if err != nil {
return
}
var admins []string
for _, a := range g.Admins {
if a != username {
admins = append(admins, a)
}
}
g.Admins = admins
p.dbHandle.groups[groupname] = g
}
func (p *MemoryProvider) removeGroupFromAdminMapping(groupname, username string) {
admin, err := p.adminExistsInternal(username)
if err != nil {
// the admin does not exist so there is no associated group
return
}
var newGroups []AdminGroupMapping
for _, g := range admin.Groups {
if g.Name != groupname {
newGroups = append(newGroups, g)
}
}
admin.Groups = newGroups
p.dbHandle.admins[admin.Username] = admin
}
func (p *MemoryProvider) addUserToGroupMapping(username, groupname string) error {
g, err := p.groupExistsInternal(groupname)
if err != nil {
return err
@ -1081,22 +1168,19 @@ func (p *MemoryProvider) addUserFromGroupMapping(username, groupname string) err
return nil
}
func (p *MemoryProvider) removeUserFromGroupMapping(username, groupname string) error {
func (p *MemoryProvider) removeUserFromGroupMapping(username, groupname string) {
g, err := p.groupExistsInternal(groupname)
if err != nil {
return err
return
}
if util.Contains(g.Users, username) {
var users []string
for _, u := range g.Users {
if u != username {
users = append(users, u)
}
var users []string
for _, u := range g.Users {
if u != username {
users = append(users, u)
}
g.Users = users
p.dbHandle.groups[groupname] = g
}
return nil
g.Users = users
p.dbHandle.groups[groupname] = g
}
func (p *MemoryProvider) joinUserVirtualFoldersFields(user *User) []vfs.VirtualFolder {
@ -1280,6 +1364,7 @@ func (p *MemoryProvider) addFolder(folder *vfs.BaseVirtualFolder) error {
}
folder.ID = p.getNextFolderID()
folder.Users = nil
folder.Groups = nil
p.dbHandle.vfolders[folder.Name] = folder.GetACopy()
p.dbHandle.vfoldersNames = append(p.dbHandle.vfoldersNames, folder.Name)
sort.Strings(p.dbHandle.vfoldersNames)
@ -1306,6 +1391,7 @@ func (p *MemoryProvider) updateFolder(folder *vfs.BaseVirtualFolder) error {
folder.UsedQuotaFiles = f.UsedQuotaFiles
folder.UsedQuotaSize = f.UsedQuotaSize
folder.Users = f.Users
folder.Groups = f.Groups
p.dbHandle.vfolders[folder.Name] = folder.GetACopy()
// now update the related users
for _, username := range folder.Users {
@ -2115,10 +2201,16 @@ func (p *MemoryProvider) addEventRule(rule *EventRule) error {
rule.ID = p.getNextRuleID()
rule.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
rule.UpdatedAt = rule.CreatedAt
var mappedActions []string
for idx := range rule.Actions {
if err := p.addRuleToActionMapping(rule.Name, rule.Actions[idx].Name); err != nil {
// try to remove action mapping
for _, a := range mappedActions {
p.removeRuleFromActionMapping(rule.Name, a)
}
return err
}
mappedActions = append(mappedActions, rule.Actions[idx].Name)
}
sort.Slice(rule.Actions, func(i, j int) bool {
return rule.Actions[i].Order < rule.Actions[j].Order
@ -2144,12 +2236,17 @@ func (p *MemoryProvider) updateEventRule(rule *EventRule) error {
return err
}
for idx := range oldRule.Actions {
if err = p.removeRuleFromActionMapping(rule.Name, oldRule.Actions[idx].Name); err != nil {
return err
}
p.removeRuleFromActionMapping(rule.Name, oldRule.Actions[idx].Name)
}
for idx := range rule.Actions {
if err = p.addRuleToActionMapping(rule.Name, rule.Actions[idx].Name); err != nil {
// try to add old mapping
for _, oldAction := range oldRule.Actions {
if errRollback := p.addRuleToActionMapping(oldRule.Name, oldAction.Name); errRollback != nil {
providerLog(logger.LevelError, "unable to rollback old action mapping %q for rule %q, error: %v",
oldAction.Name, oldRule.Name, errRollback)
}
}
return err
}
}
@ -2176,9 +2273,7 @@ func (p *MemoryProvider) deleteEventRule(rule EventRule, softDelete bool) error
}
if len(oldRule.Actions) > 0 {
for idx := range oldRule.Actions {
if err = p.removeRuleFromActionMapping(rule.Name, oldRule.Actions[idx].Name); err != nil {
return err
}
p.removeRuleFromActionMapping(rule.Name, oldRule.Actions[idx].Name)
}
}
delete(p.dbHandle.rules, rule.Name)

View file

@ -41,6 +41,7 @@ const (
"DROP TABLE IF EXISTS `{{folders_mapping}}` CASCADE;" +
"DROP TABLE IF EXISTS `{{users_folders_mapping}}` CASCADE;" +
"DROP TABLE IF EXISTS `{{users_groups_mapping}}` CASCADE;" +
"DROP TABLE IF EXISTS `{{admins_groups_mapping}}` CASCADE;" +
"DROP TABLE IF EXISTS `{{groups_folders_mapping}}` CASCADE;" +
"DROP TABLE IF EXISTS `{{admins}}` CASCADE;" +
"DROP TABLE IF EXISTS `{{folders}}` CASCADE;" +
@ -171,6 +172,16 @@ const (
"ALTER TABLE `{{users}}` ALTER COLUMN `first_upload` DROP DEFAULT;"
mysqlV21DownSQL = "ALTER TABLE `{{users}}` DROP COLUMN `first_upload`; " +
"ALTER TABLE `{{users}}` DROP COLUMN `first_download`;"
mysqlV22SQL = "CREATE TABLE `{{admins_groups_mapping}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, " +
" `admin_id` integer NOT NULL, `group_id` integer NOT NULL, `options` longtext NOT NULL);" +
"ALTER TABLE `{{admins_groups_mapping}}` ADD CONSTRAINT `{{prefix}}unique_admin_group_mapping` " +
"UNIQUE (`admin_id`, `group_id`);" +
"ALTER TABLE `{{admins_groups_mapping}}` ADD CONSTRAINT `{{prefix}}admins_groups_mapping_admin_id_fk_admins_id` " +
"FOREIGN KEY (`admin_id`) REFERENCES `{{admins}}` (`id`) ON DELETE CASCADE;" +
"ALTER TABLE `{{admins_groups_mapping}}` ADD CONSTRAINT `{{prefix}}admins_groups_mapping_group_id_fk_groups_id` " +
"FOREIGN KEY (`group_id`) REFERENCES `{{groups}}` (`id`) ON DELETE CASCADE;"
mysqlV22DownSQL = "ALTER TABLE `{{admins_groups_mapping}}` DROP INDEX `{{prefix}}unique_admin_group_mapping`;" +
"DROP TABLE `{{admins_groups_mapping}}` CASCADE;"
)
// MySQLProvider defines the auth provider for MySQL/MariaDB database
@ -676,7 +687,7 @@ func (p *MySQLProvider) migrateDatabase() error { //nolint:dupl
providerLog(logger.LevelDebug, "sql database is up to date, current version: %v", version)
return ErrNoInitRequired
case version < 19:
err = fmt.Errorf("database version %v is too old, please see the upgrading docs", version)
err = fmt.Errorf("database schema version %v is too old, please see the upgrading docs", version)
providerLog(logger.LevelError, "%v", err)
logger.ErrorToConsole("%v", err)
return err
@ -684,15 +695,17 @@ func (p *MySQLProvider) migrateDatabase() error { //nolint:dupl
return updateMySQLDatabaseFromV19(p.dbHandle)
case version == 20:
return updateMySQLDatabaseFromV20(p.dbHandle)
case version == 21:
return updateMySQLDatabaseFromV21(p.dbHandle)
default:
if version > sqlDatabaseVersion {
providerLog(logger.LevelError, "database version %v is newer than the supported one: %v", version,
providerLog(logger.LevelError, "database schema version %v is newer than the supported one: %v", version,
sqlDatabaseVersion)
logger.WarnToConsole("database version %v is newer than the supported one: %v", version,
logger.WarnToConsole("database schema version %v is newer than the supported one: %v", version,
sqlDatabaseVersion)
return nil
}
return fmt.Errorf("database version not handled: %v", version)
return fmt.Errorf("database schema version not handled: %v", version)
}
}
@ -710,8 +723,10 @@ func (p *MySQLProvider) revertDatabase(targetVersion int) error {
return downgradeMySQLDatabaseFromV20(p.dbHandle)
case 21:
return downgradeMySQLDatabaseFromV21(p.dbHandle)
case 22:
return downgradeMySQLDatabaseFromV22(p.dbHandle)
default:
return fmt.Errorf("database version not handled: %v", dbVersion.Version)
return fmt.Errorf("database schema version not handled: %v", dbVersion.Version)
}
}
@ -728,7 +743,14 @@ func updateMySQLDatabaseFromV19(dbHandle *sql.DB) error {
}
func updateMySQLDatabaseFromV20(dbHandle *sql.DB) error {
return updateMySQLDatabaseFrom20To21(dbHandle)
if err := updateMySQLDatabaseFrom20To21(dbHandle); err != nil {
return err
}
return updateMySQLDatabaseFromV21(dbHandle)
}
func updateMySQLDatabaseFromV21(dbHandle *sql.DB) error {
return updateMySQLDatabaseFrom21To22(dbHandle)
}
func downgradeMySQLDatabaseFromV20(dbHandle *sql.DB) error {
@ -742,9 +764,16 @@ func downgradeMySQLDatabaseFromV21(dbHandle *sql.DB) error {
return downgradeMySQLDatabaseFromV20(dbHandle)
}
func downgradeMySQLDatabaseFromV22(dbHandle *sql.DB) error {
if err := downgradeMySQLDatabaseFrom22To21(dbHandle); err != nil {
return err
}
return downgradeMySQLDatabaseFromV21(dbHandle)
}
func updateMySQLDatabaseFrom19To20(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database version: 19 -> 20")
providerLog(logger.LevelInfo, "updating database version: 19 -> 20")
logger.InfoToConsole("updating database schema version: 19 -> 20")
providerLog(logger.LevelInfo, "updating database schema version: 19 -> 20")
sql := strings.ReplaceAll(mysqlV20SQL, "{{events_actions}}", sqlTableEventsActions)
sql = strings.ReplaceAll(sql, "{{events_rules}}", sqlTableEventsRules)
sql = strings.ReplaceAll(sql, "{{rules_actions_mapping}}", sqlTableRulesActionsMapping)
@ -755,15 +784,25 @@ func updateMySQLDatabaseFrom19To20(dbHandle *sql.DB) error {
}
func updateMySQLDatabaseFrom20To21(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database version: 20 -> 21")
providerLog(logger.LevelInfo, "updating database version: 20 -> 21")
logger.InfoToConsole("updating database schema version: 20 -> 21")
providerLog(logger.LevelInfo, "updating database schema version: 20 -> 21")
sql := strings.ReplaceAll(mysqlV21SQL, "{{users}}", sqlTableUsers)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, strings.Split(sql, ";"), 21, true)
}
func updateMySQLDatabaseFrom21To22(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database schema version: 21 -> 22")
providerLog(logger.LevelInfo, "updating database schema version: 21 -> 22")
sql := strings.ReplaceAll(mysqlV22SQL, "{{admins_groups_mapping}}", sqlTableAdminsGroupsMapping)
sql = strings.ReplaceAll(sql, "{{admins}}", sqlTableAdmins)
sql = strings.ReplaceAll(sql, "{{groups}}", sqlTableGroups)
sql = strings.ReplaceAll(sql, "{{prefix}}", config.SQLTablesPrefix)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, strings.Split(sql, ";"), 22, true)
}
func downgradeMySQLDatabaseFrom20To19(dbHandle *sql.DB) error {
logger.InfoToConsole("downgrading database version: 20 -> 19")
providerLog(logger.LevelInfo, "downgrading database version: 20 -> 19")
logger.InfoToConsole("downgrading database schema version: 20 -> 19")
providerLog(logger.LevelInfo, "downgrading database schema version: 20 -> 19")
sql := strings.ReplaceAll(mysqlV20DownSQL, "{{events_actions}}", sqlTableEventsActions)
sql = strings.ReplaceAll(sql, "{{events_rules}}", sqlTableEventsRules)
sql = strings.ReplaceAll(sql, "{{rules_actions_mapping}}", sqlTableRulesActionsMapping)
@ -773,8 +812,16 @@ func downgradeMySQLDatabaseFrom20To19(dbHandle *sql.DB) error {
}
func downgradeMySQLDatabaseFrom21To20(dbHandle *sql.DB) error {
logger.InfoToConsole("downgrading database version: 21 -> 20")
providerLog(logger.LevelInfo, "downgrading database version: 21 -> 20")
logger.InfoToConsole("downgrading database schema version: 21 -> 20")
providerLog(logger.LevelInfo, "downgrading database schema version: 21 -> 20")
sql := strings.ReplaceAll(mysqlV21DownSQL, "{{users}}", sqlTableUsers)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, strings.Split(sql, ";"), 20, false)
}
func downgradeMySQLDatabaseFrom22To21(dbHandle *sql.DB) error {
logger.InfoToConsole("downgrading database schema version: 22 -> 21")
providerLog(logger.LevelInfo, "downgrading database schema version: 22 -> 21")
sql := strings.ReplaceAll(mysqlV22DownSQL, "{{admins_groups_mapping}}", sqlTableAdminsGroupsMapping)
sql = strings.ReplaceAll(sql, "{{prefix}}", config.SQLTablesPrefix)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, strings.Split(sql, ";"), 21, false)
}

View file

@ -39,6 +39,7 @@ const (
DROP TABLE IF EXISTS "{{folders_mapping}}" CASCADE;
DROP TABLE IF EXISTS "{{users_folders_mapping}}" CASCADE;
DROP TABLE IF EXISTS "{{users_groups_mapping}}" CASCADE;
DROP TABLE IF EXISTS "{{admins_groups_mapping}}" CASCADE;
DROP TABLE IF EXISTS "{{groups_folders_mapping}}" CASCADE;
DROP TABLE IF EXISTS "{{admins}}" CASCADE;
DROP TABLE IF EXISTS "{{folders}}" CASCADE;
@ -183,6 +184,19 @@ ALTER TABLE "{{users}}" ALTER COLUMN "first_upload" DROP DEFAULT;
`
pgsqlV21DownSQL = `ALTER TABLE "{{users}}" DROP COLUMN "first_upload" CASCADE;
ALTER TABLE "{{users}}" DROP COLUMN "first_download" CASCADE;
`
pgsqlV22SQL = `CREATE TABLE "{{admins_groups_mapping}}" ("id" serial NOT NULL PRIMARY KEY,
"admin_id" integer NOT NULL, "group_id" integer NOT NULL, "options" text NOT NULL);
ALTER TABLE "{{admins_groups_mapping}}" ADD CONSTRAINT "{{prefix}}unique_admin_group_mapping" UNIQUE ("admin_id", "group_id");
ALTER TABLE "{{admins_groups_mapping}}" ADD CONSTRAINT "{{prefix}}admins_groups_mapping_admin_id_fk_admins_id"
FOREIGN KEY ("admin_id") REFERENCES "{{admins}}" ("id") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE;
ALTER TABLE "{{admins_groups_mapping}}" ADD CONSTRAINT "{{prefix}}admins_groups_mapping_group_id_fk_groups_id"
FOREIGN KEY ("group_id") REFERENCES "{{groups}}" ("id") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE;
CREATE INDEX "{{prefix}}admins_groups_mapping_admin_id_idx" ON "{{admins_groups_mapping}}" ("admin_id");
CREATE INDEX "{{prefix}}admins_groups_mapping_group_id_idx" ON "{{admins_groups_mapping}}" ("group_id");
`
pgsqlV22DownSQL = `ALTER TABLE "{{admins_groups_mapping}}" DROP CONSTRAINT "{{prefix}}unique_admin_group_mapping";
DROP TABLE "{{admins_groups_mapping}}" CASCADE;
`
)
@ -645,7 +659,7 @@ func (p *PGSQLProvider) migrateDatabase() error { //nolint:dupl
providerLog(logger.LevelDebug, "sql database is up to date, current version: %v", version)
return ErrNoInitRequired
case version < 19:
err = fmt.Errorf("database version %v is too old, please see the upgrading docs", version)
err = fmt.Errorf("database schema version %v is too old, please see the upgrading docs", version)
providerLog(logger.LevelError, "%v", err)
logger.ErrorToConsole("%v", err)
return err
@ -653,15 +667,17 @@ func (p *PGSQLProvider) migrateDatabase() error { //nolint:dupl
return updatePgSQLDatabaseFromV19(p.dbHandle)
case version == 20:
return updatePgSQLDatabaseFromV20(p.dbHandle)
case version == 21:
return updatePgSQLDatabaseFromV21(p.dbHandle)
default:
if version > sqlDatabaseVersion {
providerLog(logger.LevelError, "database version %v is newer than the supported one: %v", version,
providerLog(logger.LevelError, "database schema version %v is newer than the supported one: %v", version,
sqlDatabaseVersion)
logger.WarnToConsole("database version %v is newer than the supported one: %v", version,
logger.WarnToConsole("database schema version %v is newer than the supported one: %v", version,
sqlDatabaseVersion)
return nil
}
return fmt.Errorf("database version not handled: %v", version)
return fmt.Errorf("database schema version not handled: %v", version)
}
}
@ -679,8 +695,10 @@ func (p *PGSQLProvider) revertDatabase(targetVersion int) error {
return downgradePgSQLDatabaseFromV20(p.dbHandle)
case 21:
return downgradePgSQLDatabaseFromV21(p.dbHandle)
case 22:
return downgradePgSQLDatabaseFromV22(p.dbHandle)
default:
return fmt.Errorf("database version not handled: %v", dbVersion.Version)
return fmt.Errorf("database schema version not handled: %v", dbVersion.Version)
}
}
@ -697,7 +715,14 @@ func updatePgSQLDatabaseFromV19(dbHandle *sql.DB) error {
}
func updatePgSQLDatabaseFromV20(dbHandle *sql.DB) error {
return updatePgSQLDatabaseFrom20To21(dbHandle)
if err := updatePgSQLDatabaseFrom20To21(dbHandle); err != nil {
return err
}
return updatePgSQLDatabaseFromV21(dbHandle)
}
func updatePgSQLDatabaseFromV21(dbHandle *sql.DB) error {
return updatePgSQLDatabaseFrom21To22(dbHandle)
}
func downgradePgSQLDatabaseFromV20(dbHandle *sql.DB) error {
@ -711,9 +736,16 @@ func downgradePgSQLDatabaseFromV21(dbHandle *sql.DB) error {
return downgradePgSQLDatabaseFromV20(dbHandle)
}
func downgradePgSQLDatabaseFromV22(dbHandle *sql.DB) error {
if err := downgradePgSQLDatabaseFrom22To21(dbHandle); err != nil {
return err
}
return downgradePgSQLDatabaseFromV21(dbHandle)
}
func updatePgSQLDatabaseFrom19To20(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database version: 19 -> 20")
providerLog(logger.LevelInfo, "updating database version: 19 -> 20")
logger.InfoToConsole("updating database schema version: 19 -> 20")
providerLog(logger.LevelInfo, "updating database schema version: 19 -> 20")
sql := strings.ReplaceAll(pgsqlV20SQL, "{{events_actions}}", sqlTableEventsActions)
sql = strings.ReplaceAll(sql, "{{events_rules}}", sqlTableEventsRules)
sql = strings.ReplaceAll(sql, "{{rules_actions_mapping}}", sqlTableRulesActionsMapping)
@ -724,15 +756,25 @@ func updatePgSQLDatabaseFrom19To20(dbHandle *sql.DB) error {
}
func updatePgSQLDatabaseFrom20To21(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database version: 20 -> 21")
providerLog(logger.LevelInfo, "updating database version: 20 -> 21")
logger.InfoToConsole("updating database schema version: 20 -> 21")
providerLog(logger.LevelInfo, "updating database schema version: 20 -> 21")
sql := strings.ReplaceAll(pgsqlV21SQL, "{{users}}", sqlTableUsers)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 21, true)
}
func updatePgSQLDatabaseFrom21To22(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database schema version: 21 -> 22")
providerLog(logger.LevelInfo, "updating database schema version: 21 -> 22")
sql := strings.ReplaceAll(pgsqlV22SQL, "{{admins_groups_mapping}}", sqlTableAdminsGroupsMapping)
sql = strings.ReplaceAll(sql, "{{admins}}", sqlTableAdmins)
sql = strings.ReplaceAll(sql, "{{groups}}", sqlTableGroups)
sql = strings.ReplaceAll(sql, "{{prefix}}", config.SQLTablesPrefix)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 22, true)
}
func downgradePgSQLDatabaseFrom20To19(dbHandle *sql.DB) error {
logger.InfoToConsole("downgrading database version: 20 -> 19")
providerLog(logger.LevelInfo, "downgrading database version: 20 -> 19")
logger.InfoToConsole("downgrading database schema version: 20 -> 19")
providerLog(logger.LevelInfo, "downgrading database schema version: 20 -> 19")
sql := strings.ReplaceAll(pgsqlV20DownSQL, "{{events_actions}}", sqlTableEventsActions)
sql = strings.ReplaceAll(sql, "{{events_rules}}", sqlTableEventsRules)
sql = strings.ReplaceAll(sql, "{{rules_actions_mapping}}", sqlTableRulesActionsMapping)
@ -742,8 +784,16 @@ func downgradePgSQLDatabaseFrom20To19(dbHandle *sql.DB) error {
}
func downgradePgSQLDatabaseFrom21To20(dbHandle *sql.DB) error {
logger.InfoToConsole("downgrading database version: 21 -> 20")
providerLog(logger.LevelInfo, "downgrading database version: 21 -> 20")
logger.InfoToConsole("downgrading database schema version: 21 -> 20")
providerLog(logger.LevelInfo, "downgrading database schema version: 21 -> 20")
sql := strings.ReplaceAll(pgsqlV21DownSQL, "{{users}}", sqlTableUsers)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 20, false)
}
func downgradePgSQLDatabaseFrom22To21(dbHandle *sql.DB) error {
logger.InfoToConsole("downgrading database schema version: 22 -> 21")
providerLog(logger.LevelInfo, "downgrading database schema version: 22 -> 21")
sql := strings.ReplaceAll(pgsqlV22DownSQL, "{{admins_groups_mapping}}", sqlTableAdminsGroupsMapping)
sql = strings.ReplaceAll(sql, "{{prefix}}", config.SQLTablesPrefix)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 21, false)
}

View file

@ -34,7 +34,7 @@ import (
)
const (
sqlDatabaseVersion = 21
sqlDatabaseVersion = 22
defaultSQLQueryTimeout = 10 * time.Second
longSQLQueryTimeout = 60 * time.Second
)
@ -65,6 +65,7 @@ func sqlReplaceAll(sql string) string {
sql = strings.ReplaceAll(sql, "{{groups}}", sqlTableGroups)
sql = strings.ReplaceAll(sql, "{{users_folders_mapping}}", sqlTableUsersFoldersMapping)
sql = strings.ReplaceAll(sql, "{{users_groups_mapping}}", sqlTableUsersGroupsMapping)
sql = strings.ReplaceAll(sql, "{{admins_groups_mapping}}", sqlTableAdminsGroupsMapping)
sql = strings.ReplaceAll(sql, "{{groups_folders_mapping}}", sqlTableGroupsFoldersMapping)
sql = strings.ReplaceAll(sql, "{{api_keys}}", sqlTableAPIKeys)
sql = strings.ReplaceAll(sql, "{{shares}}", sqlTableShares)
@ -392,7 +393,11 @@ func sqlCommonGetAdminByUsername(username string, dbHandle sqlQuerier) (Admin, e
q := getAdminByUsernameQuery()
row := dbHandle.QueryRowContext(ctx, q, username)
return getAdminFromDbRow(row)
admin, err := getAdminFromDbRow(row)
if err != nil {
return admin, err
}
return getAdminWithGroups(ctx, admin, dbHandle)
}
func sqlCommonValidateAdminAndPass(username, password, ip string, dbHandle *sql.DB) (Admin, error) {
@ -411,10 +416,6 @@ func sqlCommonAddAdmin(admin *Admin, dbHandle *sql.DB) error {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
q := getAddAdminQuery()
perms, err := json.Marshal(admin.Permissions)
if err != nil {
return err
@ -425,10 +426,19 @@ func sqlCommonAddAdmin(admin *Admin, dbHandle *sql.DB) error {
return err
}
_, err = dbHandle.ExecContext(ctx, q, admin.Username, admin.Password, admin.Status, admin.Email, string(perms),
string(filters), admin.AdditionalInfo, admin.Description, util.GetTimeAsMsSinceEpoch(time.Now()),
util.GetTimeAsMsSinceEpoch(time.Now()))
return err
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
return sqlCommonExecuteTx(ctx, dbHandle, func(tx *sql.Tx) error {
q := getAddAdminQuery()
_, err = tx.ExecContext(ctx, q, admin.Username, admin.Password, admin.Status, admin.Email, string(perms),
string(filters), admin.AdditionalInfo, admin.Description, util.GetTimeAsMsSinceEpoch(time.Now()),
util.GetTimeAsMsSinceEpoch(time.Now()))
if err != nil {
return err
}
return generateAdminGroupMapping(ctx, admin, tx)
})
}
func sqlCommonUpdateAdmin(admin *Admin, dbHandle *sql.DB) error {
@ -437,10 +447,6 @@ func sqlCommonUpdateAdmin(admin *Admin, dbHandle *sql.DB) error {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
q := getUpdateAdminQuery()
perms, err := json.Marshal(admin.Permissions)
if err != nil {
return err
@ -451,9 +457,18 @@ func sqlCommonUpdateAdmin(admin *Admin, dbHandle *sql.DB) error {
return err
}
_, err = dbHandle.ExecContext(ctx, q, admin.Password, admin.Status, admin.Email, string(perms), string(filters),
admin.AdditionalInfo, admin.Description, util.GetTimeAsMsSinceEpoch(time.Now()), admin.Username)
return err
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
return sqlCommonExecuteTx(ctx, dbHandle, func(tx *sql.Tx) error {
q := getUpdateAdminQuery()
_, err = tx.ExecContext(ctx, q, admin.Password, admin.Status, admin.Email, string(perms), string(filters),
admin.AdditionalInfo, admin.Description, util.GetTimeAsMsSinceEpoch(time.Now()), admin.Username)
if err != nil {
return err
}
return generateAdminGroupMapping(ctx, admin, tx)
})
}
func sqlCommonDeleteAdmin(admin Admin, dbHandle *sql.DB) error {
@ -488,8 +503,11 @@ func sqlCommonGetAdmins(limit, offset int, order string, dbHandle sqlQuerier) ([
a.HideConfidentialData()
admins = append(admins, a)
}
return admins, rows.Err()
err = rows.Err()
if err != nil {
return admins, err
}
return getAdminsWithGroups(ctx, admins, dbHandle)
}
func sqlCommonDumpAdmins(dbHandle sqlQuerier) ([]Admin, error) {
@ -511,8 +529,11 @@ func sqlCommonDumpAdmins(dbHandle sqlQuerier) ([]Admin, error) {
}
admins = append(admins, a)
}
return admins, rows.Err()
err = rows.Err()
if err != nil {
return admins, err
}
return getAdminsWithGroups(ctx, admins, dbHandle)
}
func sqlCommonGetGroupByName(name string, dbHandle sqlQuerier) (Group, error) {
@ -530,7 +551,11 @@ func sqlCommonGetGroupByName(name string, dbHandle sqlQuerier) (Group, error) {
if err != nil {
return group, err
}
return getGroupWithUsers(ctx, group, dbHandle)
group, err = getGroupWithUsers(ctx, group, dbHandle)
if err != nil {
return group, err
}
return getGroupWithAdmins(ctx, group, dbHandle)
}
func sqlCommonDumpGroups(dbHandle sqlQuerier) ([]Group, error) {
@ -664,6 +689,10 @@ func sqlCommonGetGroups(limit int, offset int, order string, minimal bool, dbHan
if err != nil {
return groups, err
}
groups, err = getGroupsWithAdmins(ctx, groups, dbHandle)
if err != nil {
return groups, err
}
for idx := range groups {
groups[idx].PrepareForRendering()
}
@ -2040,6 +2069,12 @@ func sqlCommonAddUserFolderMapping(ctx context.Context, user *User, folder *vfs.
return err
}
func sqlCommonClearAdminGroupMapping(ctx context.Context, admin *Admin, dbHandle sqlQuerier) error {
q := getClearAdminGroupMappingQuery()
_, err := dbHandle.ExecContext(ctx, q, admin.Username)
return err
}
func sqlCommonAddGroupFolderMapping(ctx context.Context, group *Group, folder *vfs.VirtualFolder, dbHandle sqlQuerier) error {
q := getAddGroupFolderMappingQuery()
_, err := dbHandle.ExecContext(ctx, q, folder.VirtualPath, folder.QuotaSize, folder.QuotaFiles, folder.Name, group.Name)
@ -2052,6 +2087,18 @@ func sqlCommonAddUserGroupMapping(ctx context.Context, username, groupName strin
return err
}
func sqlCommonAddAdminGroupMapping(ctx context.Context, username, groupName string, mappingOptions AdminGroupMappingOptions,
dbHandle sqlQuerier,
) error {
options, err := json.Marshal(mappingOptions)
if err != nil {
return err
}
q := getAddAdminGroupMappingQuery()
_, err = dbHandle.ExecContext(ctx, q, username, groupName, string(options))
return err
}
func generateGroupVirtualFoldersMapping(ctx context.Context, group *Group, dbHandle sqlQuerier) error {
err := sqlCommonClearGroupFolderMapping(ctx, group, dbHandle)
if err != nil {
@ -2104,6 +2151,20 @@ func generateUserGroupMapping(ctx context.Context, user *User, dbHandle sqlQueri
return err
}
func generateAdminGroupMapping(ctx context.Context, admin *Admin, dbHandle sqlQuerier) error {
err := sqlCommonClearAdminGroupMapping(ctx, admin, dbHandle)
if err != nil {
return err
}
for _, group := range admin.Groups {
err = sqlCommonAddAdminGroupMapping(ctx, admin.Username, group.Name, group.Options, dbHandle)
if err != nil {
return err
}
}
return err
}
func getDefenderHostsWithScores(ctx context.Context, hosts []DefenderEntry, from int64, idForScores []int64,
dbHandle sqlQuerier) (
[]DefenderEntry,
@ -2152,6 +2213,57 @@ func getDefenderHostsWithScores(ctx context.Context, hosts []DefenderEntry, from
return result, nil
}
func getAdminWithGroups(ctx context.Context, admin Admin, dbHandle sqlQuerier) (Admin, error) {
admins, err := getAdminsWithGroups(ctx, []Admin{admin}, dbHandle)
if err != nil {
return admin, err
}
if len(admins) == 0 {
return admin, errSQLGroupsAssociation
}
return admins[0], err
}
func getAdminsWithGroups(ctx context.Context, admins []Admin, dbHandle sqlQuerier) ([]Admin, error) {
if len(admins) == 0 {
return admins, nil
}
adminsGroups := make(map[int64][]AdminGroupMapping)
q := getRelatedGroupsForAdminsQuery(admins)
rows, err := dbHandle.QueryContext(ctx, q)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var group AdminGroupMapping
var adminID int64
var options []byte
err = rows.Scan(&group.Name, &options, &adminID)
if err != nil {
return admins, err
}
err = json.Unmarshal(options, &group.Options)
if err != nil {
return admins, err
}
adminsGroups[adminID] = append(adminsGroups[adminID], group)
}
err = rows.Err()
if err != nil {
return admins, err
}
if len(adminsGroups) == 0 {
return admins, err
}
for idx := range admins {
ref := &admins[idx]
ref.Groups = adminsGroups[ref.ID]
}
return admins, err
}
func getUserWithVirtualFolders(ctx context.Context, user User, dbHandle sqlQuerier) (User, error) {
users, err := getUsersWithVirtualFolders(ctx, []User{user}, dbHandle)
if err != nil {
@ -2271,6 +2383,17 @@ func getGroupWithUsers(ctx context.Context, group Group, dbHandle sqlQuerier) (G
return groups[0], err
}
func getGroupWithAdmins(ctx context.Context, group Group, dbHandle sqlQuerier) (Group, error) {
groups, err := getGroupsWithAdmins(ctx, []Group{group}, dbHandle)
if err != nil {
return group, err
}
if len(groups) == 0 {
return group, errSQLUsersAssociation
}
return groups[0], err
}
func getGroupWithVirtualFolders(ctx context.Context, group Group, dbHandle sqlQuerier) (Group, error) {
groups, err := getGroupsWithVirtualFolders(ctx, []Group{group}, dbHandle)
if err != nil {
@ -2368,6 +2491,40 @@ func getGroupsWithUsers(ctx context.Context, groups []Group, dbHandle sqlQuerier
return groups, err
}
func getGroupsWithAdmins(ctx context.Context, groups []Group, dbHandle sqlQuerier) ([]Group, error) {
if len(groups) == 0 {
return groups, nil
}
q := getRelatedAdminsForGroupsQuery(groups)
rows, err := dbHandle.QueryContext(ctx, q)
if err != nil {
return nil, err
}
defer rows.Close()
groupsAdmins := make(map[int64][]string)
for rows.Next() {
var groupID int64
var username string
err = rows.Scan(&groupID, &username)
if err != nil {
return groups, err
}
groupsAdmins[groupID] = append(groupsAdmins[groupID], username)
}
err = rows.Err()
if err != nil {
return groups, err
}
if len(groupsAdmins) > 0 {
for idx := range groups {
ref := &groups[idx]
ref.Admins = groupsAdmins[ref.ID]
}
}
return groups, nil
}
func getVirtualFoldersWithGroups(folders []vfs.BaseVirtualFolder, dbHandle sqlQuerier) ([]vfs.BaseVirtualFolder, error) {
if len(folders) == 0 {
return folders, nil

View file

@ -41,6 +41,7 @@ const (
DROP TABLE IF EXISTS "{{folders_mapping}}";
DROP TABLE IF EXISTS "{{users_folders_mapping}}";
DROP TABLE IF EXISTS "{{users_groups_mapping}}";
DROP TABLE IF EXISTS "{{admins_groups_mapping}}";
DROP TABLE IF EXISTS "{{groups_folders_mapping}}";
DROP TABLE IF EXISTS "{{admins}}";
DROP TABLE IF EXISTS "{{folders}}";
@ -167,6 +168,15 @@ ALTER TABLE "{{users}}" DROP COLUMN "deleted_at";
ALTER TABLE "{{users}}" ADD COLUMN "first_upload" bigint DEFAULT 0 NOT NULL;`
sqliteV21DownSQL = `ALTER TABLE "{{users}}" DROP COLUMN "first_upload";
ALTER TABLE "{{users}}" DROP COLUMN "first_download";
`
sqliteV22SQL = `CREATE TABLE "{{admins_groups_mapping}}" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"admin_id" integer NOT NULL REFERENCES "{{admins}}" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
"group_id" integer NOT NULL REFERENCES "{{groups}}" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
"options" text NOT NULL, CONSTRAINT "{{prefix}}unique_admin_group_mapping" UNIQUE ("admin_id", "group_id"));
CREATE INDEX "{{prefix}}admins_groups_mapping_admin_id_idx" ON "{{admins_groups_mapping}}" ("admin_id");
CREATE INDEX "{{prefix}}admins_groups_mapping_group_id_idx" ON "{{admins_groups_mapping}}" ("group_id");
`
sqliteV22DownSQL = `DROP TABLE "{{admins_groups_mapping}}";
`
)
@ -612,7 +622,7 @@ func (p *SQLiteProvider) migrateDatabase() error { //nolint:dupl
providerLog(logger.LevelDebug, "sql database is up to date, current version: %v", version)
return ErrNoInitRequired
case version < 19:
err = fmt.Errorf("database version %v is too old, please see the upgrading docs", version)
err = fmt.Errorf("database schema version %v is too old, please see the upgrading docs", version)
providerLog(logger.LevelError, "%v", err)
logger.ErrorToConsole("%v", err)
return err
@ -620,15 +630,17 @@ func (p *SQLiteProvider) migrateDatabase() error { //nolint:dupl
return updateSQLiteDatabaseFromV19(p.dbHandle)
case version == 20:
return updateSQLiteDatabaseFromV20(p.dbHandle)
case version == 21:
return updateSQLiteDatabaseFromV21(p.dbHandle)
default:
if version > sqlDatabaseVersion {
providerLog(logger.LevelError, "database version %v is newer than the supported one: %v", version,
providerLog(logger.LevelError, "database schema version %v is newer than the supported one: %v", version,
sqlDatabaseVersion)
logger.WarnToConsole("database version %v is newer than the supported one: %v", version,
logger.WarnToConsole("database schema version %v is newer than the supported one: %v", version,
sqlDatabaseVersion)
return nil
}
return fmt.Errorf("database version not handled: %v", version)
return fmt.Errorf("database schema version not handled: %v", version)
}
}
@ -646,8 +658,10 @@ func (p *SQLiteProvider) revertDatabase(targetVersion int) error {
return downgradeSQLiteDatabaseFromV20(p.dbHandle)
case 21:
return downgradeSQLiteDatabaseFromV21(p.dbHandle)
case 22:
return downgradeSQLiteDatabaseFromV22(p.dbHandle)
default:
return fmt.Errorf("database version not handled: %v", dbVersion.Version)
return fmt.Errorf("database schema version not handled: %v", dbVersion.Version)
}
}
@ -664,7 +678,14 @@ func updateSQLiteDatabaseFromV19(dbHandle *sql.DB) error {
}
func updateSQLiteDatabaseFromV20(dbHandle *sql.DB) error {
return updateSQLiteDatabaseFrom20To21(dbHandle)
if err := updateSQLiteDatabaseFrom20To21(dbHandle); err != nil {
return err
}
return updateSQLiteDatabaseFromV21(dbHandle)
}
func updateSQLiteDatabaseFromV21(dbHandle *sql.DB) error {
return updateSQLiteDatabaseFrom21To22(dbHandle)
}
func downgradeSQLiteDatabaseFromV20(dbHandle *sql.DB) error {
@ -678,9 +699,16 @@ func downgradeSQLiteDatabaseFromV21(dbHandle *sql.DB) error {
return downgradeSQLiteDatabaseFromV20(dbHandle)
}
func downgradeSQLiteDatabaseFromV22(dbHandle *sql.DB) error {
if err := downgradeSQLiteDatabaseFrom22To21(dbHandle); err != nil {
return err
}
return downgradeSQLiteDatabaseFromV21(dbHandle)
}
func updateSQLiteDatabaseFrom19To20(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database version: 19 -> 20")
providerLog(logger.LevelInfo, "updating database version: 19 -> 20")
logger.InfoToConsole("updating database schema version: 19 -> 20")
providerLog(logger.LevelInfo, "updating database schema version: 19 -> 20")
sql := strings.ReplaceAll(sqliteV20SQL, "{{events_actions}}", sqlTableEventsActions)
sql = strings.ReplaceAll(sql, "{{events_rules}}", sqlTableEventsRules)
sql = strings.ReplaceAll(sql, "{{rules_actions_mapping}}", sqlTableRulesActionsMapping)
@ -691,15 +719,25 @@ func updateSQLiteDatabaseFrom19To20(dbHandle *sql.DB) error {
}
func updateSQLiteDatabaseFrom20To21(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database version: 20 -> 21")
providerLog(logger.LevelInfo, "updating database version: 20 -> 21")
logger.InfoToConsole("updating database schema version: 20 -> 21")
providerLog(logger.LevelInfo, "updating database schema version: 20 -> 21")
sql := strings.ReplaceAll(sqliteV21SQL, "{{users}}", sqlTableUsers)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 21, true)
}
func updateSQLiteDatabaseFrom21To22(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database schema version: 21 -> 22")
providerLog(logger.LevelInfo, "updating database schema version: 21 -> 22")
sql := strings.ReplaceAll(sqliteV22SQL, "{{admins_groups_mapping}}", sqlTableAdminsGroupsMapping)
sql = strings.ReplaceAll(sql, "{{admins}}", sqlTableAdmins)
sql = strings.ReplaceAll(sql, "{{groups}}", sqlTableGroups)
sql = strings.ReplaceAll(sql, "{{prefix}}", config.SQLTablesPrefix)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 22, true)
}
func downgradeSQLiteDatabaseFrom20To19(dbHandle *sql.DB) error {
logger.InfoToConsole("downgrading database version: 20 -> 19")
providerLog(logger.LevelInfo, "downgrading database version: 20 -> 19")
logger.InfoToConsole("downgrading database schema version: 20 -> 19")
providerLog(logger.LevelInfo, "downgrading database schema version: 20 -> 19")
sql := strings.ReplaceAll(sqliteV20DownSQL, "{{events_actions}}", sqlTableEventsActions)
sql = strings.ReplaceAll(sql, "{{events_rules}}", sqlTableEventsRules)
sql = strings.ReplaceAll(sql, "{{rules_actions_mapping}}", sqlTableRulesActionsMapping)
@ -710,12 +748,19 @@ func downgradeSQLiteDatabaseFrom20To19(dbHandle *sql.DB) error {
}
func downgradeSQLiteDatabaseFrom21To20(dbHandle *sql.DB) error {
logger.InfoToConsole("downgrading database version: 21 -> 20")
providerLog(logger.LevelInfo, "downgrading database version: 21 -> 20")
logger.InfoToConsole("downgrading database schema version: 21 -> 20")
providerLog(logger.LevelInfo, "downgrading database schema version: 21 -> 20")
sql := strings.ReplaceAll(sqliteV21DownSQL, "{{users}}", sqlTableUsers)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 20, false)
}
func downgradeSQLiteDatabaseFrom22To21(dbHandle *sql.DB) error {
logger.InfoToConsole("downgrading database schema version: 22 -> 21")
providerLog(logger.LevelInfo, "downgrading database schema version: 22 -> 21")
sql := strings.ReplaceAll(sqliteV22DownSQL, "{{admins_groups_mapping}}", sqlTableAdminsGroupsMapping)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 21, false)
}
/*func setPragmaFK(dbHandle *sql.DB, value string) error {
ctx, cancel := context.WithTimeout(context.Background(), longSQLQueryTimeout)
defer cancel()

View file

@ -572,6 +572,18 @@ func getAddUserGroupMappingQuery() string {
sqlPlaceholders[1], sqlPlaceholders[2])
}
func getClearAdminGroupMappingQuery() string {
return fmt.Sprintf(`DELETE FROM %s WHERE admin_id = (SELECT id FROM %s WHERE username = %s)`, sqlTableAdminsGroupsMapping,
sqlTableAdmins, sqlPlaceholders[0])
}
func getAddAdminGroupMappingQuery() string {
return fmt.Sprintf(`INSERT INTO %s (admin_id,group_id,options) VALUES ((SELECT id FROM %s WHERE username = %s),
(SELECT id FROM %s WHERE name = %s),%s)`,
sqlTableAdminsGroupsMapping, sqlTableAdmins, sqlPlaceholders[0], getSQLQuotedName(sqlTableGroups),
sqlPlaceholders[1], sqlPlaceholders[2])
}
func getClearGroupFolderMappingQuery() string {
return fmt.Sprintf(`DELETE FROM %s WHERE group_id = (SELECT id FROM %s WHERE name = %s)`, sqlTableGroupsFoldersMapping,
getSQLQuotedName(sqlTableGroups), sqlPlaceholders[0])
@ -638,6 +650,23 @@ func getRelatedGroupsForUsersQuery(users []User) string {
ug.user_id IN %s ORDER BY ug.user_id`, getSQLQuotedName(sqlTableGroups), sqlTableUsersGroupsMapping, sb.String())
}
func getRelatedGroupsForAdminsQuery(admins []Admin) string {
var sb strings.Builder
for _, a := range admins {
if sb.Len() == 0 {
sb.WriteString("(")
} else {
sb.WriteString(",")
}
sb.WriteString(strconv.FormatInt(a.ID, 10))
}
if sb.Len() > 0 {
sb.WriteString(")")
}
return fmt.Sprintf(`SELECT g.name,ag.options,ag.admin_id FROM %s g INNER JOIN %s ag ON g.id = ag.group_id WHERE
ag.admin_id IN %s ORDER BY ag.admin_id`, getSQLQuotedName(sqlTableGroups), sqlTableAdminsGroupsMapping, sb.String())
}
func getRelatedFoldersForUsersQuery(users []User) string {
var sb strings.Builder
for _, u := range users {
@ -708,6 +737,23 @@ func getRelatedUsersForGroupsQuery(groups []Group) string {
WHERE um.group_id IN %s ORDER BY um.group_id`, sqlTableUsersGroupsMapping, sqlTableUsers, sb.String())
}
func getRelatedAdminsForGroupsQuery(groups []Group) string {
var sb strings.Builder
for _, g := range groups {
if sb.Len() == 0 {
sb.WriteString("(")
} else {
sb.WriteString(",")
}
sb.WriteString(strconv.FormatInt(g.ID, 10))
}
if sb.Len() > 0 {
sb.WriteString(")")
}
return fmt.Sprintf(`SELECT am.group_id,a.username FROM %s am INNER JOIN %s a ON am.admin_id = a.id
WHERE am.group_id IN %s ORDER BY am.group_id`, sqlTableAdminsGroupsMapping, sqlTableAdmins, sb.String())
}
func getRelatedFoldersForGroupsQuery(groups []Group) string {
var sb strings.Builder
for _, g := range groups {

View file

@ -1568,8 +1568,27 @@ func (u *User) HasSecondaryGroup(name string) bool {
return false
}
// HasMembershipGroup returns true if the user has the specified membership group
func (u *User) HasMembershipGroup(name string) bool {
for _, g := range u.Groups {
if g.Name == name {
return g.Type == sdk.GroupTypeMembership
}
}
return false
}
func (u *User) hasSettingsFromGroups() bool {
for _, g := range u.Groups {
if g.Type != sdk.GroupTypeMembership {
return true
}
}
return false
}
func (u *User) applyGroupSettings(groupsMapping map[string]Group) {
if len(u.Groups) == 0 {
if !u.hasSettingsFromGroups() {
return
}
if u.groupSettingsApplied {
@ -1600,7 +1619,7 @@ func (u *User) applyGroupSettings(groupsMapping map[string]Group) {
// LoadAndApplyGroupSettings update the user by loading and applying the group settings
func (u *User) LoadAndApplyGroupSettings() error {
if len(u.Groups) == 0 {
if !u.hasSettingsFromGroups() {
return nil
}
if u.groupSettingsApplied {
@ -1612,7 +1631,9 @@ func (u *User) LoadAndApplyGroupSettings() error {
if g.Type == sdk.GroupTypePrimary {
primaryGroupName = g.Name
}
names = append(names, g.Name)
if g.Type != sdk.GroupTypeMembership {
names = append(names, g.Name)
}
}
groups, err := provider.getGroupsWithNames(names)
if err != nil {

View file

@ -2742,6 +2742,102 @@ func TestBasicAdminHandling(t *testing.T) {
assert.NoError(t, err)
}
func TestAdminGroups(t *testing.T) {
group1 := getTestGroup()
group1.Name += "_1"
group1, _, err := httpdtest.AddGroup(group1, http.StatusCreated)
assert.NoError(t, err)
group2 := getTestGroup()
group2.Name += "_2"
group2, _, err = httpdtest.AddGroup(group2, http.StatusCreated)
assert.NoError(t, err)
group3 := getTestGroup()
group3.Name += "_3"
group3, _, err = httpdtest.AddGroup(group3, http.StatusCreated)
assert.NoError(t, err)
a := getTestAdmin()
a.Username = altAdminUsername
a.Groups = []dataprovider.AdminGroupMapping{
{
Name: group1.Name,
Options: dataprovider.AdminGroupMappingOptions{
AddToUsersAs: dataprovider.GroupAddToUsersAsPrimary,
},
},
{
Name: group2.Name,
Options: dataprovider.AdminGroupMappingOptions{
AddToUsersAs: dataprovider.GroupAddToUsersAsSecondary,
},
},
{
Name: group3.Name,
Options: dataprovider.AdminGroupMappingOptions{
AddToUsersAs: dataprovider.GroupAddToUsersAsMembership,
},
},
}
admin, _, err := httpdtest.AddAdmin(a, http.StatusCreated)
assert.NoError(t, err)
assert.Len(t, admin.Groups, 3)
groups, _, err := httpdtest.GetGroups(0, 0, http.StatusOK)
assert.NoError(t, err)
assert.Len(t, groups, 3)
for _, g := range groups {
if assert.Len(t, g.Admins, 1) {
assert.Equal(t, admin.Username, g.Admins[0])
}
}
admin, _, err = httpdtest.UpdateAdmin(a, http.StatusOK)
assert.NoError(t, err)
_, err = httpdtest.RemoveGroup(group1, http.StatusOK)
assert.NoError(t, err)
_, err = httpdtest.RemoveGroup(group2, http.StatusOK)
assert.NoError(t, err)
admin, _, err = httpdtest.GetAdminByUsername(admin.Username, http.StatusOK)
assert.NoError(t, err)
assert.Len(t, admin.Groups, 1)
// try to add a missing group
admin.Groups = []dataprovider.AdminGroupMapping{
{
Name: group1.Name,
Options: dataprovider.AdminGroupMappingOptions{
AddToUsersAs: dataprovider.GroupAddToUsersAsPrimary,
},
},
{
Name: group2.Name,
Options: dataprovider.AdminGroupMappingOptions{
AddToUsersAs: dataprovider.GroupAddToUsersAsSecondary,
},
},
}
group3, _, err = httpdtest.GetGroupByName(group3.Name, http.StatusOK)
assert.NoError(t, err)
assert.Len(t, group3.Admins, 1)
_, _, err = httpdtest.UpdateAdmin(admin, http.StatusOK)
assert.Error(t, err)
group3, _, err = httpdtest.GetGroupByName(group3.Name, http.StatusOK)
assert.NoError(t, err)
assert.Len(t, group3.Admins, 1)
_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)
assert.NoError(t, err)
group3, _, err = httpdtest.GetGroupByName(group3.Name, http.StatusOK)
assert.NoError(t, err)
assert.Len(t, group3.Admins, 0)
_, err = httpdtest.RemoveGroup(group3, http.StatusOK)
assert.NoError(t, err)
}
func TestChangeAdminPassword(t *testing.T) {
_, err := httpdtest.ChangeAdminPassword("wrong", defaultTokenAuthPass, http.StatusBadRequest)
assert.NoError(t, err)
@ -6001,6 +6097,11 @@ func TestProviderErrors(t *testing.T) {
setJWTCookieForReq(req, testServerToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusInternalServerError, rr)
req, err = http.NewRequest(http.MethodGet, webAdminPath, nil)
assert.NoError(t, err)
setJWTCookieForReq(req, testServerToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusInternalServerError, rr)
req, err = http.NewRequest(http.MethodGet, webGroupsPath+"?qlimit=a", nil)
assert.NoError(t, err)
setJWTCookieForReq(req, testServerToken)
@ -6529,6 +6630,11 @@ func TestLoaddata(t *testing.T) {
admin := getTestAdmin()
admin.ID = 1
admin.Username = "test_admin_restore"
admin.Groups = []dataprovider.AdminGroupMapping{
{
Name: group.Name,
},
}
apiKey := dataprovider.APIKey{
Name: util.GenerateUniqueID(),
Scope: dataprovider.APIKeyScopeAdmin,
@ -6638,8 +6744,7 @@ func TestLoaddata(t *testing.T) {
admin, _, err = httpdtest.GetAdminByUsername(admin.Username, http.StatusOK)
assert.NoError(t, err)
_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)
assert.NoError(t, err)
assert.Len(t, admin.Groups, 1)
apiKey, _, err = httpdtest.GetAPIKeyByID(apiKey.KeyID, http.StatusOK)
assert.NoError(t, err)
@ -6676,6 +6781,16 @@ func TestLoaddata(t *testing.T) {
}
}
assert.True(t, found)
found = false
if assert.GreaterOrEqual(t, len(dumpedData.Admins), 1) {
for _, a := range dumpedData.Admins {
if a.Username == admin.Username {
found = true
assert.Equal(t, len(admin.Groups), len(a.Groups))
}
}
}
assert.True(t, found)
if assert.Len(t, dumpedData.Groups, 1) {
assert.Equal(t, len(group.VirtualFolders), len(dumpedData.Groups[0].VirtualFolders))
}
@ -6714,6 +6829,8 @@ func TestLoaddata(t *testing.T) {
assert.NoError(t, err)
_, err = httpdtest.RemoveEventAction(action, http.StatusOK)
assert.NoError(t, err)
_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)
assert.NoError(t, err)
err = os.Remove(backupFilePath)
assert.NoError(t, err)
@ -16315,6 +16432,86 @@ func TestWebAdminBasicMock(t *testing.T) {
assert.Contains(t, rr.Body.String(), "Invalid token")
}
func TestWebAdminGroupsMock(t *testing.T) {
group1 := getTestGroup()
group1.Name += "_1"
group1, _, err := httpdtest.AddGroup(group1, http.StatusCreated)
assert.NoError(t, err)
group2 := getTestGroup()
group2.Name += "_2"
group2, _, err = httpdtest.AddGroup(group2, http.StatusCreated)
assert.NoError(t, err)
group3 := getTestGroup()
group3.Name += "_3"
group3, _, err = httpdtest.AddGroup(group3, http.StatusCreated)
assert.NoError(t, err)
token, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
assert.NoError(t, err)
admin := getTestAdmin()
admin.Username = altAdminUsername
admin.Password = altAdminPassword
csrfToken, err := getCSRFToken(httpBaseURL + webLoginPath)
assert.NoError(t, err)
form := make(url.Values)
form.Set(csrfFormToken, csrfToken)
form.Set("username", admin.Username)
form.Set("password", "")
form.Set("status", "1")
form.Set("permissions", "*")
form.Set("description", admin.Description)
form.Set("password", admin.Password)
form.Set("group1", group1.Name)
form.Set("add_as_group_type1", "1")
form.Set("group2", group2.Name)
form.Set("add_as_group_type2", "2")
form.Set("group3", group3.Name)
req, err := http.NewRequest(http.MethodPost, webAdminPath, bytes.NewBuffer([]byte(form.Encode())))
assert.NoError(t, err)
req.RemoteAddr = defaultRemoteAddr
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
setJWTCookieForReq(req, token)
rr := executeRequest(req)
checkResponseCode(t, http.StatusSeeOther, rr)
admin, _, err = httpdtest.GetAdminByUsername(admin.Username, http.StatusOK)
assert.NoError(t, err)
if assert.Len(t, admin.Groups, 3) {
for _, g := range admin.Groups {
switch g.Name {
case group1.Name:
assert.Equal(t, dataprovider.GroupAddToUsersAsPrimary, g.Options.AddToUsersAs)
case group2.Name:
assert.Equal(t, dataprovider.GroupAddToUsersAsSecondary, g.Options.AddToUsersAs)
case group3.Name:
assert.Equal(t, dataprovider.GroupAddToUsersAsMembership, g.Options.AddToUsersAs)
default:
t.Errorf("unexpected group %q", g.Name)
}
}
}
adminToken, err := getJWTWebTokenFromTestServer(altAdminUsername, altAdminPassword)
assert.NoError(t, err)
req, err = http.NewRequest(http.MethodGet, webUserPath, nil)
assert.NoError(t, err)
setJWTCookieForReq(req, adminToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)
assert.NoError(t, err)
req, err = http.NewRequest(http.MethodGet, webUserPath, nil)
assert.NoError(t, err)
setJWTCookieForReq(req, adminToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusInternalServerError, rr)
_, err = httpdtest.RemoveGroup(group1, http.StatusOK)
assert.NoError(t, err)
_, err = httpdtest.RemoveGroup(group2, http.StatusOK)
assert.NoError(t, err)
_, err = httpdtest.RemoveGroup(group3, http.StatusOK)
assert.NoError(t, err)
}
func TestWebAdminPermissions(t *testing.T) {
admin := getTestAdmin()
admin.Username = altAdminUsername
@ -16554,6 +16751,10 @@ func TestWebUserAddMock(t *testing.T) {
group2.Name += "_2"
group2, _, err = httpdtest.AddGroup(group2, http.StatusCreated)
assert.NoError(t, err)
group3 := getTestGroup()
group3.Name += "_3"
group3, _, err = httpdtest.AddGroup(group3, http.StatusCreated)
assert.NoError(t, err)
user := getTestUser()
user.UploadBandwidth = 32
user.DownloadBandwidth = 64
@ -16584,6 +16785,7 @@ func TestWebUserAddMock(t *testing.T) {
form.Set("password", user.Password)
form.Set("primary_group", group1.Name)
form.Set("secondary_groups", group2.Name)
form.Set("membership_groups", group3.Name)
form.Set("status", strconv.Itoa(user.Status))
form.Set("expiration_date", "")
form.Set("permissions", "*")
@ -16988,7 +17190,7 @@ func TestWebUserAddMock(t *testing.T) {
}
}
}
assert.Len(t, newUser.Groups, 2)
assert.Len(t, newUser.Groups, 3)
assert.Equal(t, sdk.TLSUsernameNone, newUser.Filters.TLSUsername)
req, _ = http.NewRequest(http.MethodDelete, path.Join(userPath, newUser.Username), nil)
setBearerForReq(req, apiToken)
@ -17002,6 +17204,8 @@ func TestWebUserAddMock(t *testing.T) {
assert.NoError(t, err)
_, err = httpdtest.RemoveGroup(group2, http.StatusOK)
assert.NoError(t, err)
_, err = httpdtest.RemoveGroup(group3, http.StatusOK)
assert.NoError(t, err)
}
func TestWebUserUpdateMock(t *testing.T) {

View file

@ -231,9 +231,10 @@ type userPage struct {
type adminPage struct {
basePage
Admin *dataprovider.Admin
Error string
IsAdd bool
Admin *dataprovider.Admin
Groups []dataprovider.Group
Error string
IsAdd bool
}
type profilePage struct {
@ -767,6 +768,10 @@ func (s *httpdServer) renderAdminSetupPage(w http.ResponseWriter, r *http.Reques
func (s *httpdServer) renderAddUpdateAdminPage(w http.ResponseWriter, r *http.Request, admin *dataprovider.Admin,
error string, isAdd bool) {
groups, err := s.getWebGroups(w, r, defaultQueryLimit, true)
if err != nil {
return
}
currentURL := webAdminPath
title := "Add a new admin"
if !isAdd {
@ -776,6 +781,7 @@ func (s *httpdServer) renderAddUpdateAdminPage(w http.ResponseWriter, r *http.Re
data := adminPage{
basePage: s.getBasePageData(title, currentURL, r),
Admin: admin,
Groups: groups,
Error: error,
IsAdd: isAdd,
}
@ -816,8 +822,22 @@ func (s *httpdServer) renderUserPage(w http.ResponseWriter, r *http.Request, use
}
}
user.FsConfig.RedactedSecret = redactedSecret
basePage := s.getBasePageData(title, currentURL, r)
if (mode == userPageModeAdd || mode == userPageModeTemplate) && len(user.Groups) == 0 {
admin, err := dataprovider.AdminExists(basePage.LoggedAdmin.Username)
if err != nil {
s.renderInternalServerErrorPage(w, r, err)
return
}
for _, group := range admin.Groups {
user.Groups = append(user.Groups, sdk.GroupMapping{
Name: group.Name,
Type: group.Options.GetUserGroupType(),
})
}
}
data := userPage{
basePage: s.getBasePageData(title, currentURL, r),
basePage: basePage,
Mode: mode,
Error: error,
User: user,
@ -1265,7 +1285,13 @@ func getGroupsFromUserPostFields(r *http.Request) []sdk.GroupMapping {
Type: sdk.GroupTypeSecondary,
})
}
membershipGroups := r.Form["membership_groups"]
for _, name := range membershipGroups {
groups = append(groups, sdk.GroupMapping{
Name: name,
Type: sdk.GroupTypeMembership,
})
}
return groups
}
@ -1532,6 +1558,27 @@ func getAdminFromPostFields(r *http.Request) (dataprovider.Admin, error) {
admin.Filters.AllowAPIKeyAuth = r.Form.Get("allow_api_key_auth") != ""
admin.AdditionalInfo = r.Form.Get("additional_info")
admin.Description = r.Form.Get("description")
for k := range r.Form {
if strings.HasPrefix(k, "group") {
groupName := strings.TrimSpace(r.Form.Get(k))
if groupName != "" {
idx := strings.TrimPrefix(k, "group")
addAsGroupType := r.Form.Get(fmt.Sprintf("add_as_group_type%s", idx))
group := dataprovider.AdminGroupMapping{
Name: groupName,
}
switch addAsGroupType {
case "1":
group.Options.AddToUsersAs = dataprovider.GroupAddToUsersAsPrimary
case "2":
group.Options.AddToUsersAs = dataprovider.GroupAddToUsersAsSecondary
default:
group.Options.AddToUsersAs = dataprovider.GroupAddToUsersAsMembership
}
admin.Groups = append(admin.Groups, group)
}
}
}
return admin, nil
}

View file

@ -1636,7 +1636,7 @@ func checkAdmin(expected, actual *dataprovider.Admin) error {
}
}
return nil
return compareAdminGroups(expected, actual)
}
func compareAdminEqualFields(expected *dataprovider.Admin, actual *dataprovider.Admin) error {
@ -1716,6 +1716,27 @@ func compareUserPermissions(expected map[string][]string, actual map[string][]st
return nil
}
func compareAdminGroups(expected *dataprovider.Admin, actual *dataprovider.Admin) error {
if len(actual.Groups) != len(expected.Groups) {
return errors.New("groups len mismatch")
}
for _, g := range actual.Groups {
found := false
for _, g1 := range expected.Groups {
if g1.Name == g.Name {
found = true
if g1.Options.AddToUsersAs != g.Options.AddToUsersAs {
return fmt.Errorf("add to users as field mismatch for group %s", g.Name)
}
}
}
if !found {
return errors.New("groups mismatch")
}
}
return nil
}
func compareUserGroups(expected *dataprovider.User, actual *dataprovider.User) error {
if len(actual.Groups) != len(expected.Groups) {
return errors.New("groups len mismatch")

View file

@ -5285,6 +5285,11 @@ components:
additional_info:
type: string
description: Free form text field
groups:
type: array
items:
$ref: '#/components/schemas/AdminGroupMapping'
description: 'Groups automatically selected for new users created by this admin. The admin will still be able to choose different groups. These settings are only used for this admin UI and they will be ignored in REST API/hooks.'
created_at:
type: integer
format: int64
@ -5870,6 +5875,11 @@ components:
items:
type: string
description: list of usernames associated with this group
admins:
type: array
items:
type: string
description: list of admins usernames associated with this group
GroupMapping:
type: object
properties:
@ -5880,10 +5890,33 @@ components:
enum:
- 1
- 2
- 3
description: |
Group type:
* `1` - Primary group
* `2` - Secondaru group
* `2` - Secondary group
* `3` - Membership only, no settings are inherited from this group type
AdminGroupMappingOptions:
type: object
properties:
add_to_users_as:
enum:
- 0
- 1
- 2
description: |
Add to new users as:
* `0` - the admin's group will be added as membership group for new users
* `1` - the admin's group will be added as primary group for new users
* `2` - the admin's group will be added as secondary group for new users
AdminGroupMapping:
type: object
properties:
name:
type: string
description: group name
options:
$ref: '#/components/schemas/AdminGroupMappingOptions'
BackupData:
type: object
properties:

View file

@ -96,6 +96,72 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</div>
</div>
<div class="card bg-light mb-3">
<div class="card-header">
<b>Groups for users</b>
</div>
<div class="card-body">
<h6 class="card-title mb-4">Groups automatically selected for new users created by this admin. The admin will still be able to choose different groups. These settings are only used for this admin UI and they will be ignored in REST API/hooks.</h6>
<div class="form-group row">
<div class="col-md-12 form_field_groups_outer">
{{range $idx, $val := .Admin.Groups}}
<div class="row form_field_groups_outer_row">
<div class="form-group col-md-7">
<select class="form-control selectpicker" data-live-search="true" id="idGroup{{$idx}}" name="group{{$idx}}">
<option value=""></option>
{{- range $.Groups}}
<option value="{{.Name}}" {{if eq $val.Name .Name}}selected{{end}}>{{.Name}}</option>
{{- end}}
</select>
</div>
<div class="form-group col-md-4">
<select class="form-control selectpicker" id="idAddAsGroupType{{$idx}}" name="add_as_group_type{{$idx}}">
<option value="0" {{if eq $val.Options.AddToUsersAs 0}}selected{{end}}>Add as membership</option>
<option value="1" {{if eq $val.Options.AddToUsersAs 1}}selected{{end}}>Add as primary</option>
<option value="2" {{if eq $val.Options.AddToUsersAs 2}}selected{{end}}>Add as secondary</option>
</select>
</div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_group_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
{{else}}
<div class="row form_field_groups_outer_row">
<div class="form-group col-md-7">
<select class="form-control selectpicker" data-live-search="true" id="idGroup0" name="group0">
<option value=""></option>
{{- range .Groups}}
<option value="{{.Name}}">{{.Name}}</option>
{{- end}}
</select>
</div>
<div class="form-group col-md-4">
<select class="form-control selectpicker" id="idAddAsGroupType0" name="add_as_group_type0">
<option value="0">Add as membership</option>
<option value="1">Add as primary</option>
<option value="2">Add as secondary</option>
</select>
</div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_group_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
{{end}}
</div>
</div>
<div class="row mx-1">
<button type="button" class="btn btn-secondary add_new_group_field_btn">
<i class="fas fa-plus"></i> Add group
</button>
</div>
</div>
</div>
<div class="form-group row">
<label for="idAllowedIP" class="col-sm-2 col-form-label">Allowed IP/Mask</label>
<div class="col-sm-10">
@ -138,4 +204,43 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{{define "extra_js"}}
<script src="{{.StaticURL}}/vendor/bootstrap-select/js/bootstrap-select.min.js"></script>
<script type="text/javascript">
$("body").on("click", ".add_new_group_field_btn", function () {
var index = $(".form_field_groups_outer").find("form_field_groups_outer_row").length;
while (document.getElementById("idGroup"+index) != null){
index++;
}
$(".form_field_groups_outer").append(`
<div class="row form_field_groups_outer_row">
<div class="form-group col-md-7">
<select class="form-control" id="idGroup${index}" name="group${index}">
<option value=""></option>
</select>
</div>
<div class="form-group col-md-4">
<select class="form-control" id="idAddAsGroupType${index}" name="add_as_group_type${index}">
<option value="0">Add as membership</option>
<option value="1">Add as primary</option>
<option value="2">Add as secondary</option>
</select>
</div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_group_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
`);
{{- range .Groups}}
$("#idGroup"+index).append($('<option>').val('{{.Name}}').text('{{.Name}}'));
{{- end}}
$("#idGroup"+index).selectpicker({'liveSearch': true});
$("#idAddAsGroupType"+index).selectpicker();
});
$("body").on("click", ".remove_group_btn_frm_field", function () {
$(this).closest(".form_field_groups_outer_row").remove();
});
</script>
{{end}}

View file

@ -54,6 +54,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<th>Allow list</th>
<th>Email</th>
<th>Description</th>
<th>Groups for users</th>
</tr>
</thead>
<tbody>
@ -68,6 +69,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<td>{{.GetAllowedIPAsString}}</td>
<td>{{.Email}}</td>
<td>{{.Description}}</td>
<td>{{.GetGroupsAsString}}</td>
</tr>
{{end}}
@ -233,6 +235,11 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{
"targets": [5,7,8],
"visible": false,
},
{
"targets": [9],
"render": $.fn.dataTable.render.ellipsis(50, true),
"visible": false,
}
],
"scrollX": false,

View file

@ -41,7 +41,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<tr>
<th>Name</th>
<th>Description</th>
<th>Members</th>
</tr>
</thead>
<tbody>
@ -49,7 +48,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<tr>
<td>{{.Name}}</td>
<td>{{.Description}}</td>
<td>{{.GetUsersAsString}}</td>
</tr>
{{end}}
</tbody>
@ -185,11 +183,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{
"targets": [0],
"className": "noVis"
},
{
"targets": [2],
"render": $.fn.dataTable.render.ellipsis(100, true)
},
}
],
"scrollX": false,
"scrollY": false,

View file

@ -161,7 +161,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<b>Groups</b>
</div>
<div class="card-body">
<h6 class="card-title mb-4">Group membership impart the group settings if no override exist</h6>
<h6 class="card-title mb-4">Group membership impart the group settings (with the exception of membership only groups) if no override exists</h6>
<div class="form-group row">
<label for="idPrimaryGroup" class="col-sm-2 col-form-label">Primary group</label>
<div class="col-sm-10">
@ -183,6 +183,16 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</select>
</div>
</div>
<div class="form-group row">
<label for="idMembershipGroup" class="col-sm-2 col-form-label">Membership groups</label>
<div class="col-sm-10">
<select class="form-control selectpicker" data-live-search="true" id="idMembershipGroup" name="membership_groups" multiple>
{{- range .Groups}}
<option value="{{.Name}}" {{if $.User.HasMembershipGroup .Name}}selected{{end}}>{{.Name}}</option>
{{- end}}
</select>
</div>
</div>
</div>
</div>
@ -317,17 +327,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</div>
</div>
<div class="form-group">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="idAllowAPIKeyAuth" name="allow_api_key_auth"
{{if .User.Filters.AllowAPIKeyAuth}}checked{{end}} aria-describedby="allowAPIKeyAuthHelpBlock">
<label for="idAllowAPIKeyAuth" class="form-check-label">Allow API key authentication</label>
<small id="allowAPIKeyAuthHelpBlock" class="form-text text-muted">
Allow to impersonate this user, in REST API, with an API key
</small>
</div>
</div>
<div class="form-group row">
<label for="idDescription" class="col-sm-2 col-form-label">Description</label>
<div class="col-sm-10">
@ -985,6 +984,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</div>
</div>
<div class="form-group">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="idAllowAPIKeyAuth" name="allow_api_key_auth"
{{if .User.Filters.AllowAPIKeyAuth}}checked{{end}} aria-describedby="allowAPIKeyAuthHelpBlock">
<label for="idAllowAPIKeyAuth" class="form-check-label">Allow API key authentication</label>
<small id="allowAPIKeyAuthHelpBlock" class="form-text text-muted">
Allow to impersonate this user, in REST API, with an API key
</small>
</div>
</div>
<div class="form-group row {{if not .User.HasExternalAuth}}d-none{{end}}">
<label for="idExtAuthCacheTime" class="col-sm-2 col-form-label">External auth cache time</label>
<div class="col-sm-10">