mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-28 18:40:35 +00:00
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:
parent
bd585d8e52
commit
ea3c1d7a3b
24 changed files with 1320 additions and 203 deletions
|
@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -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
18
go.mod
|
@ -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
31
go.sum
|
@ -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=
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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...)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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{
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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}}
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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">
|
||||||
|
|
Loading…
Reference in a new issue