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 ```shell
$ sudo su - sftpgo -s /bin/bash -c 'sftpgo initprovider -c /etc/sftpgo' $ 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 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 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 ```shell
$ sudo su - sftpgo -s /bin/bash -c 'sftpgo initprovider -c /etc/sftpgo' $ 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 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 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' $ 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.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.554 INF creating initial database schema, version 15
2022-06-02T14:54:04.698 INF updating database version: 15 -> 16 2022-06-02T14:54:04.698 INF updating database schema version: 15 -> 16
2022-06-02T14:54:07.093 INF updating database version: 16 -> 17 2022-06-02T14:54:07.093 INF updating database schema version: 16 -> 17
2022-06-02T14:54:07.672 INF updating database version: 17 -> 18 2022-06-02T14:54:07.672 INF updating database schema version: 17 -> 18
2022-06-02T14:54:07.699 INF updating database version: 18 -> 19 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 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 ```shell
$ sudo su - sftpgo -s /bin/bash -c 'sftpgo initprovider -c /etc/sftpgo' $ 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 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 schema version: 1 -> 2
2020-10-09T21:07:50.000 INF updating database version: 2 -> 3 2020-10-09T21:07:50.000 INF updating database schema 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: 3 -> 4
2020-10-09T21:07:50.000 INF Data provider successfully initialized/updated 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/s3 v1.27.9
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.22 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/aws/aws-sdk-go-v2/service/sts v1.16.17
github.com/cockroachdb/cockroach-go/v2 v2.2.15 github.com/cockroachdb/cockroach-go/v2 v2.2.16
github.com/coreos/go-oidc/v3 v3.3.0 github.com/coreos/go-oidc/v3 v3.4.0
github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001 github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001
github.com/fclairamb/ftpserverlib v0.19.1 github.com/fclairamb/ftpserverlib v0.19.1
github.com/fclairamb/go-log v0.4.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/cors v1.8.3-0.20220619195839-da52b0701de5
github.com/rs/xid v1.4.0 github.com/rs/xid v1.4.0
github.com/rs/zerolog v1.28.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/shirou/gopsutil/v3 v3.22.8
github.com/spf13/afero v1.9.2 github.com/spf13/afero v1.9.2
github.com/spf13/cobra v1.5.0 github.com/spf13/cobra v1.5.0
@ -66,9 +66,9 @@ require (
go.uber.org/automaxprocs v1.5.1 go.uber.org/automaxprocs v1.5.1
gocloud.dev v0.26.0 gocloud.dev v0.26.0
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90
golang.org/x/net v0.0.0-20220907135653-1e95f45603a7 golang.org/x/net v0.0.0-20220909164309-bea034e7d591
golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094 golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1
golang.org/x/sys v0.0.0-20220907062415-87db552b00fd golang.org/x/sys v0.0.0-20220913153101-76c7481b5158
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9
google.golang.org/api v0.95.0 google.golang.org/api v0.95.0
gopkg.in/natefinch/lumberjack.v2 v2.0.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/httpcc v1.0.1 // indirect
github.com/lestrrat-go/iter v1.0.2 // indirect github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/option v1.0.0 // 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/magiconair/properties v1.8.6 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.16 // 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/tools v0.1.12 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/appengine v1.6.7 // 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/grpc v1.49.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
@ -168,5 +168,5 @@ require (
replace ( replace (
github.com/jlaffaye/ftp => github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9 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/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-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/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/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.16 h1:t9dmZuC9J2W8IDQDSIGXmP+fBuEJSsrGXxWQz4cYqBY=
github.com/cockroachdb/cockroach-go/v2 v2.2.15/go.mod h1:xZ2VHjUEb/cySv0scXBx7YsBnHtLHkR1+w/w73b5i3M= github.com/cockroachdb/cockroach-go/v2 v2.2.16/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.4.0 h1:xz7elHb/LDwm/ERpwHd+5nb7wFHL32rsr6bBOgaeu6g=
github.com/coreos/go-oidc/v3 v3.3.0/go.mod h1:eHUXhZtXPQLgEaDrOVTgwbgmz1xGOkJNye6h3zkD2Pw= 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-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 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= 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/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 h1:LPH1dEblAOO/LoG7yHPMtBLXhQmjaga91/DDjWk9jWA=
github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9/go.mod h1:2lmrmq866uF2tnje75wQHzmPXhmSWUt7Gyx2vgK1RCU= 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-20220913160159-a08dc61b7895 h1:YZkDIISo8YO7PAOX85GYxGCayjBqAutIAjL+XsdEgkc=
github.com/drakkan/net v0.0.0-20220908074131-65c0cd1ffa8a/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= 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 h1:/ZshrfQzayqRSBDodmp3rhNCHJCff+utvgBuWRbiqu4=
github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001/go.mod h1:kltMsfRMTHSFdMbK66XdS8mfMW77+FZA1fGY1xYMF84= 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= 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 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=
github.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts= 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-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-20220913051719-115f729f3c8c h1:VtwQ41oftZwlMnOEbMWQtSEUgU64U4s+GHk7hZK+jtY=
github.com/lufia/plan9stats v0.0.0-20220517141722-cf486979b281/go.mod h1:lc+czkgO/8F7puNki5jk8QyujbfK1LOT7Wl0ON2hxyk= 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 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=
github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= 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= 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/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/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/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.20220913155952-81743fa5ded5 h1:VrrDnP3PP+UAWcxDgYfedmCBDxlkmugWZx7NdP0Dnng=
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/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 h1:a4s3hXogo5mE2PfdfJIonDbstO/P+9JszdfhAHSzD9Y=
github.com/shirou/gopsutil/v3 v3.22.8/go.mod h1:s648gW4IywYzUfE/KjXxUsqrqx/T2xO5VqOXxONeRfI= 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= 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-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-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-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-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-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-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/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-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-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-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-20220913153101-76c7481b5158 h1:XQphkCZeKYaMRSo28HqvvNYuLOoM5CIOOvTZfthvTgI=
golang.org/x/sys v0.0.0-20220907062415-87db552b00fd/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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-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 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 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-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-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-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-20220909194730-69f6226f97e5 h1:ngtP8S8JkBWfJACT9cmj5eTkS9tIWPQI5leBz/7Bq/c=
google.golang.org/genproto v0.0.0-20220902135211-223410557253/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= 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.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=

View file

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

View file

@ -22,9 +22,11 @@ import (
"fmt" "fmt"
"net" "net"
"os" "os"
"sort"
"strings" "strings"
"github.com/alexedwards/argon2id" "github.com/alexedwards/argon2id"
"github.com/sftpgo/sdk"
passwordvalidator "github.com/wagslane/go-password-validator" passwordvalidator "github.com/wagslane/go-password-validator"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
@ -57,6 +59,15 @@ const (
PermAdminManageEventRules = "manage_event_rules" 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 ( var (
validAdminPerms = []string{PermAdminAny, PermAdminAddUsers, PermAdminChangeUsers, PermAdminDeleteUsers, validAdminPerms = []string{PermAdminAny, PermAdminAddUsers, PermAdminChangeUsers, PermAdminDeleteUsers,
PermAdminViewUsers, PermAdminManageGroups, PermAdminViewConnections, PermAdminCloseConnections, PermAdminViewUsers, PermAdminManageGroups, PermAdminViewConnections, PermAdminCloseConnections,
@ -113,6 +124,36 @@ type AdminFilters struct {
RecoveryCodes []RecoveryCode `json:"recovery_codes,omitempty"` 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 // Admin defines a SFTPGo admin
type Admin struct { type Admin struct {
// Database unique identifier // Database unique identifier
@ -127,6 +168,8 @@ type Admin struct {
Filters AdminFilters `json:"filters,omitempty"` Filters AdminFilters `json:"filters,omitempty"`
Description string `json:"description,omitempty"` Description string `json:"description,omitempty"`
AdditionalInfo string `json:"additional_info,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 // Creation time as unix timestamp in milliseconds. It will be 0 for admins created before v2.2.0
CreatedAt int64 `json:"created_at"` CreatedAt int64 `json:"created_at"`
// last update time as unix timestamp in milliseconds // last update time as unix timestamp in milliseconds
@ -206,11 +249,33 @@ func (a *Admin) validatePermissions() error {
return nil 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 { func (a *Admin) validate() error {
a.SetEmptySecretsIfNil() a.SetEmptySecretsIfNil()
if a.Username == "" { if a.Username == "" {
return util.NewValidationError("username is mandatory") return util.NewValidationError("username is mandatory")
} }
if err := checkReservedUsernames(a.Username); err != nil {
return err
}
if a.Password == "" { if a.Password == "" {
return util.NewValidationError("please set 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 // CheckPassword verifies the admin password
@ -422,6 +500,15 @@ func (a *Admin) getACopy() Admin {
Used: code.Used, 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{ return Admin{
ID: a.ID, ID: a.ID,
@ -430,6 +517,7 @@ func (a *Admin) getACopy() Admin {
Password: a.Password, Password: a.Password,
Email: a.Email, Email: a.Email,
Permissions: permissions, Permissions: permissions,
Groups: groups,
Filters: filters, Filters: filters,
AdditionalInfo: a.AdditionalInfo, AdditionalInfo: a.AdditionalInfo,
Description: a.Description, Description: a.Description,

View file

@ -35,7 +35,7 @@ import (
) )
const ( const (
boltDatabaseVersion = 21 boltDatabaseVersion = 22
) )
var ( var (
@ -372,6 +372,10 @@ func (p *BoltProvider) addAdmin(admin *Admin) error {
if err != nil { if err != nil {
return err return err
} }
groupBucket, err := p.getGroupsBucket(tx)
if err != nil {
return err
}
if a := bucket.Get([]byte(admin.Username)); a != nil { if a := bucket.Get([]byte(admin.Username)); a != nil {
return fmt.Errorf("admin %v already exists", admin.Username) return fmt.Errorf("admin %v already exists", admin.Username)
} }
@ -383,6 +387,12 @@ func (p *BoltProvider) addAdmin(admin *Admin) error {
admin.LastLogin = 0 admin.LastLogin = 0
admin.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now()) admin.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
admin.UpdatedAt = 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) buf, err := json.Marshal(admin)
if err != nil { if err != nil {
return err return err
@ -401,8 +411,11 @@ func (p *BoltProvider) updateAdmin(admin *Admin) error {
if err != nil { if err != nil {
return err return err
} }
groupBucket, err := p.getGroupsBucket(tx)
if err != nil {
return err
}
var a []byte var a []byte
if a = bucket.Get([]byte(admin.Username)); a == nil { if a = bucket.Get([]byte(admin.Username)); a == nil {
return util.NewRecordNotFoundError(fmt.Sprintf("admin %v does not exist", admin.Username)) 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 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.ID = oldAdmin.ID
admin.CreatedAt = oldAdmin.CreatedAt admin.CreatedAt = oldAdmin.CreatedAt
admin.LastLogin = oldAdmin.LastLogin admin.LastLogin = oldAdmin.LastLogin
@ -431,9 +456,27 @@ func (p *BoltProvider) deleteAdmin(admin Admin) error {
return err 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)) 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 { if err := p.deleteRelatedAPIKey(tx, admin.Username, APIKeyScopeAdmin); err != nil {
return err return err
@ -1014,6 +1057,7 @@ func (p *BoltProvider) addFolder(folder *vfs.BaseVirtualFolder) error {
return fmt.Errorf("folder %v already exists", folder.Name) return fmt.Errorf("folder %v already exists", folder.Name)
} }
folder.Users = nil folder.Users = nil
folder.Groups = nil
return p.addFolderInternal(*folder, bucket) return p.addFolderInternal(*folder, bucket)
}) })
} }
@ -1044,6 +1088,7 @@ func (p *BoltProvider) updateFolder(folder *vfs.BaseVirtualFolder) error {
folder.UsedQuotaFiles = oldFolder.UsedQuotaFiles folder.UsedQuotaFiles = oldFolder.UsedQuotaFiles
folder.UsedQuotaSize = oldFolder.UsedQuotaSize folder.UsedQuotaSize = oldFolder.UsedQuotaSize
folder.Users = oldFolder.Users folder.Users = oldFolder.Users
folder.Groups = oldFolder.Groups
buf, err := json.Marshal(folder) buf, err := json.Marshal(folder)
if err != nil { if err != nil {
return err return err
@ -1332,6 +1377,8 @@ func (p *BoltProvider) addGroup(group *Group) error {
group.ID = int64(id) group.ID = int64(id)
group.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now()) group.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
group.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now()) group.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
group.Users = nil
group.Admins = nil
for idx := range group.VirtualFolders { for idx := range group.VirtualFolders {
err = p.addRelationToFolderMapping(&group.VirtualFolders[idx].BaseVirtualFolder, nil, group, foldersBucket) err = p.addRelationToFolderMapping(&group.VirtualFolders[idx].BaseVirtualFolder, nil, group, foldersBucket)
if err != nil { if err != nil {
@ -1383,6 +1430,7 @@ func (p *BoltProvider) updateGroup(group *Group) error {
group.ID = oldGroup.ID group.ID = oldGroup.ID
group.CreatedAt = oldGroup.CreatedAt group.CreatedAt = oldGroup.CreatedAt
group.Users = oldGroup.Users group.Users = oldGroup.Users
group.Admins = oldGroup.Admins
group.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now()) group.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
buf, err := json.Marshal(group) buf, err := json.Marshal(group)
if err != nil { if err != nil {
@ -1408,17 +1456,31 @@ func (p *BoltProvider) deleteGroup(group Group) error {
return err return err
} }
if len(oldGroup.Users) > 0 { 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 len(oldGroup.VirtualFolders) > 0 {
if err != nil { foldersBucket, err := p.getFoldersBucket(tx)
return err
}
for idx := range group.VirtualFolders {
err = p.removeRelationFromFolderMapping(group.VirtualFolders[idx], "", group.Name, foldersBucket)
if err != nil { if err != nil {
return err 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)) 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) providerLog(logger.LevelDebug, "bolt database is up to date, current version: %v", version)
return ErrNoInitRequired return ErrNoInitRequired
case version < 19: 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) providerLog(logger.LevelError, "%v", err)
logger.ErrorToConsole("%v", err) logger.ErrorToConsole("%v", err)
return err return err
case version == 19, version == 20: case version == 19, version == 20, version == 21:
logger.InfoToConsole(fmt.Sprintf("updating database version: %d -> 21", version)) logger.InfoToConsole(fmt.Sprintf("updating database schema version: %d -> 22", version))
providerLog(logger.LevelInfo, "updating database version: %d -> 21", version) providerLog(logger.LevelInfo, "updating database schema version: %d -> 22", version)
return updateBoltDatabaseVersion(p.dbHandle, 21) return updateBoltDatabaseVersion(p.dbHandle, 22)
default: default:
if version > boltDatabaseVersion { 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) 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) boltDatabaseVersion)
return nil 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 { switch dbVersion.Version {
case 20, 21: case 20, 21:
logger.InfoToConsole("downgrading database version: %d -> 19", dbVersion.Version) logger.InfoToConsole("downgrading database schema version: %d -> 19", dbVersion.Version)
providerLog(logger.LevelInfo, "downgrading database 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 { err := p.dbHandle.Update(func(tx *bolt.Tx) error {
for _, bucketName := range [][]byte{actionsBucket, rulesBucket} { for _, bucketName := range [][]byte{actionsBucket, rulesBucket} {
err := tx.DeleteBucket(bucketName) err := tx.DeleteBucket(bucketName)
@ -2563,7 +2625,7 @@ func (p *BoltProvider) revertDatabase(targetVersion int) error {
} }
return updateBoltDatabaseVersion(p.dbHandle, 19) return updateBoltDatabaseVersion(p.dbHandle, 19)
default: 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 { if err != nil {
return err return err
} }
if util.Contains(group.Users, username) { var users []string
var users []string for _, u := range group.Users {
for _, u := range group.Users { if u != username {
if u != username { users = append(users, u)
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) buf, err := json.Marshal(group)
if err != nil { if err != nil {
return err return err
@ -2783,6 +2863,55 @@ func (p *BoltProvider) removeUserFromGroupMapping(username, groupname string, bu
return nil 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 { func (p *BoltProvider) addRelationToFolderMapping(baseFolder *vfs.BaseVirtualFolder, user *User, group *Group, bucket *bolt.Bucket) error {
f := bucket.Get([]byte(baseFolder.Name)) f := bucket.Get([]byte(baseFolder.Name))
if f == nil { if f == nil {
@ -2836,7 +2965,7 @@ func (p *BoltProvider) removeRelationFromFolderMapping(folder vfs.VirtualFolder,
return err return err
} }
found := false found := false
if username != "" && util.Contains(baseFolder.Users, username) { if username != "" {
found = true found = true
var newUserMapping []string var newUserMapping []string
for _, u := range baseFolder.Users { for _, u := range baseFolder.Users {
@ -2846,7 +2975,7 @@ func (p *BoltProvider) removeRelationFromFolderMapping(folder vfs.VirtualFolder,
} }
baseFolder.Users = newUserMapping baseFolder.Users = newUserMapping
} }
if groupname != "" && util.Contains(baseFolder.Groups, groupname) { if groupname != "" {
found = true found = true
var newGroupMapping []string var newGroupMapping []string
for _, g := range baseFolder.Groups { for _, g := range baseFolder.Groups {
@ -3066,7 +3195,7 @@ func getBoltDatabaseVersion(dbHandle *bolt.DB) (schemaVersion, error) {
err := dbHandle.View(func(tx *bolt.Tx) error { err := dbHandle.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket(dbVersionBucket) bucket := tx.Bucket(dbVersionBucket)
if bucket == nil { 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) v := bucket.Get(dbVersionKey)
if v == nil { if v == nil {
@ -3084,7 +3213,7 @@ func updateBoltDatabaseVersion(dbHandle *bolt.DB, version int) error {
err := dbHandle.Update(func(tx *bolt.Tx) error { err := dbHandle.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket(dbVersionBucket) bucket := tx.Bucket(dbVersionBucket)
if bucket == nil { if bucket == nil {
return fmt.Errorf("unable to find database version bucket") return fmt.Errorf("unable to find database schema version bucket")
} }
newDbVersion := schemaVersion{ newDbVersion := schemaVersion{
Version: version, Version: version,

View file

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

View file

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

View file

@ -330,12 +330,18 @@ func (p *MemoryProvider) addUser(user *User) error {
user.FirstDownload = 0 user.FirstDownload = 0
user.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now()) user.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
user.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now()) user.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
user.VirtualFolders = p.joinUserVirtualFoldersFields(user) var mappedGroups []string
for idx := range user.Groups { 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 return err
} }
mappedGroups = append(mappedGroups, user.Groups[idx].Name)
} }
user.VirtualFolders = p.joinUserVirtualFoldersFields(user)
p.dbHandle.users[user.Username] = user.getACopy() p.dbHandle.users[user.Username] = user.getACopy()
p.dbHandle.usernames = append(p.dbHandle.usernames, user.Username) p.dbHandle.usernames = append(p.dbHandle.usernames, user.Username)
sort.Strings(p.dbHandle.usernames) sort.Strings(p.dbHandle.usernames)
@ -360,20 +366,25 @@ func (p *MemoryProvider) updateUser(user *User) error {
if err != nil { if err != nil {
return err 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 { for _, oldFolder := range u.VirtualFolders {
p.removeRelationFromFolderMapping(oldFolder.Name, u.Username, "") 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) 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.LastQuotaUpdate = u.LastQuotaUpdate
user.UsedQuotaSize = u.UsedQuotaSize user.UsedQuotaSize = u.UsedQuotaSize
user.UsedQuotaFiles = u.UsedQuotaFiles user.UsedQuotaFiles = u.UsedQuotaFiles
@ -405,9 +416,7 @@ func (p *MemoryProvider) deleteUser(user User, softDelete bool) error {
p.removeRelationFromFolderMapping(oldFolder.Name, u.Username, "") p.removeRelationFromFolderMapping(oldFolder.Name, u.Username, "")
} }
for idx := range u.Groups { for idx := range u.Groups {
if err = p.removeUserFromGroupMapping(u.Username, u.Groups[idx].Name); err != nil { p.removeUserFromGroupMapping(u.Username, u.Groups[idx].Name)
return err
}
} }
delete(p.dbHandle.users, user.Username) delete(p.dbHandle.users, user.Username)
// this could be more efficient // this could be more efficient
@ -644,6 +653,17 @@ func (p *MemoryProvider) addAdmin(admin *Admin) error {
admin.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now()) admin.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
admin.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now()) admin.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
admin.LastLogin = 0 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.admins[admin.Username] = admin.getACopy()
p.dbHandle.adminsUsernames = append(p.dbHandle.adminsUsernames, admin.Username) p.dbHandle.adminsUsernames = append(p.dbHandle.adminsUsernames, admin.Username)
sort.Strings(p.dbHandle.adminsUsernames) sort.Strings(p.dbHandle.adminsUsernames)
@ -664,6 +684,21 @@ func (p *MemoryProvider) updateAdmin(admin *Admin) error {
if err != nil { if err != nil {
return err 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.ID = a.ID
admin.CreatedAt = a.CreatedAt admin.CreatedAt = a.CreatedAt
admin.LastLogin = a.LastLogin admin.LastLogin = a.LastLogin
@ -678,10 +713,13 @@ func (p *MemoryProvider) deleteAdmin(admin Admin) error {
if p.dbHandle.isClosed { if p.dbHandle.isClosed {
return errMemoryProviderClosed return errMemoryProviderClosed
} }
_, err := p.adminExistsInternal(admin.Username) a, err := p.adminExistsInternal(admin.Username)
if err != nil { if err != nil {
return err return err
} }
for idx := range a.Groups {
p.removeAdminFromGroupMapping(a.Username, a.Groups[idx].Name)
}
delete(p.dbHandle.admins, admin.Username) delete(p.dbHandle.admins, admin.Username)
// this could be more efficient // this could be more efficient
@ -906,6 +944,8 @@ func (p *MemoryProvider) addGroup(group *Group) error {
group.ID = p.getNextGroupID() group.ID = p.getNextGroupID()
group.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now()) group.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
group.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now()) group.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
group.Users = nil
group.Admins = nil
group.VirtualFolders = p.joinGroupVirtualFoldersFields(group) group.VirtualFolders = p.joinGroupVirtualFoldersFields(group)
p.dbHandle.groups[group.Name] = group.getACopy() p.dbHandle.groups[group.Name] = group.getACopy()
p.dbHandle.groupnames = append(p.dbHandle.groupnames, group.Name) 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.CreatedAt = g.CreatedAt
group.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now()) group.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
group.ID = g.ID group.ID = g.ID
group.Users = g.Users
group.Admins = g.Admins
p.dbHandle.groups[group.Name] = group.getACopy() p.dbHandle.groups[group.Name] = group.getACopy()
return nil return nil
} }
@ -953,6 +995,9 @@ func (p *MemoryProvider) deleteGroup(group Group) error {
for _, oldFolder := range g.VirtualFolders { for _, oldFolder := range g.VirtualFolders {
p.removeRelationFromFolderMapping(oldFolder.Name, "", g.Name) p.removeRelationFromFolderMapping(oldFolder.Name, "", g.Name)
} }
for _, a := range g.Admins {
p.removeGroupFromAdminMapping(g.Name, a)
}
delete(p.dbHandle.groups, group.Name) delete(p.dbHandle.groups, group.Name)
// this could be more efficient // this could be more efficient
p.dbHandle.groupnames = make([]string, 0, len(p.dbHandle.groups)) p.dbHandle.groupnames = make([]string, 0, len(p.dbHandle.groups))
@ -1050,11 +1095,11 @@ func (p *MemoryProvider) addRuleToActionMapping(ruleName, actionName string) err
return nil return nil
} }
func (p *MemoryProvider) removeRuleFromActionMapping(ruleName, actionName string) error { func (p *MemoryProvider) removeRuleFromActionMapping(ruleName, actionName string) {
a, err := p.actionExistsInternal(actionName) a, err := p.actionExistsInternal(actionName)
if err != nil { if err != nil {
providerLog(logger.LevelWarn, "action %q does not exist, cannot remove from mapping", actionName) providerLog(logger.LevelWarn, "action %q does not exist, cannot remove from mapping", actionName)
return nil return
} }
if util.Contains(a.Rules, ruleName) { if util.Contains(a.Rules, ruleName) {
var rules []string var rules []string
@ -1066,10 +1111,52 @@ func (p *MemoryProvider) removeRuleFromActionMapping(ruleName, actionName string
a.Rules = rules a.Rules = rules
p.dbHandle.actions[actionName] = a 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 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) g, err := p.groupExistsInternal(groupname)
if err != nil { if err != nil {
return err return err
@ -1081,22 +1168,19 @@ func (p *MemoryProvider) addUserFromGroupMapping(username, groupname string) err
return nil return nil
} }
func (p *MemoryProvider) removeUserFromGroupMapping(username, groupname string) error { func (p *MemoryProvider) removeUserFromGroupMapping(username, groupname string) {
g, err := p.groupExistsInternal(groupname) g, err := p.groupExistsInternal(groupname)
if err != nil { if err != nil {
return err return
} }
if util.Contains(g.Users, username) { var users []string
var users []string for _, u := range g.Users {
for _, u := range g.Users { if u != username {
if u != username { users = append(users, u)
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 { 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.ID = p.getNextFolderID()
folder.Users = nil folder.Users = nil
folder.Groups = nil
p.dbHandle.vfolders[folder.Name] = folder.GetACopy() p.dbHandle.vfolders[folder.Name] = folder.GetACopy()
p.dbHandle.vfoldersNames = append(p.dbHandle.vfoldersNames, folder.Name) p.dbHandle.vfoldersNames = append(p.dbHandle.vfoldersNames, folder.Name)
sort.Strings(p.dbHandle.vfoldersNames) sort.Strings(p.dbHandle.vfoldersNames)
@ -1306,6 +1391,7 @@ func (p *MemoryProvider) updateFolder(folder *vfs.BaseVirtualFolder) error {
folder.UsedQuotaFiles = f.UsedQuotaFiles folder.UsedQuotaFiles = f.UsedQuotaFiles
folder.UsedQuotaSize = f.UsedQuotaSize folder.UsedQuotaSize = f.UsedQuotaSize
folder.Users = f.Users folder.Users = f.Users
folder.Groups = f.Groups
p.dbHandle.vfolders[folder.Name] = folder.GetACopy() p.dbHandle.vfolders[folder.Name] = folder.GetACopy()
// now update the related users // now update the related users
for _, username := range folder.Users { for _, username := range folder.Users {
@ -2115,10 +2201,16 @@ func (p *MemoryProvider) addEventRule(rule *EventRule) error {
rule.ID = p.getNextRuleID() rule.ID = p.getNextRuleID()
rule.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now()) rule.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
rule.UpdatedAt = rule.CreatedAt rule.UpdatedAt = rule.CreatedAt
var mappedActions []string
for idx := range rule.Actions { for idx := range rule.Actions {
if err := p.addRuleToActionMapping(rule.Name, rule.Actions[idx].Name); err != nil { 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 return err
} }
mappedActions = append(mappedActions, rule.Actions[idx].Name)
} }
sort.Slice(rule.Actions, func(i, j int) bool { sort.Slice(rule.Actions, func(i, j int) bool {
return rule.Actions[i].Order < rule.Actions[j].Order return rule.Actions[i].Order < rule.Actions[j].Order
@ -2144,12 +2236,17 @@ func (p *MemoryProvider) updateEventRule(rule *EventRule) error {
return err return err
} }
for idx := range oldRule.Actions { for idx := range oldRule.Actions {
if err = p.removeRuleFromActionMapping(rule.Name, oldRule.Actions[idx].Name); err != nil { p.removeRuleFromActionMapping(rule.Name, oldRule.Actions[idx].Name)
return err
}
} }
for idx := range rule.Actions { for idx := range rule.Actions {
if err = p.addRuleToActionMapping(rule.Name, rule.Actions[idx].Name); err != nil { 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 return err
} }
} }
@ -2176,9 +2273,7 @@ func (p *MemoryProvider) deleteEventRule(rule EventRule, softDelete bool) error
} }
if len(oldRule.Actions) > 0 { if len(oldRule.Actions) > 0 {
for idx := range oldRule.Actions { for idx := range oldRule.Actions {
if err = p.removeRuleFromActionMapping(rule.Name, oldRule.Actions[idx].Name); err != nil { p.removeRuleFromActionMapping(rule.Name, oldRule.Actions[idx].Name)
return err
}
} }
} }
delete(p.dbHandle.rules, rule.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 `{{folders_mapping}}` CASCADE;" +
"DROP TABLE IF EXISTS `{{users_folders_mapping}}` CASCADE;" + "DROP TABLE IF EXISTS `{{users_folders_mapping}}` CASCADE;" +
"DROP TABLE IF EXISTS `{{users_groups_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 `{{groups_folders_mapping}}` CASCADE;" +
"DROP TABLE IF EXISTS `{{admins}}` CASCADE;" + "DROP TABLE IF EXISTS `{{admins}}` CASCADE;" +
"DROP TABLE IF EXISTS `{{folders}}` CASCADE;" + "DROP TABLE IF EXISTS `{{folders}}` CASCADE;" +
@ -171,6 +172,16 @@ const (
"ALTER TABLE `{{users}}` ALTER COLUMN `first_upload` DROP DEFAULT;" "ALTER TABLE `{{users}}` ALTER COLUMN `first_upload` DROP DEFAULT;"
mysqlV21DownSQL = "ALTER TABLE `{{users}}` DROP COLUMN `first_upload`; " + mysqlV21DownSQL = "ALTER TABLE `{{users}}` DROP COLUMN `first_upload`; " +
"ALTER TABLE `{{users}}` DROP COLUMN `first_download`;" "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 // 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) providerLog(logger.LevelDebug, "sql database is up to date, current version: %v", version)
return ErrNoInitRequired return ErrNoInitRequired
case version < 19: 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) providerLog(logger.LevelError, "%v", err)
logger.ErrorToConsole("%v", err) logger.ErrorToConsole("%v", err)
return err return err
@ -684,15 +695,17 @@ func (p *MySQLProvider) migrateDatabase() error { //nolint:dupl
return updateMySQLDatabaseFromV19(p.dbHandle) return updateMySQLDatabaseFromV19(p.dbHandle)
case version == 20: case version == 20:
return updateMySQLDatabaseFromV20(p.dbHandle) return updateMySQLDatabaseFromV20(p.dbHandle)
case version == 21:
return updateMySQLDatabaseFromV21(p.dbHandle)
default: default:
if version > sqlDatabaseVersion { 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) 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) sqlDatabaseVersion)
return nil 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) return downgradeMySQLDatabaseFromV20(p.dbHandle)
case 21: case 21:
return downgradeMySQLDatabaseFromV21(p.dbHandle) return downgradeMySQLDatabaseFromV21(p.dbHandle)
case 22:
return downgradeMySQLDatabaseFromV22(p.dbHandle)
default: 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 { 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 { func downgradeMySQLDatabaseFromV20(dbHandle *sql.DB) error {
@ -742,9 +764,16 @@ func downgradeMySQLDatabaseFromV21(dbHandle *sql.DB) error {
return downgradeMySQLDatabaseFromV20(dbHandle) 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 { func updateMySQLDatabaseFrom19To20(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database version: 19 -> 20") logger.InfoToConsole("updating database schema version: 19 -> 20")
providerLog(logger.LevelInfo, "updating database version: 19 -> 20") providerLog(logger.LevelInfo, "updating database schema version: 19 -> 20")
sql := strings.ReplaceAll(mysqlV20SQL, "{{events_actions}}", sqlTableEventsActions) sql := strings.ReplaceAll(mysqlV20SQL, "{{events_actions}}", sqlTableEventsActions)
sql = strings.ReplaceAll(sql, "{{events_rules}}", sqlTableEventsRules) sql = strings.ReplaceAll(sql, "{{events_rules}}", sqlTableEventsRules)
sql = strings.ReplaceAll(sql, "{{rules_actions_mapping}}", sqlTableRulesActionsMapping) sql = strings.ReplaceAll(sql, "{{rules_actions_mapping}}", sqlTableRulesActionsMapping)
@ -755,15 +784,25 @@ func updateMySQLDatabaseFrom19To20(dbHandle *sql.DB) error {
} }
func updateMySQLDatabaseFrom20To21(dbHandle *sql.DB) error { func updateMySQLDatabaseFrom20To21(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database version: 20 -> 21") logger.InfoToConsole("updating database schema version: 20 -> 21")
providerLog(logger.LevelInfo, "updating database version: 20 -> 21") providerLog(logger.LevelInfo, "updating database schema version: 20 -> 21")
sql := strings.ReplaceAll(mysqlV21SQL, "{{users}}", sqlTableUsers) sql := strings.ReplaceAll(mysqlV21SQL, "{{users}}", sqlTableUsers)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, strings.Split(sql, ";"), 21, true) 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 { func downgradeMySQLDatabaseFrom20To19(dbHandle *sql.DB) error {
logger.InfoToConsole("downgrading database version: 20 -> 19") logger.InfoToConsole("downgrading database schema version: 20 -> 19")
providerLog(logger.LevelInfo, "downgrading database version: 20 -> 19") providerLog(logger.LevelInfo, "downgrading database schema version: 20 -> 19")
sql := strings.ReplaceAll(mysqlV20DownSQL, "{{events_actions}}", sqlTableEventsActions) sql := strings.ReplaceAll(mysqlV20DownSQL, "{{events_actions}}", sqlTableEventsActions)
sql = strings.ReplaceAll(sql, "{{events_rules}}", sqlTableEventsRules) sql = strings.ReplaceAll(sql, "{{events_rules}}", sqlTableEventsRules)
sql = strings.ReplaceAll(sql, "{{rules_actions_mapping}}", sqlTableRulesActionsMapping) sql = strings.ReplaceAll(sql, "{{rules_actions_mapping}}", sqlTableRulesActionsMapping)
@ -773,8 +812,16 @@ func downgradeMySQLDatabaseFrom20To19(dbHandle *sql.DB) error {
} }
func downgradeMySQLDatabaseFrom21To20(dbHandle *sql.DB) error { func downgradeMySQLDatabaseFrom21To20(dbHandle *sql.DB) error {
logger.InfoToConsole("downgrading database version: 21 -> 20") logger.InfoToConsole("downgrading database schema version: 21 -> 20")
providerLog(logger.LevelInfo, "downgrading database version: 21 -> 20") providerLog(logger.LevelInfo, "downgrading database schema version: 21 -> 20")
sql := strings.ReplaceAll(mysqlV21DownSQL, "{{users}}", sqlTableUsers) sql := strings.ReplaceAll(mysqlV21DownSQL, "{{users}}", sqlTableUsers)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, strings.Split(sql, ";"), 20, false) 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 "{{folders_mapping}}" CASCADE;
DROP TABLE IF EXISTS "{{users_folders_mapping}}" CASCADE; DROP TABLE IF EXISTS "{{users_folders_mapping}}" CASCADE;
DROP TABLE IF EXISTS "{{users_groups_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 "{{groups_folders_mapping}}" CASCADE;
DROP TABLE IF EXISTS "{{admins}}" CASCADE; DROP TABLE IF EXISTS "{{admins}}" CASCADE;
DROP TABLE IF EXISTS "{{folders}}" 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; pgsqlV21DownSQL = `ALTER TABLE "{{users}}" DROP COLUMN "first_upload" CASCADE;
ALTER TABLE "{{users}}" DROP COLUMN "first_download" 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) providerLog(logger.LevelDebug, "sql database is up to date, current version: %v", version)
return ErrNoInitRequired return ErrNoInitRequired
case version < 19: 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) providerLog(logger.LevelError, "%v", err)
logger.ErrorToConsole("%v", err) logger.ErrorToConsole("%v", err)
return err return err
@ -653,15 +667,17 @@ func (p *PGSQLProvider) migrateDatabase() error { //nolint:dupl
return updatePgSQLDatabaseFromV19(p.dbHandle) return updatePgSQLDatabaseFromV19(p.dbHandle)
case version == 20: case version == 20:
return updatePgSQLDatabaseFromV20(p.dbHandle) return updatePgSQLDatabaseFromV20(p.dbHandle)
case version == 21:
return updatePgSQLDatabaseFromV21(p.dbHandle)
default: default:
if version > sqlDatabaseVersion { 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) 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) sqlDatabaseVersion)
return nil 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) return downgradePgSQLDatabaseFromV20(p.dbHandle)
case 21: case 21:
return downgradePgSQLDatabaseFromV21(p.dbHandle) return downgradePgSQLDatabaseFromV21(p.dbHandle)
case 22:
return downgradePgSQLDatabaseFromV22(p.dbHandle)
default: 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 { 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 { func downgradePgSQLDatabaseFromV20(dbHandle *sql.DB) error {
@ -711,9 +736,16 @@ func downgradePgSQLDatabaseFromV21(dbHandle *sql.DB) error {
return downgradePgSQLDatabaseFromV20(dbHandle) 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 { func updatePgSQLDatabaseFrom19To20(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database version: 19 -> 20") logger.InfoToConsole("updating database schema version: 19 -> 20")
providerLog(logger.LevelInfo, "updating database version: 19 -> 20") providerLog(logger.LevelInfo, "updating database schema version: 19 -> 20")
sql := strings.ReplaceAll(pgsqlV20SQL, "{{events_actions}}", sqlTableEventsActions) sql := strings.ReplaceAll(pgsqlV20SQL, "{{events_actions}}", sqlTableEventsActions)
sql = strings.ReplaceAll(sql, "{{events_rules}}", sqlTableEventsRules) sql = strings.ReplaceAll(sql, "{{events_rules}}", sqlTableEventsRules)
sql = strings.ReplaceAll(sql, "{{rules_actions_mapping}}", sqlTableRulesActionsMapping) sql = strings.ReplaceAll(sql, "{{rules_actions_mapping}}", sqlTableRulesActionsMapping)
@ -724,15 +756,25 @@ func updatePgSQLDatabaseFrom19To20(dbHandle *sql.DB) error {
} }
func updatePgSQLDatabaseFrom20To21(dbHandle *sql.DB) error { func updatePgSQLDatabaseFrom20To21(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database version: 20 -> 21") logger.InfoToConsole("updating database schema version: 20 -> 21")
providerLog(logger.LevelInfo, "updating database version: 20 -> 21") providerLog(logger.LevelInfo, "updating database schema version: 20 -> 21")
sql := strings.ReplaceAll(pgsqlV21SQL, "{{users}}", sqlTableUsers) sql := strings.ReplaceAll(pgsqlV21SQL, "{{users}}", sqlTableUsers)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 21, true) 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 { func downgradePgSQLDatabaseFrom20To19(dbHandle *sql.DB) error {
logger.InfoToConsole("downgrading database version: 20 -> 19") logger.InfoToConsole("downgrading database schema version: 20 -> 19")
providerLog(logger.LevelInfo, "downgrading database version: 20 -> 19") providerLog(logger.LevelInfo, "downgrading database schema version: 20 -> 19")
sql := strings.ReplaceAll(pgsqlV20DownSQL, "{{events_actions}}", sqlTableEventsActions) sql := strings.ReplaceAll(pgsqlV20DownSQL, "{{events_actions}}", sqlTableEventsActions)
sql = strings.ReplaceAll(sql, "{{events_rules}}", sqlTableEventsRules) sql = strings.ReplaceAll(sql, "{{events_rules}}", sqlTableEventsRules)
sql = strings.ReplaceAll(sql, "{{rules_actions_mapping}}", sqlTableRulesActionsMapping) sql = strings.ReplaceAll(sql, "{{rules_actions_mapping}}", sqlTableRulesActionsMapping)
@ -742,8 +784,16 @@ func downgradePgSQLDatabaseFrom20To19(dbHandle *sql.DB) error {
} }
func downgradePgSQLDatabaseFrom21To20(dbHandle *sql.DB) error { func downgradePgSQLDatabaseFrom21To20(dbHandle *sql.DB) error {
logger.InfoToConsole("downgrading database version: 21 -> 20") logger.InfoToConsole("downgrading database schema version: 21 -> 20")
providerLog(logger.LevelInfo, "downgrading database version: 21 -> 20") providerLog(logger.LevelInfo, "downgrading database schema version: 21 -> 20")
sql := strings.ReplaceAll(pgsqlV21DownSQL, "{{users}}", sqlTableUsers) sql := strings.ReplaceAll(pgsqlV21DownSQL, "{{users}}", sqlTableUsers)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 20, false) 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 ( const (
sqlDatabaseVersion = 21 sqlDatabaseVersion = 22
defaultSQLQueryTimeout = 10 * time.Second defaultSQLQueryTimeout = 10 * time.Second
longSQLQueryTimeout = 60 * 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, "{{groups}}", sqlTableGroups)
sql = strings.ReplaceAll(sql, "{{users_folders_mapping}}", sqlTableUsersFoldersMapping) sql = strings.ReplaceAll(sql, "{{users_folders_mapping}}", sqlTableUsersFoldersMapping)
sql = strings.ReplaceAll(sql, "{{users_groups_mapping}}", sqlTableUsersGroupsMapping) 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, "{{groups_folders_mapping}}", sqlTableGroupsFoldersMapping)
sql = strings.ReplaceAll(sql, "{{api_keys}}", sqlTableAPIKeys) sql = strings.ReplaceAll(sql, "{{api_keys}}", sqlTableAPIKeys)
sql = strings.ReplaceAll(sql, "{{shares}}", sqlTableShares) sql = strings.ReplaceAll(sql, "{{shares}}", sqlTableShares)
@ -392,7 +393,11 @@ func sqlCommonGetAdminByUsername(username string, dbHandle sqlQuerier) (Admin, e
q := getAdminByUsernameQuery() q := getAdminByUsernameQuery()
row := dbHandle.QueryRowContext(ctx, q, username) 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) { 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 return err
} }
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
q := getAddAdminQuery()
perms, err := json.Marshal(admin.Permissions) perms, err := json.Marshal(admin.Permissions)
if err != nil { if err != nil {
return err return err
@ -425,10 +426,19 @@ func sqlCommonAddAdmin(admin *Admin, dbHandle *sql.DB) error {
return err return err
} }
_, err = dbHandle.ExecContext(ctx, q, admin.Username, admin.Password, admin.Status, admin.Email, string(perms), ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
string(filters), admin.AdditionalInfo, admin.Description, util.GetTimeAsMsSinceEpoch(time.Now()), defer cancel()
util.GetTimeAsMsSinceEpoch(time.Now()))
return err 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 { func sqlCommonUpdateAdmin(admin *Admin, dbHandle *sql.DB) error {
@ -437,10 +447,6 @@ func sqlCommonUpdateAdmin(admin *Admin, dbHandle *sql.DB) error {
return err return err
} }
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
q := getUpdateAdminQuery()
perms, err := json.Marshal(admin.Permissions) perms, err := json.Marshal(admin.Permissions)
if err != nil { if err != nil {
return err return err
@ -451,9 +457,18 @@ func sqlCommonUpdateAdmin(admin *Admin, dbHandle *sql.DB) error {
return err return err
} }
_, err = dbHandle.ExecContext(ctx, q, admin.Password, admin.Status, admin.Email, string(perms), string(filters), ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
admin.AdditionalInfo, admin.Description, util.GetTimeAsMsSinceEpoch(time.Now()), admin.Username) defer cancel()
return err
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 { func sqlCommonDeleteAdmin(admin Admin, dbHandle *sql.DB) error {
@ -488,8 +503,11 @@ func sqlCommonGetAdmins(limit, offset int, order string, dbHandle sqlQuerier) ([
a.HideConfidentialData() a.HideConfidentialData()
admins = append(admins, a) admins = append(admins, a)
} }
err = rows.Err()
return admins, rows.Err() if err != nil {
return admins, err
}
return getAdminsWithGroups(ctx, admins, dbHandle)
} }
func sqlCommonDumpAdmins(dbHandle sqlQuerier) ([]Admin, error) { func sqlCommonDumpAdmins(dbHandle sqlQuerier) ([]Admin, error) {
@ -511,8 +529,11 @@ func sqlCommonDumpAdmins(dbHandle sqlQuerier) ([]Admin, error) {
} }
admins = append(admins, a) admins = append(admins, a)
} }
err = rows.Err()
return admins, rows.Err() if err != nil {
return admins, err
}
return getAdminsWithGroups(ctx, admins, dbHandle)
} }
func sqlCommonGetGroupByName(name string, dbHandle sqlQuerier) (Group, error) { func sqlCommonGetGroupByName(name string, dbHandle sqlQuerier) (Group, error) {
@ -530,7 +551,11 @@ func sqlCommonGetGroupByName(name string, dbHandle sqlQuerier) (Group, error) {
if err != nil { if err != nil {
return group, err 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) { func sqlCommonDumpGroups(dbHandle sqlQuerier) ([]Group, error) {
@ -664,6 +689,10 @@ func sqlCommonGetGroups(limit int, offset int, order string, minimal bool, dbHan
if err != nil { if err != nil {
return groups, err return groups, err
} }
groups, err = getGroupsWithAdmins(ctx, groups, dbHandle)
if err != nil {
return groups, err
}
for idx := range groups { for idx := range groups {
groups[idx].PrepareForRendering() groups[idx].PrepareForRendering()
} }
@ -2040,6 +2069,12 @@ func sqlCommonAddUserFolderMapping(ctx context.Context, user *User, folder *vfs.
return err 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 { func sqlCommonAddGroupFolderMapping(ctx context.Context, group *Group, folder *vfs.VirtualFolder, dbHandle sqlQuerier) error {
q := getAddGroupFolderMappingQuery() q := getAddGroupFolderMappingQuery()
_, err := dbHandle.ExecContext(ctx, q, folder.VirtualPath, folder.QuotaSize, folder.QuotaFiles, folder.Name, group.Name) _, 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 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 { func generateGroupVirtualFoldersMapping(ctx context.Context, group *Group, dbHandle sqlQuerier) error {
err := sqlCommonClearGroupFolderMapping(ctx, group, dbHandle) err := sqlCommonClearGroupFolderMapping(ctx, group, dbHandle)
if err != nil { if err != nil {
@ -2104,6 +2151,20 @@ func generateUserGroupMapping(ctx context.Context, user *User, dbHandle sqlQueri
return err 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, func getDefenderHostsWithScores(ctx context.Context, hosts []DefenderEntry, from int64, idForScores []int64,
dbHandle sqlQuerier) ( dbHandle sqlQuerier) (
[]DefenderEntry, []DefenderEntry,
@ -2152,6 +2213,57 @@ func getDefenderHostsWithScores(ctx context.Context, hosts []DefenderEntry, from
return result, nil 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) { func getUserWithVirtualFolders(ctx context.Context, user User, dbHandle sqlQuerier) (User, error) {
users, err := getUsersWithVirtualFolders(ctx, []User{user}, dbHandle) users, err := getUsersWithVirtualFolders(ctx, []User{user}, dbHandle)
if err != nil { if err != nil {
@ -2271,6 +2383,17 @@ func getGroupWithUsers(ctx context.Context, group Group, dbHandle sqlQuerier) (G
return groups[0], err 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) { func getGroupWithVirtualFolders(ctx context.Context, group Group, dbHandle sqlQuerier) (Group, error) {
groups, err := getGroupsWithVirtualFolders(ctx, []Group{group}, dbHandle) groups, err := getGroupsWithVirtualFolders(ctx, []Group{group}, dbHandle)
if err != nil { if err != nil {
@ -2368,6 +2491,40 @@ func getGroupsWithUsers(ctx context.Context, groups []Group, dbHandle sqlQuerier
return groups, err 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) { func getVirtualFoldersWithGroups(folders []vfs.BaseVirtualFolder, dbHandle sqlQuerier) ([]vfs.BaseVirtualFolder, error) {
if len(folders) == 0 { if len(folders) == 0 {
return folders, nil return folders, nil

View file

@ -41,6 +41,7 @@ const (
DROP TABLE IF EXISTS "{{folders_mapping}}"; DROP TABLE IF EXISTS "{{folders_mapping}}";
DROP TABLE IF EXISTS "{{users_folders_mapping}}"; DROP TABLE IF EXISTS "{{users_folders_mapping}}";
DROP TABLE IF EXISTS "{{users_groups_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 "{{groups_folders_mapping}}";
DROP TABLE IF EXISTS "{{admins}}"; DROP TABLE IF EXISTS "{{admins}}";
DROP TABLE IF EXISTS "{{folders}}"; 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;` ALTER TABLE "{{users}}" ADD COLUMN "first_upload" bigint DEFAULT 0 NOT NULL;`
sqliteV21DownSQL = `ALTER TABLE "{{users}}" DROP COLUMN "first_upload"; sqliteV21DownSQL = `ALTER TABLE "{{users}}" DROP COLUMN "first_upload";
ALTER TABLE "{{users}}" DROP COLUMN "first_download"; 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) providerLog(logger.LevelDebug, "sql database is up to date, current version: %v", version)
return ErrNoInitRequired return ErrNoInitRequired
case version < 19: 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) providerLog(logger.LevelError, "%v", err)
logger.ErrorToConsole("%v", err) logger.ErrorToConsole("%v", err)
return err return err
@ -620,15 +630,17 @@ func (p *SQLiteProvider) migrateDatabase() error { //nolint:dupl
return updateSQLiteDatabaseFromV19(p.dbHandle) return updateSQLiteDatabaseFromV19(p.dbHandle)
case version == 20: case version == 20:
return updateSQLiteDatabaseFromV20(p.dbHandle) return updateSQLiteDatabaseFromV20(p.dbHandle)
case version == 21:
return updateSQLiteDatabaseFromV21(p.dbHandle)
default: default:
if version > sqlDatabaseVersion { 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) 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) sqlDatabaseVersion)
return nil 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) return downgradeSQLiteDatabaseFromV20(p.dbHandle)
case 21: case 21:
return downgradeSQLiteDatabaseFromV21(p.dbHandle) return downgradeSQLiteDatabaseFromV21(p.dbHandle)
case 22:
return downgradeSQLiteDatabaseFromV22(p.dbHandle)
default: 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 { 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 { func downgradeSQLiteDatabaseFromV20(dbHandle *sql.DB) error {
@ -678,9 +699,16 @@ func downgradeSQLiteDatabaseFromV21(dbHandle *sql.DB) error {
return downgradeSQLiteDatabaseFromV20(dbHandle) 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 { func updateSQLiteDatabaseFrom19To20(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database version: 19 -> 20") logger.InfoToConsole("updating database schema version: 19 -> 20")
providerLog(logger.LevelInfo, "updating database version: 19 -> 20") providerLog(logger.LevelInfo, "updating database schema version: 19 -> 20")
sql := strings.ReplaceAll(sqliteV20SQL, "{{events_actions}}", sqlTableEventsActions) sql := strings.ReplaceAll(sqliteV20SQL, "{{events_actions}}", sqlTableEventsActions)
sql = strings.ReplaceAll(sql, "{{events_rules}}", sqlTableEventsRules) sql = strings.ReplaceAll(sql, "{{events_rules}}", sqlTableEventsRules)
sql = strings.ReplaceAll(sql, "{{rules_actions_mapping}}", sqlTableRulesActionsMapping) sql = strings.ReplaceAll(sql, "{{rules_actions_mapping}}", sqlTableRulesActionsMapping)
@ -691,15 +719,25 @@ func updateSQLiteDatabaseFrom19To20(dbHandle *sql.DB) error {
} }
func updateSQLiteDatabaseFrom20To21(dbHandle *sql.DB) error { func updateSQLiteDatabaseFrom20To21(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database version: 20 -> 21") logger.InfoToConsole("updating database schema version: 20 -> 21")
providerLog(logger.LevelInfo, "updating database version: 20 -> 21") providerLog(logger.LevelInfo, "updating database schema version: 20 -> 21")
sql := strings.ReplaceAll(sqliteV21SQL, "{{users}}", sqlTableUsers) sql := strings.ReplaceAll(sqliteV21SQL, "{{users}}", sqlTableUsers)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 21, true) 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 { func downgradeSQLiteDatabaseFrom20To19(dbHandle *sql.DB) error {
logger.InfoToConsole("downgrading database version: 20 -> 19") logger.InfoToConsole("downgrading database schema version: 20 -> 19")
providerLog(logger.LevelInfo, "downgrading database version: 20 -> 19") providerLog(logger.LevelInfo, "downgrading database schema version: 20 -> 19")
sql := strings.ReplaceAll(sqliteV20DownSQL, "{{events_actions}}", sqlTableEventsActions) sql := strings.ReplaceAll(sqliteV20DownSQL, "{{events_actions}}", sqlTableEventsActions)
sql = strings.ReplaceAll(sql, "{{events_rules}}", sqlTableEventsRules) sql = strings.ReplaceAll(sql, "{{events_rules}}", sqlTableEventsRules)
sql = strings.ReplaceAll(sql, "{{rules_actions_mapping}}", sqlTableRulesActionsMapping) sql = strings.ReplaceAll(sql, "{{rules_actions_mapping}}", sqlTableRulesActionsMapping)
@ -710,12 +748,19 @@ func downgradeSQLiteDatabaseFrom20To19(dbHandle *sql.DB) error {
} }
func downgradeSQLiteDatabaseFrom21To20(dbHandle *sql.DB) error { func downgradeSQLiteDatabaseFrom21To20(dbHandle *sql.DB) error {
logger.InfoToConsole("downgrading database version: 21 -> 20") logger.InfoToConsole("downgrading database schema version: 21 -> 20")
providerLog(logger.LevelInfo, "downgrading database version: 21 -> 20") providerLog(logger.LevelInfo, "downgrading database schema version: 21 -> 20")
sql := strings.ReplaceAll(sqliteV21DownSQL, "{{users}}", sqlTableUsers) sql := strings.ReplaceAll(sqliteV21DownSQL, "{{users}}", sqlTableUsers)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 20, false) 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 { /*func setPragmaFK(dbHandle *sql.DB, value string) error {
ctx, cancel := context.WithTimeout(context.Background(), longSQLQueryTimeout) ctx, cancel := context.WithTimeout(context.Background(), longSQLQueryTimeout)
defer cancel() defer cancel()

View file

@ -572,6 +572,18 @@ func getAddUserGroupMappingQuery() string {
sqlPlaceholders[1], sqlPlaceholders[2]) 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 { func getClearGroupFolderMappingQuery() string {
return fmt.Sprintf(`DELETE FROM %s WHERE group_id = (SELECT id FROM %s WHERE name = %s)`, sqlTableGroupsFoldersMapping, return fmt.Sprintf(`DELETE FROM %s WHERE group_id = (SELECT id FROM %s WHERE name = %s)`, sqlTableGroupsFoldersMapping,
getSQLQuotedName(sqlTableGroups), sqlPlaceholders[0]) 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()) 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 { func getRelatedFoldersForUsersQuery(users []User) string {
var sb strings.Builder var sb strings.Builder
for _, u := range users { 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()) 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 { func getRelatedFoldersForGroupsQuery(groups []Group) string {
var sb strings.Builder var sb strings.Builder
for _, g := range groups { for _, g := range groups {

View file

@ -1568,8 +1568,27 @@ func (u *User) HasSecondaryGroup(name string) bool {
return false 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) { func (u *User) applyGroupSettings(groupsMapping map[string]Group) {
if len(u.Groups) == 0 { if !u.hasSettingsFromGroups() {
return return
} }
if u.groupSettingsApplied { 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 // LoadAndApplyGroupSettings update the user by loading and applying the group settings
func (u *User) LoadAndApplyGroupSettings() error { func (u *User) LoadAndApplyGroupSettings() error {
if len(u.Groups) == 0 { if !u.hasSettingsFromGroups() {
return nil return nil
} }
if u.groupSettingsApplied { if u.groupSettingsApplied {
@ -1612,7 +1631,9 @@ func (u *User) LoadAndApplyGroupSettings() error {
if g.Type == sdk.GroupTypePrimary { if g.Type == sdk.GroupTypePrimary {
primaryGroupName = g.Name primaryGroupName = g.Name
} }
names = append(names, g.Name) if g.Type != sdk.GroupTypeMembership {
names = append(names, g.Name)
}
} }
groups, err := provider.getGroupsWithNames(names) groups, err := provider.getGroupsWithNames(names)
if err != nil { if err != nil {

View file

@ -2742,6 +2742,102 @@ func TestBasicAdminHandling(t *testing.T) {
assert.NoError(t, err) 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) { func TestChangeAdminPassword(t *testing.T) {
_, err := httpdtest.ChangeAdminPassword("wrong", defaultTokenAuthPass, http.StatusBadRequest) _, err := httpdtest.ChangeAdminPassword("wrong", defaultTokenAuthPass, http.StatusBadRequest)
assert.NoError(t, err) assert.NoError(t, err)
@ -6001,6 +6097,11 @@ func TestProviderErrors(t *testing.T) {
setJWTCookieForReq(req, testServerToken) setJWTCookieForReq(req, testServerToken)
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusInternalServerError, rr) 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) req, err = http.NewRequest(http.MethodGet, webGroupsPath+"?qlimit=a", nil)
assert.NoError(t, err) assert.NoError(t, err)
setJWTCookieForReq(req, testServerToken) setJWTCookieForReq(req, testServerToken)
@ -6529,6 +6630,11 @@ func TestLoaddata(t *testing.T) {
admin := getTestAdmin() admin := getTestAdmin()
admin.ID = 1 admin.ID = 1
admin.Username = "test_admin_restore" admin.Username = "test_admin_restore"
admin.Groups = []dataprovider.AdminGroupMapping{
{
Name: group.Name,
},
}
apiKey := dataprovider.APIKey{ apiKey := dataprovider.APIKey{
Name: util.GenerateUniqueID(), Name: util.GenerateUniqueID(),
Scope: dataprovider.APIKeyScopeAdmin, Scope: dataprovider.APIKeyScopeAdmin,
@ -6638,8 +6744,7 @@ func TestLoaddata(t *testing.T) {
admin, _, err = httpdtest.GetAdminByUsername(admin.Username, http.StatusOK) admin, _, err = httpdtest.GetAdminByUsername(admin.Username, http.StatusOK)
assert.NoError(t, err) assert.NoError(t, err)
_, err = httpdtest.RemoveAdmin(admin, http.StatusOK) assert.Len(t, admin.Groups, 1)
assert.NoError(t, err)
apiKey, _, err = httpdtest.GetAPIKeyByID(apiKey.KeyID, http.StatusOK) apiKey, _, err = httpdtest.GetAPIKeyByID(apiKey.KeyID, http.StatusOK)
assert.NoError(t, err) assert.NoError(t, err)
@ -6676,6 +6781,16 @@ func TestLoaddata(t *testing.T) {
} }
} }
assert.True(t, found) 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) { if assert.Len(t, dumpedData.Groups, 1) {
assert.Equal(t, len(group.VirtualFolders), len(dumpedData.Groups[0].VirtualFolders)) assert.Equal(t, len(group.VirtualFolders), len(dumpedData.Groups[0].VirtualFolders))
} }
@ -6714,6 +6829,8 @@ func TestLoaddata(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
_, err = httpdtest.RemoveEventAction(action, http.StatusOK) _, err = httpdtest.RemoveEventAction(action, http.StatusOK)
assert.NoError(t, err) assert.NoError(t, err)
_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)
assert.NoError(t, err)
err = os.Remove(backupFilePath) err = os.Remove(backupFilePath)
assert.NoError(t, err) assert.NoError(t, err)
@ -16315,6 +16432,86 @@ func TestWebAdminBasicMock(t *testing.T) {
assert.Contains(t, rr.Body.String(), "Invalid token") 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) { func TestWebAdminPermissions(t *testing.T) {
admin := getTestAdmin() admin := getTestAdmin()
admin.Username = altAdminUsername admin.Username = altAdminUsername
@ -16554,6 +16751,10 @@ func TestWebUserAddMock(t *testing.T) {
group2.Name += "_2" group2.Name += "_2"
group2, _, err = httpdtest.AddGroup(group2, http.StatusCreated) group2, _, err = httpdtest.AddGroup(group2, http.StatusCreated)
assert.NoError(t, err) assert.NoError(t, err)
group3 := getTestGroup()
group3.Name += "_3"
group3, _, err = httpdtest.AddGroup(group3, http.StatusCreated)
assert.NoError(t, err)
user := getTestUser() user := getTestUser()
user.UploadBandwidth = 32 user.UploadBandwidth = 32
user.DownloadBandwidth = 64 user.DownloadBandwidth = 64
@ -16584,6 +16785,7 @@ func TestWebUserAddMock(t *testing.T) {
form.Set("password", user.Password) form.Set("password", user.Password)
form.Set("primary_group", group1.Name) form.Set("primary_group", group1.Name)
form.Set("secondary_groups", group2.Name) form.Set("secondary_groups", group2.Name)
form.Set("membership_groups", group3.Name)
form.Set("status", strconv.Itoa(user.Status)) form.Set("status", strconv.Itoa(user.Status))
form.Set("expiration_date", "") form.Set("expiration_date", "")
form.Set("permissions", "*") 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) assert.Equal(t, sdk.TLSUsernameNone, newUser.Filters.TLSUsername)
req, _ = http.NewRequest(http.MethodDelete, path.Join(userPath, newUser.Username), nil) req, _ = http.NewRequest(http.MethodDelete, path.Join(userPath, newUser.Username), nil)
setBearerForReq(req, apiToken) setBearerForReq(req, apiToken)
@ -17002,6 +17204,8 @@ func TestWebUserAddMock(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
_, err = httpdtest.RemoveGroup(group2, http.StatusOK) _, err = httpdtest.RemoveGroup(group2, http.StatusOK)
assert.NoError(t, err) assert.NoError(t, err)
_, err = httpdtest.RemoveGroup(group3, http.StatusOK)
assert.NoError(t, err)
} }
func TestWebUserUpdateMock(t *testing.T) { func TestWebUserUpdateMock(t *testing.T) {

View file

@ -231,9 +231,10 @@ type userPage struct {
type adminPage struct { type adminPage struct {
basePage basePage
Admin *dataprovider.Admin Admin *dataprovider.Admin
Error string Groups []dataprovider.Group
IsAdd bool Error string
IsAdd bool
} }
type profilePage struct { 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, func (s *httpdServer) renderAddUpdateAdminPage(w http.ResponseWriter, r *http.Request, admin *dataprovider.Admin,
error string, isAdd bool) { error string, isAdd bool) {
groups, err := s.getWebGroups(w, r, defaultQueryLimit, true)
if err != nil {
return
}
currentURL := webAdminPath currentURL := webAdminPath
title := "Add a new admin" title := "Add a new admin"
if !isAdd { if !isAdd {
@ -776,6 +781,7 @@ func (s *httpdServer) renderAddUpdateAdminPage(w http.ResponseWriter, r *http.Re
data := adminPage{ data := adminPage{
basePage: s.getBasePageData(title, currentURL, r), basePage: s.getBasePageData(title, currentURL, r),
Admin: admin, Admin: admin,
Groups: groups,
Error: error, Error: error,
IsAdd: isAdd, IsAdd: isAdd,
} }
@ -816,8 +822,22 @@ func (s *httpdServer) renderUserPage(w http.ResponseWriter, r *http.Request, use
} }
} }
user.FsConfig.RedactedSecret = redactedSecret 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{ data := userPage{
basePage: s.getBasePageData(title, currentURL, r), basePage: basePage,
Mode: mode, Mode: mode,
Error: error, Error: error,
User: user, User: user,
@ -1265,7 +1285,13 @@ func getGroupsFromUserPostFields(r *http.Request) []sdk.GroupMapping {
Type: sdk.GroupTypeSecondary, Type: sdk.GroupTypeSecondary,
}) })
} }
membershipGroups := r.Form["membership_groups"]
for _, name := range membershipGroups {
groups = append(groups, sdk.GroupMapping{
Name: name,
Type: sdk.GroupTypeMembership,
})
}
return groups 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.Filters.AllowAPIKeyAuth = r.Form.Get("allow_api_key_auth") != ""
admin.AdditionalInfo = r.Form.Get("additional_info") admin.AdditionalInfo = r.Form.Get("additional_info")
admin.Description = r.Form.Get("description") 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 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 { 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 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 { func compareUserGroups(expected *dataprovider.User, actual *dataprovider.User) error {
if len(actual.Groups) != len(expected.Groups) { if len(actual.Groups) != len(expected.Groups) {
return errors.New("groups len mismatch") return errors.New("groups len mismatch")

View file

@ -5285,6 +5285,11 @@ components:
additional_info: additional_info:
type: string type: string
description: Free form text field 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: created_at:
type: integer type: integer
format: int64 format: int64
@ -5870,6 +5875,11 @@ components:
items: items:
type: string type: string
description: list of usernames associated with this group description: list of usernames associated with this group
admins:
type: array
items:
type: string
description: list of admins usernames associated with this group
GroupMapping: GroupMapping:
type: object type: object
properties: properties:
@ -5880,10 +5890,33 @@ components:
enum: enum:
- 1 - 1
- 2 - 2
- 3
description: | description: |
Group type: Group type:
* `1` - Primary group * `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: BackupData:
type: object type: object
properties: properties:

View file

@ -96,6 +96,72 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</div> </div>
</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"> <div class="form-group row">
<label for="idAllowedIP" class="col-sm-2 col-form-label">Allowed IP/Mask</label> <label for="idAllowedIP" class="col-sm-2 col-form-label">Allowed IP/Mask</label>
<div class="col-sm-10"> <div class="col-sm-10">
@ -138,4 +204,43 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{{define "extra_js"}} {{define "extra_js"}}
<script src="{{.StaticURL}}/vendor/bootstrap-select/js/bootstrap-select.min.js"></script> <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}} {{end}}

View file

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

View file

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

View file

@ -161,7 +161,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<b>Groups</b> <b>Groups</b>
</div> </div>
<div class="card-body"> <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"> <div class="form-group row">
<label for="idPrimaryGroup" class="col-sm-2 col-form-label">Primary group</label> <label for="idPrimaryGroup" class="col-sm-2 col-form-label">Primary group</label>
<div class="col-sm-10"> <div class="col-sm-10">
@ -183,6 +183,16 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</select> </select>
</div> </div>
</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>
</div> </div>
@ -317,17 +327,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</div> </div>
</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"> <div class="form-group row">
<label for="idDescription" class="col-sm-2 col-form-label">Description</label> <label for="idDescription" class="col-sm-2 col-form-label">Description</label>
<div class="col-sm-10"> <div class="col-sm-10">
@ -985,6 +984,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</div> </div>
</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}}"> <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> <label for="idExtAuthCacheTime" class="col-sm-2 col-form-label">External auth cache time</label>
<div class="col-sm-10"> <div class="col-sm-10">