add roles
Fixes #837 Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
a9207857cf
commit
5a222807b7
83 changed files with 4285 additions and 806 deletions
|
@ -35,10 +35,6 @@ RUN apk add --update --no-cache ca-certificates tzdata mailcap
|
||||||
|
|
||||||
RUN if [ "${INSTALL_OPTIONAL_PACKAGES}" = "true" ]; then apk add --update --no-cache jq git rsync; fi
|
RUN if [ "${INSTALL_OPTIONAL_PACKAGES}" = "true" ]; then apk add --update --no-cache jq git rsync; fi
|
||||||
|
|
||||||
# set up nsswitch.conf for Go's "netgo" implementation
|
|
||||||
# https://github.com/gliderlabs/docker-alpine/issues/367#issuecomment-424546457
|
|
||||||
RUN test ! -e /etc/nsswitch.conf && echo 'hosts: files dns' > /etc/nsswitch.conf
|
|
||||||
|
|
||||||
RUN mkdir -p /etc/sftpgo /var/lib/sftpgo /usr/share/sftpgo /srv/sftpgo/data /srv/sftpgo/backups
|
RUN mkdir -p /etc/sftpgo /var/lib/sftpgo /usr/share/sftpgo /srv/sftpgo/data /srv/sftpgo/backups
|
||||||
|
|
||||||
RUN addgroup -g 1000 -S sftpgo && \
|
RUN addgroup -g 1000 -S sftpgo && \
|
||||||
|
|
|
@ -64,6 +64,7 @@ If you report an invalid issue or ask for step-by-step support, your issue will
|
||||||
- Per-user authentication methods.
|
- Per-user authentication methods.
|
||||||
- [Two-factor authentication](./docs/howto/two-factor-authentication.md) based on time-based one time passwords (RFC 6238) which works with Authy, Google Authenticator and other compatible apps.
|
- [Two-factor authentication](./docs/howto/two-factor-authentication.md) based on time-based one time passwords (RFC 6238) which works with Authy, Google Authenticator and other compatible apps.
|
||||||
- Simplified user administrations using [groups](./docs/groups.md).
|
- Simplified user administrations using [groups](./docs/groups.md).
|
||||||
|
- [Roles](./docs/roles.md) allow you to create limited administrators who can only create and manage users with their role.
|
||||||
- Custom authentication via [external programs/HTTP API](./docs/external-auth.md).
|
- Custom authentication via [external programs/HTTP API](./docs/external-auth.md).
|
||||||
- Web Client and Web Admin user interfaces support [OpenID Connect](https://openid.net/connect/) authentication and so they can be integrated with identity providers such as [Keycloak](https://www.keycloak.org/). You can find more details [here](./docs/oidc.md).
|
- Web Client and Web Admin user interfaces support [OpenID Connect](https://openid.net/connect/) authentication and so they can be integrated with identity providers such as [Keycloak](https://www.keycloak.org/). You can find more details [here](./docs/oidc.md).
|
||||||
- [Data At Rest Encryption](./docs/dare.md).
|
- [Data At Rest Encryption](./docs/dare.md).
|
||||||
|
@ -134,7 +135,7 @@ APT and YUM repositories are [available](./docs/repo.md).
|
||||||
SFTPGo is also available on some marketplaces:
|
SFTPGo is also available on some marketplaces:
|
||||||
|
|
||||||
- [AWS Marketplace](https://aws.amazon.com/marketplace/seller-profile?id=6e849ab8-70a6-47de-9a43-13c3fa849335)
|
- [AWS Marketplace](https://aws.amazon.com/marketplace/seller-profile?id=6e849ab8-70a6-47de-9a43-13c3fa849335)
|
||||||
- [Azure Marketplace](https://azuremarketplace.microsoft.com/en-us/marketplace/apps/prasselsrl1645470739547.sftpgo_linux)
|
- [Azure Marketplace](https://azuremarketplace.microsoft.com/en-us/marketplace/apps/eliamarzia1667381463185.sftpgo_linux)
|
||||||
- [Elest.io](https://elest.io/open-source/sftpgo)
|
- [Elest.io](https://elest.io/open-source/sftpgo)
|
||||||
|
|
||||||
Purchasing from there will help keep SFTPGo a long-term sustainable project.
|
Purchasing from there will help keep SFTPGo a long-term sustainable project.
|
||||||
|
|
|
@ -125,7 +125,7 @@ SFTPGo 基于 Linux 开发和创建。在每一次提交之后,代码会自动
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
SFTPGo 在 [AWS Marketplace](https://aws.amazon.com/marketplace/seller-profile?id=6e849ab8-70a6-47de-9a43-13c3fa849335) 和 [Azure Marketplace](https://azuremarketplace.microsoft.com/en-us/marketplace/apps/prasselsrl1645470739547.sftpgo_linux) 同样可用,在此付费可以帮助 SFTPGo 成为一个可持续发展的长期项目。
|
SFTPGo 在 [AWS Marketplace](https://aws.amazon.com/marketplace/seller-profile?id=6e849ab8-70a6-47de-9a43-13c3fa849335) 和 [Azure Marketplace](https://azuremarketplace.microsoft.com/en-us/marketplace/apps/eliamarzia1667381463185.sftpgo_linux) 同样可用,在此付费可以帮助 SFTPGo 成为一个可持续发展的长期项目。
|
||||||
|
|
||||||
<details><summary>Windows 包</summary>
|
<details><summary>Windows 包</summary>
|
||||||
|
|
||||||
|
|
|
@ -4,12 +4,12 @@ SFTPGo provides an official Docker image, it is available on both [Docker Hub](h
|
||||||
|
|
||||||
## Supported tags and respective Dockerfile links
|
## Supported tags and respective Dockerfile links
|
||||||
|
|
||||||
- [v2.4.0, v2.4, v2, latest](https://github.com/drakkan/sftpgo/blob/v2.4.0/Dockerfile)
|
- [v2.4.1, v2.4, v2, latest](https://github.com/drakkan/sftpgo/blob/v2.4.1/Dockerfile)
|
||||||
- [v2.4.0-plugins, v2.4-plugins, v2-plugins, plugins](https://github.com/drakkan/sftpgo/blob/v2.4.0/Dockerfile)
|
- [v2.4.1-plugins, v2.4-plugins, v2-plugins, plugins](https://github.com/drakkan/sftpgo/blob/v2.4.1/Dockerfile)
|
||||||
- [v2.4.0-alpine, v2.4-alpine, v2-alpine, alpine](https://github.com/drakkan/sftpgo/blob/v2.4.0/Dockerfile.alpine)
|
- [v2.4.1-alpine, v2.4-alpine, v2-alpine, alpine](https://github.com/drakkan/sftpgo/blob/v2.4.1/Dockerfile.alpine)
|
||||||
- [v2.4.0-slim, v2.4-slim, v2-slim, slim](https://github.com/drakkan/sftpgo/blob/v2.4.0/Dockerfile)
|
- [v2.4.1-slim, v2.4-slim, v2-slim, slim](https://github.com/drakkan/sftpgo/blob/v2.4.1/Dockerfile)
|
||||||
- [v2.4.0-alpine-slim, v2.4-alpine-slim, v2-alpine-slim, alpine-slim](https://github.com/drakkan/sftpgo/blob/v2.4.0/Dockerfile.alpine)
|
- [v2.4.1-alpine-slim, v2.4-alpine-slim, v2-alpine-slim, alpine-slim](https://github.com/drakkan/sftpgo/blob/v2.4.1/Dockerfile.alpine)
|
||||||
- [v2.4.0-distroless-slim, v2.4-distroless-slim, v2-distroless-slim, distroless-slim](https://github.com/drakkan/sftpgo/blob/v2.4.0/Dockerfile.distroless)
|
- [v2.4.1-distroless-slim, v2.4-distroless-slim, v2-distroless-slim, distroless-slim](https://github.com/drakkan/sftpgo/blob/v2.4.1/Dockerfile.distroless)
|
||||||
- [edge](../Dockerfile)
|
- [edge](../Dockerfile)
|
||||||
- [edge-plugins](../Dockerfile)
|
- [edge-plugins](../Dockerfile)
|
||||||
- [edge-alpine](../Dockerfile.alpine)
|
- [edge-alpine](../Dockerfile.alpine)
|
||||||
|
|
|
@ -65,7 +65,7 @@ The configuration file contains the following sections:
|
||||||
- `hook`, string. Absolute path to the command to execute or HTTP URL to notify.
|
- `hook`, string. Absolute path to the command to execute or HTTP URL to notify.
|
||||||
- `setstat_mode`, integer. 0 means "normal mode": requests for changing permissions, owner/group and access/modification times are executed. 1 means "ignore mode": requests for changing permissions, owner/group and access/modification times are silently ignored. 2 means "ignore mode if not supported": requests for changing permissions and owner/group are silently ignored for cloud filesystems and executed for local/SFTP filesystem. Requests for changing modification times are always executed for local/SFTP filesystems and are executed for cloud based filesystems if the target is a file and there is a metadata plugin available. A metadata plugin can be found [here](https://github.com/sftpgo/sftpgo-plugin-metadata).
|
- `setstat_mode`, integer. 0 means "normal mode": requests for changing permissions, owner/group and access/modification times are executed. 1 means "ignore mode": requests for changing permissions, owner/group and access/modification times are silently ignored. 2 means "ignore mode if not supported": requests for changing permissions and owner/group are silently ignored for cloud filesystems and executed for local/SFTP filesystem. Requests for changing modification times are always executed for local/SFTP filesystems and are executed for cloud based filesystems if the target is a file and there is a metadata plugin available. A metadata plugin can be found [here](https://github.com/sftpgo/sftpgo-plugin-metadata).
|
||||||
- `temp_path`, string. Defines the path for temporary files such as those used for atomic uploads or file pipes. If you set this option you must make sure that the defined path exists, is accessible for writing by the user running SFTPGo, and is on the same filesystem as the users home directories otherwise the renaming for atomic uploads will become a copy and therefore may take a long time. The temporary files are not namespaced. The default is generally fine. Leave empty for the default.
|
- `temp_path`, string. Defines the path for temporary files such as those used for atomic uploads or file pipes. If you set this option you must make sure that the defined path exists, is accessible for writing by the user running SFTPGo, and is on the same filesystem as the users home directories otherwise the renaming for atomic uploads will become a copy and therefore may take a long time. The temporary files are not namespaced. The default is generally fine. Leave empty for the default.
|
||||||
- `proxy_protocol`, integer. Support for [HAProxy PROXY protocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt). If you are running SFTPGo behind a proxy server such as HAProxy, AWS ELB or NGINX, you can enable the proxy protocol. It provides a convenient way to safely transport connection information such as a client's address across multiple layers of NAT or TCP proxies to get the real client IP address instead of the proxy IP. Both protocol versions 1 and 2 are supported. If the proxy protocol is enabled in SFTPGo then you have to enable the protocol in your proxy configuration too. For example, for HAProxy, add `send-proxy` or `send-proxy-v2` to each server configuration line. The following modes are supported:
|
- `proxy_protocol`, integer. Support for [HAProxy PROXY protocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt). If you are running SFTPGo behind a proxy server such as HAProxy, AWS ELB or NGINX, you can enable the proxy protocol. It provides a convenient way to safely transport connection information such as a client's address across multiple layers of NAT or TCP proxies to get the real client IP address instead of the proxy IP. Both protocol versions 1 and 2 are supported. If the proxy protocol is enabled in SFTPGo then you have to enable the protocol in your proxy configuration too. For example, for HAProxy, add `send-proxy` or `send-proxy-v2` to each server configuration line. The PROXY protocol is supported for SSH/SFTP and FTP/S. The following modes are supported:
|
||||||
- 0, disabled
|
- 0, disabled
|
||||||
- 1, enabled. If the upstream IP is not allowed to send a proxy header the header be ignored. Using this mode does not mean that we can accept connections with and without the proxy header. We always try to read the proxy header and we ignore it if the upstream IP is not allowed to send a proxy header
|
- 1, enabled. If the upstream IP is not allowed to send a proxy header the header be ignored. Using this mode does not mean that we can accept connections with and without the proxy header. We always try to read the proxy header and we ignore it if the upstream IP is not allowed to send a proxy header
|
||||||
- 2, required. If the upstream IP is not allowed to send a proxy header the connection will be rejected
|
- 2, required. If the upstream IP is not allowed to send a proxy header the connection will be rejected
|
||||||
|
|
13
docs/roles.md
Normal file
13
docs/roles.md
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
# Roles
|
||||||
|
|
||||||
|
Roles can be assigned to users and administrators. Admins with a role are limited administrators, they can only view and manage users with their own role and they cannot have the following permissions:
|
||||||
|
|
||||||
|
- manage_admins
|
||||||
|
- manage_system
|
||||||
|
- manage_event_rules
|
||||||
|
- manage_roles
|
||||||
|
- view_events
|
||||||
|
|
||||||
|
Users created by role administrators automatically inherit their role.
|
||||||
|
|
||||||
|
Admins without a role are global administrators and can manage all users (with and without a role) and assign a specific role to users.
|
|
@ -47,3 +47,5 @@ python convertusers proftpd.passwd proftpd pro_users.json
|
||||||
The generated json file can be used as input for the `loaddata` REST API.
|
The generated json file can be used as input for the `loaddata` REST API.
|
||||||
|
|
||||||
Please note that when importing Linux/Unix users the input file is not required: `/etc/passwd` and `/etc/shadow` are automatically parsed. `/etc/shadow` read permission is typically granted to the `root` user only, so you need to execute `convertusers` as `root`.
|
Please note that when importing Linux/Unix users the input file is not required: `/etc/passwd` and `/etc/shadow` are automatically parsed. `/etc/shadow` read permission is typically granted to the `root` user only, so you need to execute `convertusers` as `root`.
|
||||||
|
|
||||||
|
:warning: SFTPGo does not currently support `yescrypt` hashed passwords.
|
||||||
|
|
73
go.mod
73
go.mod
|
@ -3,21 +3,21 @@ module github.com/drakkan/sftpgo/v2
|
||||||
go 1.19
|
go 1.19
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go/storage v1.27.0
|
cloud.google.com/go/storage v1.28.0
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.2.0
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.2.0
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.5.1
|
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.5.1
|
||||||
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
|
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
|
||||||
github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387
|
github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387
|
||||||
github.com/aws/aws-sdk-go-v2 v1.17.1
|
github.com/aws/aws-sdk-go-v2 v1.17.1
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.17.10
|
github.com/aws/aws-sdk-go-v2/config v1.18.0
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.12.23
|
github.com/aws/aws-sdk-go-v2/credentials v1.13.0
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.19
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.19
|
||||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.37
|
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.39
|
||||||
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.21
|
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.22
|
||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.29.1
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.29.2
|
||||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.16.4
|
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.16.5
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.17.1
|
github.com/aws/aws-sdk-go-v2/service/sts v1.17.2
|
||||||
github.com/cockroachdb/cockroach-go/v2 v2.2.16
|
github.com/cockroachdb/cockroach-go/v2 v2.2.18
|
||||||
github.com/coreos/go-oidc/v3 v3.4.0
|
github.com/coreos/go-oidc/v3 v3.4.0
|
||||||
github.com/drakkan/webdav v0.0.0-20221101181759-17ed21f9337b
|
github.com/drakkan/webdav v0.0.0-20221101181759-17ed21f9337b
|
||||||
github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001
|
github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001
|
||||||
|
@ -25,62 +25,62 @@ require (
|
||||||
github.com/fclairamb/go-log v0.4.1
|
github.com/fclairamb/go-log v0.4.1
|
||||||
github.com/go-acme/lego/v4 v4.9.0
|
github.com/go-acme/lego/v4 v4.9.0
|
||||||
github.com/go-chi/chi/v5 v5.0.8-0.20221018120124-e5529d9db4d3
|
github.com/go-chi/chi/v5 v5.0.8-0.20221018120124-e5529d9db4d3
|
||||||
github.com/go-chi/jwtauth/v5 v5.0.2
|
github.com/go-chi/jwtauth/v5 v5.1.0
|
||||||
github.com/go-chi/render v1.0.2
|
github.com/go-chi/render v1.0.2
|
||||||
github.com/go-sql-driver/mysql v1.6.0
|
github.com/go-sql-driver/mysql v1.6.0
|
||||||
github.com/golang/mock v1.6.0
|
github.com/golang/mock v1.6.0
|
||||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
||||||
github.com/google/uuid v1.3.0
|
github.com/google/uuid v1.3.0
|
||||||
github.com/hashicorp/go-hclog v1.3.1
|
github.com/hashicorp/go-hclog v1.3.1
|
||||||
github.com/hashicorp/go-plugin v1.4.5
|
github.com/hashicorp/go-plugin v1.4.6
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.1
|
github.com/hashicorp/go-retryablehttp v0.7.1
|
||||||
github.com/jackc/pgx/v5 v5.0.4
|
github.com/jackc/pgx/v5 v5.1.0
|
||||||
github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126
|
github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126
|
||||||
github.com/klauspost/compress v1.15.12
|
github.com/klauspost/compress v1.15.12
|
||||||
github.com/lestrrat-go/jwx v1.2.25
|
github.com/lestrrat-go/jwx/v2 v2.0.7
|
||||||
github.com/lithammer/shortuuid/v3 v3.0.7
|
github.com/lithammer/shortuuid/v3 v3.0.7
|
||||||
github.com/mattn/go-sqlite3 v1.14.16
|
github.com/mattn/go-sqlite3 v1.14.16
|
||||||
github.com/mhale/smtpd v0.8.0
|
github.com/mhale/smtpd v0.8.0
|
||||||
github.com/minio/sio v0.3.0
|
github.com/minio/sio v0.3.0
|
||||||
github.com/otiai10/copy v1.7.0
|
github.com/otiai10/copy v1.9.0
|
||||||
github.com/pires/go-proxyproto v0.6.2
|
github.com/pires/go-proxyproto v0.6.2
|
||||||
github.com/pkg/sftp v1.13.6-0.20221020054726-e4133ab7e9bd
|
github.com/pkg/sftp v1.13.6-0.20221020054726-e4133ab7e9bd
|
||||||
github.com/pquerna/otp v1.3.0
|
github.com/pquerna/otp v1.3.0
|
||||||
github.com/prometheus/client_golang v1.13.1
|
github.com/prometheus/client_golang v1.14.0
|
||||||
github.com/robfig/cron/v3 v3.0.1
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
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.3-0.20221105153737-bae9afc6b356
|
github.com/sftpgo/sdk v0.1.3-0.20221116180328-3fc64e926700
|
||||||
github.com/shirou/gopsutil/v3 v3.22.10
|
github.com/shirou/gopsutil/v3 v3.22.10
|
||||||
github.com/spf13/afero v1.9.2
|
github.com/spf13/afero v1.9.3
|
||||||
github.com/spf13/cobra v1.6.1
|
github.com/spf13/cobra v1.6.1
|
||||||
github.com/spf13/viper v1.13.0
|
github.com/spf13/viper v1.14.0
|
||||||
github.com/stretchr/testify v1.8.1
|
github.com/stretchr/testify v1.8.1
|
||||||
github.com/studio-b12/gowebdav v0.0.0-20221102155456-200a600c0272
|
github.com/studio-b12/gowebdav v0.0.0-20221109171924-60ec5ad56012
|
||||||
github.com/subosito/gotenv v1.4.1
|
github.com/subosito/gotenv v1.4.1
|
||||||
github.com/unrolled/secure v1.13.0
|
github.com/unrolled/secure v1.13.0
|
||||||
github.com/wagslane/go-password-validator v0.3.0
|
github.com/wagslane/go-password-validator v0.3.0
|
||||||
github.com/xhit/go-simple-mail/v2 v2.12.0
|
github.com/xhit/go-simple-mail/v2 v2.13.0
|
||||||
github.com/yl2chen/cidranger v1.0.3-0.20210928021809-d1cb2c52f37a
|
github.com/yl2chen/cidranger v1.0.3-0.20210928021809-d1cb2c52f37a
|
||||||
go.etcd.io/bbolt v1.3.6
|
go.etcd.io/bbolt v1.3.6
|
||||||
go.uber.org/automaxprocs v1.5.1
|
go.uber.org/automaxprocs v1.5.1
|
||||||
gocloud.dev v0.27.0
|
gocloud.dev v0.27.0
|
||||||
golang.org/x/crypto v0.1.0
|
golang.org/x/crypto v0.2.0
|
||||||
golang.org/x/net v0.1.0
|
golang.org/x/net v0.2.0
|
||||||
golang.org/x/oauth2 v0.1.0
|
golang.org/x/oauth2 v0.2.0
|
||||||
golang.org/x/sys v0.1.0
|
golang.org/x/sys v0.2.0
|
||||||
golang.org/x/time v0.1.0
|
golang.org/x/time v0.2.0
|
||||||
google.golang.org/api v0.102.0
|
google.golang.org/api v0.103.0
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go v0.105.0 // indirect
|
cloud.google.com/go v0.107.0 // indirect
|
||||||
cloud.google.com/go/compute v1.12.1 // indirect
|
cloud.google.com/go/compute v1.12.1 // indirect
|
||||||
cloud.google.com/go/compute/metadata v0.2.1 // indirect
|
cloud.google.com/go/compute/metadata v0.2.1 // indirect
|
||||||
cloud.google.com/go/iam v0.7.0 // indirect
|
cloud.google.com/go/iam v0.7.0 // indirect
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.1 // indirect
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.1 // indirect
|
||||||
github.com/ajg/form v1.5.1 // indirect
|
github.com/ajg/form v1.5.1 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.9 // indirect
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.9 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.25 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.25 // indirect
|
||||||
|
@ -98,7 +98,7 @@ require (
|
||||||
github.com/boombuler/barcode v1.0.1 // indirect
|
github.com/boombuler/barcode v1.0.1 // indirect
|
||||||
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
|
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||||
github.com/coreos/go-systemd/v22 v22.4.0 // indirect
|
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect
|
||||||
|
@ -119,11 +119,11 @@ require (
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
|
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
|
||||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.1.2 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.1 // indirect
|
||||||
github.com/kr/fs v0.1.0 // indirect
|
github.com/kr/fs v0.1.0 // indirect
|
||||||
github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect
|
|
||||||
github.com/lestrrat-go/blackmagic v1.0.1 // indirect
|
github.com/lestrrat-go/blackmagic v1.0.1 // indirect
|
||||||
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||||
|
github.com/lestrrat-go/httprc v1.0.4 // 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/lib/pq v1.10.7 // indirect
|
github.com/lib/pq v1.10.7 // indirect
|
||||||
|
@ -139,7 +139,6 @@ require (
|
||||||
github.com/oklog/run v1.1.0 // indirect
|
github.com/oklog/run v1.1.0 // indirect
|
||||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.0.5 // indirect
|
github.com/pelletier/go-toml/v2 v2.0.5 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c // indirect
|
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c // indirect
|
||||||
github.com/prometheus/client_model v0.3.0 // indirect
|
github.com/prometheus/client_model v0.3.0 // indirect
|
||||||
|
@ -149,17 +148,17 @@ require (
|
||||||
github.com/spf13/cast v1.5.0 // indirect
|
github.com/spf13/cast v1.5.0 // indirect
|
||||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
github.com/tklauser/go-sysconf v0.3.10 // indirect
|
github.com/tklauser/go-sysconf v0.3.11 // indirect
|
||||||
github.com/tklauser/numcpus v0.5.0 // indirect
|
github.com/tklauser/numcpus v0.6.0 // indirect
|
||||||
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 // indirect
|
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 // indirect
|
||||||
github.com/yusufpapurcu/wmi v1.2.2 // indirect
|
github.com/yusufpapurcu/wmi v1.2.2 // indirect
|
||||||
go.opencensus.io v0.24.0 // indirect
|
go.opencensus.io v0.24.0 // indirect
|
||||||
golang.org/x/mod v0.6.0 // indirect
|
golang.org/x/mod v0.7.0 // indirect
|
||||||
golang.org/x/text v0.4.0 // indirect
|
golang.org/x/text v0.4.0 // indirect
|
||||||
golang.org/x/tools v0.2.0 // indirect
|
golang.org/x/tools v0.3.0 // 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-20221027153422-115e99e71e1c // indirect
|
google.golang.org/genproto v0.0.0-20221114212237-e4508ebdbee1 // indirect
|
||||||
google.golang.org/grpc v1.50.1 // indirect
|
google.golang.org/grpc v1.50.1 // 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
|
||||||
|
|
158
go.sum
158
go.sum
|
@ -36,8 +36,8 @@ cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w9
|
||||||
cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc=
|
cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc=
|
||||||
cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU=
|
cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU=
|
||||||
cloud.google.com/go v0.103.0/go.mod h1:vwLx1nqLrzLX/fpwSMOXmFIqBOyHsvHbnAdbGSJ+mKk=
|
cloud.google.com/go v0.103.0/go.mod h1:vwLx1nqLrzLX/fpwSMOXmFIqBOyHsvHbnAdbGSJ+mKk=
|
||||||
cloud.google.com/go v0.105.0 h1:DNtEKRBAAzeS4KyIory52wWHuClNaXJ5x1F7xa4q+5Y=
|
cloud.google.com/go v0.107.0 h1:qkj22L7bgkl6vIeZDlOY2po43Mx/TIa2Wsa7VR+PEww=
|
||||||
cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM=
|
cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I=
|
||||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||||
|
@ -63,8 +63,8 @@ cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp
|
||||||
cloud.google.com/go/iam v0.7.0 h1:k4MuwOsS7zGJJ+QfZ5vBK8SgHBAvYN/23BWsiihJ1vs=
|
cloud.google.com/go/iam v0.7.0 h1:k4MuwOsS7zGJJ+QfZ5vBK8SgHBAvYN/23BWsiihJ1vs=
|
||||||
cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg=
|
cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg=
|
||||||
cloud.google.com/go/kms v1.4.0/go.mod h1:fajBHndQ+6ubNw6Ss2sSd+SWvjL26RNo/dr7uxsnnOA=
|
cloud.google.com/go/kms v1.4.0/go.mod h1:fajBHndQ+6ubNw6Ss2sSd+SWvjL26RNo/dr7uxsnnOA=
|
||||||
cloud.google.com/go/kms v1.5.0 h1:uc58n3b/n/F2yDMJzHMbXORkJSh3fzO4/+jju6eR7Zg=
|
cloud.google.com/go/kms v1.6.0 h1:OWRZzrPmOZUzurjI2FBGtgY2mB1WaJkqhw6oIwSj0Yg=
|
||||||
cloud.google.com/go/longrunning v0.1.1 h1:y50CXG4j0+qvEukslYFBCrzaXX0qpFbBzc3PchSu/LE=
|
cloud.google.com/go/longrunning v0.3.0 h1:NjljC+FYPV3uh5/OwWT6pVU+doBqMg2x/rZlE+CamDs=
|
||||||
cloud.google.com/go/monitoring v1.1.0/go.mod h1:L81pzz7HKn14QCMaCs6NTQkdBnE87TElyanS95vIcl4=
|
cloud.google.com/go/monitoring v1.1.0/go.mod h1:L81pzz7HKn14QCMaCs6NTQkdBnE87TElyanS95vIcl4=
|
||||||
cloud.google.com/go/monitoring v1.5.0/go.mod h1:/o9y8NYX5j91JjD/JvGLYbi86kL11OjyJXq2XziLJu4=
|
cloud.google.com/go/monitoring v1.5.0/go.mod h1:/o9y8NYX5j91JjD/JvGLYbi86kL11OjyJXq2XziLJu4=
|
||||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||||
|
@ -82,8 +82,8 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f
|
||||||
cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y=
|
cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y=
|
||||||
cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc=
|
cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc=
|
||||||
cloud.google.com/go/storage v1.24.0/go.mod h1:3xrJEFMXBsQLgxwThyjuD3aYlroL0TMRec1ypGUQ0KE=
|
cloud.google.com/go/storage v1.24.0/go.mod h1:3xrJEFMXBsQLgxwThyjuD3aYlroL0TMRec1ypGUQ0KE=
|
||||||
cloud.google.com/go/storage v1.27.0 h1:YOO045NZI9RKfCj1c5A/ZtuuENUc8OAW+gHdGnDgyMQ=
|
cloud.google.com/go/storage v1.28.0 h1:DLrIZ6xkeZX6K70fU/boWx5INJumt6f+nwwWSHXzzGY=
|
||||||
cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s=
|
cloud.google.com/go/storage v1.28.0/go.mod h1:qlgZML35PXA3zoEnIkiPLY4/TOkUleufRlu6qmcf7sI=
|
||||||
cloud.google.com/go/trace v1.0.0/go.mod h1:4iErSByzxkyHWzzlAj63/Gmjz0NH1ASqhJguHpGcr6A=
|
cloud.google.com/go/trace v1.0.0/go.mod h1:4iErSByzxkyHWzzlAj63/Gmjz0NH1ASqhJguHpGcr6A=
|
||||||
cloud.google.com/go/trace v1.2.0/go.mod h1:Wc8y/uYyOhPy12KEnXG9XGrvfMz5F5SrYecQlbW1rwM=
|
cloud.google.com/go/trace v1.2.0/go.mod h1:Wc8y/uYyOhPy12KEnXG9XGrvfMz5F5SrYecQlbW1rwM=
|
||||||
code.cloudfoundry.org/clock v0.0.0-20180518195852-02e53af36e6c/go.mod h1:QD9Lzhd/ux6eNQVUDVRJX/RKTigpewimNYBi7ivZKY8=
|
code.cloudfoundry.org/clock v0.0.0-20180518195852-02e53af36e6c/go.mod h1:QD9Lzhd/ux6eNQVUDVRJX/RKTigpewimNYBi7ivZKY8=
|
||||||
|
@ -108,8 +108,8 @@ github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.0.0/go.mod h1:+6sju8gk8FRmSa
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.1.0 h1:QkAcEIAKbNL4KoFr4SathZPhDhF4mVwpBMFlYjyAqy8=
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.1.0 h1:QkAcEIAKbNL4KoFr4SathZPhDhF4mVwpBMFlYjyAqy8=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8=
|
github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w=
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.1 h1:XUNQ4mw+zJmaA2KXzP9JlQiecy1SI+Eog7xVkPiqIbg=
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.1 h1:Oj853U9kG+RLTCQXpjvOnrv0WaZHxgmZz1TlLywgOPY=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.1/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w=
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.1/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.0.2/go.mod h1:LH9XQnMr2ZYxQdVdCrzLO9mxeDyrDFa6wbSI3x5zCZk=
|
github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.0.2/go.mod h1:LH9XQnMr2ZYxQdVdCrzLO9mxeDyrDFa6wbSI3x5zCZk=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.4.1/go.mod h1:eZ4g6GUvXiGulfIbbhh1Xr4XwUYaYaWMqzGD/284wCA=
|
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.4.1/go.mod h1:eZ4g6GUvXiGulfIbbhh1Xr4XwUYaYaWMqzGD/284wCA=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.5.1 h1:BMTdr+ib5ljLa9MxTJK8x/Ds0MbBb4MfuW5BL0zMJnI=
|
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.5.1 h1:BMTdr+ib5ljLa9MxTJK8x/Ds0MbBb4MfuW5BL0zMJnI=
|
||||||
|
@ -233,17 +233,17 @@ github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.3/go.mod h1:gNsR5CaXK
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.9 h1:RKci2D7tMwpvGpDNZnGQw9wk6v7o/xSwFcUAuNPoB8k=
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.9 h1:RKci2D7tMwpvGpDNZnGQw9wk6v7o/xSwFcUAuNPoB8k=
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.9/go.mod h1:vCmV1q1VK8eoQJ5+aYE7PkK1K6v41qJ5pJdK3ggCDvg=
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.9/go.mod h1:vCmV1q1VK8eoQJ5+aYE7PkK1K6v41qJ5pJdK3ggCDvg=
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.15.15/go.mod h1:A1Lzyy/o21I5/s2FbyX5AevQfSVXpvvIDCoVFD0BC4E=
|
github.com/aws/aws-sdk-go-v2/config v1.15.15/go.mod h1:A1Lzyy/o21I5/s2FbyX5AevQfSVXpvvIDCoVFD0BC4E=
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.17.10 h1:zBy5QQ/mkvHElM1rygHPAzuH+sl8nsdSaxSWj0+rpdE=
|
github.com/aws/aws-sdk-go-v2/config v1.18.0 h1:ULASZmfhKR/QE9UeZ7mzYjUzsnIydy/K1YMT6uH1KC0=
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.17.10/go.mod h1:/4np+UiJJKpWHN7Q+LZvqXYgyjgeXm5+lLfDI6TPZao=
|
github.com/aws/aws-sdk-go-v2/config v1.18.0/go.mod h1:H13DRX9Nv5tAcQvPABrE3dm5XnLp1RC7fVSM3OWiLvA=
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.12.10/go.mod h1:g5eIM5XRs/OzIIK81QMBl+dAuDyoLN0VYaLP+tBqEOk=
|
github.com/aws/aws-sdk-go-v2/credentials v1.12.10/go.mod h1:g5eIM5XRs/OzIIK81QMBl+dAuDyoLN0VYaLP+tBqEOk=
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.12.23 h1:LctvcJMIb8pxvk5hQhChpCu0WlU6oKQmcYb1HA4IZSA=
|
github.com/aws/aws-sdk-go-v2/credentials v1.13.0 h1:W5f73j1qurASap+jdScUo4aGzSXxaC7wq1i7CiwhvU8=
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.12.23/go.mod h1:0awX9iRr/+UO7OwRQFpV1hNtXxOVuehpjVEzrIAYNcA=
|
github.com/aws/aws-sdk-go-v2/credentials v1.13.0/go.mod h1:prZpUfBu1KZLBLVX482Sq4DpDXGugAre08TPEc21GUg=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.9/go.mod h1:KDCCm4ONIdHtUloDcFvK2+vshZvx4Zmj7UMDfusuz5s=
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.9/go.mod h1:KDCCm4ONIdHtUloDcFvK2+vshZvx4Zmj7UMDfusuz5s=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.19 h1:E3PXZSI3F2bzyj6XxUXdTIfvp425HHhwKsFvmzBwHgs=
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.19 h1:E3PXZSI3F2bzyj6XxUXdTIfvp425HHhwKsFvmzBwHgs=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.19/go.mod h1:VihW95zQpeKQWVPGkwT+2+WJNQV8UXFfMTWdU6VErL8=
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.19/go.mod h1:VihW95zQpeKQWVPGkwT+2+WJNQV8UXFfMTWdU6VErL8=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.21/go.mod h1:iIYPrQ2rYfZiB/iADYlhj9HHZ9TTi6PqKQPAqygohbE=
|
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.21/go.mod h1:iIYPrQ2rYfZiB/iADYlhj9HHZ9TTi6PqKQPAqygohbE=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.37 h1:e1VtTBo+cLNjres0wTlMkmwCGGRjDEkkrz3frxxcaCs=
|
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.39 h1:Fz/t08vTFdz63ZnlrQBLOMgBCNqdKmMQWE/XjKm1jt4=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.37/go.mod h1:kdAV1UMnCkyG6tZJUC4mHbPoRjPA3dIK0L8mnsHERiM=
|
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.39/go.mod h1:763L1Xloj/mjT0do5k9d0qh0vEAVuomDPn1iOG2AdB4=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.15/go.mod h1:pWrr2OoHlT7M/Pd2y4HV3gJyPb3qj5qMmnPkKSNPYK4=
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.15/go.mod h1:pWrr2OoHlT7M/Pd2y4HV3gJyPb3qj5qMmnPkKSNPYK4=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.25 h1:nBO/RFxeq/IS5G9Of+ZrgucRciie2qpLy++3UGZ+q2E=
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.25 h1:nBO/RFxeq/IS5G9Of+ZrgucRciie2qpLy++3UGZ+q2E=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.25/go.mod h1:Zb29PYkf42vVYQY6pvSyJCJcFHlPIiY+YKdPtwnvMkY=
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.25/go.mod h1:Zb29PYkf42vVYQY6pvSyJCJcFHlPIiY+YKdPtwnvMkY=
|
||||||
|
@ -269,14 +269,14 @@ github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.9/go.mod h1:Rc5+wn2
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.19 h1:piDBAaWkaxkkVV3xJJbTehXCZRXYs49kvpi/LG6LR2o=
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.19 h1:piDBAaWkaxkkVV3xJJbTehXCZRXYs49kvpi/LG6LR2o=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.19/go.mod h1:BmQWRVkLTmyNzYPFAZgon53qKLWBNSvonugD1MrSWUs=
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.19/go.mod h1:BmQWRVkLTmyNzYPFAZgon53qKLWBNSvonugD1MrSWUs=
|
||||||
github.com/aws/aws-sdk-go-v2/service/kms v1.18.1/go.mod h1:4PZMUkc9rXHWGVB5J9vKaZy3D7Nai79ORworQ3ASMiM=
|
github.com/aws/aws-sdk-go-v2/service/kms v1.18.1/go.mod h1:4PZMUkc9rXHWGVB5J9vKaZy3D7Nai79ORworQ3ASMiM=
|
||||||
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.21 h1:LpIut7TpOhp8RuTD72PBj8ksPy3+RelT3LPwGgQ8+Hg=
|
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.22 h1:abXVcIh8pjiuWdPBk8vZYwxO5yZCmRHR8grHf6ZqCdY=
|
||||||
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.21/go.mod h1:mRGY+k3s1yt7yQA3AfzJhnr68OCs1xDfQfIABFUk+ek=
|
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.22/go.mod h1:mRGY+k3s1yt7yQA3AfzJhnr68OCs1xDfQfIABFUk+ek=
|
||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.27.2/go.mod h1:u+566cosFI+d+motIz3USXEh6sN8Nq4GrNXSg2RXVMo=
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.27.2/go.mod h1:u+566cosFI+d+motIz3USXEh6sN8Nq4GrNXSg2RXVMo=
|
||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.29.1 h1:/EMdFPW/Ppieh0WUtQf1+qCGNLdsq5UWUyevBQ6vMVc=
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.29.2 h1:l29X5biLks99HzZzQgC78plJpwiMv/pGNhmaTM2z62A=
|
||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.29.1/go.mod h1:/NHbqPRiwxSPVOB2Xr+StDEH+GWV/64WwnUjv4KYzV0=
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.29.2/go.mod h1:/NHbqPRiwxSPVOB2Xr+StDEH+GWV/64WwnUjv4KYzV0=
|
||||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.14/go.mod h1:xakbH8KMsQQKqzX87uyyzTHshc/0/Df8bsTneTS5pFU=
|
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.14/go.mod h1:xakbH8KMsQQKqzX87uyyzTHshc/0/Df8bsTneTS5pFU=
|
||||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.16.4 h1:Hx79EGrkKNJya2iz2U5A7nyr7DjOu/TGTRefThfBZ1w=
|
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.16.5 h1:De+sGzRmk6+/lzKqZXa6RdC1ZVGLPHI1nvjOxw4ooj0=
|
||||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.16.4/go.mod h1:k6CPuxyzO247nYEM1baEwHH1kRtosRCvgahAepaaShw=
|
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.16.5/go.mod h1:k6CPuxyzO247nYEM1baEwHH1kRtosRCvgahAepaaShw=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sns v1.17.10/go.mod h1:uITsRNVMeCB3MkWpXxXw0eDz8pW4TYLzj+eyQtbhSxM=
|
github.com/aws/aws-sdk-go-v2/service/sns v1.17.10/go.mod h1:uITsRNVMeCB3MkWpXxXw0eDz8pW4TYLzj+eyQtbhSxM=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sqs v1.19.1/go.mod h1:A94o564Gj+Yn+7QO1eLFeI7UVv3riy/YBFOfICVqFvU=
|
github.com/aws/aws-sdk-go-v2/service/sqs v1.19.1/go.mod h1:A94o564Gj+Yn+7QO1eLFeI7UVv3riy/YBFOfICVqFvU=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.27.6/go.mod h1:fiFzQgj4xNOg4/wqmAiPvzgDMXPD+cUEplX/CYn+0j0=
|
github.com/aws/aws-sdk-go-v2/service/ssm v1.27.6/go.mod h1:fiFzQgj4xNOg4/wqmAiPvzgDMXPD+cUEplX/CYn+0j0=
|
||||||
|
@ -286,8 +286,8 @@ github.com/aws/aws-sdk-go-v2/service/sso v1.11.25/go.mod h1:IARHuzTXmj1C0KS35vbo
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.8 h1:jcw6kKZrtNfBPJkaHrscDOZoe5gvi9wjudnxvozYFJo=
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.8 h1:jcw6kKZrtNfBPJkaHrscDOZoe5gvi9wjudnxvozYFJo=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.8/go.mod h1:er2JHN+kBY6FcMfcBBKNGCT3CarImmdFzishsqBmSRI=
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.8/go.mod h1:er2JHN+kBY6FcMfcBBKNGCT3CarImmdFzishsqBmSRI=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.16.10/go.mod h1:cftkHYN6tCDNfkSasAmclSfl4l7cySoay8vz7p/ce0E=
|
github.com/aws/aws-sdk-go-v2/service/sts v1.16.10/go.mod h1:cftkHYN6tCDNfkSasAmclSfl4l7cySoay8vz7p/ce0E=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.17.1 h1:KRAix/KHvjGODaHAMXnxRk9t0D+4IJVUuS/uwXxngXk=
|
github.com/aws/aws-sdk-go-v2/service/sts v1.17.2 h1:tpwEMRdMf2UsplengAOnmSIRdvAxf75oUFR+blBr92I=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.17.1/go.mod h1:bXcN3koeVYiJcdDU89n3kCYILob7Y34AeLopUbZgLT4=
|
github.com/aws/aws-sdk-go-v2/service/sts v1.17.2/go.mod h1:bXcN3koeVYiJcdDU89n3kCYILob7Y34AeLopUbZgLT4=
|
||||||
github.com/aws/smithy-go v1.12.0/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
|
github.com/aws/smithy-go v1.12.0/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
|
||||||
github.com/aws/smithy-go v1.13.4 h1:/RN2z1txIJWeXeOkzX+Hk/4Uuvv7dWtCjbmVJcrskyk=
|
github.com/aws/smithy-go v1.13.4 h1:/RN2z1txIJWeXeOkzX+Hk/4Uuvv7dWtCjbmVJcrskyk=
|
||||||
github.com/aws/smithy-go v1.13.4/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
|
github.com/aws/smithy-go v1.13.4/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
|
||||||
|
@ -358,8 +358,8 @@ github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWH
|
||||||
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/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/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.16 h1:t9dmZuC9J2W8IDQDSIGXmP+fBuEJSsrGXxWQz4cYqBY=
|
github.com/cockroachdb/cockroach-go/v2 v2.2.18 h1:bRNZWqzRSZesVYFjDAl1jgFb1jhIFIEreWIC2kPbcdY=
|
||||||
github.com/cockroachdb/cockroach-go/v2 v2.2.16/go.mod h1:xZ2VHjUEb/cySv0scXBx7YsBnHtLHkR1+w/w73b5i3M=
|
github.com/cockroachdb/cockroach-go/v2 v2.2.18/go.mod h1:mzlIDDBALQfEjv/7DU12fb2AfQ/MUYTlychcMpWp9QI=
|
||||||
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
|
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
|
||||||
github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo=
|
github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo=
|
||||||
github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA=
|
github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA=
|
||||||
|
@ -480,8 +480,8 @@ github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+
|
||||||
github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
|
github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
|
||||||
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=
|
||||||
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
github.com/coreos/go-systemd/v22 v22.4.0 h1:y9YHcjnjynCd/DVbg5j9L/33jQM3MxJlbj/zWskzfGU=
|
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
||||||
github.com/coreos/go-systemd/v22 v22.4.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||||
|
@ -501,7 +501,6 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
|
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE=
|
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4=
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4=
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc=
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc=
|
||||||
github.com/denisenkom/go-mssqldb v0.12.2/go.mod h1:lnIw1mZukFRZDJYQ0Pb833QS2IaC3l5HkEfra2LJ+sk=
|
github.com/denisenkom/go-mssqldb v0.12.2/go.mod h1:lnIw1mZukFRZDJYQ0Pb833QS2IaC3l5HkEfra2LJ+sk=
|
||||||
|
@ -608,11 +607,10 @@ github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwv
|
||||||
github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
|
github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
|
||||||
github.com/go-acme/lego/v4 v4.9.0 h1:8Hjj44IqRS7cigshMyFQ+0pIZvwgkG/+9A0UnNh7G8A=
|
github.com/go-acme/lego/v4 v4.9.0 h1:8Hjj44IqRS7cigshMyFQ+0pIZvwgkG/+9A0UnNh7G8A=
|
||||||
github.com/go-acme/lego/v4 v4.9.0/go.mod h1:g3JRUyWS3L/VObpp4bCxzJftKyf/Wba8QrSSnoiqjg4=
|
github.com/go-acme/lego/v4 v4.9.0/go.mod h1:g3JRUyWS3L/VObpp4bCxzJftKyf/Wba8QrSSnoiqjg4=
|
||||||
github.com/go-chi/chi/v5 v5.0.4/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
|
||||||
github.com/go-chi/chi/v5 v5.0.8-0.20221018120124-e5529d9db4d3 h1:qzwVVqrbdP93ZaSHy0yWQRYnig+t+j1OxnVtEs8SFuQ=
|
github.com/go-chi/chi/v5 v5.0.8-0.20221018120124-e5529d9db4d3 h1:qzwVVqrbdP93ZaSHy0yWQRYnig+t+j1OxnVtEs8SFuQ=
|
||||||
github.com/go-chi/chi/v5 v5.0.8-0.20221018120124-e5529d9db4d3/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
github.com/go-chi/chi/v5 v5.0.8-0.20221018120124-e5529d9db4d3/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||||
github.com/go-chi/jwtauth/v5 v5.0.2 h1:CSKtr+b6Jnfy5T27sMaiBPxaVE/bjnjS3ramFQ0526w=
|
github.com/go-chi/jwtauth/v5 v5.1.0 h1:wJyf2YZ/ohPvNJBwPOzZaQbyzwgMZZceE1m8FOzXLeA=
|
||||||
github.com/go-chi/jwtauth/v5 v5.0.2/go.mod h1:TeA7vmPe3uYThvHw8O8W13HOOpOd4MTgToxL41gZyjs=
|
github.com/go-chi/jwtauth/v5 v5.1.0/go.mod h1:MA93hc1au3tAQwCKry+fI4LqJ5MIVN4XSsglOo+lSc8=
|
||||||
github.com/go-chi/render v1.0.2 h1:4ER/udB0+fMWB2Jlf15RV3F4A2FDuYi/9f+lFttR/Lg=
|
github.com/go-chi/render v1.0.2 h1:4ER/udB0+fMWB2Jlf15RV3F4A2FDuYi/9f+lFttR/Lg=
|
||||||
github.com/go-chi/render v1.0.2/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
|
github.com/go-chi/render v1.0.2/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
|
||||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||||
|
@ -712,8 +710,6 @@ github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY9
|
||||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||||
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||||
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||||
github.com/goccy/go-json v0.7.6/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
|
||||||
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
|
||||||
github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk=
|
github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk=
|
||||||
github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
github.com/goccy/go-yaml v1.9.5/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA=
|
github.com/goccy/go-yaml v1.9.5/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA=
|
||||||
|
@ -923,8 +919,8 @@ github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:
|
||||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||||
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
|
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
|
||||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||||
github.com/hashicorp/go-plugin v1.4.5 h1:oTE/oQR4eghggRg8VY7PAz3dr++VwDNBGCcOfIvHpBo=
|
github.com/hashicorp/go-plugin v1.4.6 h1:MDV3UrKQBM3du3G7MApDGvOsMYy3JQJ4exhSoKBAeVA=
|
||||||
github.com/hashicorp/go-plugin v1.4.5/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s=
|
github.com/hashicorp/go-plugin v1.4.6/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s=
|
||||||
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
|
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.1 h1:sUiuQAnLlbvmExtFQs72iFW/HXeUn8Z1aJLQ4LJJbTQ=
|
github.com/hashicorp/go-retryablehttp v0.7.1 h1:sUiuQAnLlbvmExtFQs72iFW/HXeUn8Z1aJLQ4LJJbTQ=
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.1/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
|
github.com/hashicorp/go-retryablehttp v0.7.1/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
|
||||||
|
@ -1013,8 +1009,8 @@ github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQ
|
||||||
github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
|
github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
|
||||||
github.com/jackc/pgx/v4 v4.16.0/go.mod h1:N0A9sFdWzkw/Jy1lwoiB64F2+ugFZi987zRxcPez/wI=
|
github.com/jackc/pgx/v4 v4.16.0/go.mod h1:N0A9sFdWzkw/Jy1lwoiB64F2+ugFZi987zRxcPez/wI=
|
||||||
github.com/jackc/pgx/v4 v4.16.1/go.mod h1:SIhx0D5hoADaiXZVyv+3gSm3LCIIINTVO0PficsvWGQ=
|
github.com/jackc/pgx/v4 v4.16.1/go.mod h1:SIhx0D5hoADaiXZVyv+3gSm3LCIIINTVO0PficsvWGQ=
|
||||||
github.com/jackc/pgx/v5 v5.0.4 h1:r5O6y84qHX/z/HZV40JBdx2obsHz7/uRj5b+CcYEdeY=
|
github.com/jackc/pgx/v5 v5.1.0 h1:Z7pLKUb65HK6m18No8GGKT87K34NhIIEHa86rRdjxbU=
|
||||||
github.com/jackc/pgx/v5 v5.0.4/go.mod h1:U0ynklHtgg43fue9Ly30w3OCSTDPlXjig9ghrNGaguQ=
|
github.com/jackc/pgx/v5 v5.1.0/go.mod h1:Ptn7zmohNsWEsdxRawMzk3gaKma2obW+NWTnKa0S4nk=
|
||||||
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||||
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||||
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||||
|
@ -1063,8 +1059,9 @@ github.com/klauspost/compress v1.15.1/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47e
|
||||||
github.com/klauspost/compress v1.15.12 h1:YClS/PImqYbn+UILDnqxQCZ3RehC9N318SU3kElDUEM=
|
github.com/klauspost/compress v1.15.12 h1:YClS/PImqYbn+UILDnqxQCZ3RehC9N318SU3kElDUEM=
|
||||||
github.com/klauspost/compress v1.15.12/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM=
|
github.com/klauspost/compress v1.15.12/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
github.com/klauspost/cpuid/v2 v2.1.2 h1:XhdX4fqAJUA0yj+kUwMavO0hHrSPAecYdYf1ZmxHvak=
|
|
||||||
github.com/klauspost/cpuid/v2 v2.1.2/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
github.com/klauspost/cpuid/v2 v2.1.2/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.1 h1:U33DW0aiEj633gHYw3LoDNfkDiYnE5Q8M/TKJn2f2jI=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.1/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||||
github.com/kolo/xmlrpc v0.0.0-20201022064351-38db28db192b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM=
|
github.com/kolo/xmlrpc v0.0.0-20201022064351-38db28db192b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
@ -1087,21 +1084,16 @@ github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LE
|
||||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||||
github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A=
|
|
||||||
github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y=
|
|
||||||
github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ=
|
|
||||||
github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80=
|
github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80=
|
||||||
github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
|
github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
|
||||||
github.com/lestrrat-go/codegen v1.0.1/go.mod h1:JhJw6OQAuPEfVKUCLItpaVLumDGWQznd1VaXrBk9TdM=
|
|
||||||
github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++0Gf8MBnAvE=
|
|
||||||
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
|
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
|
||||||
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
|
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
|
||||||
github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc=
|
github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8=
|
||||||
|
github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
|
||||||
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
|
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
|
||||||
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
|
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
|
||||||
github.com/lestrrat-go/jwx v1.2.6/go.mod h1:tJuGuAI3LC71IicTx82Mz1n3w9woAs2bYJZpkjJQ5aU=
|
github.com/lestrrat-go/jwx/v2 v2.0.7 h1:vNh7cA5pKS/1muWYpM1GeUHBCf/r1UFxYN60iv7LFRA=
|
||||||
github.com/lestrrat-go/jwx v1.2.25 h1:tAx93jN2SdPvFn08fHNAhqFJazn5mBBOB8Zli0g0otA=
|
github.com/lestrrat-go/jwx/v2 v2.0.7/go.mod h1:zLxnyv9rTlEvOUHbc48FAfIL8iYu2hHvIRaTFGc8mT0=
|
||||||
github.com/lestrrat-go/jwx v1.2.25/go.mod h1:zoNuZymNl5lgdcu6P7K6ie2QRll5HVfF4xwxBBK1NxY=
|
|
||||||
github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4=
|
github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4=
|
||||||
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
|
@ -1304,13 +1296,13 @@ github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxS
|
||||||
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
|
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
|
||||||
github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
|
github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
|
||||||
github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
|
github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
|
||||||
github.com/otiai10/copy v1.7.0 h1:hVoPiN+t+7d2nzzwMiDHPSOogsWAStewq3TwU05+clE=
|
github.com/otiai10/copy v1.9.0 h1:7KFNiCgZ91Ru4qW4CWPf/7jqtxLagGRmIxWldPP9VY4=
|
||||||
github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U=
|
github.com/otiai10/copy v1.9.0/go.mod h1:hsfX19wcn0UWIHUQ3/4fHuehhk2UyArQ9dVFAn3FczI=
|
||||||
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
|
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
|
||||||
github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
|
github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
|
||||||
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
|
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
|
||||||
github.com/otiai10/mint v1.3.3 h1:7JgpsBaN0uMkyju4tbYHu0mnM55hNKVYLsXmwr15NQI=
|
github.com/otiai10/mint v1.4.0 h1:umwcf7gbpEwf7WFzqmWwSv0CzbeMsae2u9ZvpP8j2q4=
|
||||||
github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
|
github.com/otiai10/mint v1.4.0/go.mod h1:gifjb2MYOoULtKLqUAEILUG/9KONW6f7YsJ6vQLTlFI=
|
||||||
github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
|
github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
|
||||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||||
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||||
|
@ -1367,8 +1359,8 @@ github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP
|
||||||
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
|
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
|
||||||
github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
|
github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
|
||||||
github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
|
github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
|
||||||
github.com/prometheus/client_golang v1.13.1 h1:3gMjIY2+/hzmqhtUC/aQNYldJA6DtH3CgQvwS+02K1c=
|
github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw=
|
||||||
github.com/prometheus/client_golang v1.13.1/go.mod h1:vTeo+zgvILHsnnj/39Ou/1fPN5nJFOEMgftOUOmlvYQ=
|
github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y=
|
||||||
github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||||
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||||
|
@ -1453,8 +1445,8 @@ github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg
|
||||||
github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
|
github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
|
||||||
github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg=
|
github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg=
|
||||||
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.3-0.20221105153737-bae9afc6b356 h1:VwFpy5W/pP0X+082xKU2yu4OAwuk8Qqa8j2ofImJ1bM=
|
github.com/sftpgo/sdk v0.1.3-0.20221116180328-3fc64e926700 h1:hfUjwmNPMqE9o5oIBxDQZE0FJ8IqlJwVVhATQYwe2Ao=
|
||||||
github.com/sftpgo/sdk v0.1.3-0.20221105153737-bae9afc6b356/go.mod h1:Giy5vj7Gmju0nGlmBNd28DwPo0G0o1nr9XkE+vu3i+o=
|
github.com/sftpgo/sdk v0.1.3-0.20221116180328-3fc64e926700/go.mod h1:Giy5vj7Gmju0nGlmBNd28DwPo0G0o1nr9XkE+vu3i+o=
|
||||||
github.com/shirou/gopsutil/v3 v3.22.10 h1:4KMHdfBRYXGF9skjDWiL4RA2N+E8dRdodU/bOZpPoVg=
|
github.com/shirou/gopsutil/v3 v3.22.10 h1:4KMHdfBRYXGF9skjDWiL4RA2N+E8dRdodU/bOZpPoVg=
|
||||||
github.com/shirou/gopsutil/v3 v3.22.10/go.mod h1:QNza6r4YQoydyCfo6rH0blGfKahgibh4dQmV5xdFkQk=
|
github.com/shirou/gopsutil/v3 v3.22.10/go.mod h1:QNza6r4YQoydyCfo6rH0blGfKahgibh4dQmV5xdFkQk=
|
||||||
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=
|
||||||
|
@ -1483,8 +1475,8 @@ github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B
|
||||||
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
||||||
github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=
|
github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=
|
||||||
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
|
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
|
||||||
github.com/spf13/afero v1.9.2 h1:j49Hj62F0n+DaZ1dDCvhABaPNSGNkt32oRFxI33IEMw=
|
github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk=
|
||||||
github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
|
github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
|
||||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||||
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||||
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
|
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
|
||||||
|
@ -1506,8 +1498,8 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
|
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
|
||||||
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
|
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
|
||||||
github.com/spf13/viper v1.13.0 h1:BWSJ/M+f+3nmdz9bxB+bWX28kkALN2ok11D0rSo8EJU=
|
github.com/spf13/viper v1.14.0 h1:Rg7d3Lo706X9tHsJMUjdiwMpHB7W8WnSVOssIY+JElU=
|
||||||
github.com/spf13/viper v1.13.0/go.mod h1:Icm2xNL3/8uyh/wFuB1jI7TiTNKp8632Nwegu+zgdYw=
|
github.com/spf13/viper v1.14.0/go.mod h1:WT//axPky3FdvXHzGw33dNdXXXfFQqmEalje+egj8As=
|
||||||
github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8=
|
github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8=
|
||||||
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
|
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
|
||||||
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
|
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
|
||||||
|
@ -1532,8 +1524,8 @@ github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/studio-b12/gowebdav v0.0.0-20221102155456-200a600c0272 h1:dXbdJSdxf0EnR4SkcsfRNuGCvoEk9lavXbSCFXN2gJc=
|
github.com/studio-b12/gowebdav v0.0.0-20221109171924-60ec5ad56012 h1:ZC+dlnsjxqrcB68nEFbIEfo4iXsog3Sg8FlXKytAjhY=
|
||||||
github.com/studio-b12/gowebdav v0.0.0-20221102155456-200a600c0272/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=
|
github.com/studio-b12/gowebdav v0.0.0-20221109171924-60ec5ad56012/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=
|
||||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||||
github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs=
|
github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs=
|
||||||
github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
|
github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
|
||||||
|
@ -1543,11 +1535,12 @@ github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG
|
||||||
github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I=
|
github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I=
|
||||||
github.com/tedsuo/ifrit v0.0.0-20180802180643-bea94bb476cc/go.mod h1:eyZnKCc955uh98WQvzOm0dgAeLnf2O0Rz0LPoC5ze+0=
|
github.com/tedsuo/ifrit v0.0.0-20180802180643-bea94bb476cc/go.mod h1:eyZnKCc955uh98WQvzOm0dgAeLnf2O0Rz0LPoC5ze+0=
|
||||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||||
github.com/tklauser/go-sysconf v0.3.10 h1:IJ1AZGZRWbY8T5Vfk04D9WOA5WSejdflXxP03OUqALw=
|
|
||||||
github.com/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk=
|
github.com/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk=
|
||||||
|
github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM=
|
||||||
|
github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI=
|
||||||
github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ=
|
github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ=
|
||||||
github.com/tklauser/numcpus v0.5.0 h1:ooe7gN0fg6myJ0EKoTAf5hebTZrH52px3New/D9iJ+A=
|
github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms=
|
||||||
github.com/tklauser/numcpus v0.5.0/go.mod h1:OGzpTxpcIMNGYQdit2BYL1pvk/dSOaJWjKoflh+RQjo=
|
github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4=
|
||||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||||
|
@ -1583,8 +1576,8 @@ github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6
|
||||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||||
github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs=
|
github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs=
|
||||||
github.com/xhit/go-simple-mail/v2 v2.12.0 h1:KweA6NO8Z6fZyeckMPNpvElU6QDIyBShlpce1sYUZgg=
|
github.com/xhit/go-simple-mail/v2 v2.13.0 h1:OANWU9jHZrVfBkNkvLf8Ww0fexwpQVF/v/5f96fFTLI=
|
||||||
github.com/xhit/go-simple-mail/v2 v2.12.0/go.mod h1:b7P5ygho6SYE+VIqpxA6QkYfv4teeyG4MKqB3utRu98=
|
github.com/xhit/go-simple-mail/v2 v2.13.0/go.mod h1:b7P5ygho6SYE+VIqpxA6QkYfv4teeyG4MKqB3utRu98=
|
||||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||||
github.com/xlab/treeprint v1.1.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=
|
github.com/xlab/treeprint v1.1.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=
|
||||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||||
|
@ -1741,8 +1734,8 @@ golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||||
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
|
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I=
|
golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA=
|
||||||
golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
|
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
@ -1819,8 +1812,9 @@ golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.0.0-20220802222814-0bcc04d9c69b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
golang.org/x/net v0.0.0-20220802222814-0bcc04d9c69b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||||
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||||
golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0=
|
|
||||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||||
|
golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
|
||||||
|
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
|
@ -1847,8 +1841,8 @@ golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7Lm
|
||||||
golang.org/x/oauth2 v0.0.0-20220628200809-02e64fa58f26/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=
|
golang.org/x/oauth2 v0.0.0-20220628200809-02e64fa58f26/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=
|
||||||
golang.org/x/oauth2 v0.0.0-20220722155238-128564f6959c/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
|
golang.org/x/oauth2 v0.0.0-20220722155238-128564f6959c/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
|
||||||
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.1.0 h1:isLCZuhj4v+tYv7eskaN4v/TM+A1begWWgyVJDdl1+Y=
|
golang.org/x/oauth2 v0.2.0 h1:GtQkldQ9m7yvzCL1V+LrYow3Khe0eJH0w7RbX/VbaIU=
|
||||||
golang.org/x/oauth2 v0.1.0/go.mod h1:G9FE4dLTsbXUu90h/Pf85g4w1D+SSAgR+q46nJZ8M4A=
|
golang.org/x/oauth2 v0.2.0/go.mod h1:Cwn6afJ8jrQwYMxQDTpISoXmXW9I6qF6vDeuuoX3Ibs=
|
||||||
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=
|
||||||
|
@ -2004,19 +1998,21 @@ golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||||
golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b/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-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/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-20220731174439-a90be440212d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220731174439-a90be440212d/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-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
|
|
||||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
|
||||||
|
golang.org/x/sys v0.2.0/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-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
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=
|
||||||
golang.org/x/term v0.1.0 h1:g6Z6vPFA9dYBAF7DWcH6sCcOntplXsDKcliusYijMlw=
|
|
||||||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.2.0 h1:z85xZCsEl7bi/KwbNADeBYoOP0++7W1ipu+aGnpwzRM=
|
||||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
@ -2040,8 +2036,8 @@ golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxb
|
||||||
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20220609170525-579cf78fd858/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20220609170525-579cf78fd858/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA=
|
golang.org/x/time v0.2.0 h1:52I/1L54xyEQAYdtcSuxtiT84KGYTBGXwayxmIpNJhE=
|
||||||
golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.2.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
@ -2108,7 +2104,6 @@ golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc
|
||||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||||
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
|
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
|
||||||
golang.org/x/tools v0.0.0-20200916195026-c9a70fc28ce3/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
golang.org/x/tools v0.0.0-20200916195026-c9a70fc28ce3/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
||||||
golang.org/x/tools v0.0.0-20200918232735-d647fc253266/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
|
||||||
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
|
@ -2116,7 +2111,6 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f
|
||||||
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.0.0-20210114065538-d78b04bdf963/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
|
||||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||||
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||||
|
@ -2128,8 +2122,8 @@ golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
|
||||||
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
|
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
|
||||||
golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4=
|
golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE=
|
golang.org/x/tools v0.3.0 h1:SrNbZl6ECOS1qFzgTdQfWXZM9XBkiA6tkFrH9YSTPHM=
|
||||||
golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
|
golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
|
||||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
@ -2189,8 +2183,8 @@ google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6F
|
||||||
google.golang.org/api v0.86.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
|
google.golang.org/api v0.86.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
|
||||||
google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
|
google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
|
||||||
google.golang.org/api v0.91.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
|
google.golang.org/api v0.91.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
|
||||||
google.golang.org/api v0.102.0 h1:JxJl2qQ85fRMPNvlZY/enexbxpCjLwGhZUtgfGeQ51I=
|
google.golang.org/api v0.103.0 h1:9yuVqlu2JCvcLg9p8S3fcFLZij8EPSyvODIY1rkMizQ=
|
||||||
google.golang.org/api v0.102.0/go.mod h1:3VFl6/fzoA+qNuS1N1/VfXY4LjoXN/wzeIp7TweWwGo=
|
google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0=
|
||||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
|
@ -2301,8 +2295,8 @@ google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljW
|
||||||
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-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
|
google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
|
||||||
google.golang.org/genproto v0.0.0-20220802133213-ce4fa296bf78/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc=
|
google.golang.org/genproto v0.0.0-20220802133213-ce4fa296bf78/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc=
|
||||||
google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c h1:QgY/XxIAIeccR+Ca/rDdKubLIU9rcJ3xfy1DC/Wd2Oo=
|
google.golang.org/genproto v0.0.0-20221114212237-e4508ebdbee1 h1:jCw9YRd2s40X9Vxi4zKsPRvSPlHWNqadVkpbMsCPzPQ=
|
||||||
google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c/go.mod h1:CGI5F/G+E5bKwmfYo09AXuVN4dD894kIKUFmVbP2/Fo=
|
google.golang.org/genproto v0.0.0-20221114212237-e4508ebdbee1/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg=
|
||||||
google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
|
|
|
@ -65,6 +65,12 @@ Please take a look at the usage below to customize the options.`,
|
||||||
logger.ErrorToConsole("Unable to initialize KMS: %v", err)
|
logger.ErrorToConsole("Unable to initialize KMS: %v", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
mfaConfig := config.GetMFAConfig()
|
||||||
|
err = mfaConfig.Initialize()
|
||||||
|
if err != nil {
|
||||||
|
logger.ErrorToConsole("Unable to initialize MFA: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
providerConf := config.GetProviderConf()
|
providerConf := config.GetProviderConf()
|
||||||
// ignore actions
|
// ignore actions
|
||||||
providerConf.Actions.Hook = ""
|
providerConf.Actions.Hook = ""
|
||||||
|
|
|
@ -56,7 +56,7 @@ Please take a look at the usage below to customize the options.`,
|
||||||
}
|
}
|
||||||
providerConf := config.GetProviderConf()
|
providerConf := config.GetProviderConf()
|
||||||
if !resetProviderForce {
|
if !resetProviderForce {
|
||||||
logger.WarnToConsole("You are about to delete all the SFTPGo data for provider %#v, config file: %#v",
|
logger.WarnToConsole("You are about to delete all the SFTPGo data for provider %q, config file: %q",
|
||||||
providerConf.Driver, viper.ConfigFileUsed())
|
providerConf.Driver, viper.ConfigFileUsed())
|
||||||
logger.WarnToConsole("Are you sure? (Y/n)")
|
logger.WarnToConsole("Are you sure? (Y/n)")
|
||||||
reader := bufio.NewReader(os.Stdin)
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
@ -70,7 +70,7 @@ Please take a look at the usage below to customize the options.`,
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logger.InfoToConsole("Resetting provider: %#v, config file: %#v", providerConf.Driver, viper.ConfigFileUsed())
|
logger.InfoToConsole("Resetting provider: %q, config file: %q", providerConf.Driver, viper.ConfigFileUsed())
|
||||||
err = dataprovider.ResetDatabase(providerConf, configDir)
|
err = dataprovider.ResetDatabase(providerConf, configDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.WarnToConsole("Error resetting provider: %v", err)
|
logger.WarnToConsole("Error resetting provider: %v", err)
|
||||||
|
|
|
@ -40,8 +40,8 @@ Please take a look at the usage below to customize the options.`,
|
||||||
Run: func(_ *cobra.Command, _ []string) {
|
Run: func(_ *cobra.Command, _ []string) {
|
||||||
logger.DisableLogger()
|
logger.DisableLogger()
|
||||||
logger.EnableConsoleLogger(zerolog.DebugLevel)
|
logger.EnableConsoleLogger(zerolog.DebugLevel)
|
||||||
if revertProviderTargetVersion != 19 {
|
if revertProviderTargetVersion != 23 {
|
||||||
logger.WarnToConsole("Unsupported target version, 19 is the only supported one")
|
logger.WarnToConsole("Unsupported target version, 23 is the only supported one")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
configDir = util.CleanDirInput(configDir)
|
configDir = util.CleanDirInput(configDir)
|
||||||
|
@ -57,7 +57,7 @@ Please take a look at the usage below to customize the options.`,
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
providerConf := config.GetProviderConf()
|
providerConf := config.GetProviderConf()
|
||||||
logger.InfoToConsole("Reverting provider: %#v config file: %#v target version %v", providerConf.Driver,
|
logger.InfoToConsole("Reverting provider: %q config file: %q target version %d", providerConf.Driver,
|
||||||
viper.ConfigFileUsed(), revertProviderTargetVersion)
|
viper.ConfigFileUsed(), revertProviderTargetVersion)
|
||||||
err = dataprovider.RevertDatabase(providerConf, configDir, revertProviderTargetVersion)
|
err = dataprovider.RevertDatabase(providerConf, configDir, revertProviderTargetVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -71,7 +71,7 @@ Please take a look at the usage below to customize the options.`,
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
addConfigFlags(revertProviderCmd)
|
addConfigFlags(revertProviderCmd)
|
||||||
revertProviderCmd.Flags().IntVar(&revertProviderTargetVersion, "to-version", 19, `19 means the version supported in v2.3.x`)
|
revertProviderCmd.Flags().IntVar(&revertProviderTargetVersion, "to-version", 23, `23 means the version supported in v2.4.x`)
|
||||||
|
|
||||||
rootCmd.AddCommand(revertProviderCmd)
|
rootCmd.AddCommand(revertProviderCmd)
|
||||||
}
|
}
|
||||||
|
|
|
@ -137,7 +137,7 @@ Command-line flags should be specified in the Subsystem declaration.
|
||||||
logger.Error(logSender, connectionID, "unable to initialize commands configuration: %v", err)
|
logger.Error(logSender, connectionID, "unable to initialize commands configuration: %v", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
user, err := dataprovider.UserExists(username)
|
user, err := dataprovider.UserExists(username, "")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if user.HomeDir != filepath.Clean(homedir) && !preserveHomeDir {
|
if user.HomeDir != filepath.Clean(homedir) && !preserveHomeDir {
|
||||||
// update the user
|
// update the user
|
||||||
|
|
|
@ -417,6 +417,7 @@ type ActiveTransfer interface {
|
||||||
type ActiveConnection interface {
|
type ActiveConnection interface {
|
||||||
GetID() string
|
GetID() string
|
||||||
GetUsername() string
|
GetUsername() string
|
||||||
|
GetRole() string
|
||||||
GetMaxSessions() int
|
GetMaxSessions() int
|
||||||
GetLocalAddress() string
|
GetLocalAddress() string
|
||||||
GetRemoteAddress() string
|
GetRemoteAddress() string
|
||||||
|
@ -945,7 +946,7 @@ func (conns *ActiveConnections) Remove(connectionID string) {
|
||||||
|
|
||||||
// Close closes an active connection.
|
// Close closes an active connection.
|
||||||
// It returns true on success
|
// It returns true on success
|
||||||
func (conns *ActiveConnections) Close(connectionID string) bool {
|
func (conns *ActiveConnections) Close(connectionID, role string) bool {
|
||||||
conns.RLock()
|
conns.RLock()
|
||||||
|
|
||||||
var result bool
|
var result bool
|
||||||
|
@ -953,11 +954,13 @@ func (conns *ActiveConnections) Close(connectionID string) bool {
|
||||||
if idx, ok := conns.mapping[connectionID]; ok {
|
if idx, ok := conns.mapping[connectionID]; ok {
|
||||||
c := conns.connections[idx]
|
c := conns.connections[idx]
|
||||||
|
|
||||||
defer func(conn ActiveConnection) {
|
if role == "" || c.GetRole() == role {
|
||||||
err := conn.Disconnect()
|
defer func(conn ActiveConnection) {
|
||||||
logger.Debug(conn.GetProtocol(), conn.GetID(), "close connection requested, close err: %v", err)
|
err := conn.Disconnect()
|
||||||
}(c)
|
logger.Debug(conn.GetProtocol(), conn.GetID(), "close connection requested, close err: %v", err)
|
||||||
result = true
|
}(c)
|
||||||
|
result = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
conns.RUnlock()
|
conns.RUnlock()
|
||||||
|
@ -1161,26 +1164,28 @@ func (conns *ActiveConnections) IsNewConnectionAllowed(ipAddr string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetStats returns stats for active connections
|
// GetStats returns stats for active connections
|
||||||
func (conns *ActiveConnections) GetStats() []ConnectionStatus {
|
func (conns *ActiveConnections) GetStats(role string) []ConnectionStatus {
|
||||||
conns.RLock()
|
conns.RLock()
|
||||||
defer conns.RUnlock()
|
defer conns.RUnlock()
|
||||||
|
|
||||||
stats := make([]ConnectionStatus, 0, len(conns.connections))
|
stats := make([]ConnectionStatus, 0, len(conns.connections))
|
||||||
node := dataprovider.GetNodeName()
|
node := dataprovider.GetNodeName()
|
||||||
for _, c := range conns.connections {
|
for _, c := range conns.connections {
|
||||||
stat := ConnectionStatus{
|
if role == "" || c.GetRole() == role {
|
||||||
Username: c.GetUsername(),
|
stat := ConnectionStatus{
|
||||||
ConnectionID: c.GetID(),
|
Username: c.GetUsername(),
|
||||||
ClientVersion: c.GetClientVersion(),
|
ConnectionID: c.GetID(),
|
||||||
RemoteAddress: c.GetRemoteAddress(),
|
ClientVersion: c.GetClientVersion(),
|
||||||
ConnectionTime: util.GetTimeAsMsSinceEpoch(c.GetConnectionTime()),
|
RemoteAddress: c.GetRemoteAddress(),
|
||||||
LastActivity: util.GetTimeAsMsSinceEpoch(c.GetLastActivity()),
|
ConnectionTime: util.GetTimeAsMsSinceEpoch(c.GetConnectionTime()),
|
||||||
Protocol: c.GetProtocol(),
|
LastActivity: util.GetTimeAsMsSinceEpoch(c.GetLastActivity()),
|
||||||
Command: c.GetCommand(),
|
Protocol: c.GetProtocol(),
|
||||||
Transfers: c.GetTransfers(),
|
Command: c.GetCommand(),
|
||||||
Node: node,
|
Transfers: c.GetTransfers(),
|
||||||
|
Node: node,
|
||||||
|
}
|
||||||
|
stats = append(stats, stat)
|
||||||
}
|
}
|
||||||
stats = append(stats, stat)
|
|
||||||
}
|
}
|
||||||
return stats
|
return stats
|
||||||
}
|
}
|
||||||
|
@ -1253,7 +1258,8 @@ type ActiveQuotaScan struct {
|
||||||
// Username to which the quota scan refers
|
// Username to which the quota scan refers
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
// quota scan start time as unix timestamp in milliseconds
|
// quota scan start time as unix timestamp in milliseconds
|
||||||
StartTime int64 `json:"start_time"`
|
StartTime int64 `json:"start_time"`
|
||||||
|
Role string `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ActiveVirtualFolderQuotaScan defines an active quota scan for a virtual folder
|
// ActiveVirtualFolderQuotaScan defines an active quota scan for a virtual folder
|
||||||
|
@ -1272,18 +1278,26 @@ type ActiveScans struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUsersQuotaScans returns the active users quota scans
|
// GetUsersQuotaScans returns the active users quota scans
|
||||||
func (s *ActiveScans) GetUsersQuotaScans() []ActiveQuotaScan {
|
func (s *ActiveScans) GetUsersQuotaScans(role string) []ActiveQuotaScan {
|
||||||
s.RLock()
|
s.RLock()
|
||||||
defer s.RUnlock()
|
defer s.RUnlock()
|
||||||
|
|
||||||
scans := make([]ActiveQuotaScan, len(s.UserScans))
|
scans := make([]ActiveQuotaScan, 0, len(s.UserScans))
|
||||||
copy(scans, s.UserScans)
|
for _, scan := range s.UserScans {
|
||||||
|
if role == "" || role == scan.Role {
|
||||||
|
scans = append(scans, ActiveQuotaScan{
|
||||||
|
Username: scan.Username,
|
||||||
|
StartTime: scan.StartTime,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return scans
|
return scans
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddUserQuotaScan adds a user to the ones with active quota scans.
|
// AddUserQuotaScan adds a user to the ones with active quota scans.
|
||||||
// Returns false if the user has a quota scan already running
|
// Returns false if the user has a quota scan already running
|
||||||
func (s *ActiveScans) AddUserQuotaScan(username string) bool {
|
func (s *ActiveScans) AddUserQuotaScan(username, role string) bool {
|
||||||
s.Lock()
|
s.Lock()
|
||||||
defer s.Unlock()
|
defer s.Unlock()
|
||||||
|
|
||||||
|
@ -1295,6 +1309,7 @@ func (s *ActiveScans) AddUserQuotaScan(username string) bool {
|
||||||
s.UserScans = append(s.UserScans, ActiveQuotaScan{
|
s.UserScans = append(s.UserScans, ActiveQuotaScan{
|
||||||
Username: username,
|
Username: username,
|
||||||
StartTime: util.GetTimeAsMsSinceEpoch(time.Now()),
|
StartTime: util.GetTimeAsMsSinceEpoch(time.Now()),
|
||||||
|
Role: role,
|
||||||
})
|
})
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -1367,7 +1382,8 @@ type MetadataCheck struct {
|
||||||
// Username to which the metadata check refers
|
// Username to which the metadata check refers
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
// check start time as unix timestamp in milliseconds
|
// check start time as unix timestamp in milliseconds
|
||||||
StartTime int64 `json:"start_time"`
|
StartTime int64 `json:"start_time"`
|
||||||
|
Role string `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// MetadataChecks holds the active metadata checks
|
// MetadataChecks holds the active metadata checks
|
||||||
|
@ -1377,19 +1393,26 @@ type MetadataChecks struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get returns the active metadata checks
|
// Get returns the active metadata checks
|
||||||
func (c *MetadataChecks) Get() []MetadataCheck {
|
func (c *MetadataChecks) Get(role string) []MetadataCheck {
|
||||||
c.RLock()
|
c.RLock()
|
||||||
defer c.RUnlock()
|
defer c.RUnlock()
|
||||||
|
|
||||||
checks := make([]MetadataCheck, len(c.checks))
|
checks := make([]MetadataCheck, 0, len(c.checks))
|
||||||
copy(checks, c.checks)
|
for _, check := range c.checks {
|
||||||
|
if role == "" || role == check.Role {
|
||||||
|
checks = append(checks, MetadataCheck{
|
||||||
|
Username: check.Username,
|
||||||
|
StartTime: check.StartTime,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return checks
|
return checks
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add adds a user to the ones with active metadata checks.
|
// Add adds a user to the ones with active metadata checks.
|
||||||
// Return false if a metadata check is already active for the specified user
|
// Return false if a metadata check is already active for the specified user
|
||||||
func (c *MetadataChecks) Add(username string) bool {
|
func (c *MetadataChecks) Add(username, role string) bool {
|
||||||
c.Lock()
|
c.Lock()
|
||||||
defer c.Unlock()
|
defer c.Unlock()
|
||||||
|
|
||||||
|
@ -1402,6 +1425,7 @@ func (c *MetadataChecks) Add(username string) bool {
|
||||||
c.checks = append(c.checks, MetadataCheck{
|
c.checks = append(c.checks, MetadataCheck{
|
||||||
Username: username,
|
Username: username,
|
||||||
StartTime: util.GetTimeAsMsSinceEpoch(time.Now()),
|
StartTime: util.GetTimeAsMsSinceEpoch(time.Now()),
|
||||||
|
Role: role,
|
||||||
})
|
})
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|
|
@ -540,7 +540,7 @@ func TestUserMaxSessions(t *testing.T) {
|
||||||
Connections.Lock()
|
Connections.Lock()
|
||||||
Connections.removeUserConnection(userTestUsername)
|
Connections.removeUserConnection(userTestUsername)
|
||||||
Connections.Unlock()
|
Connections.Unlock()
|
||||||
assert.Len(t, Connections.GetStats(), 0)
|
assert.Len(t, Connections.GetStats(""), 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMaxConnections(t *testing.T) {
|
func TestMaxConnections(t *testing.T) {
|
||||||
|
@ -562,12 +562,12 @@ func TestMaxConnections(t *testing.T) {
|
||||||
}
|
}
|
||||||
err := Connections.Add(fakeConn)
|
err := Connections.Add(fakeConn)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, Connections.GetStats(), 1)
|
assert.Len(t, Connections.GetStats(""), 1)
|
||||||
assert.Error(t, Connections.IsNewConnectionAllowed(ipAddr))
|
assert.Error(t, Connections.IsNewConnectionAllowed(ipAddr))
|
||||||
|
|
||||||
res := Connections.Close(fakeConn.GetID())
|
res := Connections.Close(fakeConn.GetID(), "")
|
||||||
assert.True(t, res)
|
assert.True(t, res)
|
||||||
assert.Eventually(t, func() bool { return len(Connections.GetStats()) == 0 }, 300*time.Millisecond, 50*time.Millisecond)
|
assert.Eventually(t, func() bool { return len(Connections.GetStats("")) == 0 }, 300*time.Millisecond, 50*time.Millisecond)
|
||||||
|
|
||||||
assert.NoError(t, Connections.IsNewConnectionAllowed(ipAddr))
|
assert.NoError(t, Connections.IsNewConnectionAllowed(ipAddr))
|
||||||
Connections.AddClientConnection(ipAddr)
|
Connections.AddClientConnection(ipAddr)
|
||||||
|
@ -580,6 +580,33 @@ func TestMaxConnections(t *testing.T) {
|
||||||
Config.MaxTotalConnections = oldValue
|
Config.MaxTotalConnections = oldValue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConnectionRoles(t *testing.T) {
|
||||||
|
username := "testUsername"
|
||||||
|
role1 := "testRole1"
|
||||||
|
role2 := "testRole2"
|
||||||
|
c := NewBaseConnection("id", ProtocolSFTP, "", "", dataprovider.User{
|
||||||
|
BaseUser: sdk.BaseUser{
|
||||||
|
Username: username,
|
||||||
|
Role: role1,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
fakeConn := &fakeConnection{
|
||||||
|
BaseConnection: c,
|
||||||
|
}
|
||||||
|
err := Connections.Add(fakeConn)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, Connections.GetStats(""), 1)
|
||||||
|
assert.Len(t, Connections.GetStats(role1), 1)
|
||||||
|
assert.Len(t, Connections.GetStats(role2), 0)
|
||||||
|
|
||||||
|
res := Connections.Close(fakeConn.GetID(), role2)
|
||||||
|
assert.False(t, res)
|
||||||
|
assert.Len(t, Connections.GetStats(""), 1)
|
||||||
|
res = Connections.Close(fakeConn.GetID(), role1)
|
||||||
|
assert.True(t, res)
|
||||||
|
assert.Eventually(t, func() bool { return len(Connections.GetStats("")) == 0 }, 300*time.Millisecond, 50*time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
func TestMaxConnectionPerHost(t *testing.T) {
|
func TestMaxConnectionPerHost(t *testing.T) {
|
||||||
oldValue := Config.MaxPerHostConnections
|
oldValue := Config.MaxPerHostConnections
|
||||||
|
|
||||||
|
@ -660,7 +687,7 @@ func TestIdleConnections(t *testing.T) {
|
||||||
err = Connections.Add(fakeConn)
|
err = Connections.Add(fakeConn)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, Connections.GetActiveSessions(username), 2)
|
assert.Equal(t, Connections.GetActiveSessions(username), 2)
|
||||||
assert.Len(t, Connections.GetStats(), 3)
|
assert.Len(t, Connections.GetStats(""), 3)
|
||||||
Connections.RLock()
|
Connections.RLock()
|
||||||
assert.Len(t, Connections.sshConnections, 2)
|
assert.Len(t, Connections.sshConnections, 2)
|
||||||
Connections.RUnlock()
|
Connections.RUnlock()
|
||||||
|
@ -673,12 +700,12 @@ func TestIdleConnections(t *testing.T) {
|
||||||
return len(Connections.sshConnections) == 1
|
return len(Connections.sshConnections) == 1
|
||||||
}, 1*time.Second, 200*time.Millisecond)
|
}, 1*time.Second, 200*time.Millisecond)
|
||||||
stopEventScheduler()
|
stopEventScheduler()
|
||||||
assert.Len(t, Connections.GetStats(), 2)
|
assert.Len(t, Connections.GetStats(""), 2)
|
||||||
c.lastActivity.Store(time.Now().Add(-24 * time.Hour).UnixNano())
|
c.lastActivity.Store(time.Now().Add(-24 * time.Hour).UnixNano())
|
||||||
cFTP.lastActivity.Store(time.Now().Add(-24 * time.Hour).UnixNano())
|
cFTP.lastActivity.Store(time.Now().Add(-24 * time.Hour).UnixNano())
|
||||||
sshConn2.lastActivity.Store(c.lastActivity.Load())
|
sshConn2.lastActivity.Store(c.lastActivity.Load())
|
||||||
startPeriodicChecks(100 * time.Millisecond)
|
startPeriodicChecks(100 * time.Millisecond)
|
||||||
assert.Eventually(t, func() bool { return len(Connections.GetStats()) == 0 }, 2*time.Second, 200*time.Millisecond)
|
assert.Eventually(t, func() bool { return len(Connections.GetStats("")) == 0 }, 2*time.Second, 200*time.Millisecond)
|
||||||
assert.Eventually(t, func() bool {
|
assert.Eventually(t, func() bool {
|
||||||
Connections.RLock()
|
Connections.RLock()
|
||||||
defer Connections.RUnlock()
|
defer Connections.RUnlock()
|
||||||
|
@ -700,11 +727,11 @@ func TestCloseConnection(t *testing.T) {
|
||||||
assert.NoError(t, Connections.IsNewConnectionAllowed("127.0.0.1"))
|
assert.NoError(t, Connections.IsNewConnectionAllowed("127.0.0.1"))
|
||||||
err := Connections.Add(fakeConn)
|
err := Connections.Add(fakeConn)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, Connections.GetStats(), 1)
|
assert.Len(t, Connections.GetStats(""), 1)
|
||||||
res := Connections.Close(fakeConn.GetID())
|
res := Connections.Close(fakeConn.GetID(), "")
|
||||||
assert.True(t, res)
|
assert.True(t, res)
|
||||||
assert.Eventually(t, func() bool { return len(Connections.GetStats()) == 0 }, 300*time.Millisecond, 50*time.Millisecond)
|
assert.Eventually(t, func() bool { return len(Connections.GetStats("")) == 0 }, 300*time.Millisecond, 50*time.Millisecond)
|
||||||
res = Connections.Close(fakeConn.GetID())
|
res = Connections.Close(fakeConn.GetID(), "")
|
||||||
assert.False(t, res)
|
assert.False(t, res)
|
||||||
Connections.Remove(fakeConn.GetID())
|
Connections.Remove(fakeConn.GetID())
|
||||||
}
|
}
|
||||||
|
@ -716,8 +743,8 @@ func TestSwapConnection(t *testing.T) {
|
||||||
}
|
}
|
||||||
err := Connections.Add(fakeConn)
|
err := Connections.Add(fakeConn)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
if assert.Len(t, Connections.GetStats(), 1) {
|
if assert.Len(t, Connections.GetStats(""), 1) {
|
||||||
assert.Equal(t, "", Connections.GetStats()[0].Username)
|
assert.Equal(t, "", Connections.GetStats("")[0].Username)
|
||||||
}
|
}
|
||||||
c = NewBaseConnection("id", ProtocolFTP, "", "", dataprovider.User{
|
c = NewBaseConnection("id", ProtocolFTP, "", "", dataprovider.User{
|
||||||
BaseUser: sdk.BaseUser{
|
BaseUser: sdk.BaseUser{
|
||||||
|
@ -743,12 +770,12 @@ func TestSwapConnection(t *testing.T) {
|
||||||
Connections.Remove(fakeConn1.ID)
|
Connections.Remove(fakeConn1.ID)
|
||||||
err = Connections.Swap(fakeConn)
|
err = Connections.Swap(fakeConn)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
if assert.Len(t, Connections.GetStats(), 1) {
|
if assert.Len(t, Connections.GetStats(""), 1) {
|
||||||
assert.Equal(t, userTestUsername, Connections.GetStats()[0].Username)
|
assert.Equal(t, userTestUsername, Connections.GetStats("")[0].Username)
|
||||||
}
|
}
|
||||||
res := Connections.Close(fakeConn.GetID())
|
res := Connections.Close(fakeConn.GetID(), "")
|
||||||
assert.True(t, res)
|
assert.True(t, res)
|
||||||
assert.Eventually(t, func() bool { return len(Connections.GetStats()) == 0 }, 300*time.Millisecond, 50*time.Millisecond)
|
assert.Eventually(t, func() bool { return len(Connections.GetStats("")) == 0 }, 300*time.Millisecond, 50*time.Millisecond)
|
||||||
err = Connections.Swap(fakeConn)
|
err = Connections.Swap(fakeConn)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
}
|
}
|
||||||
|
@ -800,7 +827,7 @@ func TestConnectionStatus(t *testing.T) {
|
||||||
err = Connections.Add(fakeConn3)
|
err = Connections.Add(fakeConn3)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
stats := Connections.GetStats()
|
stats := Connections.GetStats("")
|
||||||
assert.Len(t, stats, 3)
|
assert.Len(t, stats, 3)
|
||||||
for _, stat := range stats {
|
for _, stat := range stats {
|
||||||
assert.Equal(t, stat.Username, username)
|
assert.Equal(t, stat.Username, username)
|
||||||
|
@ -838,24 +865,24 @@ func TestConnectionStatus(t *testing.T) {
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
|
|
||||||
Connections.Remove(fakeConn1.GetID())
|
Connections.Remove(fakeConn1.GetID())
|
||||||
stats = Connections.GetStats()
|
stats = Connections.GetStats("")
|
||||||
assert.Len(t, stats, 2)
|
assert.Len(t, stats, 2)
|
||||||
assert.Equal(t, fakeConn3.GetID(), stats[0].ConnectionID)
|
assert.Equal(t, fakeConn3.GetID(), stats[0].ConnectionID)
|
||||||
assert.Equal(t, fakeConn2.GetID(), stats[1].ConnectionID)
|
assert.Equal(t, fakeConn2.GetID(), stats[1].ConnectionID)
|
||||||
Connections.Remove(fakeConn2.GetID())
|
Connections.Remove(fakeConn2.GetID())
|
||||||
stats = Connections.GetStats()
|
stats = Connections.GetStats("")
|
||||||
assert.Len(t, stats, 1)
|
assert.Len(t, stats, 1)
|
||||||
assert.Equal(t, fakeConn3.GetID(), stats[0].ConnectionID)
|
assert.Equal(t, fakeConn3.GetID(), stats[0].ConnectionID)
|
||||||
Connections.Remove(fakeConn3.GetID())
|
Connections.Remove(fakeConn3.GetID())
|
||||||
stats = Connections.GetStats()
|
stats = Connections.GetStats("")
|
||||||
assert.Len(t, stats, 0)
|
assert.Len(t, stats, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestQuotaScans(t *testing.T) {
|
func TestQuotaScans(t *testing.T) {
|
||||||
username := "username"
|
username := "username"
|
||||||
assert.True(t, QuotaScans.AddUserQuotaScan(username))
|
assert.True(t, QuotaScans.AddUserQuotaScan(username, ""))
|
||||||
assert.False(t, QuotaScans.AddUserQuotaScan(username))
|
assert.False(t, QuotaScans.AddUserQuotaScan(username, ""))
|
||||||
usersScans := QuotaScans.GetUsersQuotaScans()
|
usersScans := QuotaScans.GetUsersQuotaScans("")
|
||||||
if assert.Len(t, usersScans, 1) {
|
if assert.Len(t, usersScans, 1) {
|
||||||
assert.Equal(t, usersScans[0].Username, username)
|
assert.Equal(t, usersScans[0].Username, username)
|
||||||
assert.Equal(t, QuotaScans.UserScans[0].StartTime, usersScans[0].StartTime)
|
assert.Equal(t, QuotaScans.UserScans[0].StartTime, usersScans[0].StartTime)
|
||||||
|
@ -865,7 +892,7 @@ func TestQuotaScans(t *testing.T) {
|
||||||
|
|
||||||
assert.True(t, QuotaScans.RemoveUserQuotaScan(username))
|
assert.True(t, QuotaScans.RemoveUserQuotaScan(username))
|
||||||
assert.False(t, QuotaScans.RemoveUserQuotaScan(username))
|
assert.False(t, QuotaScans.RemoveUserQuotaScan(username))
|
||||||
assert.Len(t, QuotaScans.GetUsersQuotaScans(), 0)
|
assert.Len(t, QuotaScans.GetUsersQuotaScans(""), 0)
|
||||||
assert.Len(t, usersScans, 1)
|
assert.Len(t, usersScans, 1)
|
||||||
|
|
||||||
folderName := "folder"
|
folderName := "folder"
|
||||||
|
@ -880,6 +907,24 @@ func TestQuotaScans(t *testing.T) {
|
||||||
assert.Len(t, QuotaScans.GetVFoldersQuotaScans(), 0)
|
assert.Len(t, QuotaScans.GetVFoldersQuotaScans(), 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestQuotaScansRole(t *testing.T) {
|
||||||
|
username := "u"
|
||||||
|
role1 := "r1"
|
||||||
|
role2 := "r2"
|
||||||
|
assert.True(t, QuotaScans.AddUserQuotaScan(username, role1))
|
||||||
|
assert.False(t, QuotaScans.AddUserQuotaScan(username, ""))
|
||||||
|
usersScans := QuotaScans.GetUsersQuotaScans("")
|
||||||
|
assert.Len(t, usersScans, 1)
|
||||||
|
assert.Empty(t, usersScans[0].Role)
|
||||||
|
usersScans = QuotaScans.GetUsersQuotaScans(role1)
|
||||||
|
assert.Len(t, usersScans, 1)
|
||||||
|
usersScans = QuotaScans.GetUsersQuotaScans(role2)
|
||||||
|
assert.Len(t, usersScans, 0)
|
||||||
|
assert.True(t, QuotaScans.RemoveUserQuotaScan(username))
|
||||||
|
assert.False(t, QuotaScans.RemoveUserQuotaScan(username))
|
||||||
|
assert.Len(t, QuotaScans.GetUsersQuotaScans(""), 0)
|
||||||
|
}
|
||||||
|
|
||||||
func TestProxyProtocolVersion(t *testing.T) {
|
func TestProxyProtocolVersion(t *testing.T) {
|
||||||
c := Configuration{
|
c := Configuration{
|
||||||
ProxyProtocol: 0,
|
ProxyProtocol: 0,
|
||||||
|
@ -1342,13 +1387,13 @@ func TestUpdateTransferTimestamps(t *testing.T) {
|
||||||
|
|
||||||
err = dataprovider.UpdateUserTransferTimestamps(username, true)
|
err = dataprovider.UpdateUserTransferTimestamps(username, true)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
userGet, err := dataprovider.UserExists(username)
|
userGet, err := dataprovider.UserExists(username, "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Greater(t, userGet.FirstUpload, int64(0))
|
assert.Greater(t, userGet.FirstUpload, int64(0))
|
||||||
assert.Equal(t, int64(0), user.FirstDownload)
|
assert.Equal(t, int64(0), user.FirstDownload)
|
||||||
err = dataprovider.UpdateUserTransferTimestamps(username, false)
|
err = dataprovider.UpdateUserTransferTimestamps(username, false)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
userGet, err = dataprovider.UserExists(username)
|
userGet, err = dataprovider.UserExists(username, "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Greater(t, userGet.FirstUpload, int64(0))
|
assert.Greater(t, userGet.FirstUpload, int64(0))
|
||||||
assert.Greater(t, userGet.FirstDownload, int64(0))
|
assert.Greater(t, userGet.FirstDownload, int64(0))
|
||||||
|
@ -1358,23 +1403,40 @@ func TestUpdateTransferTimestamps(t *testing.T) {
|
||||||
err = dataprovider.UpdateUserTransferTimestamps(username, false)
|
err = dataprovider.UpdateUserTransferTimestamps(username, false)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
// cleanup
|
// cleanup
|
||||||
err = dataprovider.DeleteUser(username, "", "")
|
err = dataprovider.DeleteUser(username, "", "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMetadataAPI(t *testing.T) {
|
func TestMetadataAPI(t *testing.T) {
|
||||||
username := "metadatauser"
|
username := "metadatauser"
|
||||||
require.False(t, ActiveMetadataChecks.Remove(username))
|
require.False(t, ActiveMetadataChecks.Remove(username))
|
||||||
require.True(t, ActiveMetadataChecks.Add(username))
|
require.True(t, ActiveMetadataChecks.Add(username, ""))
|
||||||
require.False(t, ActiveMetadataChecks.Add(username))
|
require.False(t, ActiveMetadataChecks.Add(username, ""))
|
||||||
checks := ActiveMetadataChecks.Get()
|
checks := ActiveMetadataChecks.Get("")
|
||||||
require.Len(t, checks, 1)
|
require.Len(t, checks, 1)
|
||||||
checks[0].Username = username + "a"
|
checks[0].Username = username + "a"
|
||||||
checks = ActiveMetadataChecks.Get()
|
checks = ActiveMetadataChecks.Get("")
|
||||||
require.Len(t, checks, 1)
|
require.Len(t, checks, 1)
|
||||||
require.Equal(t, username, checks[0].Username)
|
require.Equal(t, username, checks[0].Username)
|
||||||
require.True(t, ActiveMetadataChecks.Remove(username))
|
require.True(t, ActiveMetadataChecks.Remove(username))
|
||||||
require.Len(t, ActiveMetadataChecks.Get(), 0)
|
require.Len(t, ActiveMetadataChecks.Get(""), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMetadataAPIRole(t *testing.T) {
|
||||||
|
username := "muser"
|
||||||
|
role1 := "r1"
|
||||||
|
role2 := "r2"
|
||||||
|
require.True(t, ActiveMetadataChecks.Add(username, role2))
|
||||||
|
require.False(t, ActiveMetadataChecks.Add(username, ""))
|
||||||
|
checks := ActiveMetadataChecks.Get("")
|
||||||
|
require.Len(t, checks, 1)
|
||||||
|
assert.Empty(t, checks[0].Role)
|
||||||
|
checks = ActiveMetadataChecks.Get(role1)
|
||||||
|
require.Len(t, checks, 0)
|
||||||
|
checks = ActiveMetadataChecks.Get(role2)
|
||||||
|
require.Len(t, checks, 1)
|
||||||
|
require.True(t, ActiveMetadataChecks.Remove(username))
|
||||||
|
require.Len(t, ActiveMetadataChecks.Get(""), 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkBcryptHashing(b *testing.B) {
|
func BenchmarkBcryptHashing(b *testing.B) {
|
||||||
|
|
|
@ -98,6 +98,11 @@ func (c *BaseConnection) GetUsername() string {
|
||||||
return c.User.Username
|
return c.User.Username
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetRole returns the role for the user associated with this connection
|
||||||
|
func (c *BaseConnection) GetRole() string {
|
||||||
|
return c.User.Role
|
||||||
|
}
|
||||||
|
|
||||||
// GetMaxSessions returns the maximum number of concurrent sessions allowed
|
// GetMaxSessions returns the maximum number of concurrent sessions allowed
|
||||||
func (c *BaseConnection) GetMaxSessions() int {
|
func (c *BaseConnection) GetMaxSessions() int {
|
||||||
return c.User.MaxSessions
|
return c.User.MaxSessions
|
||||||
|
|
|
@ -62,23 +62,25 @@ type ActiveRetentionChecks struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get returns the active retention checks
|
// Get returns the active retention checks
|
||||||
func (c *ActiveRetentionChecks) Get() []RetentionCheck {
|
func (c *ActiveRetentionChecks) Get(role string) []RetentionCheck {
|
||||||
c.RLock()
|
c.RLock()
|
||||||
defer c.RUnlock()
|
defer c.RUnlock()
|
||||||
|
|
||||||
checks := make([]RetentionCheck, 0, len(c.Checks))
|
checks := make([]RetentionCheck, 0, len(c.Checks))
|
||||||
for _, check := range c.Checks {
|
for _, check := range c.Checks {
|
||||||
foldersCopy := make([]dataprovider.FolderRetention, len(check.Folders))
|
if role == "" || role == check.Role {
|
||||||
copy(foldersCopy, check.Folders)
|
foldersCopy := make([]dataprovider.FolderRetention, len(check.Folders))
|
||||||
notificationsCopy := make([]string, len(check.Notifications))
|
copy(foldersCopy, check.Folders)
|
||||||
copy(notificationsCopy, check.Notifications)
|
notificationsCopy := make([]string, len(check.Notifications))
|
||||||
checks = append(checks, RetentionCheck{
|
copy(notificationsCopy, check.Notifications)
|
||||||
Username: check.Username,
|
checks = append(checks, RetentionCheck{
|
||||||
StartTime: check.StartTime,
|
Username: check.Username,
|
||||||
Notifications: notificationsCopy,
|
StartTime: check.StartTime,
|
||||||
Email: check.Email,
|
Notifications: notificationsCopy,
|
||||||
Folders: foldersCopy,
|
Email: check.Email,
|
||||||
})
|
Folders: foldersCopy,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return checks
|
return checks
|
||||||
}
|
}
|
||||||
|
@ -100,6 +102,7 @@ func (c *ActiveRetentionChecks) Add(check RetentionCheck, user *dataprovider.Use
|
||||||
conn.SetProtocol(ProtocolDataRetention)
|
conn.SetProtocol(ProtocolDataRetention)
|
||||||
conn.ID = fmt.Sprintf("data_retention_%v", user.Username)
|
conn.ID = fmt.Sprintf("data_retention_%v", user.Username)
|
||||||
check.Username = user.Username
|
check.Username = user.Username
|
||||||
|
check.Role = user.Role
|
||||||
check.StartTime = util.GetTimeAsMsSinceEpoch(time.Now())
|
check.StartTime = util.GetTimeAsMsSinceEpoch(time.Now())
|
||||||
check.conn = conn
|
check.conn = conn
|
||||||
check.updateUserPermissions()
|
check.updateUserPermissions()
|
||||||
|
@ -148,6 +151,7 @@ type RetentionCheck struct {
|
||||||
Notifications []RetentionCheckNotification `json:"notifications,omitempty"`
|
Notifications []RetentionCheckNotification `json:"notifications,omitempty"`
|
||||||
// email to use if the notification method is set to email
|
// email to use if the notification method is set to email
|
||||||
Email string `json:"email,omitempty"`
|
Email string `json:"email,omitempty"`
|
||||||
|
Role string `json:"-"`
|
||||||
// Cleanup results
|
// Cleanup results
|
||||||
results []folderRetentionCheckResult `json:"-"`
|
results []folderRetentionCheckResult `json:"-"`
|
||||||
conn *BaseConnection
|
conn *BaseConnection
|
||||||
|
|
|
@ -316,7 +316,7 @@ func TestRetentionCheckAddRemove(t *testing.T) {
|
||||||
Notifications: []RetentionCheckNotification{RetentionCheckNotificationHook},
|
Notifications: []RetentionCheckNotification{RetentionCheckNotificationHook},
|
||||||
}
|
}
|
||||||
assert.NotNil(t, RetentionChecks.Add(check, &user))
|
assert.NotNil(t, RetentionChecks.Add(check, &user))
|
||||||
checks := RetentionChecks.Get()
|
checks := RetentionChecks.Get("")
|
||||||
require.Len(t, checks, 1)
|
require.Len(t, checks, 1)
|
||||||
assert.Equal(t, username, checks[0].Username)
|
assert.Equal(t, username, checks[0].Username)
|
||||||
assert.Greater(t, checks[0].StartTime, int64(0))
|
assert.Greater(t, checks[0].StartTime, int64(0))
|
||||||
|
@ -328,10 +328,45 @@ func TestRetentionCheckAddRemove(t *testing.T) {
|
||||||
|
|
||||||
assert.Nil(t, RetentionChecks.Add(check, &user))
|
assert.Nil(t, RetentionChecks.Add(check, &user))
|
||||||
assert.True(t, RetentionChecks.remove(username))
|
assert.True(t, RetentionChecks.remove(username))
|
||||||
require.Len(t, RetentionChecks.Get(), 0)
|
require.Len(t, RetentionChecks.Get(""), 0)
|
||||||
assert.False(t, RetentionChecks.remove(username))
|
assert.False(t, RetentionChecks.remove(username))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRetentionCheckRole(t *testing.T) {
|
||||||
|
username := "retuser"
|
||||||
|
role1 := "retrole1"
|
||||||
|
role2 := "retrole2"
|
||||||
|
user := dataprovider.User{
|
||||||
|
BaseUser: sdk.BaseUser{
|
||||||
|
Username: username,
|
||||||
|
Role: role1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
user.Permissions = make(map[string][]string)
|
||||||
|
user.Permissions["/"] = []string{dataprovider.PermAny}
|
||||||
|
check := RetentionCheck{
|
||||||
|
Folders: []dataprovider.FolderRetention{
|
||||||
|
{
|
||||||
|
Path: "/",
|
||||||
|
Retention: 48,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Notifications: []RetentionCheckNotification{RetentionCheckNotificationHook},
|
||||||
|
}
|
||||||
|
assert.NotNil(t, RetentionChecks.Add(check, &user))
|
||||||
|
checks := RetentionChecks.Get("")
|
||||||
|
require.Len(t, checks, 1)
|
||||||
|
assert.Empty(t, checks[0].Role)
|
||||||
|
checks = RetentionChecks.Get(role1)
|
||||||
|
require.Len(t, checks, 1)
|
||||||
|
checks = RetentionChecks.Get(role2)
|
||||||
|
require.Len(t, checks, 0)
|
||||||
|
user.Role = ""
|
||||||
|
assert.Nil(t, RetentionChecks.Add(check, &user))
|
||||||
|
assert.True(t, RetentionChecks.remove(username))
|
||||||
|
require.Len(t, RetentionChecks.Get(""), 0)
|
||||||
|
}
|
||||||
|
|
||||||
func TestCleanupErrors(t *testing.T) {
|
func TestCleanupErrors(t *testing.T) {
|
||||||
user := dataprovider.User{
|
user := dataprovider.User{
|
||||||
BaseUser: sdk.BaseUser{
|
BaseUser: sdk.BaseUser{
|
||||||
|
|
|
@ -531,7 +531,7 @@ func (p *EventParams) getUserFromSender() (dataprovider.User, error) {
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
user, err := dataprovider.UserExists(p.sender)
|
user, err := dataprovider.UserExists(p.sender, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
eventManagerLog(logger.LevelError, "unable to get user %q: %+v", p.sender, err)
|
eventManagerLog(logger.LevelError, "unable to get user %q: %+v", p.sender, err)
|
||||||
return user, fmt.Errorf("error getting user %q", p.sender)
|
return user, fmt.Errorf("error getting user %q", p.sender)
|
||||||
|
@ -1652,7 +1652,7 @@ func executeQuotaResetForUser(user dataprovider.User) error {
|
||||||
user.Username, err)
|
user.Username, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !QuotaScans.AddUserQuotaScan(user.Username) {
|
if !QuotaScans.AddUserQuotaScan(user.Username, user.Role) {
|
||||||
eventManagerLog(logger.LevelError, "another quota scan is already in progress for user %q", user.Username)
|
eventManagerLog(logger.LevelError, "another quota scan is already in progress for user %q", user.Username)
|
||||||
return fmt.Errorf("another quota scan is in progress for user %q", user.Username)
|
return fmt.Errorf("another quota scan is in progress for user %q", user.Username)
|
||||||
}
|
}
|
||||||
|
@ -1874,7 +1874,7 @@ func executeMetadataCheckForUser(user dataprovider.User) error {
|
||||||
user.Username, err)
|
user.Username, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !ActiveMetadataChecks.Add(user.Username) {
|
if !ActiveMetadataChecks.Add(user.Username, user.Role) {
|
||||||
eventManagerLog(logger.LevelError, "another metadata check is already in progress for user %q", user.Username)
|
eventManagerLog(logger.LevelError, "another metadata check is already in progress for user %q", user.Username)
|
||||||
return fmt.Errorf("another metadata check is in progress for user %q", user.Username)
|
return fmt.Errorf("another metadata check is in progress for user %q", user.Username)
|
||||||
}
|
}
|
||||||
|
|
|
@ -645,12 +645,12 @@ func TestEventRuleActions(t *testing.T) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
userGet, err := dataprovider.UserExists(username1)
|
userGet, err := dataprovider.UserExists(username1, "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, 1, userGet.UsedQuotaFiles)
|
assert.Equal(t, 1, userGet.UsedQuotaFiles)
|
||||||
assert.Equal(t, int64(4), userGet.UsedQuotaSize)
|
assert.Equal(t, int64(4), userGet.UsedQuotaSize)
|
||||||
// simulate another quota scan in progress
|
// simulate another quota scan in progress
|
||||||
assert.True(t, QuotaScans.AddUserQuotaScan(username1))
|
assert.True(t, QuotaScans.AddUserQuotaScan(username1, ""))
|
||||||
err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{
|
err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{
|
||||||
Names: []dataprovider.ConditionPattern{
|
Names: []dataprovider.ConditionPattern{
|
||||||
{
|
{
|
||||||
|
@ -694,7 +694,7 @@ func TestEventRuleActions(t *testing.T) {
|
||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
// simulate another metadata check in progress
|
// simulate another metadata check in progress
|
||||||
assert.True(t, ActiveMetadataChecks.Add(username1))
|
assert.True(t, ActiveMetadataChecks.Add(username1, ""))
|
||||||
err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{
|
err = executeRuleAction(action, &EventParams{}, dataprovider.ConditionOptions{
|
||||||
Names: []dataprovider.ConditionPattern{
|
Names: []dataprovider.ConditionPattern{
|
||||||
{
|
{
|
||||||
|
@ -850,7 +850,7 @@ func TestEventRuleActions(t *testing.T) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
userGet, err = dataprovider.UserExists(username1)
|
userGet, err = dataprovider.UserExists(username1, "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, int64(0), userGet.UsedDownloadDataTransfer)
|
assert.Equal(t, int64(0), userGet.UsedDownloadDataTransfer)
|
||||||
assert.Equal(t, int64(0), userGet.UsedUploadDataTransfer)
|
assert.Equal(t, int64(0), userGet.UsedUploadDataTransfer)
|
||||||
|
@ -948,9 +948,9 @@ func TestEventRuleActions(t *testing.T) {
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, getErrorString(err), "no file/folder compressed")
|
assert.Contains(t, getErrorString(err), "no file/folder compressed")
|
||||||
|
|
||||||
err = dataprovider.DeleteUser(username1, "", "")
|
err = dataprovider.DeleteUser(username1, "", "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = dataprovider.DeleteUser(username2, "", "")
|
err = dataprovider.DeleteUser(username2, "", "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
// test folder quota reset
|
// test folder quota reset
|
||||||
foldername1 := "f1"
|
foldername1 := "f1"
|
||||||
|
@ -1085,7 +1085,7 @@ func TestEventRuleActionsNoGroupMatching(t *testing.T) {
|
||||||
assert.Contains(t, err.Error(), "no retention check executed")
|
assert.Contains(t, err.Error(), "no retention check executed")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = dataprovider.DeleteUser(username, "", "")
|
err = dataprovider.DeleteUser(username, "", "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = os.RemoveAll(user.GetHomeDir())
|
err = os.RemoveAll(user.GetHomeDir())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -1147,7 +1147,7 @@ func TestGetFileContent(t *testing.T) {
|
||||||
_, err = getMailAttachments(user, []string{"/file.txt"}, replacer)
|
_, err = getMailAttachments(user, []string{"/file.txt"}, replacer)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
|
|
||||||
err = dataprovider.DeleteUser(username, "", "")
|
err = dataprovider.DeleteUser(username, "", "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = os.RemoveAll(user.GetHomeDir())
|
err = os.RemoveAll(user.GetHomeDir())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -1232,7 +1232,7 @@ func TestFilesystemActionErrors(t *testing.T) {
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
user.FsConfig.Provider = sdk.LocalFilesystemProvider
|
user.FsConfig.Provider = sdk.LocalFilesystemProvider
|
||||||
user.Permissions["/"] = []string{dataprovider.PermUpload}
|
user.Permissions["/"] = []string{dataprovider.PermUpload}
|
||||||
err = dataprovider.DeleteUser(username, "", "")
|
err = dataprovider.DeleteUser(username, "", "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = dataprovider.AddUser(&user, "", "")
|
err = dataprovider.AddUser(&user, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -1344,7 +1344,7 @@ func TestFilesystemActionErrors(t *testing.T) {
|
||||||
assert.Contains(t, getErrorString(err), "is outside base dir")
|
assert.Contains(t, getErrorString(err), "is outside base dir")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = dataprovider.DeleteUser(username, "", "")
|
err = dataprovider.DeleteUser(username, "", "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = os.RemoveAll(user.GetHomeDir())
|
err = os.RemoveAll(user.GetHomeDir())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -1400,7 +1400,7 @@ func TestQuotaActionsWithQuotaTrackDisabled(t *testing.T) {
|
||||||
|
|
||||||
err = os.RemoveAll(user.GetHomeDir())
|
err = os.RemoveAll(user.GetHomeDir())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = dataprovider.DeleteUser(username, "", "")
|
err = dataprovider.DeleteUser(username, "", "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
foldername := "f1"
|
foldername := "f1"
|
||||||
|
|
|
@ -3067,7 +3067,7 @@ func TestUserPasswordHashing(t *testing.T) {
|
||||||
err = dataprovider.Initialize(providerConf, configDir, true)
|
err = dataprovider.Initialize(providerConf, configDir, true)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
currentUser, err := dataprovider.UserExists(user.Username)
|
currentUser, err := dataprovider.UserExists(user.Username, "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.True(t, strings.HasPrefix(currentUser.Password, "$2a$"))
|
assert.True(t, strings.HasPrefix(currentUser.Password, "$2a$"))
|
||||||
|
|
||||||
|
@ -3088,7 +3088,7 @@ func TestUserPasswordHashing(t *testing.T) {
|
||||||
user, _, err = httpdtest.AddUser(u, http.StatusCreated)
|
user, _, err = httpdtest.AddUser(u, http.StatusCreated)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
currentUser, err = dataprovider.UserExists(user.Username)
|
currentUser, err = dataprovider.UserExists(user.Username, "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.True(t, strings.HasPrefix(currentUser.Password, "$argon2id$"))
|
assert.True(t, strings.HasPrefix(currentUser.Password, "$argon2id$"))
|
||||||
|
|
||||||
|
@ -3193,7 +3193,7 @@ func TestDelayedQuotaUpdater(t *testing.T) {
|
||||||
assert.Equal(t, int64(100), ulSize)
|
assert.Equal(t, int64(100), ulSize)
|
||||||
assert.Equal(t, int64(200), dlSize)
|
assert.Equal(t, int64(200), dlSize)
|
||||||
|
|
||||||
userGet, err := dataprovider.UserExists(user.Username)
|
userGet, err := dataprovider.UserExists(user.Username, "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, 0, userGet.UsedQuotaFiles)
|
assert.Equal(t, 0, userGet.UsedQuotaFiles)
|
||||||
assert.Equal(t, int64(0), userGet.UsedQuotaSize)
|
assert.Equal(t, int64(0), userGet.UsedQuotaSize)
|
||||||
|
@ -3211,7 +3211,7 @@ func TestDelayedQuotaUpdater(t *testing.T) {
|
||||||
assert.Equal(t, int64(100), ulSize)
|
assert.Equal(t, int64(100), ulSize)
|
||||||
assert.Equal(t, int64(200), dlSize)
|
assert.Equal(t, int64(200), dlSize)
|
||||||
|
|
||||||
userGet, err = dataprovider.UserExists(user.Username)
|
userGet, err = dataprovider.UserExists(user.Username, "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, 10, userGet.UsedQuotaFiles)
|
assert.Equal(t, 10, userGet.UsedQuotaFiles)
|
||||||
assert.Equal(t, int64(6000), userGet.UsedQuotaSize)
|
assert.Equal(t, int64(6000), userGet.UsedQuotaSize)
|
||||||
|
@ -5609,7 +5609,7 @@ func TestEventRuleIPBlocked(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = dataprovider.DeleteEventAction(action2.Name, "", "")
|
err = dataprovider.DeleteEventAction(action2.Name, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = dataprovider.DeleteUser(user.Username, "", "")
|
err = dataprovider.DeleteUser(user.Username, "", "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = os.RemoveAll(user.GetHomeDir())
|
err = os.RemoveAll(user.GetHomeDir())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -5815,7 +5815,7 @@ func TestRetentionAPI(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
assert.Eventually(t, func() bool {
|
assert.Eventually(t, func() bool {
|
||||||
return len(common.RetentionChecks.Get()) == 0
|
return len(common.RetentionChecks.Get("")) == 0
|
||||||
}, 1000*time.Millisecond, 50*time.Millisecond)
|
}, 1000*time.Millisecond, 50*time.Millisecond)
|
||||||
|
|
||||||
_, err = client.Stat(uploadPath)
|
_, err = client.Stat(uploadPath)
|
||||||
|
@ -5828,7 +5828,7 @@ func TestRetentionAPI(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
assert.Eventually(t, func() bool {
|
assert.Eventually(t, func() bool {
|
||||||
return len(common.RetentionChecks.Get()) == 0
|
return len(common.RetentionChecks.Get("")) == 0
|
||||||
}, 1000*time.Millisecond, 50*time.Millisecond)
|
}, 1000*time.Millisecond, 50*time.Millisecond)
|
||||||
|
|
||||||
_, err = client.Stat(uploadPath)
|
_, err = client.Stat(uploadPath)
|
||||||
|
@ -5850,7 +5850,7 @@ func TestRetentionAPI(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
assert.Eventually(t, func() bool {
|
assert.Eventually(t, func() bool {
|
||||||
return len(common.RetentionChecks.Get()) == 0
|
return len(common.RetentionChecks.Get("")) == 0
|
||||||
}, 1000*time.Millisecond, 50*time.Millisecond)
|
}, 1000*time.Millisecond, 50*time.Millisecond)
|
||||||
|
|
||||||
_, err = client.Stat(uploadPath)
|
_, err = client.Stat(uploadPath)
|
||||||
|
@ -5905,7 +5905,7 @@ func TestRetentionAPI(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
assert.Eventually(t, func() bool {
|
assert.Eventually(t, func() bool {
|
||||||
return len(common.RetentionChecks.Get()) == 0
|
return len(common.RetentionChecks.Get("")) == 0
|
||||||
}, 1000*time.Millisecond, 50*time.Millisecond)
|
}, 1000*time.Millisecond, 50*time.Millisecond)
|
||||||
|
|
||||||
_, err = client.Stat(uploadPath)
|
_, err = client.Stat(uploadPath)
|
||||||
|
@ -5918,7 +5918,7 @@ func TestRetentionAPI(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
assert.Eventually(t, func() bool {
|
assert.Eventually(t, func() bool {
|
||||||
return len(common.RetentionChecks.Get()) == 0
|
return len(common.RetentionChecks.Get("")) == 0
|
||||||
}, 1000*time.Millisecond, 50*time.Millisecond)
|
}, 1000*time.Millisecond, 50*time.Millisecond)
|
||||||
|
|
||||||
_, err = client.Stat(uploadPath)
|
_, err = client.Stat(uploadPath)
|
||||||
|
@ -5940,7 +5940,7 @@ func TestRetentionAPI(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
assert.Eventually(t, func() bool {
|
assert.Eventually(t, func() bool {
|
||||||
return len(common.RetentionChecks.Get()) == 0
|
return len(common.RetentionChecks.Get("")) == 0
|
||||||
}, 1000*time.Millisecond, 50*time.Millisecond)
|
}, 1000*time.Millisecond, 50*time.Millisecond)
|
||||||
|
|
||||||
_, err = client.Stat(innerUploadFilePath)
|
_, err = client.Stat(innerUploadFilePath)
|
||||||
|
@ -5975,7 +5975,7 @@ func TestRetentionAPI(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
assert.Eventually(t, func() bool {
|
assert.Eventually(t, func() bool {
|
||||||
return len(common.RetentionChecks.Get()) == 0
|
return len(common.RetentionChecks.Get("")) == 0
|
||||||
}, 1000*time.Millisecond, 50*time.Millisecond)
|
}, 1000*time.Millisecond, 50*time.Millisecond)
|
||||||
|
|
||||||
err = os.Chmod(dirPath, 0555)
|
err = os.Chmod(dirPath, 0555)
|
||||||
|
@ -5985,7 +5985,7 @@ func TestRetentionAPI(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
assert.Eventually(t, func() bool {
|
assert.Eventually(t, func() bool {
|
||||||
return len(common.RetentionChecks.Get()) == 0
|
return len(common.RetentionChecks.Get("")) == 0
|
||||||
}, 1000*time.Millisecond, 50*time.Millisecond)
|
}, 1000*time.Millisecond, 50*time.Millisecond)
|
||||||
|
|
||||||
err = os.Chmod(dirPath, os.ModePerm)
|
err = os.Chmod(dirPath, os.ModePerm)
|
||||||
|
@ -5995,7 +5995,7 @@ func TestRetentionAPI(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
assert.Eventually(t, func() bool {
|
assert.Eventually(t, func() bool {
|
||||||
return len(common.RetentionChecks.Get()) == 0
|
return len(common.RetentionChecks.Get("")) == 0
|
||||||
}, 1000*time.Millisecond, 50*time.Millisecond)
|
}, 1000*time.Millisecond, 50*time.Millisecond)
|
||||||
|
|
||||||
assert.NoDirExists(t, dirPath)
|
assert.NoDirExists(t, dirPath)
|
||||||
|
|
|
@ -83,7 +83,7 @@ func TestTransfersCheckerDiskQuota(t *testing.T) {
|
||||||
assert.Equal(t, int64(120), group.UserSettings.QuotaSize)
|
assert.Equal(t, int64(120), group.UserSettings.QuotaSize)
|
||||||
err = dataprovider.AddUser(&user, "", "")
|
err = dataprovider.AddUser(&user, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
user, err = dataprovider.GetUserWithGroupSettings(username)
|
user, err = dataprovider.GetUserWithGroupSettings(username, "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
connID1 := xid.New().String()
|
connID1 := xid.New().String()
|
||||||
|
@ -243,10 +243,10 @@ func TestTransfersCheckerDiskQuota(t *testing.T) {
|
||||||
Connections.Remove(fakeConn3.GetID())
|
Connections.Remove(fakeConn3.GetID())
|
||||||
Connections.Remove(fakeConn4.GetID())
|
Connections.Remove(fakeConn4.GetID())
|
||||||
Connections.Remove(fakeConn5.GetID())
|
Connections.Remove(fakeConn5.GetID())
|
||||||
stats := Connections.GetStats()
|
stats := Connections.GetStats("")
|
||||||
assert.Len(t, stats, 0)
|
assert.Len(t, stats, 0)
|
||||||
|
|
||||||
err = dataprovider.DeleteUser(user.Username, "", "")
|
err = dataprovider.DeleteUser(user.Username, "", "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = os.RemoveAll(user.GetHomeDir())
|
err = os.RemoveAll(user.GetHomeDir())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -366,10 +366,10 @@ func TestTransferCheckerTransferQuota(t *testing.T) {
|
||||||
|
|
||||||
Connections.Remove(fakeConn3.GetID())
|
Connections.Remove(fakeConn3.GetID())
|
||||||
Connections.Remove(fakeConn4.GetID())
|
Connections.Remove(fakeConn4.GetID())
|
||||||
stats := Connections.GetStats()
|
stats := Connections.GetStats("")
|
||||||
assert.Len(t, stats, 0)
|
assert.Len(t, stats, 0)
|
||||||
|
|
||||||
err = dataprovider.DeleteUser(user.Username, "", "")
|
err = dataprovider.DeleteUser(user.Username, "", "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = os.RemoveAll(user.GetHomeDir())
|
err = os.RemoveAll(user.GetHomeDir())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -671,7 +671,7 @@ func TestGetUsersForQuotaCheck(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < 40; i++ {
|
for i := 0; i < 40; i++ {
|
||||||
err = dataprovider.DeleteUser(fmt.Sprintf("user%v", i), "", "")
|
err = dataprovider.DeleteUser(fmt.Sprintf("user%v", i), "", "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = dataprovider.DeleteFolder(fmt.Sprintf("f%v", i), "", "")
|
err = dataprovider.DeleteFolder(fmt.Sprintf("f%v", i), "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
|
@ -50,6 +50,7 @@ const (
|
||||||
actionObjectShare = "share"
|
actionObjectShare = "share"
|
||||||
actionObjectEventAction = "event_action"
|
actionObjectEventAction = "event_action"
|
||||||
actionObjectEventRule = "event_rule"
|
actionObjectEventRule = "event_rule"
|
||||||
|
actionObjectRole = "role"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
|
@ -56,6 +56,7 @@ const (
|
||||||
PermAdminMetadataChecks = "metadata_checks"
|
PermAdminMetadataChecks = "metadata_checks"
|
||||||
PermAdminViewEvents = "view_events"
|
PermAdminViewEvents = "view_events"
|
||||||
PermAdminManageEventRules = "manage_event_rules"
|
PermAdminManageEventRules = "manage_event_rules"
|
||||||
|
PermAdminManageRoles = "manage_roles"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -70,9 +71,11 @@ const (
|
||||||
var (
|
var (
|
||||||
validAdminPerms = []string{PermAdminAny, PermAdminAddUsers, PermAdminChangeUsers, PermAdminDeleteUsers,
|
validAdminPerms = []string{PermAdminAny, PermAdminAddUsers, PermAdminChangeUsers, PermAdminDeleteUsers,
|
||||||
PermAdminViewUsers, PermAdminManageGroups, PermAdminViewConnections, PermAdminCloseConnections,
|
PermAdminViewUsers, PermAdminManageGroups, PermAdminViewConnections, PermAdminCloseConnections,
|
||||||
PermAdminViewServerStatus, PermAdminManageAdmins, PermAdminManageAPIKeys, PermAdminQuotaScans,
|
PermAdminViewServerStatus, PermAdminManageAdmins, PermAdminManageRoles, PermAdminManageEventRules,
|
||||||
PermAdminManageSystem, PermAdminManageDefender, PermAdminViewDefender, PermAdminRetentionChecks,
|
PermAdminManageAPIKeys, PermAdminQuotaScans, PermAdminManageSystem, PermAdminManageDefender,
|
||||||
PermAdminMetadataChecks, PermAdminViewEvents}
|
PermAdminViewDefender, PermAdminRetentionChecks, PermAdminMetadataChecks, PermAdminViewEvents}
|
||||||
|
forbiddenPermsForRoleAdmins = []string{PermAdminAny, PermAdminManageAdmins, PermAdminManageSystem,
|
||||||
|
PermAdminManageEventRules, PermAdminManageRoles, PermAdminViewEvents}
|
||||||
)
|
)
|
||||||
|
|
||||||
// AdminTOTPConfig defines the time-based one time password configuration
|
// AdminTOTPConfig defines the time-based one time password configuration
|
||||||
|
@ -255,6 +258,14 @@ type Admin struct {
|
||||||
UpdatedAt int64 `json:"updated_at"`
|
UpdatedAt int64 `json:"updated_at"`
|
||||||
// Last login as unix timestamp in milliseconds
|
// Last login as unix timestamp in milliseconds
|
||||||
LastLogin int64 `json:"last_login"`
|
LastLogin int64 `json:"last_login"`
|
||||||
|
// Role name. If set the admin can only administer users with the same role.
|
||||||
|
// Role admins cannot have the following permissions:
|
||||||
|
// - manage_admins
|
||||||
|
// - manage_apikeys
|
||||||
|
// - manage_system
|
||||||
|
// - manage_event_rules
|
||||||
|
// - manage_roles
|
||||||
|
Role string `json:"role,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CountUnusedRecoveryCodes returns the number of unused recovery codes
|
// CountUnusedRecoveryCodes returns the number of unused recovery codes
|
||||||
|
@ -322,7 +333,13 @@ func (a *Admin) validatePermissions() error {
|
||||||
}
|
}
|
||||||
for _, perm := range a.Permissions {
|
for _, perm := range a.Permissions {
|
||||||
if !util.Contains(validAdminPerms, perm) {
|
if !util.Contains(validAdminPerms, perm) {
|
||||||
return util.NewValidationError(fmt.Sprintf("invalid permission: %#v", perm))
|
return util.NewValidationError(fmt.Sprintf("invalid permission: %q", perm))
|
||||||
|
}
|
||||||
|
if a.Role != "" {
|
||||||
|
if util.Contains(forbiddenPermsForRoleAdmins, perm) {
|
||||||
|
return util.NewValidationError(fmt.Sprintf("a role admin cannot have the following permissions: %q",
|
||||||
|
strings.Join(forbiddenPermsForRoleAdmins, ",")))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -604,6 +621,7 @@ func (a *Admin) getACopy() Admin {
|
||||||
LastLogin: a.LastLogin,
|
LastLogin: a.LastLogin,
|
||||||
CreatedAt: a.CreatedAt,
|
CreatedAt: a.CreatedAt,
|
||||||
UpdatedAt: a.UpdatedAt,
|
UpdatedAt: a.UpdatedAt,
|
||||||
|
Role: a.Role,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -165,7 +165,7 @@ func (k *APIKey) validate() error {
|
||||||
k.Admin = ""
|
k.Admin = ""
|
||||||
}
|
}
|
||||||
if k.User != "" {
|
if k.User != "" {
|
||||||
_, err := provider.userExists(k.User)
|
_, err := provider.userExists(k.User, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.NewValidationError(fmt.Sprintf("unable to check API key user %v: %v", k.User, err))
|
return util.NewValidationError(fmt.Sprintf("unable to check API key user %v: %v", k.User, err))
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
boltDatabaseVersion = 23
|
boltDatabaseVersion = 24
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -47,10 +47,11 @@ var (
|
||||||
sharesBucket = []byte("shares")
|
sharesBucket = []byte("shares")
|
||||||
actionsBucket = []byte("events_actions")
|
actionsBucket = []byte("events_actions")
|
||||||
rulesBucket = []byte("events_rules")
|
rulesBucket = []byte("events_rules")
|
||||||
|
rolesBucket = []byte("roles")
|
||||||
dbVersionBucket = []byte("db_version")
|
dbVersionBucket = []byte("db_version")
|
||||||
dbVersionKey = []byte("version")
|
dbVersionKey = []byte("version")
|
||||||
boltBuckets = [][]byte{usersBucket, groupsBucket, foldersBucket, adminsBucket, apiKeysBucket,
|
boltBuckets = [][]byte{usersBucket, groupsBucket, foldersBucket, adminsBucket, apiKeysBucket,
|
||||||
sharesBucket, actionsBucket, rulesBucket, dbVersionBucket}
|
sharesBucket, actionsBucket, rulesBucket, rolesBucket, dbVersionBucket}
|
||||||
)
|
)
|
||||||
|
|
||||||
// BoltProvider defines the auth provider for bolt key/value store
|
// BoltProvider defines the auth provider for bolt key/value store
|
||||||
|
@ -105,7 +106,7 @@ func (p *BoltProvider) validateUserAndTLSCert(username, protocol string, tlsCert
|
||||||
if tlsCert == nil {
|
if tlsCert == nil {
|
||||||
return user, errors.New("TLS certificate cannot be null or empty")
|
return user, errors.New("TLS certificate cannot be null or empty")
|
||||||
}
|
}
|
||||||
user, err := p.userExists(username)
|
user, err := p.userExists(username, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
providerLog(logger.LevelWarn, "error authenticating user %#v: %v", username, err)
|
providerLog(logger.LevelWarn, "error authenticating user %#v: %v", username, err)
|
||||||
return user, err
|
return user, err
|
||||||
|
@ -114,7 +115,7 @@ func (p *BoltProvider) validateUserAndTLSCert(username, protocol string, tlsCert
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *BoltProvider) validateUserAndPass(username, password, ip, protocol string) (User, error) {
|
func (p *BoltProvider) validateUserAndPass(username, password, ip, protocol string) (User, error) {
|
||||||
user, err := p.userExists(username)
|
user, err := p.userExists(username, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
providerLog(logger.LevelWarn, "error authenticating user %#v: %v", username, err)
|
providerLog(logger.LevelWarn, "error authenticating user %#v: %v", username, err)
|
||||||
return user, err
|
return user, err
|
||||||
|
@ -137,7 +138,7 @@ func (p *BoltProvider) validateUserAndPubKey(username string, pubKey []byte, isS
|
||||||
if len(pubKey) == 0 {
|
if len(pubKey) == 0 {
|
||||||
return user, "", errors.New("credentials cannot be null or empty")
|
return user, "", errors.New("credentials cannot be null or empty")
|
||||||
}
|
}
|
||||||
user, err := p.userExists(username)
|
user, err := p.userExists(username, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
providerLog(logger.LevelWarn, "error authenticating user %#v: %v", username, err)
|
providerLog(logger.LevelWarn, "error authenticating user %#v: %v", username, err)
|
||||||
return user, "", err
|
return user, "", err
|
||||||
|
@ -336,7 +337,7 @@ func (p *BoltProvider) updateQuota(username string, filesAdd int, sizeAdd int64,
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *BoltProvider) getUsedQuota(username string) (int, int64, int64, int64, error) {
|
func (p *BoltProvider) getUsedQuota(username string) (int, int64, int64, int64, error) {
|
||||||
user, err := p.userExists(username)
|
user, err := p.userExists(username, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
providerLog(logger.LevelError, "unable to get quota for user %v error: %v", username, err)
|
providerLog(logger.LevelError, "unable to get quota for user %v error: %v", username, err)
|
||||||
return 0, 0, 0, 0, err
|
return 0, 0, 0, 0, err
|
||||||
|
@ -376,8 +377,12 @@ func (p *BoltProvider) addAdmin(admin *Admin) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
rolesBucket, err := p.getRolesBucket(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 %q already exists", admin.Username)
|
||||||
}
|
}
|
||||||
id, err := bucket.NextSequence()
|
id, err := bucket.NextSequence()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -393,6 +398,10 @@ func (p *BoltProvider) addAdmin(admin *Admin) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if err = p.addAdminToRole(admin.Username, admin.Role, rolesBucket); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
buf, err := json.Marshal(admin)
|
buf, err := json.Marshal(admin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -415,6 +424,10 @@ func (p *BoltProvider) updateAdmin(admin *Admin) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
rolesBucket, err := p.getRolesBucket(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))
|
||||||
|
@ -425,12 +438,18 @@ func (p *BoltProvider) updateAdmin(admin *Admin) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err = p.removeAdminFromRole(oldAdmin.Username, oldAdmin.Role, rolesBucket); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
for idx := range oldAdmin.Groups {
|
for idx := range oldAdmin.Groups {
|
||||||
err = p.removeAdminFromGroupMapping(oldAdmin.Username, oldAdmin.Groups[idx].Name, groupBucket)
|
err = p.removeAdminFromGroupMapping(oldAdmin.Username, oldAdmin.Groups[idx].Name, groupBucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if err = p.addAdminToRole(admin.Username, admin.Role, rolesBucket); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
for idx := range admin.Groups {
|
for idx := range admin.Groups {
|
||||||
err = p.addAdminToGroupMapping(admin.Username, admin.Groups[idx].Name, groupBucket)
|
err = p.addAdminToGroupMapping(admin.Username, admin.Groups[idx].Name, groupBucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -477,6 +496,15 @@ func (p *BoltProvider) deleteAdmin(admin Admin) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if oldAdmin.Role != "" {
|
||||||
|
rolesBucket, err := p.getRolesBucket(tx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = p.removeAdminFromRole(oldAdmin.Username, oldAdmin.Role, rolesBucket); 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
|
||||||
|
@ -560,7 +588,7 @@ func (p *BoltProvider) dumpAdmins() ([]Admin, error) {
|
||||||
return admins, err
|
return admins, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *BoltProvider) userExists(username string) (User, error) {
|
func (p *BoltProvider) userExists(username, role string) (User, error) {
|
||||||
var user User
|
var user User
|
||||||
err := p.dbHandle.View(func(tx *bolt.Tx) error {
|
err := p.dbHandle.View(func(tx *bolt.Tx) error {
|
||||||
bucket, err := p.getUsersBucket(tx)
|
bucket, err := p.getUsersBucket(tx)
|
||||||
|
@ -569,14 +597,20 @@ func (p *BoltProvider) userExists(username string) (User, error) {
|
||||||
}
|
}
|
||||||
u := bucket.Get([]byte(username))
|
u := bucket.Get([]byte(username))
|
||||||
if u == nil {
|
if u == nil {
|
||||||
return util.NewRecordNotFoundError(fmt.Sprintf("username %#v does not exist", username))
|
return util.NewRecordNotFoundError(fmt.Sprintf("username %q does not exist", username))
|
||||||
}
|
}
|
||||||
foldersBucket, err := p.getFoldersBucket(tx)
|
foldersBucket, err := p.getFoldersBucket(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
user, err = p.joinUserAndFolders(u, foldersBucket)
|
user, err = p.joinUserAndFolders(u, foldersBucket)
|
||||||
return err
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !user.hasRole(role) {
|
||||||
|
return util.NewRecordNotFoundError(fmt.Sprintf("username %q does not exist", username))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
})
|
})
|
||||||
return user, err
|
return user, err
|
||||||
}
|
}
|
||||||
|
@ -599,6 +633,10 @@ func (p *BoltProvider) addUser(user *User) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
rolesBucket, err := p.getRolesBucket(tx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if u := bucket.Get([]byte(user.Username)); u != nil {
|
if u := bucket.Get([]byte(user.Username)); u != nil {
|
||||||
return fmt.Errorf("username %v already exists", user.Username)
|
return fmt.Errorf("username %v already exists", user.Username)
|
||||||
}
|
}
|
||||||
|
@ -617,6 +655,9 @@ func (p *BoltProvider) addUser(user *User) error {
|
||||||
user.FirstUpload = 0
|
user.FirstUpload = 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())
|
||||||
|
if err := p.addUserToRole(user.Username, user.Role, rolesBucket); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
for idx := range user.VirtualFolders {
|
for idx := range user.VirtualFolders {
|
||||||
err = p.addRelationToFolderMapping(&user.VirtualFolders[idx].BaseVirtualFolder, user, nil, foldersBucket)
|
err = p.addRelationToFolderMapping(&user.VirtualFolders[idx].BaseVirtualFolder, user, nil, foldersBucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -689,6 +730,18 @@ func (p *BoltProvider) deleteUser(user User, softDelete bool) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
foldersBucket, err := p.getFoldersBucket(tx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
groupBucket, err := p.getGroupsBucket(tx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rolesBucket, err := p.getRolesBucket(tx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
var u []byte
|
var u []byte
|
||||||
if u = bucket.Get([]byte(user.Username)); u == nil {
|
if u = bucket.Get([]byte(user.Username)); u == nil {
|
||||||
return util.NewRecordNotFoundError(fmt.Sprintf("username %q does not exist", user.Username))
|
return util.NewRecordNotFoundError(fmt.Sprintf("username %q does not exist", user.Username))
|
||||||
|
@ -698,30 +751,20 @@ func (p *BoltProvider) deleteUser(user User, softDelete bool) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := p.removeUserFromRole(oldUser.Username, oldUser.Role, rolesBucket); err != nil {
|
||||||
if len(oldUser.VirtualFolders) > 0 {
|
return err
|
||||||
foldersBucket, err := p.getFoldersBucket(tx)
|
}
|
||||||
|
for idx := range oldUser.VirtualFolders {
|
||||||
|
err = p.removeRelationFromFolderMapping(oldUser.VirtualFolders[idx], oldUser.Username, "", foldersBucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for idx := range oldUser.VirtualFolders {
|
|
||||||
err = p.removeRelationFromFolderMapping(oldUser.VirtualFolders[idx], oldUser.Username, "", foldersBucket)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if len(oldUser.Groups) > 0 {
|
for idx := range oldUser.Groups {
|
||||||
groupBucket, err := p.getGroupsBucket(tx)
|
err = p.removeUserFromGroupMapping(oldUser.Username, oldUser.Groups[idx].Name, groupBucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for idx := range oldUser.Groups {
|
|
||||||
err = p.removeUserFromGroupMapping(oldUser.Username, oldUser.Groups[idx].Name, groupBucket)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if err := p.deleteRelatedAPIKey(tx, user.Username, APIKeyScopeUser); err != nil {
|
if err := p.deleteRelatedAPIKey(tx, user.Username, APIKeyScopeUser); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -901,7 +944,7 @@ func (p *BoltProvider) getUsersForQuotaCheck(toFetch map[string]bool) ([]User, e
|
||||||
return users, err
|
return users, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *BoltProvider) getUsers(limit int, offset int, order string) ([]User, error) {
|
func (p *BoltProvider) getUsers(limit int, offset int, order, role string) ([]User, error) {
|
||||||
users := make([]User, 0, limit)
|
users := make([]User, 0, limit)
|
||||||
var err error
|
var err error
|
||||||
if limit <= 0 {
|
if limit <= 0 {
|
||||||
|
@ -928,6 +971,9 @@ func (p *BoltProvider) getUsers(limit int, offset int, order string) ([]User, er
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if !user.hasRole(role) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
user.PrepareForRendering()
|
user.PrepareForRendering()
|
||||||
users = append(users, user)
|
users = append(users, user)
|
||||||
if len(users) >= limit {
|
if len(users) >= limit {
|
||||||
|
@ -944,6 +990,9 @@ func (p *BoltProvider) getUsers(limit int, offset int, order string) ([]User, er
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if !user.hasRole(role) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
user.PrepareForRendering()
|
user.PrepareForRendering()
|
||||||
users = append(users, user)
|
users = append(users, user)
|
||||||
if len(users) >= limit {
|
if len(users) >= limit {
|
||||||
|
@ -2519,6 +2568,187 @@ func (*BoltProvider) cleanupNodes() error {
|
||||||
return ErrNotImplemented
|
return ErrNotImplemented
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *BoltProvider) roleExists(name string) (Role, error) {
|
||||||
|
var role Role
|
||||||
|
err := p.dbHandle.View(func(tx *bolt.Tx) error {
|
||||||
|
bucket, err := p.getRolesBucket(tx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
r := bucket.Get([]byte(name))
|
||||||
|
if r == nil {
|
||||||
|
return util.NewRecordNotFoundError(fmt.Sprintf("role %q does not exist", name))
|
||||||
|
}
|
||||||
|
return json.Unmarshal(r, &role)
|
||||||
|
})
|
||||||
|
return role, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *BoltProvider) addRole(role *Role) error {
|
||||||
|
if err := role.validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return p.dbHandle.Update(func(tx *bolt.Tx) error {
|
||||||
|
bucket, err := p.getRolesBucket(tx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if r := bucket.Get([]byte(role.Name)); r != nil {
|
||||||
|
return fmt.Errorf("role %q already exists", role.Name)
|
||||||
|
}
|
||||||
|
id, err := bucket.NextSequence()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
role.ID = int64(id)
|
||||||
|
role.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
|
||||||
|
role.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
|
||||||
|
role.Users = nil
|
||||||
|
role.Admins = nil
|
||||||
|
buf, err := json.Marshal(role)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return bucket.Put([]byte(role.Name), buf)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *BoltProvider) updateRole(role *Role) error {
|
||||||
|
if err := role.validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return p.dbHandle.Update(func(tx *bolt.Tx) error {
|
||||||
|
bucket, err := p.getRolesBucket(tx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var r []byte
|
||||||
|
if r = bucket.Get([]byte(role.Name)); r == nil {
|
||||||
|
return fmt.Errorf("role %q does not exist", role.Name)
|
||||||
|
}
|
||||||
|
var oldRole Role
|
||||||
|
err = json.Unmarshal(r, &oldRole)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
role.ID = oldRole.ID
|
||||||
|
role.CreatedAt = oldRole.CreatedAt
|
||||||
|
role.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
|
||||||
|
role.Users = oldRole.Users
|
||||||
|
role.Admins = oldRole.Admins
|
||||||
|
buf, err := json.Marshal(role)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return bucket.Put([]byte(role.Name), buf)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *BoltProvider) deleteRole(role Role) error {
|
||||||
|
return p.dbHandle.Update(func(tx *bolt.Tx) error {
|
||||||
|
bucket, err := p.getRolesBucket(tx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var r []byte
|
||||||
|
if r = bucket.Get([]byte(role.Name)); r == nil {
|
||||||
|
return fmt.Errorf("role %q does not exist", role.Name)
|
||||||
|
}
|
||||||
|
var oldRole Role
|
||||||
|
err = json.Unmarshal(r, &oldRole)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(oldRole.Admins) > 0 {
|
||||||
|
return util.NewValidationError(fmt.Sprintf("the role %q is referenced, it cannot be removed", oldRole.Name))
|
||||||
|
}
|
||||||
|
if len(oldRole.Users) > 0 {
|
||||||
|
bucket, err := p.getUsersBucket(tx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, username := range oldRole.Users {
|
||||||
|
if err := p.removeRoleFromUser(username, oldRole.Name, bucket); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bucket.Delete([]byte(role.Name))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *BoltProvider) getRoles(limit int, offset int, order string, minimal bool) ([]Role, error) {
|
||||||
|
roles := make([]Role, 0, limit)
|
||||||
|
if limit <= 0 {
|
||||||
|
return roles, nil
|
||||||
|
}
|
||||||
|
err := p.dbHandle.View(func(tx *bolt.Tx) error {
|
||||||
|
bucket, err := p.getRolesBucket(tx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cursor := bucket.Cursor()
|
||||||
|
itNum := 0
|
||||||
|
if order == OrderASC {
|
||||||
|
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||||
|
itNum++
|
||||||
|
if itNum <= offset {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var role Role
|
||||||
|
err = json.Unmarshal(v, &role)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
roles = append(roles, role)
|
||||||
|
if len(roles) >= limit {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for k, v := cursor.Last(); k != nil; k, v = cursor.Prev() {
|
||||||
|
itNum++
|
||||||
|
if itNum <= offset {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var role Role
|
||||||
|
err = json.Unmarshal(v, &role)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
roles = append(roles, role)
|
||||||
|
if len(roles) >= limit {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return roles, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *BoltProvider) dumpRoles() ([]Role, error) {
|
||||||
|
roles := make([]Role, 0, 10)
|
||||||
|
err := p.dbHandle.View(func(tx *bolt.Tx) error {
|
||||||
|
bucket, err := p.getRolesBucket(tx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cursor := bucket.Cursor()
|
||||||
|
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||||
|
var role Role
|
||||||
|
err = json.Unmarshal(v, &role)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
roles = append(roles, role)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
return roles, err
|
||||||
|
}
|
||||||
|
|
||||||
func (p *BoltProvider) setFirstDownloadTimestamp(username string) error {
|
func (p *BoltProvider) setFirstDownloadTimestamp(username string) error {
|
||||||
return p.dbHandle.Update(func(tx *bolt.Tx) error {
|
return p.dbHandle.Update(func(tx *bolt.Tx) error {
|
||||||
bucket, err := p.getUsersBucket(tx)
|
bucket, err := p.getUsersBucket(tx)
|
||||||
|
@ -2603,6 +2833,10 @@ func (p *BoltProvider) migrateDatabase() error {
|
||||||
providerLog(logger.LevelError, "%v", err)
|
providerLog(logger.LevelError, "%v", err)
|
||||||
logger.ErrorToConsole("%v", err)
|
logger.ErrorToConsole("%v", err)
|
||||||
return err
|
return err
|
||||||
|
case version == 23:
|
||||||
|
logger.InfoToConsole(fmt.Sprintf("updating database schema version: %d -> 24", version))
|
||||||
|
providerLog(logger.LevelInfo, "updating database schema version: %d -> 24", version)
|
||||||
|
return updateBoltDatabaseVersion(p.dbHandle, 24)
|
||||||
default:
|
default:
|
||||||
if version > boltDatabaseVersion {
|
if version > boltDatabaseVersion {
|
||||||
providerLog(logger.LevelError, "database schema version %d is newer than the supported one: %d", version,
|
providerLog(logger.LevelError, "database schema version %d is newer than the supported one: %d", version,
|
||||||
|
@ -2624,6 +2858,44 @@ func (p *BoltProvider) revertDatabase(targetVersion int) error {
|
||||||
return errors.New("current version match target version, nothing to do")
|
return errors.New("current version match target version, nothing to do")
|
||||||
}
|
}
|
||||||
switch dbVersion.Version {
|
switch dbVersion.Version {
|
||||||
|
case 24:
|
||||||
|
logger.InfoToConsole("downgrading database schema version: %d -> 23", dbVersion.Version)
|
||||||
|
providerLog(logger.LevelInfo, "downgrading database schema version: %d -> 23", dbVersion.Version)
|
||||||
|
err := p.dbHandle.Update(func(tx *bolt.Tx) error {
|
||||||
|
roles, err := p.dumpRoles()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
adminsBucket, err := p.getAdminsBucket(tx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
usersBucket, err := p.getUsersBucket(tx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, role := range roles {
|
||||||
|
for _, admin := range role.Admins {
|
||||||
|
if err := p.removeRoleFromAdmin(admin, role.Name, adminsBucket); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, user := range role.Users {
|
||||||
|
if err := p.removeRoleFromUser(user, role.Name, usersBucket); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = tx.DeleteBucket(rolesBucket)
|
||||||
|
if err != nil && !errors.Is(err, bolt.ErrBucketNotFound) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return updateBoltDatabaseVersion(p.dbHandle, 23)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("database schema version not handled: %v", dbVersion.Version)
|
return fmt.Errorf("database schema version not handled: %v", dbVersion.Version)
|
||||||
}
|
}
|
||||||
|
@ -2748,6 +3020,163 @@ func (p *BoltProvider) addFolderInternal(folder vfs.BaseVirtualFolder, bucket *b
|
||||||
return bucket.Put([]byte(folder.Name), buf)
|
return bucket.Put([]byte(folder.Name), buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *BoltProvider) removeRoleFromUser(username, role string, bucket *bolt.Bucket) error {
|
||||||
|
u := bucket.Get([]byte(username))
|
||||||
|
if u == nil {
|
||||||
|
providerLog(logger.LevelWarn, "user %q does not exist, cannot remove role %q", username, role)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var user User
|
||||||
|
err := json.Unmarshal(u, &user)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if user.Role == role {
|
||||||
|
user.Role = ""
|
||||||
|
buf, err := json.Marshal(user)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return bucket.Put([]byte(user.Username), buf)
|
||||||
|
}
|
||||||
|
providerLog(logger.LevelError, "user %q does not have the expected role %q, actual %q", username, role, user.Role)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *BoltProvider) removeRoleFromAdmin(username, role string, bucket *bolt.Bucket) error {
|
||||||
|
a := bucket.Get([]byte(username))
|
||||||
|
if a == nil {
|
||||||
|
providerLog(logger.LevelWarn, "admin %q does not exist, cannot remove role %q", username, role)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var admin Admin
|
||||||
|
err := json.Unmarshal(a, &admin)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if admin.Role == role {
|
||||||
|
admin.Role = ""
|
||||||
|
buf, err := json.Marshal(admin)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return bucket.Put([]byte(admin.Username), buf)
|
||||||
|
}
|
||||||
|
providerLog(logger.LevelError, "admin %q does not have the expected role %q, actual %q", username, role, admin.Role)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *BoltProvider) addAdminToRole(username, roleName string, bucket *bolt.Bucket) error {
|
||||||
|
if roleName == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
r := bucket.Get([]byte(roleName))
|
||||||
|
if r == nil {
|
||||||
|
return util.NewGenericError(fmt.Sprintf("role %q does not exist", roleName))
|
||||||
|
}
|
||||||
|
var role Role
|
||||||
|
err := json.Unmarshal(r, &role)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !util.Contains(role.Admins, username) {
|
||||||
|
role.Admins = append(role.Admins, username)
|
||||||
|
buf, err := json.Marshal(role)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return bucket.Put([]byte(role.Name), buf)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *BoltProvider) removeAdminFromRole(username, roleName string, bucket *bolt.Bucket) error {
|
||||||
|
if roleName == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
r := bucket.Get([]byte(roleName))
|
||||||
|
if r == nil {
|
||||||
|
providerLog(logger.LevelWarn, "role %q does not exist, cannot remove admin %q", roleName, username)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var role Role
|
||||||
|
err := json.Unmarshal(r, &role)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if util.Contains(role.Admins, username) {
|
||||||
|
var admins []string
|
||||||
|
for _, admin := range role.Admins {
|
||||||
|
if admin != username {
|
||||||
|
admins = append(admins, admin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
role.Admins = util.RemoveDuplicates(admins, false)
|
||||||
|
buf, err := json.Marshal(role)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return bucket.Put([]byte(role.Name), buf)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *BoltProvider) addUserToRole(username, roleName string, bucket *bolt.Bucket) error {
|
||||||
|
if roleName == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
r := bucket.Get([]byte(roleName))
|
||||||
|
if r == nil {
|
||||||
|
return util.NewGenericError(fmt.Sprintf("role %q does not exist", roleName))
|
||||||
|
}
|
||||||
|
var role Role
|
||||||
|
err := json.Unmarshal(r, &role)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !util.Contains(role.Users, username) {
|
||||||
|
role.Users = append(role.Users, username)
|
||||||
|
buf, err := json.Marshal(role)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return bucket.Put([]byte(role.Name), buf)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *BoltProvider) removeUserFromRole(username, roleName string, bucket *bolt.Bucket) error {
|
||||||
|
if roleName == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
r := bucket.Get([]byte(roleName))
|
||||||
|
if r == nil {
|
||||||
|
providerLog(logger.LevelWarn, "role %q does not exist, cannot remove admin %q", roleName, username)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var role Role
|
||||||
|
err := json.Unmarshal(r, &role)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if util.Contains(role.Users, username) {
|
||||||
|
var users []string
|
||||||
|
for _, user := range role.Users {
|
||||||
|
if user != username {
|
||||||
|
users = append(users, user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
users = util.RemoveDuplicates(users, false)
|
||||||
|
role.Users = users
|
||||||
|
buf, err := json.Marshal(role)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return bucket.Put([]byte(role.Name), buf)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (p *BoltProvider) addRuleToActionMapping(ruleName, actionName string, bucket *bolt.Bucket) error {
|
func (p *BoltProvider) addRuleToActionMapping(ruleName, actionName string, bucket *bolt.Bucket) error {
|
||||||
a := bucket.Get([]byte(actionName))
|
a := bucket.Get([]byte(actionName))
|
||||||
if a == nil {
|
if a == nil {
|
||||||
|
@ -3000,7 +3429,11 @@ func (p *BoltProvider) updateUserRelations(tx *bolt.Tx, user *User, oldUser User
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
groupBucket, err := p.getGroupsBucket(tx)
|
groupsBucket, err := p.getGroupsBucket(tx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rolesBucket, err := p.getRolesBucket(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -3011,11 +3444,14 @@ func (p *BoltProvider) updateUserRelations(tx *bolt.Tx, user *User, oldUser User
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for idx := range oldUser.Groups {
|
for idx := range oldUser.Groups {
|
||||||
err = p.removeUserFromGroupMapping(user.Username, oldUser.Groups[idx].Name, groupBucket)
|
err = p.removeUserFromGroupMapping(user.Username, oldUser.Groups[idx].Name, groupsBucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if err = p.removeUserFromRole(oldUser.Username, oldUser.Role, rolesBucket); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
for idx := range user.VirtualFolders {
|
for idx := range user.VirtualFolders {
|
||||||
err = p.addRelationToFolderMapping(&user.VirtualFolders[idx].BaseVirtualFolder, user, nil, foldersBucket)
|
err = p.addRelationToFolderMapping(&user.VirtualFolders[idx].BaseVirtualFolder, user, nil, foldersBucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -3023,12 +3459,12 @@ func (p *BoltProvider) updateUserRelations(tx *bolt.Tx, user *User, oldUser User
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for idx := range user.Groups {
|
for idx := range user.Groups {
|
||||||
err = p.addUserToGroupMapping(user.Username, user.Groups[idx].Name, groupBucket)
|
err = p.addUserToGroupMapping(user.Username, user.Groups[idx].Name, groupsBucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return p.addUserToRole(user.Username, user.Role, rolesBucket)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *BoltProvider) adminExistsInternal(tx *bolt.Tx, username string) error {
|
func (p *BoltProvider) adminExistsInternal(tx *bolt.Tx, username string) error {
|
||||||
|
@ -3163,6 +3599,15 @@ func (p *BoltProvider) getGroupsBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
|
||||||
return bucket, err
|
return bucket, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *BoltProvider) getRolesBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
|
||||||
|
var err error
|
||||||
|
bucket := tx.Bucket(rolesBucket)
|
||||||
|
if bucket == nil {
|
||||||
|
err = fmt.Errorf("unable to find roles bucket, bolt database structure not correcly defined")
|
||||||
|
}
|
||||||
|
return bucket, err
|
||||||
|
}
|
||||||
|
|
||||||
func (p *BoltProvider) getFoldersBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
|
func (p *BoltProvider) getFoldersBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
|
||||||
var err error
|
var err error
|
||||||
bucket := tx.Bucket(foldersBucket)
|
bucket := tx.Bucket(foldersBucket)
|
||||||
|
@ -3209,7 +3654,7 @@ func getBoltDatabaseVersion(dbHandle *bolt.DB) (schemaVersion, error) {
|
||||||
return dbVersion, err
|
return dbVersion, err
|
||||||
}
|
}
|
||||||
|
|
||||||
/*func updateBoltDatabaseVersion(dbHandle *bolt.DB, version int) error {
|
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 {
|
||||||
|
@ -3225,4 +3670,4 @@ func getBoltDatabaseVersion(dbHandle *bolt.DB) (schemaVersion, error) {
|
||||||
return bucket.Put(dbVersionKey, buf)
|
return bucket.Put(dbVersionKey, buf)
|
||||||
})
|
})
|
||||||
return err
|
return err
|
||||||
}*/
|
}
|
||||||
|
|
|
@ -87,7 +87,7 @@ const (
|
||||||
CockroachDataProviderName = "cockroachdb"
|
CockroachDataProviderName = "cockroachdb"
|
||||||
// DumpVersion defines the version for the dump.
|
// DumpVersion defines the version for the dump.
|
||||||
// For restore/load we support the current version and the previous one
|
// For restore/load we support the current version and the previous one
|
||||||
DumpVersion = 13
|
DumpVersion = 14
|
||||||
|
|
||||||
argonPwdPrefix = "$argon2id$"
|
argonPwdPrefix = "$argon2id$"
|
||||||
bcryptPwdPrefix = "$2a$"
|
bcryptPwdPrefix = "$2a$"
|
||||||
|
@ -190,6 +190,7 @@ var (
|
||||||
sqlTableRulesActionsMapping string
|
sqlTableRulesActionsMapping string
|
||||||
sqlTableTasks string
|
sqlTableTasks string
|
||||||
sqlTableNodes string
|
sqlTableNodes string
|
||||||
|
sqlTableRoles string
|
||||||
sqlTableSchemaVersion string
|
sqlTableSchemaVersion string
|
||||||
argon2Params *argon2id.Params
|
argon2Params *argon2id.Params
|
||||||
lastLoginMinDelay = 10 * time.Minute
|
lastLoginMinDelay = 10 * time.Minute
|
||||||
|
@ -221,6 +222,7 @@ func initSQLTables() {
|
||||||
sqlTableRulesActionsMapping = "rules_actions_mapping"
|
sqlTableRulesActionsMapping = "rules_actions_mapping"
|
||||||
sqlTableTasks = "tasks"
|
sqlTableTasks = "tasks"
|
||||||
sqlTableNodes = "nodes"
|
sqlTableNodes = "nodes"
|
||||||
|
sqlTableRoles = "roles"
|
||||||
sqlTableSchemaVersion = "schema_version"
|
sqlTableSchemaVersion = "schema_version"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -660,6 +662,7 @@ type BackupData struct {
|
||||||
Shares []Share `json:"shares"`
|
Shares []Share `json:"shares"`
|
||||||
EventActions []BaseEventAction `json:"event_actions"`
|
EventActions []BaseEventAction `json:"event_actions"`
|
||||||
EventRules []EventRule `json:"event_rules"`
|
EventRules []EventRule `json:"event_rules"`
|
||||||
|
Roles []Role `json:"roles"`
|
||||||
Version int `json:"version"`
|
Version int `json:"version"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -706,12 +709,12 @@ type Provider interface {
|
||||||
updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error
|
updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error
|
||||||
updateTransferQuota(username string, uploadSize, downloadSize int64, reset bool) error
|
updateTransferQuota(username string, uploadSize, downloadSize int64, reset bool) error
|
||||||
getUsedQuota(username string) (int, int64, int64, int64, error)
|
getUsedQuota(username string) (int, int64, int64, int64, error)
|
||||||
userExists(username string) (User, error)
|
userExists(username, role string) (User, error)
|
||||||
addUser(user *User) error
|
addUser(user *User) error
|
||||||
updateUser(user *User) error
|
updateUser(user *User) error
|
||||||
deleteUser(user User, softDelete bool) error
|
deleteUser(user User, softDelete bool) error
|
||||||
updateUserPassword(username, password string) error
|
updateUserPassword(username, password string) error
|
||||||
getUsers(limit int, offset int, order string) ([]User, error)
|
getUsers(limit int, offset int, order, role string) ([]User, error)
|
||||||
dumpUsers() ([]User, error)
|
dumpUsers() ([]User, error)
|
||||||
getRecentlyUpdatedUsers(after int64) ([]User, error)
|
getRecentlyUpdatedUsers(after int64) ([]User, error)
|
||||||
getUsersForQuotaCheck(toFetch map[string]bool) ([]User, error)
|
getUsersForQuotaCheck(toFetch map[string]bool) ([]User, error)
|
||||||
|
@ -796,6 +799,12 @@ type Provider interface {
|
||||||
getNodes() ([]Node, error)
|
getNodes() ([]Node, error)
|
||||||
updateNodeTimestamp() error
|
updateNodeTimestamp() error
|
||||||
cleanupNodes() error
|
cleanupNodes() error
|
||||||
|
roleExists(name string) (Role, error)
|
||||||
|
addRole(role *Role) error
|
||||||
|
updateRole(role *Role) error
|
||||||
|
deleteRole(role Role) error
|
||||||
|
getRoles(limit int, offset int, order string, minimal bool) ([]Role, error)
|
||||||
|
dumpRoles() ([]Role, error)
|
||||||
checkAvailability() error
|
checkAvailability() error
|
||||||
close() error
|
close() error
|
||||||
reloadConfig() error
|
reloadConfig() error
|
||||||
|
@ -974,16 +983,17 @@ func validateSQLTablesPrefix() error {
|
||||||
sqlTableRulesActionsMapping = config.SQLTablesPrefix + sqlTableRulesActionsMapping
|
sqlTableRulesActionsMapping = config.SQLTablesPrefix + sqlTableRulesActionsMapping
|
||||||
sqlTableTasks = config.SQLTablesPrefix + sqlTableTasks
|
sqlTableTasks = config.SQLTablesPrefix + sqlTableTasks
|
||||||
sqlTableNodes = config.SQLTablesPrefix + sqlTableNodes
|
sqlTableNodes = config.SQLTablesPrefix + sqlTableNodes
|
||||||
|
sqlTableRoles = config.SQLTablesPrefix + sqlTableRoles
|
||||||
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 admins groups mapping %q groups folders mapping %q shared sessions %q "+
|
"users groups mapping %q admins groups mapping %q groups folders mapping %q shared sessions %q "+
|
||||||
"schema version %q events actions %q events rules %q rules actions mapping %q tasks %q nodes %q",
|
"schema version %q events actions %q events rules %q rules actions mapping %q tasks %q nodes %q roles %q",
|
||||||
sqlTableUsers, sqlTableFolders, sqlTableUsersFoldersMapping, sqlTableAdmins, sqlTableAPIKeys,
|
sqlTableUsers, sqlTableFolders, sqlTableUsersFoldersMapping, sqlTableAdmins, sqlTableAPIKeys,
|
||||||
sqlTableShares, sqlTableDefenderHosts, sqlTableDefenderEvents, sqlTableActiveTransfers, sqlTableGroups,
|
sqlTableShares, sqlTableDefenderHosts, sqlTableDefenderEvents, sqlTableActiveTransfers, sqlTableGroups,
|
||||||
sqlTableUsersGroupsMapping, sqlTableAdminsGroupsMapping, sqlTableGroupsFoldersMapping, sqlTableSharedSessions,
|
sqlTableUsersGroupsMapping, sqlTableAdminsGroupsMapping, sqlTableGroupsFoldersMapping, sqlTableSharedSessions,
|
||||||
sqlTableSchemaVersion, sqlTableEventsActions, sqlTableEventsRules, sqlTableRulesActionsMapping,
|
sqlTableSchemaVersion, sqlTableEventsActions, sqlTableEventsRules, sqlTableRulesActionsMapping,
|
||||||
sqlTableTasks, sqlTableNodes)
|
sqlTableTasks, sqlTableNodes, sqlTableRoles)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -1158,7 +1168,7 @@ func CheckUserBeforeTLSAuth(username, ip, protocol string, tlsCert *x509.Certifi
|
||||||
err = user.LoadAndApplyGroupSettings()
|
err = user.LoadAndApplyGroupSettings()
|
||||||
return user, err
|
return user, err
|
||||||
}
|
}
|
||||||
user, err := UserExists(username)
|
user, err := UserExists(username, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return user, err
|
return user, err
|
||||||
}
|
}
|
||||||
|
@ -1261,7 +1271,7 @@ func CheckKeyboardInteractiveAuth(username, authHook string, client ssh.Keyboard
|
||||||
} else if config.PreLoginHook != "" {
|
} else if config.PreLoginHook != "" {
|
||||||
user, err = executePreLoginHook(username, SSHLoginMethodKeyboardInteractive, ip, protocol, nil)
|
user, err = executePreLoginHook(username, SSHLoginMethodKeyboardInteractive, ip, protocol, nil)
|
||||||
} else {
|
} else {
|
||||||
user, err = provider.userExists(username)
|
user, err = provider.userExists(username, "")
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return user, err
|
return user, err
|
||||||
|
@ -1279,7 +1289,7 @@ func GetFTPPreAuthUser(username, ip string) (User, error) {
|
||||||
if config.PreLoginHook != "" {
|
if config.PreLoginHook != "" {
|
||||||
user, err = executePreLoginHook(username, "", ip, protocolFTP, nil)
|
user, err = executePreLoginHook(username, "", ip, protocolFTP, nil)
|
||||||
} else {
|
} else {
|
||||||
user, err = UserExists(username)
|
user, err = UserExists(username, "")
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return user, err
|
return user, err
|
||||||
|
@ -1298,7 +1308,7 @@ func GetUserAfterIDPAuth(username, ip, protocol string, oidcTokenFields *map[str
|
||||||
if config.PreLoginHook != "" {
|
if config.PreLoginHook != "" {
|
||||||
user, err = executePreLoginHook(username, LoginMethodIDP, ip, protocol, oidcTokenFields)
|
user, err = executePreLoginHook(username, LoginMethodIDP, ip, protocol, oidcTokenFields)
|
||||||
} else {
|
} else {
|
||||||
user, err = UserExists(username)
|
user, err = UserExists(username, "")
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return user, err
|
return user, err
|
||||||
|
@ -1532,6 +1542,57 @@ func ShareExists(shareID, username string) (Share, error) {
|
||||||
return provider.shareExists(shareID, username)
|
return provider.shareExists(shareID, username)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddRole adds a new role
|
||||||
|
func AddRole(role *Role, executor, ipAddress string) error {
|
||||||
|
role.Name = config.convertName(role.Name)
|
||||||
|
err := provider.addRole(role)
|
||||||
|
if err == nil {
|
||||||
|
executeAction(operationAdd, executor, ipAddress, actionObjectRole, role.Name, role)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateRole updates an existing Role
|
||||||
|
func UpdateRole(role *Role, executor, ipAddress string) error {
|
||||||
|
err := provider.updateRole(role)
|
||||||
|
if err == nil {
|
||||||
|
executeAction(operationUpdate, executor, ipAddress, actionObjectRole, role.Name, role)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteRole deletes an existing Role
|
||||||
|
func DeleteRole(name string, executor, ipAddress string) error {
|
||||||
|
name = config.convertName(name)
|
||||||
|
role, err := provider.roleExists(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(role.Admins) > 0 {
|
||||||
|
errorString := fmt.Sprintf("the role %q is referenced, it cannot be removed", role.Name)
|
||||||
|
return util.NewValidationError(errorString)
|
||||||
|
}
|
||||||
|
err = provider.deleteRole(role)
|
||||||
|
if err == nil {
|
||||||
|
executeAction(operationDelete, executor, ipAddress, actionObjectRole, role.Name, &role)
|
||||||
|
for _, user := range role.Users {
|
||||||
|
provider.setUpdatedAt(user)
|
||||||
|
u, err := provider.userExists(user, "")
|
||||||
|
if err == nil {
|
||||||
|
webDAVUsersCache.swap(&u)
|
||||||
|
executeAction(operationUpdate, executor, ipAddress, actionObjectUser, u.Username, &u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoleExists returns the Role with the given name if it exists
|
||||||
|
func RoleExists(name string) (Role, error) {
|
||||||
|
name = config.convertName(name)
|
||||||
|
return provider.roleExists(name)
|
||||||
|
}
|
||||||
|
|
||||||
// AddGroup adds a new group
|
// AddGroup adds a new group
|
||||||
func AddGroup(group *Group, executor, ipAddress string) error {
|
func AddGroup(group *Group, executor, ipAddress string) error {
|
||||||
group.Name = config.convertName(group.Name)
|
group.Name = config.convertName(group.Name)
|
||||||
|
@ -1548,7 +1609,7 @@ func UpdateGroup(group *Group, users []string, executor, ipAddress string) error
|
||||||
if err == nil {
|
if err == nil {
|
||||||
for _, user := range users {
|
for _, user := range users {
|
||||||
provider.setUpdatedAt(user)
|
provider.setUpdatedAt(user)
|
||||||
u, err := provider.userExists(user)
|
u, err := provider.userExists(user, "")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
webDAVUsersCache.swap(&u)
|
webDAVUsersCache.swap(&u)
|
||||||
executeAction(operationUpdate, executor, ipAddress, actionObjectUser, u.Username, &u)
|
executeAction(operationUpdate, executor, ipAddress, actionObjectUser, u.Username, &u)
|
||||||
|
@ -1569,14 +1630,14 @@ func DeleteGroup(name string, executor, ipAddress string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if len(group.Users) > 0 {
|
if len(group.Users) > 0 {
|
||||||
errorString := fmt.Sprintf("the group %#v is referenced, it cannot be removed", group.Name)
|
errorString := fmt.Sprintf("the group %q is referenced, it cannot be removed", group.Name)
|
||||||
return util.NewValidationError(errorString)
|
return util.NewValidationError(errorString)
|
||||||
}
|
}
|
||||||
err = provider.deleteGroup(group)
|
err = provider.deleteGroup(group)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
for _, user := range group.Users {
|
for _, user := range group.Users {
|
||||||
provider.setUpdatedAt(user)
|
provider.setUpdatedAt(user)
|
||||||
u, err := provider.userExists(user)
|
u, err := provider.userExists(user, "")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
executeAction(operationUpdate, executor, ipAddress, actionObjectUser, u.Username, &u)
|
executeAction(operationUpdate, executor, ipAddress, actionObjectUser, u.Username, &u)
|
||||||
}
|
}
|
||||||
|
@ -1840,16 +1901,16 @@ func AdminExists(username string) (Admin, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserExists checks if the given SFTPGo username exists, returns an error if no match is found
|
// UserExists checks if the given SFTPGo username exists, returns an error if no match is found
|
||||||
func UserExists(username string) (User, error) {
|
func UserExists(username, role string) (User, error) {
|
||||||
username = config.convertName(username)
|
username = config.convertName(username)
|
||||||
return provider.userExists(username)
|
return provider.userExists(username, role)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserWithGroupSettings tries to return the user with the specified username
|
// GetUserWithGroupSettings tries to return the user with the specified username
|
||||||
// loading also the group settings
|
// loading also the group settings
|
||||||
func GetUserWithGroupSettings(username string) (User, error) {
|
func GetUserWithGroupSettings(username, role string) (User, error) {
|
||||||
username = config.convertName(username)
|
username = config.convertName(username)
|
||||||
user, err := provider.userExists(username)
|
user, err := provider.userExists(username, role)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return user, err
|
return user, err
|
||||||
}
|
}
|
||||||
|
@ -1859,9 +1920,9 @@ func GetUserWithGroupSettings(username string) (User, error) {
|
||||||
|
|
||||||
// GetUserVariants tries to return the user with the specified username with and without
|
// GetUserVariants tries to return the user with the specified username with and without
|
||||||
// group settings applied
|
// group settings applied
|
||||||
func GetUserVariants(username string) (User, User, error) {
|
func GetUserVariants(username, role string) (User, User, error) {
|
||||||
username = config.convertName(username)
|
username = config.convertName(username)
|
||||||
user, err := provider.userExists(username)
|
user, err := provider.userExists(username, role)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return user, User{}, err
|
return user, User{}, err
|
||||||
}
|
}
|
||||||
|
@ -1872,10 +1933,6 @@ func GetUserVariants(username string) (User, User, error) {
|
||||||
|
|
||||||
// AddUser adds a new SFTPGo user.
|
// AddUser adds a new SFTPGo user.
|
||||||
func AddUser(user *User, executor, ipAddress string) error {
|
func AddUser(user *User, executor, ipAddress string) error {
|
||||||
user.Filters.RecoveryCodes = nil
|
|
||||||
user.Filters.TOTPConfig = UserTOTPConfig{
|
|
||||||
Enabled: false,
|
|
||||||
}
|
|
||||||
user.Username = config.convertName(user.Username)
|
user.Username = config.convertName(user.Username)
|
||||||
err := provider.addUser(user)
|
err := provider.addUser(user)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
@ -1914,9 +1971,9 @@ func UpdateUser(user *User, executor, ipAddress string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteUser deletes an existing SFTPGo user.
|
// DeleteUser deletes an existing SFTPGo user.
|
||||||
func DeleteUser(username, executor, ipAddress string) error {
|
func DeleteUser(username, executor, ipAddress, role string) error {
|
||||||
username = config.convertName(username)
|
username = config.convertName(username)
|
||||||
user, err := provider.userExists(username)
|
user, err := provider.userExists(username, role)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -2028,14 +2085,19 @@ func GetAdmins(limit, offset int, order string) ([]Admin, error) {
|
||||||
return provider.getAdmins(limit, offset, order)
|
return provider.getAdmins(limit, offset, order)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetRoles returns an array of roles respecting limit and offset
|
||||||
|
func GetRoles(limit, offset int, order string, minimal bool) ([]Role, error) {
|
||||||
|
return provider.getRoles(limit, offset, order, minimal)
|
||||||
|
}
|
||||||
|
|
||||||
// GetGroups returns an array of groups respecting limit and offset
|
// GetGroups returns an array of groups respecting limit and offset
|
||||||
func GetGroups(limit, offset int, order string, minimal bool) ([]Group, error) {
|
func GetGroups(limit, offset int, order string, minimal bool) ([]Group, error) {
|
||||||
return provider.getGroups(limit, offset, order, minimal)
|
return provider.getGroups(limit, offset, order, minimal)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUsers returns an array of users respecting limit and offset
|
// GetUsers returns an array of users respecting limit and offset
|
||||||
func GetUsers(limit, offset int, order string) ([]User, error) {
|
func GetUsers(limit, offset int, order, role string) ([]User, error) {
|
||||||
return provider.getUsers(limit, offset, order)
|
return provider.getUsers(limit, offset, order, role)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUsersForQuotaCheck returns the users with the fields required for a quota check
|
// GetUsersForQuotaCheck returns the users with the fields required for a quota check
|
||||||
|
@ -2067,7 +2129,7 @@ func UpdateFolder(folder *vfs.BaseVirtualFolder, users []string, groups []string
|
||||||
}
|
}
|
||||||
for _, user := range users {
|
for _, user := range users {
|
||||||
provider.setUpdatedAt(user)
|
provider.setUpdatedAt(user)
|
||||||
u, err := provider.userExists(user)
|
u, err := provider.userExists(user, "")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
webDAVUsersCache.swap(&u)
|
webDAVUsersCache.swap(&u)
|
||||||
executeAction(operationUpdate, executor, ipAddress, actionObjectUser, u.Username, &u)
|
executeAction(operationUpdate, executor, ipAddress, actionObjectUser, u.Username, &u)
|
||||||
|
@ -2099,7 +2161,7 @@ func DeleteFolder(folderName, executor, ipAddress string) error {
|
||||||
}
|
}
|
||||||
for _, user := range users {
|
for _, user := range users {
|
||||||
provider.setUpdatedAt(user)
|
provider.setUpdatedAt(user)
|
||||||
u, err := provider.userExists(user)
|
u, err := provider.userExists(user, "")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
executeAction(operationUpdate, executor, ipAddress, actionObjectUser, u.Username, &u)
|
executeAction(operationUpdate, executor, ipAddress, actionObjectUser, u.Username, &u)
|
||||||
}
|
}
|
||||||
|
@ -2166,6 +2228,10 @@ func DumpData() (BackupData, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return data, err
|
return data, err
|
||||||
}
|
}
|
||||||
|
roles, err := provider.dumpRoles()
|
||||||
|
if err != nil {
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
data.Users = users
|
data.Users = users
|
||||||
data.Groups = groups
|
data.Groups = groups
|
||||||
data.Folders = folders
|
data.Folders = folders
|
||||||
|
@ -2174,6 +2240,7 @@ func DumpData() (BackupData, error) {
|
||||||
data.Shares = shares
|
data.Shares = shares
|
||||||
data.EventActions = actions
|
data.EventActions = actions
|
||||||
data.EventRules = rules
|
data.EventRules = rules
|
||||||
|
data.Roles = roles
|
||||||
data.Version = DumpVersion
|
data.Version = DumpVersion
|
||||||
return data, err
|
return data, err
|
||||||
}
|
}
|
||||||
|
@ -2749,7 +2816,7 @@ func validateBaseParams(user *User) error {
|
||||||
return util.NewValidationError(fmt.Sprintf("email %#v is not valid", user.Email))
|
return util.NewValidationError(fmt.Sprintf("email %#v is not valid", user.Email))
|
||||||
}
|
}
|
||||||
if config.NamingRules&1 == 0 && !usernameRegex.MatchString(user.Username) {
|
if config.NamingRules&1 == 0 && !usernameRegex.MatchString(user.Username) {
|
||||||
return util.NewValidationError(fmt.Sprintf("username %#v is not valid, the following characters are allowed: a-zA-Z0-9-_.~",
|
return util.NewValidationError(fmt.Sprintf("username %q is not valid, the following characters are allowed: a-zA-Z0-9-_.~",
|
||||||
user.Username))
|
user.Username))
|
||||||
}
|
}
|
||||||
if user.hasRedactedSecret() {
|
if user.hasRedactedSecret() {
|
||||||
|
@ -2821,7 +2888,7 @@ func ValidateFolder(folder *vfs.BaseVirtualFolder) error {
|
||||||
return util.NewValidationError("folder name is mandatory")
|
return util.NewValidationError("folder name is mandatory")
|
||||||
}
|
}
|
||||||
if config.NamingRules&1 == 0 && !usernameRegex.MatchString(folder.Name) {
|
if config.NamingRules&1 == 0 && !usernameRegex.MatchString(folder.Name) {
|
||||||
return util.NewValidationError(fmt.Sprintf("folder name %#v is not valid, the following characters are allowed: a-zA-Z0-9-_.~",
|
return util.NewValidationError(fmt.Sprintf("folder name %q is not valid, the following characters are allowed: a-zA-Z0-9-_.~",
|
||||||
folder.Name))
|
folder.Name))
|
||||||
}
|
}
|
||||||
if folder.FsConfig.Provider == sdk.LocalFilesystemProvider || folder.FsConfig.Provider == sdk.CryptedFilesystemProvider ||
|
if folder.FsConfig.Provider == sdk.LocalFilesystemProvider || folder.FsConfig.Provider == sdk.CryptedFilesystemProvider ||
|
||||||
|
@ -3671,7 +3738,7 @@ func executePreLoginHook(username, loginMethod, ip, protocol string, oidcTokenFi
|
||||||
}
|
}
|
||||||
providerLog(logger.LevelDebug, "user %#v added/updated from pre-login hook response, id: %v", username, userID)
|
providerLog(logger.LevelDebug, "user %#v added/updated from pre-login hook response, id: %v", username, userID)
|
||||||
if userID == 0 {
|
if userID == 0 {
|
||||||
return provider.userExists(username)
|
return provider.userExists(username, "")
|
||||||
}
|
}
|
||||||
return u, nil
|
return u, nil
|
||||||
}
|
}
|
||||||
|
@ -3875,7 +3942,7 @@ func doExternalAuth(username, password string, pubKey []byte, keyboardInteractiv
|
||||||
// returns "user" in both cases, so we use the username returned from
|
// returns "user" in both cases, so we use the username returned from
|
||||||
// external auth and not the one used to login
|
// external auth and not the one used to login
|
||||||
if user.Username != username {
|
if user.Username != username {
|
||||||
u, err = provider.userExists(user.Username)
|
u, err = provider.userExists(user.Username, "")
|
||||||
}
|
}
|
||||||
if u.ID > 0 && err == nil {
|
if u.ID > 0 && err == nil {
|
||||||
user.ID = u.ID
|
user.ID = u.ID
|
||||||
|
@ -3903,7 +3970,7 @@ func doExternalAuth(username, password string, pubKey []byte, keyboardInteractiv
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return user, err
|
return user, err
|
||||||
}
|
}
|
||||||
return provider.userExists(user.Username)
|
return provider.userExists(user.Username, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
func doPluginAuth(username, password string, pubKey []byte, ip, protocol string,
|
func doPluginAuth(username, password string, pubKey []byte, ip, protocol string,
|
||||||
|
@ -3975,11 +4042,11 @@ func doPluginAuth(username, password string, pubKey []byte, ip, protocol string,
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return user, err
|
return user, err
|
||||||
}
|
}
|
||||||
return provider.userExists(user.Username)
|
return provider.userExists(user.Username, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
func getUserForHook(username string, oidcTokenFields *map[string]any) (User, User, error) {
|
func getUserForHook(username string, oidcTokenFields *map[string]any) (User, User, error) {
|
||||||
u, err := provider.userExists(username)
|
u, err := provider.userExists(username, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if _, ok := err.(*util.RecordNotFoundError); !ok {
|
if _, ok := err.(*util.RecordNotFoundError); !ok {
|
||||||
return u, u, err
|
return u, u, err
|
||||||
|
|
|
@ -138,7 +138,7 @@ func (g *Group) validate() error {
|
||||||
return util.NewValidationError("name is mandatory")
|
return util.NewValidationError("name is mandatory")
|
||||||
}
|
}
|
||||||
if config.NamingRules&1 == 0 && !usernameRegex.MatchString(g.Name) {
|
if config.NamingRules&1 == 0 && !usernameRegex.MatchString(g.Name) {
|
||||||
return util.NewValidationError(fmt.Sprintf("name %#v is not valid, the following characters are allowed: a-zA-Z0-9-_.~", g.Name))
|
return util.NewValidationError(fmt.Sprintf("name %q is not valid, the following characters are allowed: a-zA-Z0-9-_.~", g.Name))
|
||||||
}
|
}
|
||||||
if g.hasRedactedSecret() {
|
if g.hasRedactedSecret() {
|
||||||
return util.NewValidationError("cannot save a user with a redacted secret")
|
return util.NewValidationError("cannot save a user with a redacted secret")
|
||||||
|
|
|
@ -70,6 +70,10 @@ type memoryProviderHandle struct {
|
||||||
rules map[string]EventRule
|
rules map[string]EventRule
|
||||||
// slice with ordered rules
|
// slice with ordered rules
|
||||||
rulesNames []string
|
rulesNames []string
|
||||||
|
// map for roles, name is the key
|
||||||
|
roles map[string]Role
|
||||||
|
// slice with ordered roles
|
||||||
|
roleNames []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// MemoryProvider defines the auth provider for a memory store
|
// MemoryProvider defines the auth provider for a memory store
|
||||||
|
@ -104,6 +108,8 @@ func initializeMemoryProvider(basePath string) {
|
||||||
actionsNames: []string{},
|
actionsNames: []string{},
|
||||||
rules: make(map[string]EventRule),
|
rules: make(map[string]EventRule),
|
||||||
rulesNames: []string{},
|
rulesNames: []string{},
|
||||||
|
roles: map[string]Role{},
|
||||||
|
roleNames: []string{},
|
||||||
configFile: configFile,
|
configFile: configFile,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -137,7 +143,7 @@ func (p *MemoryProvider) validateUserAndTLSCert(username, protocol string, tlsCe
|
||||||
if tlsCert == nil {
|
if tlsCert == nil {
|
||||||
return user, errors.New("TLS certificate cannot be null or empty")
|
return user, errors.New("TLS certificate cannot be null or empty")
|
||||||
}
|
}
|
||||||
user, err := p.userExists(username)
|
user, err := p.userExists(username, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
providerLog(logger.LevelWarn, "error authenticating user %#v: %v", username, err)
|
providerLog(logger.LevelWarn, "error authenticating user %#v: %v", username, err)
|
||||||
return user, err
|
return user, err
|
||||||
|
@ -146,7 +152,7 @@ func (p *MemoryProvider) validateUserAndTLSCert(username, protocol string, tlsCe
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *MemoryProvider) validateUserAndPass(username, password, ip, protocol string) (User, error) {
|
func (p *MemoryProvider) validateUserAndPass(username, password, ip, protocol string) (User, error) {
|
||||||
user, err := p.userExists(username)
|
user, err := p.userExists(username, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
providerLog(logger.LevelWarn, "error authenticating user %#v: %v", username, err)
|
providerLog(logger.LevelWarn, "error authenticating user %#v: %v", username, err)
|
||||||
return user, err
|
return user, err
|
||||||
|
@ -159,7 +165,7 @@ func (p *MemoryProvider) validateUserAndPubKey(username string, pubKey []byte, i
|
||||||
if len(pubKey) == 0 {
|
if len(pubKey) == 0 {
|
||||||
return user, "", errors.New("credentials cannot be null or empty")
|
return user, "", errors.New("credentials cannot be null or empty")
|
||||||
}
|
}
|
||||||
user, err := p.userExists(username)
|
user, err := p.userExists(username, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
providerLog(logger.LevelWarn, "error authenticating user %#v: %v", username, err)
|
providerLog(logger.LevelWarn, "error authenticating user %#v: %v", username, err)
|
||||||
return user, "", err
|
return user, "", err
|
||||||
|
@ -317,7 +323,7 @@ func (p *MemoryProvider) addUser(user *User) error {
|
||||||
|
|
||||||
_, err = p.userExistsInternal(user.Username)
|
_, err = p.userExistsInternal(user.Username)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return fmt.Errorf("username %#v already exists", user.Username)
|
return fmt.Errorf("username %q already exists", user.Username)
|
||||||
}
|
}
|
||||||
user.ID = p.getNextID()
|
user.ID = p.getNextID()
|
||||||
user.LastQuotaUpdate = 0
|
user.LastQuotaUpdate = 0
|
||||||
|
@ -330,6 +336,9 @@ 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())
|
||||||
|
if err := p.addUserToRole(user.Username, user.Role); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
var mappedGroups []string
|
var mappedGroups []string
|
||||||
for idx := range user.Groups {
|
for idx := range user.Groups {
|
||||||
if err = p.addUserToGroupMapping(user.Username, user.Groups[idx].Name); err != nil {
|
if err = p.addUserToGroupMapping(user.Username, user.Groups[idx].Name); err != nil {
|
||||||
|
@ -366,6 +375,15 @@ func (p *MemoryProvider) updateUser(user *User) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
p.removeUserFromRole(u.Username, u.Role)
|
||||||
|
if err := p.addUserToRole(user.Username, user.Role); err != nil {
|
||||||
|
// try ro add old role
|
||||||
|
if errRollback := p.addUserToRole(u.Username, u.Role); errRollback != nil {
|
||||||
|
providerLog(logger.LevelError, "unable to rollback old role %q for user %q, error: %v",
|
||||||
|
u.Role, u.Username, errRollback)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
for idx := range u.Groups {
|
for idx := range u.Groups {
|
||||||
p.removeUserFromGroupMapping(u.Username, u.Groups[idx].Name)
|
p.removeUserFromGroupMapping(u.Username, u.Groups[idx].Name)
|
||||||
}
|
}
|
||||||
|
@ -412,6 +430,7 @@ func (p *MemoryProvider) deleteUser(user User, softDelete bool) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
p.removeUserFromRole(u.Username, u.Role)
|
||||||
for _, oldFolder := range u.VirtualFolders {
|
for _, oldFolder := range u.VirtualFolders {
|
||||||
p.removeRelationFromFolderMapping(oldFolder.Name, u.Username, "")
|
p.removeRelationFromFolderMapping(oldFolder.Name, u.Username, "")
|
||||||
}
|
}
|
||||||
|
@ -546,7 +565,7 @@ func (p *MemoryProvider) getUsersForQuotaCheck(toFetch map[string]bool) ([]User,
|
||||||
return users, nil
|
return users, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *MemoryProvider) getUsers(limit int, offset int, order string) ([]User, error) {
|
func (p *MemoryProvider) getUsers(limit int, offset int, order, role string) ([]User, error) {
|
||||||
users := make([]User, 0, limit)
|
users := make([]User, 0, limit)
|
||||||
var err error
|
var err error
|
||||||
p.dbHandle.Lock()
|
p.dbHandle.Lock()
|
||||||
|
@ -566,6 +585,9 @@ func (p *MemoryProvider) getUsers(limit int, offset int, order string) ([]User,
|
||||||
}
|
}
|
||||||
u := p.dbHandle.users[username]
|
u := p.dbHandle.users[username]
|
||||||
user := u.getACopy()
|
user := u.getACopy()
|
||||||
|
if !user.hasRole(role) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
p.addVirtualFoldersToUser(&user)
|
p.addVirtualFoldersToUser(&user)
|
||||||
user.PrepareForRendering()
|
user.PrepareForRendering()
|
||||||
users = append(users, user)
|
users = append(users, user)
|
||||||
|
@ -582,6 +604,9 @@ func (p *MemoryProvider) getUsers(limit int, offset int, order string) ([]User,
|
||||||
username := p.dbHandle.usernames[i]
|
username := p.dbHandle.usernames[i]
|
||||||
u := p.dbHandle.users[username]
|
u := p.dbHandle.users[username]
|
||||||
user := u.getACopy()
|
user := u.getACopy()
|
||||||
|
if !user.hasRole(role) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
p.addVirtualFoldersToUser(&user)
|
p.addVirtualFoldersToUser(&user)
|
||||||
user.PrepareForRendering()
|
user.PrepareForRendering()
|
||||||
users = append(users, user)
|
users = append(users, user)
|
||||||
|
@ -593,7 +618,7 @@ func (p *MemoryProvider) getUsers(limit int, offset int, order string) ([]User,
|
||||||
return users, err
|
return users, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *MemoryProvider) userExists(username string) (User, error) {
|
func (p *MemoryProvider) userExists(username, role string) (User, error) {
|
||||||
p.dbHandle.Lock()
|
p.dbHandle.Lock()
|
||||||
defer p.dbHandle.Unlock()
|
defer p.dbHandle.Unlock()
|
||||||
if p.dbHandle.isClosed {
|
if p.dbHandle.isClosed {
|
||||||
|
@ -603,6 +628,9 @@ func (p *MemoryProvider) userExists(username string) (User, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return user, err
|
return user, err
|
||||||
}
|
}
|
||||||
|
if !user.hasRole(role) {
|
||||||
|
return User{}, util.NewRecordNotFoundError(fmt.Sprintf("username %q does not exist", username))
|
||||||
|
}
|
||||||
p.addVirtualFoldersToUser(&user)
|
p.addVirtualFoldersToUser(&user)
|
||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
@ -635,6 +663,13 @@ func (p *MemoryProvider) ruleExistsInternal(name string) (EventRule, error) {
|
||||||
return EventRule{}, util.NewRecordNotFoundError(fmt.Sprintf("event rule %q does not exist", name))
|
return EventRule{}, util.NewRecordNotFoundError(fmt.Sprintf("event rule %q does not exist", name))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *MemoryProvider) roleExistsInternal(name string) (Role, error) {
|
||||||
|
if val, ok := p.dbHandle.roles[name]; ok {
|
||||||
|
return val.getACopy(), nil
|
||||||
|
}
|
||||||
|
return Role{}, util.NewRecordNotFoundError(fmt.Sprintf("role %q does not exist", name))
|
||||||
|
}
|
||||||
|
|
||||||
func (p *MemoryProvider) addAdmin(admin *Admin) error {
|
func (p *MemoryProvider) addAdmin(admin *Admin) error {
|
||||||
p.dbHandle.Lock()
|
p.dbHandle.Lock()
|
||||||
defer p.dbHandle.Unlock()
|
defer p.dbHandle.Unlock()
|
||||||
|
@ -653,6 +688,9 @@ 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
|
||||||
|
if err := p.addAdminToRole(admin.Username, admin.Role); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
var mappedAdmins []string
|
var mappedAdmins []string
|
||||||
for idx := range admin.Groups {
|
for idx := range admin.Groups {
|
||||||
if err = p.addAdminToGroupMapping(admin.Username, admin.Groups[idx].Name); err != nil {
|
if err = p.addAdminToGroupMapping(admin.Username, admin.Groups[idx].Name); err != nil {
|
||||||
|
@ -684,6 +722,15 @@ func (p *MemoryProvider) updateAdmin(admin *Admin) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
p.removeAdminFromRole(a.Username, a.Role)
|
||||||
|
if err := p.addAdminToRole(admin.Username, admin.Role); err != nil {
|
||||||
|
// try ro add old role
|
||||||
|
if errRollback := p.addAdminToRole(a.Username, a.Role); errRollback != nil {
|
||||||
|
providerLog(logger.LevelError, "unable to rollback old role %q for admin %q, error: %v",
|
||||||
|
a.Role, a.Username, errRollback)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
for idx := range a.Groups {
|
for idx := range a.Groups {
|
||||||
p.removeAdminFromGroupMapping(a.Username, a.Groups[idx].Name)
|
p.removeAdminFromGroupMapping(a.Username, a.Groups[idx].Name)
|
||||||
}
|
}
|
||||||
|
@ -717,6 +764,7 @@ func (p *MemoryProvider) deleteAdmin(admin Admin) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
p.removeAdminFromRole(a.Username, a.Role)
|
||||||
for idx := range a.Groups {
|
for idx := range a.Groups {
|
||||||
p.removeAdminFromGroupMapping(a.Username, a.Groups[idx].Name)
|
p.removeAdminFromGroupMapping(a.Username, a.Groups[idx].Name)
|
||||||
}
|
}
|
||||||
|
@ -1183,6 +1231,74 @@ func (p *MemoryProvider) removeUserFromGroupMapping(username, groupname string)
|
||||||
p.dbHandle.groups[groupname] = g
|
p.dbHandle.groups[groupname] = g
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *MemoryProvider) addAdminToRole(username, role string) error {
|
||||||
|
if role == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
r, err := p.roleExistsInternal(role)
|
||||||
|
if err != nil {
|
||||||
|
return util.NewGenericError(fmt.Sprintf("role %q does not exist", role))
|
||||||
|
}
|
||||||
|
if !util.Contains(r.Admins, username) {
|
||||||
|
r.Admins = append(r.Admins, username)
|
||||||
|
p.dbHandle.roles[role] = r
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MemoryProvider) removeAdminFromRole(username, role string) {
|
||||||
|
if role == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r, err := p.roleExistsInternal(role)
|
||||||
|
if err != nil {
|
||||||
|
providerLog(logger.LevelWarn, "role %q does not exist, cannot remove admin %q", role, username)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var admins []string
|
||||||
|
for _, a := range r.Admins {
|
||||||
|
if a != username {
|
||||||
|
admins = append(admins, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r.Admins = admins
|
||||||
|
p.dbHandle.roles[role] = r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MemoryProvider) addUserToRole(username, role string) error {
|
||||||
|
if role == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
r, err := p.roleExistsInternal(role)
|
||||||
|
if err != nil {
|
||||||
|
return util.NewGenericError(fmt.Sprintf("role %q does not exist", role))
|
||||||
|
}
|
||||||
|
if !util.Contains(r.Users, username) {
|
||||||
|
r.Users = append(r.Users, username)
|
||||||
|
p.dbHandle.roles[role] = r
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MemoryProvider) removeUserFromRole(username, role string) {
|
||||||
|
if role == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r, err := p.roleExistsInternal(role)
|
||||||
|
if err != nil {
|
||||||
|
providerLog(logger.LevelWarn, "role %q does not exist, cannot remove user %q", role, username)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var users []string
|
||||||
|
for _, u := range r.Users {
|
||||||
|
if u != username {
|
||||||
|
users = append(users, u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r.Users = users
|
||||||
|
p.dbHandle.roles[role] = r
|
||||||
|
}
|
||||||
|
|
||||||
func (p *MemoryProvider) joinUserVirtualFoldersFields(user *User) []vfs.VirtualFolder {
|
func (p *MemoryProvider) joinUserVirtualFoldersFields(user *User) []vfs.VirtualFolder {
|
||||||
var folders []vfs.VirtualFolder
|
var folders []vfs.VirtualFolder
|
||||||
for idx := range user.VirtualFolders {
|
for idx := range user.VirtualFolders {
|
||||||
|
@ -1714,7 +1830,7 @@ func (p *MemoryProvider) addShare(share *Share) error {
|
||||||
|
|
||||||
_, err = p.shareExistsInternal(share.ShareID, share.Username)
|
_, err = p.shareExistsInternal(share.ShareID, share.Username)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return fmt.Errorf("share %#v already exists", share.ShareID)
|
return fmt.Errorf("share %q already exists", share.ShareID)
|
||||||
}
|
}
|
||||||
if _, err := p.userExistsInternal(share.Username); err != nil {
|
if _, err := p.userExistsInternal(share.Username); err != nil {
|
||||||
return util.NewValidationError(fmt.Sprintf("related user %#v does not exists", share.Username))
|
return util.NewValidationError(fmt.Sprintf("related user %#v does not exists", share.Username))
|
||||||
|
@ -1753,7 +1869,7 @@ func (p *MemoryProvider) updateShare(share *Share) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if _, err := p.userExistsInternal(share.Username); err != nil {
|
if _, err := p.userExistsInternal(share.Username); err != nil {
|
||||||
return util.NewValidationError(fmt.Sprintf("related user %#v does not exists", share.Username))
|
return util.NewValidationError(fmt.Sprintf("related user %q does not exists", share.Username))
|
||||||
}
|
}
|
||||||
share.ID = s.ID
|
share.ID = s.ID
|
||||||
share.ShareID = s.ShareID
|
share.ShareID = s.ShareID
|
||||||
|
@ -2322,6 +2438,158 @@ func (*MemoryProvider) cleanupNodes() error {
|
||||||
return ErrNotImplemented
|
return ErrNotImplemented
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *MemoryProvider) roleExists(name string) (Role, error) {
|
||||||
|
p.dbHandle.Lock()
|
||||||
|
defer p.dbHandle.Unlock()
|
||||||
|
if p.dbHandle.isClosed {
|
||||||
|
return Role{}, errMemoryProviderClosed
|
||||||
|
}
|
||||||
|
role, err := p.roleExistsInternal(name)
|
||||||
|
if err != nil {
|
||||||
|
return role, err
|
||||||
|
}
|
||||||
|
return role, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MemoryProvider) addRole(role *Role) error {
|
||||||
|
if err := role.validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.dbHandle.Lock()
|
||||||
|
defer p.dbHandle.Unlock()
|
||||||
|
if p.dbHandle.isClosed {
|
||||||
|
return errMemoryProviderClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := p.roleExistsInternal(role.Name)
|
||||||
|
if err == nil {
|
||||||
|
return fmt.Errorf("role %q already exists", role.Name)
|
||||||
|
}
|
||||||
|
role.ID = p.getNextRoleID()
|
||||||
|
role.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
|
||||||
|
role.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
|
||||||
|
role.Users = nil
|
||||||
|
role.Admins = nil
|
||||||
|
p.dbHandle.roles[role.Name] = role.getACopy()
|
||||||
|
p.dbHandle.roleNames = append(p.dbHandle.roleNames, role.Name)
|
||||||
|
sort.Strings(p.dbHandle.roleNames)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MemoryProvider) updateRole(role *Role) error {
|
||||||
|
if err := role.validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.dbHandle.Lock()
|
||||||
|
defer p.dbHandle.Unlock()
|
||||||
|
if p.dbHandle.isClosed {
|
||||||
|
return errMemoryProviderClosed
|
||||||
|
}
|
||||||
|
oldRole, err := p.roleExistsInternal(role.Name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
role.ID = oldRole.ID
|
||||||
|
role.CreatedAt = oldRole.CreatedAt
|
||||||
|
role.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
|
||||||
|
role.Users = oldRole.Users
|
||||||
|
role.Admins = oldRole.Admins
|
||||||
|
p.dbHandle.roles[role.Name] = role.getACopy()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MemoryProvider) deleteRole(role Role) error {
|
||||||
|
p.dbHandle.Lock()
|
||||||
|
defer p.dbHandle.Unlock()
|
||||||
|
if p.dbHandle.isClosed {
|
||||||
|
return errMemoryProviderClosed
|
||||||
|
}
|
||||||
|
oldRole, err := p.roleExistsInternal(role.Name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(oldRole.Admins) > 0 {
|
||||||
|
return util.NewValidationError(fmt.Sprintf("the role %q is referenced, it cannot be removed", oldRole.Name))
|
||||||
|
}
|
||||||
|
for _, username := range oldRole.Users {
|
||||||
|
user, err := p.userExistsInternal(username)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if user.Role == role.Name {
|
||||||
|
user.Role = ""
|
||||||
|
p.dbHandle.users[username] = user
|
||||||
|
} else {
|
||||||
|
providerLog(logger.LevelError, "user %q does not have the expected role %q, actual %q", username, role.Name, user.Role)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete(p.dbHandle.roles, role.Name)
|
||||||
|
p.dbHandle.roleNames = make([]string, 0, len(p.dbHandle.roles))
|
||||||
|
for name := range p.dbHandle.roles {
|
||||||
|
p.dbHandle.roleNames = append(p.dbHandle.roleNames, name)
|
||||||
|
}
|
||||||
|
sort.Strings(p.dbHandle.roleNames)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MemoryProvider) getRoles(limit int, offset int, order string, minimal bool) ([]Role, error) {
|
||||||
|
p.dbHandle.Lock()
|
||||||
|
defer p.dbHandle.Unlock()
|
||||||
|
|
||||||
|
if p.dbHandle.isClosed {
|
||||||
|
return nil, errMemoryProviderClosed
|
||||||
|
}
|
||||||
|
if limit <= 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
roles := make([]Role, 0, 10)
|
||||||
|
itNum := 0
|
||||||
|
if order == OrderASC {
|
||||||
|
for _, name := range p.dbHandle.roleNames {
|
||||||
|
itNum++
|
||||||
|
if itNum <= offset {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
r := p.dbHandle.roles[name]
|
||||||
|
role := r.getACopy()
|
||||||
|
roles = append(roles, role)
|
||||||
|
if len(roles) >= limit {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for i := len(p.dbHandle.roleNames) - 1; i >= 0; i-- {
|
||||||
|
itNum++
|
||||||
|
if itNum <= offset {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := p.dbHandle.roleNames[i]
|
||||||
|
r := p.dbHandle.roles[name]
|
||||||
|
role := r.getACopy()
|
||||||
|
roles = append(roles, role)
|
||||||
|
if len(roles) >= limit {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return roles, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MemoryProvider) dumpRoles() ([]Role, error) {
|
||||||
|
p.dbHandle.Lock()
|
||||||
|
defer p.dbHandle.Unlock()
|
||||||
|
if p.dbHandle.isClosed {
|
||||||
|
return nil, errMemoryProviderClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
roles := make([]Role, 0, len(p.dbHandle.roles))
|
||||||
|
for _, name := range p.dbHandle.roleNames {
|
||||||
|
r := p.dbHandle.roles[name]
|
||||||
|
roles = append(roles, r.getACopy())
|
||||||
|
}
|
||||||
|
return roles, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (p *MemoryProvider) setFirstDownloadTimestamp(username string) error {
|
func (p *MemoryProvider) setFirstDownloadTimestamp(username string) error {
|
||||||
p.dbHandle.Lock()
|
p.dbHandle.Lock()
|
||||||
defer p.dbHandle.Unlock()
|
defer p.dbHandle.Unlock()
|
||||||
|
@ -2333,7 +2601,7 @@ func (p *MemoryProvider) setFirstDownloadTimestamp(username string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if user.FirstDownload > 0 {
|
if user.FirstDownload > 0 {
|
||||||
return util.NewGenericError(fmt.Sprintf("first download already set to %v",
|
return util.NewGenericError(fmt.Sprintf("first download already set to %s",
|
||||||
util.GetTimeFromMsecSinceEpoch(user.FirstDownload)))
|
util.GetTimeFromMsecSinceEpoch(user.FirstDownload)))
|
||||||
}
|
}
|
||||||
user.FirstDownload = util.GetTimeAsMsSinceEpoch(time.Now())
|
user.FirstDownload = util.GetTimeAsMsSinceEpoch(time.Now())
|
||||||
|
@ -2352,7 +2620,7 @@ func (p *MemoryProvider) setFirstUploadTimestamp(username string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if user.FirstUpload > 0 {
|
if user.FirstUpload > 0 {
|
||||||
return util.NewGenericError(fmt.Sprintf("first upload already set to %v",
|
return util.NewGenericError(fmt.Sprintf("first upload already set to %s",
|
||||||
util.GetTimeFromMsecSinceEpoch(user.FirstUpload)))
|
util.GetTimeFromMsecSinceEpoch(user.FirstUpload)))
|
||||||
}
|
}
|
||||||
user.FirstUpload = util.GetTimeAsMsSinceEpoch(time.Now())
|
user.FirstUpload = util.GetTimeAsMsSinceEpoch(time.Now())
|
||||||
|
@ -2420,11 +2688,24 @@ func (p *MemoryProvider) getNextRuleID() int64 {
|
||||||
return nextID
|
return nextID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *MemoryProvider) getNextRoleID() int64 {
|
||||||
|
nextID := int64(1)
|
||||||
|
for _, r := range p.dbHandle.roles {
|
||||||
|
if r.ID >= nextID {
|
||||||
|
nextID = r.ID + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nextID
|
||||||
|
}
|
||||||
|
|
||||||
func (p *MemoryProvider) clear() {
|
func (p *MemoryProvider) clear() {
|
||||||
p.dbHandle.Lock()
|
p.dbHandle.Lock()
|
||||||
defer p.dbHandle.Unlock()
|
defer p.dbHandle.Unlock()
|
||||||
|
|
||||||
p.dbHandle.usernames = []string{}
|
p.dbHandle.usernames = []string{}
|
||||||
p.dbHandle.users = make(map[string]User)
|
p.dbHandle.users = make(map[string]User)
|
||||||
|
p.dbHandle.groupnames = []string{}
|
||||||
|
p.dbHandle.groups = map[string]Group{}
|
||||||
p.dbHandle.vfoldersNames = []string{}
|
p.dbHandle.vfoldersNames = []string{}
|
||||||
p.dbHandle.vfolders = make(map[string]vfs.BaseVirtualFolder)
|
p.dbHandle.vfolders = make(map[string]vfs.BaseVirtualFolder)
|
||||||
p.dbHandle.admins = make(map[string]Admin)
|
p.dbHandle.admins = make(map[string]Admin)
|
||||||
|
@ -2433,6 +2714,12 @@ func (p *MemoryProvider) clear() {
|
||||||
p.dbHandle.apiKeysIDs = []string{}
|
p.dbHandle.apiKeysIDs = []string{}
|
||||||
p.dbHandle.shares = make(map[string]Share)
|
p.dbHandle.shares = make(map[string]Share)
|
||||||
p.dbHandle.sharesIDs = []string{}
|
p.dbHandle.sharesIDs = []string{}
|
||||||
|
p.dbHandle.actions = map[string]BaseEventAction{}
|
||||||
|
p.dbHandle.actionsNames = []string{}
|
||||||
|
p.dbHandle.rules = map[string]EventRule{}
|
||||||
|
p.dbHandle.rulesNames = []string{}
|
||||||
|
p.dbHandle.roles = map[string]Role{}
|
||||||
|
p.dbHandle.roleNames = []string{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *MemoryProvider) reloadConfig() error {
|
func (p *MemoryProvider) reloadConfig() error {
|
||||||
|
@ -2466,8 +2753,16 @@ func (p *MemoryProvider) reloadConfig() error {
|
||||||
providerLog(logger.LevelError, "error loading dump: %v", err)
|
providerLog(logger.LevelError, "error loading dump: %v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
return p.restoreDump(dump)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MemoryProvider) restoreDump(dump BackupData) error {
|
||||||
p.clear()
|
p.clear()
|
||||||
|
|
||||||
|
if err := p.restoreRoles(dump); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if err := p.restoreFolders(dump); err != nil {
|
if err := p.restoreFolders(dump); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -2619,6 +2914,31 @@ func (p *MemoryProvider) restoreAdmins(dump BackupData) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *MemoryProvider) restoreRoles(dump BackupData) error {
|
||||||
|
for _, role := range dump.Roles {
|
||||||
|
role := role // pin
|
||||||
|
role.Name = config.convertName(role.Name)
|
||||||
|
r, err := p.roleExists(role.Name)
|
||||||
|
if err == nil {
|
||||||
|
role.ID = r.ID
|
||||||
|
err = UpdateRole(&role, ActionExecutorSystem, "")
|
||||||
|
if err != nil {
|
||||||
|
providerLog(logger.LevelError, "error updating role %q: %v", role.Name, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
role.Admins = nil
|
||||||
|
role.Users = nil
|
||||||
|
err = AddRole(&role, ActionExecutorSystem, "")
|
||||||
|
if err != nil {
|
||||||
|
providerLog(logger.LevelError, "error adding role %q: %v", role.Name, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (p *MemoryProvider) restoreGroups(dump BackupData) error {
|
func (p *MemoryProvider) restoreGroups(dump BackupData) error {
|
||||||
for _, group := range dump.Groups {
|
for _, group := range dump.Groups {
|
||||||
group := group // pin
|
group := group // pin
|
||||||
|
@ -2671,7 +2991,7 @@ func (p *MemoryProvider) restoreUsers(dump BackupData) error {
|
||||||
for _, user := range dump.Users {
|
for _, user := range dump.Users {
|
||||||
user := user // pin
|
user := user // pin
|
||||||
user.Username = config.convertName(user.Username)
|
user.Username = config.convertName(user.Username)
|
||||||
u, err := p.userExists(user.Username)
|
u, err := p.userExists(user.Username, "")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
user.ID = u.ID
|
user.ID = u.ID
|
||||||
err = UpdateUser(&user, ActionExecutorSystem, "")
|
err = UpdateUser(&user, ActionExecutorSystem, "")
|
||||||
|
|
|
@ -57,6 +57,7 @@ const (
|
||||||
"DROP TABLE IF EXISTS `{{events_rules}}` CASCADE;" +
|
"DROP TABLE IF EXISTS `{{events_rules}}` CASCADE;" +
|
||||||
"DROP TABLE IF EXISTS `{{tasks}}` CASCADE;" +
|
"DROP TABLE IF EXISTS `{{tasks}}` CASCADE;" +
|
||||||
"DROP TABLE IF EXISTS `{{nodes}}` CASCADE;" +
|
"DROP TABLE IF EXISTS `{{nodes}}` CASCADE;" +
|
||||||
|
"DROP TABLE IF EXISTS `{{roles}}` CASCADE;" +
|
||||||
"DROP TABLE IF EXISTS `{{schema_version}}` CASCADE;"
|
"DROP TABLE IF EXISTS `{{schema_version}}` CASCADE;"
|
||||||
mysqlInitialSQL = "CREATE TABLE `{{schema_version}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `version` integer NOT NULL);" +
|
mysqlInitialSQL = "CREATE TABLE `{{schema_version}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `version` integer NOT NULL);" +
|
||||||
"CREATE TABLE `{{admins}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `username` varchar(255) NOT NULL UNIQUE, " +
|
"CREATE TABLE `{{admins}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `username` varchar(255) NOT NULL UNIQUE, " +
|
||||||
|
@ -171,6 +172,17 @@ const (
|
||||||
"CREATE INDEX `{{prefix}}events_rules_trigger_idx` ON `{{events_rules}}` (`trigger`);" +
|
"CREATE INDEX `{{prefix}}events_rules_trigger_idx` ON `{{events_rules}}` (`trigger`);" +
|
||||||
"CREATE INDEX `{{prefix}}rules_actions_mapping_order_idx` ON `{{rules_actions_mapping}}` (`order`);" +
|
"CREATE INDEX `{{prefix}}rules_actions_mapping_order_idx` ON `{{rules_actions_mapping}}` (`order`);" +
|
||||||
"INSERT INTO {{schema_version}} (version) VALUES (23);"
|
"INSERT INTO {{schema_version}} (version) VALUES (23);"
|
||||||
|
mysqlV24SQL = "CREATE TABLE `{{roles}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `name` varchar(255) NOT NULL UNIQUE, " +
|
||||||
|
"`description` varchar(512) NULL, `created_at` bigint NOT NULL, `updated_at` bigint NOT NULL);" +
|
||||||
|
"ALTER TABLE `{{admins}}` ADD COLUMN `role_id` integer NULL , " +
|
||||||
|
"ADD CONSTRAINT `{{prefix}}admins_role_id_fk_roles_id` FOREIGN KEY (`role_id`) REFERENCES `{{roles}}`(`id`) ON DELETE NO ACTION;" +
|
||||||
|
"ALTER TABLE `{{users}}` ADD COLUMN `role_id` integer NULL , " +
|
||||||
|
"ADD CONSTRAINT `{{prefix}}users_role_id_fk_roles_id` FOREIGN KEY (`role_id`) REFERENCES `{{roles}}`(`id`) ON DELETE SET NULL;"
|
||||||
|
mysqlV24DownSQL = "ALTER TABLE `{{users}}` DROP FOREIGN KEY `{{prefix}}users_role_id_fk_roles_id`;" +
|
||||||
|
"ALTER TABLE `{{admins}}` DROP FOREIGN KEY `{{prefix}}admins_role_id_fk_roles_id`;" +
|
||||||
|
"ALTER TABLE `{{users}}` DROP COLUMN `role_id`;" +
|
||||||
|
"ALTER TABLE `{{admins}}` DROP COLUMN `role_id`;" +
|
||||||
|
"DROP TABLE `{{roles}}` CASCADE;"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MySQLProvider defines the auth provider for MySQL/MariaDB database
|
// MySQLProvider defines the auth provider for MySQL/MariaDB database
|
||||||
|
@ -312,8 +324,8 @@ func (p *MySQLProvider) updateAdminLastLogin(username string) error {
|
||||||
return sqlCommonUpdateAdminLastLogin(username, p.dbHandle)
|
return sqlCommonUpdateAdminLastLogin(username, p.dbHandle)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *MySQLProvider) userExists(username string) (User, error) {
|
func (p *MySQLProvider) userExists(username, role string) (User, error) {
|
||||||
return sqlCommonGetUserByUsername(username, p.dbHandle)
|
return sqlCommonGetUserByUsername(username, role, p.dbHandle)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *MySQLProvider) addUser(user *User) error {
|
func (p *MySQLProvider) addUser(user *User) error {
|
||||||
|
@ -340,8 +352,8 @@ func (p *MySQLProvider) getRecentlyUpdatedUsers(after int64) ([]User, error) {
|
||||||
return sqlCommonGetRecentlyUpdatedUsers(after, p.dbHandle)
|
return sqlCommonGetRecentlyUpdatedUsers(after, p.dbHandle)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *MySQLProvider) getUsers(limit int, offset int, order string) ([]User, error) {
|
func (p *MySQLProvider) getUsers(limit int, offset int, order, role string) ([]User, error) {
|
||||||
return sqlCommonGetUsers(limit, offset, order, p.dbHandle)
|
return sqlCommonGetUsers(limit, offset, order, role, p.dbHandle)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *MySQLProvider) getUsersForQuotaCheck(toFetch map[string]bool) ([]User, error) {
|
func (p *MySQLProvider) getUsersForQuotaCheck(toFetch map[string]bool) ([]User, error) {
|
||||||
|
@ -654,6 +666,30 @@ func (p *MySQLProvider) cleanupNodes() error {
|
||||||
return sqlCommonCleanupNodes(p.dbHandle)
|
return sqlCommonCleanupNodes(p.dbHandle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *MySQLProvider) roleExists(name string) (Role, error) {
|
||||||
|
return sqlCommonGetRoleByName(name, p.dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MySQLProvider) addRole(role *Role) error {
|
||||||
|
return sqlCommonAddRole(role, p.dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MySQLProvider) updateRole(role *Role) error {
|
||||||
|
return sqlCommonUpdateRole(role, p.dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MySQLProvider) deleteRole(role Role) error {
|
||||||
|
return sqlCommonDeleteRole(role, p.dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MySQLProvider) getRoles(limit int, offset int, order string, minimal bool) ([]Role, error) {
|
||||||
|
return sqlCommonGetRoles(limit, offset, order, minimal, p.dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MySQLProvider) dumpRoles() ([]Role, error) {
|
||||||
|
return sqlCommonDumpRoles(p.dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
func (p *MySQLProvider) setFirstDownloadTimestamp(username string) error {
|
func (p *MySQLProvider) setFirstDownloadTimestamp(username string) error {
|
||||||
return sqlCommonSetFirstDownloadTimestamp(username, p.dbHandle)
|
return sqlCommonSetFirstDownloadTimestamp(username, p.dbHandle)
|
||||||
}
|
}
|
||||||
|
@ -701,6 +737,8 @@ func (p *MySQLProvider) migrateDatabase() error { //nolint:dupl
|
||||||
providerLog(logger.LevelError, "%v", err)
|
providerLog(logger.LevelError, "%v", err)
|
||||||
logger.ErrorToConsole("%v", err)
|
logger.ErrorToConsole("%v", err)
|
||||||
return err
|
return err
|
||||||
|
case version == 23:
|
||||||
|
return updateMySQLDatabaseFromV23(p.dbHandle)
|
||||||
default:
|
default:
|
||||||
if version > sqlDatabaseVersion {
|
if version > sqlDatabaseVersion {
|
||||||
providerLog(logger.LevelError, "database schema version %d is newer than the supported one: %d", version,
|
providerLog(logger.LevelError, "database schema version %d is newer than the supported one: %d", version,
|
||||||
|
@ -723,6 +761,8 @@ func (p *MySQLProvider) revertDatabase(targetVersion int) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
switch dbVersion.Version {
|
switch dbVersion.Version {
|
||||||
|
case 24:
|
||||||
|
return downgradeMySQLDatabaseFromV24(p.dbHandle)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("database schema version not handled: %d", dbVersion.Version)
|
return fmt.Errorf("database schema version not handled: %d", dbVersion.Version)
|
||||||
}
|
}
|
||||||
|
@ -732,3 +772,31 @@ func (p *MySQLProvider) resetDatabase() error {
|
||||||
sql := sqlReplaceAll(mysqlResetSQL)
|
sql := sqlReplaceAll(mysqlResetSQL)
|
||||||
return sqlCommonExecSQLAndUpdateDBVersion(p.dbHandle, strings.Split(sql, ";"), 0, false)
|
return sqlCommonExecSQLAndUpdateDBVersion(p.dbHandle, strings.Split(sql, ";"), 0, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateMySQLDatabaseFromV23(dbHandle *sql.DB) error {
|
||||||
|
return updateMySQLDatabaseFrom23To24(dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func downgradeMySQLDatabaseFromV24(dbHandle *sql.DB) error {
|
||||||
|
return downgradeMySQLDatabaseFrom24To23(dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateMySQLDatabaseFrom23To24(dbHandle *sql.DB) error {
|
||||||
|
logger.InfoToConsole("updating database schema version: 23 -> 24")
|
||||||
|
providerLog(logger.LevelInfo, "updating database schema version: 23 -> 24")
|
||||||
|
sql := strings.ReplaceAll(mysqlV24SQL, "{{roles}}", sqlTableRoles)
|
||||||
|
sql = strings.ReplaceAll(sql, "{{admins}}", sqlTableAdmins)
|
||||||
|
sql = strings.ReplaceAll(sql, "{{users}}", sqlTableUsers)
|
||||||
|
sql = strings.ReplaceAll(sql, "{{prefix}}", config.SQLTablesPrefix)
|
||||||
|
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, strings.Split(sql, ";"), 24, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func downgradeMySQLDatabaseFrom24To23(dbHandle *sql.DB) error {
|
||||||
|
logger.InfoToConsole("downgrading database schema version: 24 -> 23")
|
||||||
|
providerLog(logger.LevelInfo, "downgrading database schema version: 24 -> 23")
|
||||||
|
sql := strings.ReplaceAll(mysqlV24DownSQL, "{{roles}}", sqlTableRoles)
|
||||||
|
sql = strings.ReplaceAll(sql, "{{admins}}", sqlTableAdmins)
|
||||||
|
sql = strings.ReplaceAll(sql, "{{users}}", sqlTableUsers)
|
||||||
|
sql = strings.ReplaceAll(sql, "{{prefix}}", config.SQLTablesPrefix)
|
||||||
|
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, strings.Split(sql, ";"), 23, false)
|
||||||
|
}
|
||||||
|
|
|
@ -25,8 +25,8 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/lestrrat-go/jwx/jwa"
|
"github.com/lestrrat-go/jwx/v2/jwa"
|
||||||
"github.com/lestrrat-go/jwx/jwt"
|
"github.com/lestrrat-go/jwx/v2/jwt"
|
||||||
"github.com/rs/xid"
|
"github.com/rs/xid"
|
||||||
|
|
||||||
"github.com/drakkan/sftpgo/v2/internal/httpclient"
|
"github.com/drakkan/sftpgo/v2/internal/httpclient"
|
||||||
|
@ -120,27 +120,33 @@ func (n *Node) validate() error {
|
||||||
return n.Data.validate()
|
return n.Data.validate()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Node) authenticate(token string) (string, error) {
|
func (n *Node) authenticate(token string) (string, string, error) {
|
||||||
if err := n.Data.Key.TryDecrypt(); err != nil {
|
if err := n.Data.Key.TryDecrypt(); err != nil {
|
||||||
providerLog(logger.LevelError, "unable to decrypt node key: %v", err)
|
providerLog(logger.LevelError, "unable to decrypt node key: %v", err)
|
||||||
return "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
if token == "" {
|
if token == "" {
|
||||||
return "", ErrInvalidCredentials
|
return "", "", ErrInvalidCredentials
|
||||||
}
|
}
|
||||||
t, err := jwt.Parse([]byte(token), jwt.WithVerify(jwa.HS256, []byte(n.Data.Key.GetPayload())))
|
t, err := jwt.Parse([]byte(token), jwt.WithKey(jwa.HS256, []byte(n.Data.Key.GetPayload())), jwt.WithValidate(true))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("unable to parse token: %v", err)
|
return "", "", fmt.Errorf("unable to parse and validate token: %v", err)
|
||||||
}
|
|
||||||
if err := jwt.Validate(t); err != nil {
|
|
||||||
return "", fmt.Errorf("unable to validate token: %v", err)
|
|
||||||
}
|
}
|
||||||
|
var adminUsername, role string
|
||||||
if admin, ok := t.Get("admin"); ok {
|
if admin, ok := t.Get("admin"); ok {
|
||||||
if val, ok := admin.(string); ok && val != "" {
|
if val, ok := admin.(string); ok && val != "" {
|
||||||
return val, nil
|
adminUsername = val
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "", errors.New("no admin username associated with node token")
|
if adminUsername == "" {
|
||||||
|
return "", "", errors.New("no admin username associated with node token")
|
||||||
|
}
|
||||||
|
if r, ok := t.Get("role"); ok {
|
||||||
|
if val, ok := r.(string); ok && val != "" {
|
||||||
|
role = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return adminUsername, role, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getBaseURL returns the base URL for this node
|
// getBaseURL returns the base URL for this node
|
||||||
|
@ -157,7 +163,7 @@ func (n *Node) getBaseURL() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// generateAuthToken generates a new auth token
|
// generateAuthToken generates a new auth token
|
||||||
func (n *Node) generateAuthToken(username string) (string, error) {
|
func (n *Node) generateAuthToken(username, role string) (string, error) {
|
||||||
if err := n.Data.Key.TryDecrypt(); err != nil {
|
if err := n.Data.Key.TryDecrypt(); err != nil {
|
||||||
return "", fmt.Errorf("unable to decrypt node key: %w", err)
|
return "", fmt.Errorf("unable to decrypt node key: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -165,18 +171,19 @@ func (n *Node) generateAuthToken(username string) (string, error) {
|
||||||
|
|
||||||
t := jwt.New()
|
t := jwt.New()
|
||||||
t.Set("admin", username) //nolint:errcheck
|
t.Set("admin", username) //nolint:errcheck
|
||||||
|
t.Set("role", role) //nolint:errcheck
|
||||||
t.Set(jwt.JwtIDKey, xid.New().String()) //nolint:errcheck
|
t.Set(jwt.JwtIDKey, xid.New().String()) //nolint:errcheck
|
||||||
t.Set(jwt.NotBeforeKey, now.Add(-30*time.Second)) //nolint:errcheck
|
t.Set(jwt.NotBeforeKey, now.Add(-30*time.Second)) //nolint:errcheck
|
||||||
t.Set(jwt.ExpirationKey, now.Add(1*time.Minute)) //nolint:errcheck
|
t.Set(jwt.ExpirationKey, now.Add(1*time.Minute)) //nolint:errcheck
|
||||||
|
|
||||||
payload, err := jwt.Sign(t, jwa.HS256, []byte(n.Data.Key.GetPayload()))
|
payload, err := jwt.Sign(t, jwt.WithKey(jwa.HS256, []byte(n.Data.Key.GetPayload())))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("unable to sign authentication token: %w", err)
|
return "", fmt.Errorf("unable to sign authentication token: %w", err)
|
||||||
}
|
}
|
||||||
return string(payload), nil
|
return string(payload), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Node) prepareRequest(ctx context.Context, username, relativeURL, method string,
|
func (n *Node) prepareRequest(ctx context.Context, username, role, relativeURL, method string,
|
||||||
body io.Reader,
|
body io.Reader,
|
||||||
) (*http.Request, error) {
|
) (*http.Request, error) {
|
||||||
url := fmt.Sprintf("%s%s", n.getBaseURL(), relativeURL)
|
url := fmt.Sprintf("%s%s", n.getBaseURL(), relativeURL)
|
||||||
|
@ -184,7 +191,7 @@ func (n *Node) prepareRequest(ctx context.Context, username, relativeURL, method
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
token, err := n.generateAuthToken(username)
|
token, err := n.generateAuthToken(username, role)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -194,11 +201,11 @@ func (n *Node) prepareRequest(ctx context.Context, username, relativeURL, method
|
||||||
|
|
||||||
// SendGetRequest sends an HTTP GET request to this node.
|
// SendGetRequest sends an HTTP GET request to this node.
|
||||||
// The responseHolder must be a pointer
|
// The responseHolder must be a pointer
|
||||||
func (n *Node) SendGetRequest(username, relativeURL string, responseHolder any) error {
|
func (n *Node) SendGetRequest(username, role, relativeURL string, responseHolder any) error {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), nodeReqTimeout)
|
ctx, cancel := context.WithTimeout(context.Background(), nodeReqTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
req, err := n.prepareRequest(ctx, username, relativeURL, http.MethodGet, nil)
|
req, err := n.prepareRequest(ctx, username, role, relativeURL, http.MethodGet, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -222,11 +229,11 @@ func (n *Node) SendGetRequest(username, relativeURL string, responseHolder any)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendDeleteRequest sends an HTTP DELETE request to this node
|
// SendDeleteRequest sends an HTTP DELETE request to this node
|
||||||
func (n *Node) SendDeleteRequest(username, relativeURL string) error {
|
func (n *Node) SendDeleteRequest(username, role, relativeURL string) error {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), nodeReqTimeout)
|
ctx, cancel := context.WithTimeout(context.Background(), nodeReqTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
req, err := n.prepareRequest(ctx, username, relativeURL, http.MethodDelete, nil)
|
req, err := n.prepareRequest(ctx, username, role, relativeURL, http.MethodDelete, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -246,9 +253,9 @@ func (n *Node) SendDeleteRequest(username, relativeURL string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthenticateNodeToken check the validity of the provided token
|
// AuthenticateNodeToken check the validity of the provided token
|
||||||
func AuthenticateNodeToken(token string) (string, error) {
|
func AuthenticateNodeToken(token string) (string, string, error) {
|
||||||
if currentNode == nil {
|
if currentNode == nil {
|
||||||
return "", errNoClusterNodes
|
return "", "", errNoClusterNodes
|
||||||
}
|
}
|
||||||
return currentNode.authenticate(token)
|
return currentNode.authenticate(token)
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
// we import pgx here to be able to disable PostgreSQL support using a build tag
|
// we import pgx here to be able to disable PostgreSQL support using a build tag
|
||||||
|
@ -54,6 +55,7 @@ DROP TABLE IF EXISTS "{{events_actions}}" CASCADE;
|
||||||
DROP TABLE IF EXISTS "{{events_rules}}" CASCADE;
|
DROP TABLE IF EXISTS "{{events_rules}}" CASCADE;
|
||||||
DROP TABLE IF EXISTS "{{tasks}}" CASCADE;
|
DROP TABLE IF EXISTS "{{tasks}}" CASCADE;
|
||||||
DROP TABLE IF EXISTS "{{nodes}}" CASCADE;
|
DROP TABLE IF EXISTS "{{nodes}}" CASCADE;
|
||||||
|
DROP TABLE IF EXISTS "{{roles}}" CASCADE;
|
||||||
DROP TABLE IF EXISTS "{{schema_version}}" CASCADE;
|
DROP TABLE IF EXISTS "{{schema_version}}" CASCADE;
|
||||||
`
|
`
|
||||||
pgsqlInitial = `CREATE TABLE "{{schema_version}}" ("id" serial NOT NULL PRIMARY KEY, "version" integer NOT NULL);
|
pgsqlInitial = `CREATE TABLE "{{schema_version}}" ("id" serial NOT NULL PRIMARY KEY, "version" integer NOT NULL);
|
||||||
|
@ -178,6 +180,19 @@ CREATE INDEX "{{prefix}}rules_actions_mapping_order_idx" ON "{{rules_actions_map
|
||||||
CREATE INDEX "{{prefix}}admins_groups_mapping_admin_id_idx" ON "{{admins_groups_mapping}}" ("admin_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");
|
CREATE INDEX "{{prefix}}admins_groups_mapping_group_id_idx" ON "{{admins_groups_mapping}}" ("group_id");
|
||||||
INSERT INTO {{schema_version}} (version) VALUES (23);
|
INSERT INTO {{schema_version}} (version) VALUES (23);
|
||||||
|
`
|
||||||
|
pgsqlV24SQL = `CREATE TABLE "{{roles}}" ("id" serial NOT NULL PRIMARY KEY, "name" varchar(255) NOT NULL UNIQUE,
|
||||||
|
"description" varchar(512) NULL, "created_at" bigint NOT NULL, "updated_at" bigint NOT NULL);
|
||||||
|
ALTER TABLE "{{admins}}" ADD COLUMN "role_id" integer NULL CONSTRAINT "{{prefix}}admins_role_id_fk_roles_id"
|
||||||
|
REFERENCES "{{roles}}"("id") ON DELETE NO ACTION;
|
||||||
|
ALTER TABLE "{{users}}" ADD COLUMN "role_id" integer NULL CONSTRAINT "{{prefix}}users_role_id_fk_roles_id"
|
||||||
|
REFERENCES "{{roles}}"("id") ON DELETE SET NULL;
|
||||||
|
CREATE INDEX "{{prefix}}admins_role_id_idx" ON "{{admins}}" ("role_id");
|
||||||
|
CREATE INDEX "{{prefix}}users_role_id_idx" ON "{{users}}" ("role_id");
|
||||||
|
`
|
||||||
|
pgsqlV24DownSQL = `ALTER TABLE "{{users}}" DROP COLUMN "role_id" CASCADE;
|
||||||
|
ALTER TABLE "{{admins}}" DROP COLUMN "role_id" CASCADE;
|
||||||
|
DROP TABLE "{{roles}}" CASCADE;
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -279,8 +294,8 @@ func (p *PGSQLProvider) updateAdminLastLogin(username string) error {
|
||||||
return sqlCommonUpdateAdminLastLogin(username, p.dbHandle)
|
return sqlCommonUpdateAdminLastLogin(username, p.dbHandle)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PGSQLProvider) userExists(username string) (User, error) {
|
func (p *PGSQLProvider) userExists(username, role string) (User, error) {
|
||||||
return sqlCommonGetUserByUsername(username, p.dbHandle)
|
return sqlCommonGetUserByUsername(username, role, p.dbHandle)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PGSQLProvider) addUser(user *User) error {
|
func (p *PGSQLProvider) addUser(user *User) error {
|
||||||
|
@ -307,8 +322,8 @@ func (p *PGSQLProvider) getRecentlyUpdatedUsers(after int64) ([]User, error) {
|
||||||
return sqlCommonGetRecentlyUpdatedUsers(after, p.dbHandle)
|
return sqlCommonGetRecentlyUpdatedUsers(after, p.dbHandle)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PGSQLProvider) getUsers(limit int, offset int, order string) ([]User, error) {
|
func (p *PGSQLProvider) getUsers(limit int, offset int, order, role string) ([]User, error) {
|
||||||
return sqlCommonGetUsers(limit, offset, order, p.dbHandle)
|
return sqlCommonGetUsers(limit, offset, order, role, p.dbHandle)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PGSQLProvider) getUsersForQuotaCheck(toFetch map[string]bool) ([]User, error) {
|
func (p *PGSQLProvider) getUsersForQuotaCheck(toFetch map[string]bool) ([]User, error) {
|
||||||
|
@ -621,6 +636,30 @@ func (p *PGSQLProvider) cleanupNodes() error {
|
||||||
return sqlCommonCleanupNodes(p.dbHandle)
|
return sqlCommonCleanupNodes(p.dbHandle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *PGSQLProvider) roleExists(name string) (Role, error) {
|
||||||
|
return sqlCommonGetRoleByName(name, p.dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PGSQLProvider) addRole(role *Role) error {
|
||||||
|
return sqlCommonAddRole(role, p.dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PGSQLProvider) updateRole(role *Role) error {
|
||||||
|
return sqlCommonUpdateRole(role, p.dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PGSQLProvider) deleteRole(role Role) error {
|
||||||
|
return sqlCommonDeleteRole(role, p.dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PGSQLProvider) getRoles(limit int, offset int, order string, minimal bool) ([]Role, error) {
|
||||||
|
return sqlCommonGetRoles(limit, offset, order, minimal, p.dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PGSQLProvider) dumpRoles() ([]Role, error) {
|
||||||
|
return sqlCommonDumpRoles(p.dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
func (p *PGSQLProvider) setFirstDownloadTimestamp(username string) error {
|
func (p *PGSQLProvider) setFirstDownloadTimestamp(username string) error {
|
||||||
return sqlCommonSetFirstDownloadTimestamp(username, p.dbHandle)
|
return sqlCommonSetFirstDownloadTimestamp(username, p.dbHandle)
|
||||||
}
|
}
|
||||||
|
@ -668,6 +707,8 @@ func (p *PGSQLProvider) migrateDatabase() error { //nolint:dupl
|
||||||
providerLog(logger.LevelError, "%v", err)
|
providerLog(logger.LevelError, "%v", err)
|
||||||
logger.ErrorToConsole("%v", err)
|
logger.ErrorToConsole("%v", err)
|
||||||
return err
|
return err
|
||||||
|
case version == 23:
|
||||||
|
return updatePgSQLDatabaseFromV23(p.dbHandle)
|
||||||
default:
|
default:
|
||||||
if version > sqlDatabaseVersion {
|
if version > sqlDatabaseVersion {
|
||||||
providerLog(logger.LevelError, "database schema version %d is newer than the supported one: %d", version,
|
providerLog(logger.LevelError, "database schema version %d is newer than the supported one: %d", version,
|
||||||
|
@ -690,6 +731,8 @@ func (p *PGSQLProvider) revertDatabase(targetVersion int) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
switch dbVersion.Version {
|
switch dbVersion.Version {
|
||||||
|
case 24:
|
||||||
|
return downgradePgSQLDatabaseFromV24(p.dbHandle)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("database schema version not handled: %d", dbVersion.Version)
|
return fmt.Errorf("database schema version not handled: %d", dbVersion.Version)
|
||||||
}
|
}
|
||||||
|
@ -699,3 +742,31 @@ func (p *PGSQLProvider) resetDatabase() error {
|
||||||
sql := sqlReplaceAll(pgsqlResetSQL)
|
sql := sqlReplaceAll(pgsqlResetSQL)
|
||||||
return sqlCommonExecSQLAndUpdateDBVersion(p.dbHandle, []string{sql}, 0, false)
|
return sqlCommonExecSQLAndUpdateDBVersion(p.dbHandle, []string{sql}, 0, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updatePgSQLDatabaseFromV23(dbHandle *sql.DB) error {
|
||||||
|
return updatePgSQLDatabaseFrom23To24(dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func downgradePgSQLDatabaseFromV24(dbHandle *sql.DB) error {
|
||||||
|
return downgradePgSQLDatabaseFrom24To23(dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updatePgSQLDatabaseFrom23To24(dbHandle *sql.DB) error {
|
||||||
|
logger.InfoToConsole("updating database schema version: 23 -> 24")
|
||||||
|
providerLog(logger.LevelInfo, "updating database schema version: 23 -> 24")
|
||||||
|
sql := strings.ReplaceAll(pgsqlV24SQL, "{{roles}}", sqlTableRoles)
|
||||||
|
sql = strings.ReplaceAll(sql, "{{admins}}", sqlTableAdmins)
|
||||||
|
sql = strings.ReplaceAll(sql, "{{users}}", sqlTableUsers)
|
||||||
|
sql = strings.ReplaceAll(sql, "{{prefix}}", config.SQLTablesPrefix)
|
||||||
|
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 24, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func downgradePgSQLDatabaseFrom24To23(dbHandle *sql.DB) error {
|
||||||
|
logger.InfoToConsole("downgrading database schema version: 24 -> 23")
|
||||||
|
providerLog(logger.LevelInfo, "downgrading database schema version: 24 -> 23")
|
||||||
|
sql := strings.ReplaceAll(pgsqlV24DownSQL, "{{roles}}", sqlTableRoles)
|
||||||
|
sql = strings.ReplaceAll(sql, "{{admins}}", sqlTableAdmins)
|
||||||
|
sql = strings.ReplaceAll(sql, "{{users}}", sqlTableUsers)
|
||||||
|
sql = strings.ReplaceAll(sql, "{{prefix}}", config.SQLTablesPrefix)
|
||||||
|
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 23, false)
|
||||||
|
}
|
||||||
|
|
94
internal/dataprovider/role.go
Normal file
94
internal/dataprovider/role.go
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
// Copyright (C) 2019-2022 Nicola Murino
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published
|
||||||
|
// by the Free Software Foundation, version 3.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package dataprovider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/drakkan/sftpgo/v2/internal/logger"
|
||||||
|
"github.com/drakkan/sftpgo/v2/internal/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Role defines an SFTPGo role.
|
||||||
|
type Role struct {
|
||||||
|
// Data provider unique identifier
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
// Role name
|
||||||
|
Name string `json:"name"`
|
||||||
|
// optional description
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
// Creation time as unix timestamp in milliseconds
|
||||||
|
CreatedAt int64 `json:"created_at"`
|
||||||
|
// last update time as unix timestamp in milliseconds
|
||||||
|
UpdatedAt int64 `json:"updated_at"`
|
||||||
|
// list of admins associated with this role
|
||||||
|
Admins []string `json:"admins,omitempty"`
|
||||||
|
// list of usernames associated with this role
|
||||||
|
Users []string `json:"users,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderAsJSON implements the renderer interface used within plugins
|
||||||
|
func (r *Role) RenderAsJSON(reload bool) ([]byte, error) {
|
||||||
|
if reload {
|
||||||
|
role, err := provider.roleExists(r.Name)
|
||||||
|
if err != nil {
|
||||||
|
providerLog(logger.LevelError, "unable to reload role before rendering as json: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return json.Marshal(role)
|
||||||
|
}
|
||||||
|
return json.Marshal(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Role) validate() error {
|
||||||
|
if r.Name == "" {
|
||||||
|
return util.NewValidationError("name is mandatory")
|
||||||
|
}
|
||||||
|
if config.NamingRules&1 == 0 && !usernameRegex.MatchString(r.Name) {
|
||||||
|
return util.NewValidationError(fmt.Sprintf("name %q is not valid, the following characters are allowed: a-zA-Z0-9-_.~", r.Name))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Role) getACopy() Role {
|
||||||
|
users := make([]string, len(r.Users))
|
||||||
|
copy(users, r.Users)
|
||||||
|
admins := make([]string, len(r.Admins))
|
||||||
|
copy(admins, r.Admins)
|
||||||
|
|
||||||
|
return Role{
|
||||||
|
ID: r.ID,
|
||||||
|
Name: r.Name,
|
||||||
|
Description: r.Description,
|
||||||
|
CreatedAt: r.CreatedAt,
|
||||||
|
UpdatedAt: r.UpdatedAt,
|
||||||
|
Users: users,
|
||||||
|
Admins: admins,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMembersAsString returns a string representation for the role members
|
||||||
|
func (r *Role) GetMembersAsString() string {
|
||||||
|
var sb strings.Builder
|
||||||
|
if len(r.Users) > 0 {
|
||||||
|
sb.WriteString(fmt.Sprintf("Users: %d. ", len(r.Users)))
|
||||||
|
}
|
||||||
|
if len(r.Admins) > 0 {
|
||||||
|
sb.WriteString(fmt.Sprintf("Admins: %d. ", len(r.Admins)))
|
||||||
|
}
|
||||||
|
return sb.String()
|
||||||
|
}
|
|
@ -34,7 +34,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
sqlDatabaseVersion = 23
|
sqlDatabaseVersion = 24
|
||||||
defaultSQLQueryTimeout = 10 * time.Second
|
defaultSQLQueryTimeout = 10 * time.Second
|
||||||
longSQLQueryTimeout = 60 * time.Second
|
longSQLQueryTimeout = 60 * time.Second
|
||||||
)
|
)
|
||||||
|
@ -78,6 +78,7 @@ func sqlReplaceAll(sql string) string {
|
||||||
sql = strings.ReplaceAll(sql, "{{rules_actions_mapping}}", sqlTableRulesActionsMapping)
|
sql = strings.ReplaceAll(sql, "{{rules_actions_mapping}}", sqlTableRulesActionsMapping)
|
||||||
sql = strings.ReplaceAll(sql, "{{tasks}}", sqlTableTasks)
|
sql = strings.ReplaceAll(sql, "{{tasks}}", sqlTableTasks)
|
||||||
sql = strings.ReplaceAll(sql, "{{nodes}}", sqlTableNodes)
|
sql = strings.ReplaceAll(sql, "{{nodes}}", sqlTableNodes)
|
||||||
|
sql = strings.ReplaceAll(sql, "{{roles}}", sqlTableRoles)
|
||||||
sql = strings.ReplaceAll(sql, "{{prefix}}", config.SQLTablesPrefix)
|
sql = strings.ReplaceAll(sql, "{{prefix}}", config.SQLTablesPrefix)
|
||||||
return sql
|
return sql
|
||||||
}
|
}
|
||||||
|
@ -105,7 +106,7 @@ func sqlCommonAddShare(share *Share, dbHandle *sql.DB) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := provider.userExists(share.Username)
|
user, err := provider.userExists(share.Username, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.NewValidationError(fmt.Sprintf("unable to validate user %#v", share.Username))
|
return util.NewValidationError(fmt.Sprintf("unable to validate user %#v", share.Username))
|
||||||
}
|
}
|
||||||
|
@ -165,7 +166,7 @@ func sqlCommonUpdateShare(share *Share, dbHandle *sql.DB) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := provider.userExists(share.Username)
|
user, err := provider.userExists(share.Username, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.NewValidationError(fmt.Sprintf("unable to validate user %#v", share.Username))
|
return util.NewValidationError(fmt.Sprintf("unable to validate user %#v", share.Username))
|
||||||
}
|
}
|
||||||
|
@ -431,10 +432,10 @@ func sqlCommonAddAdmin(admin *Admin, dbHandle *sql.DB) error {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
return sqlCommonExecuteTx(ctx, dbHandle, func(tx *sql.Tx) error {
|
return sqlCommonExecuteTx(ctx, dbHandle, func(tx *sql.Tx) error {
|
||||||
q := getAddAdminQuery()
|
q := getAddAdminQuery(admin.Role)
|
||||||
_, err = tx.ExecContext(ctx, q, admin.Username, admin.Password, admin.Status, admin.Email, string(perms),
|
_, 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()),
|
string(filters), admin.AdditionalInfo, admin.Description, util.GetTimeAsMsSinceEpoch(time.Now()),
|
||||||
util.GetTimeAsMsSinceEpoch(time.Now()))
|
util.GetTimeAsMsSinceEpoch(time.Now()), admin.Role)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -462,9 +463,9 @@ func sqlCommonUpdateAdmin(admin *Admin, dbHandle *sql.DB) error {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
return sqlCommonExecuteTx(ctx, dbHandle, func(tx *sql.Tx) error {
|
return sqlCommonExecuteTx(ctx, dbHandle, func(tx *sql.Tx) error {
|
||||||
q := getUpdateAdminQuery()
|
q := getUpdateAdminQuery(admin.Role)
|
||||||
_, err = tx.ExecContext(ctx, q, admin.Password, admin.Status, admin.Email, string(perms), string(filters),
|
_, 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)
|
admin.AdditionalInfo, admin.Description, util.GetTimeAsMsSinceEpoch(time.Now()), admin.Role, admin.Username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -537,6 +538,122 @@ func sqlCommonDumpAdmins(dbHandle sqlQuerier) ([]Admin, error) {
|
||||||
return getAdminsWithGroups(ctx, admins, dbHandle)
|
return getAdminsWithGroups(ctx, admins, dbHandle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sqlCommonGetRoleByName(name string, dbHandle sqlQuerier) (Role, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
q := getRoleByNameQuery()
|
||||||
|
row := dbHandle.QueryRowContext(ctx, q, name)
|
||||||
|
role, err := getRoleFromDbRow(row)
|
||||||
|
if err != nil {
|
||||||
|
return role, err
|
||||||
|
}
|
||||||
|
role, err = getRoleWithUsers(ctx, role, dbHandle)
|
||||||
|
if err != nil {
|
||||||
|
return role, err
|
||||||
|
}
|
||||||
|
return getRoleWithAdmins(ctx, role, dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sqlCommonDumpRoles(dbHandle sqlQuerier) ([]Role, error) {
|
||||||
|
roles := make([]Role, 0, 10)
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), longSQLQueryTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
q := getDumpRolesQuery()
|
||||||
|
|
||||||
|
rows, err := dbHandle.QueryContext(ctx, q)
|
||||||
|
if err != nil {
|
||||||
|
return roles, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
role, err := getRoleFromDbRow(rows)
|
||||||
|
if err != nil {
|
||||||
|
return roles, err
|
||||||
|
}
|
||||||
|
roles = append(roles, role)
|
||||||
|
}
|
||||||
|
return roles, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func sqlCommonGetRoles(limit int, offset int, order string, minimal bool, dbHandle sqlQuerier) ([]Role, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
q := getRolesQuery(order, minimal)
|
||||||
|
|
||||||
|
roles := make([]Role, 0, limit)
|
||||||
|
rows, err := dbHandle.QueryContext(ctx, q, limit, offset)
|
||||||
|
if err != nil {
|
||||||
|
return roles, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var role Role
|
||||||
|
if minimal {
|
||||||
|
err = rows.Scan(&role.ID, &role.Name)
|
||||||
|
} else {
|
||||||
|
role, err = getRoleFromDbRow(rows)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return roles, err
|
||||||
|
}
|
||||||
|
roles = append(roles, role)
|
||||||
|
}
|
||||||
|
err = rows.Err()
|
||||||
|
if err != nil {
|
||||||
|
return roles, err
|
||||||
|
}
|
||||||
|
if minimal {
|
||||||
|
return roles, nil
|
||||||
|
}
|
||||||
|
roles, err = getRolesWithUsers(ctx, roles, dbHandle)
|
||||||
|
if err != nil {
|
||||||
|
return roles, err
|
||||||
|
}
|
||||||
|
return getRolesWithAdmins(ctx, roles, dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sqlCommonAddRole(role *Role, dbHandle *sql.DB) error {
|
||||||
|
if err := role.validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
q := getAddRoleQuery()
|
||||||
|
_, err := dbHandle.ExecContext(ctx, q, role.Name, role.Description, util.GetTimeAsMsSinceEpoch(time.Now()),
|
||||||
|
util.GetTimeAsMsSinceEpoch(time.Now()))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func sqlCommonUpdateRole(role *Role, dbHandle *sql.DB) error {
|
||||||
|
if err := role.validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
q := getUpdateRoleQuery()
|
||||||
|
_, err := dbHandle.ExecContext(ctx, q, role.Description, util.GetTimeAsMsSinceEpoch(time.Now()), role.Name)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func sqlCommonDeleteRole(role Role, dbHandle *sql.DB) error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
q := getDeleteRoleQuery()
|
||||||
|
res, err := dbHandle.ExecContext(ctx, q, role.Name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return sqlCommonRequireRowAffected(res)
|
||||||
|
}
|
||||||
|
|
||||||
func sqlCommonGetGroupByName(name string, dbHandle sqlQuerier) (Group, error) {
|
func sqlCommonGetGroupByName(name string, dbHandle sqlQuerier) (Group, error) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
@ -756,12 +873,16 @@ func sqlCommonDeleteGroup(group Group, dbHandle *sql.DB) error {
|
||||||
return sqlCommonRequireRowAffected(res)
|
return sqlCommonRequireRowAffected(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
func sqlCommonGetUserByUsername(username string, dbHandle sqlQuerier) (User, error) {
|
func sqlCommonGetUserByUsername(username, role string, dbHandle sqlQuerier) (User, error) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
q := getUserByUsernameQuery()
|
q := getUserByUsernameQuery(role)
|
||||||
row := dbHandle.QueryRowContext(ctx, q, username)
|
args := []any{username}
|
||||||
|
if role != "" {
|
||||||
|
args = append(args, role)
|
||||||
|
}
|
||||||
|
row := dbHandle.QueryRowContext(ctx, q, args...)
|
||||||
user, err := getUserFromDbRow(row)
|
user, err := getUserFromDbRow(row)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return user, err
|
return user, err
|
||||||
|
@ -774,7 +895,7 @@ func sqlCommonGetUserByUsername(username string, dbHandle sqlQuerier) (User, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func sqlCommonValidateUserAndPass(username, password, ip, protocol string, dbHandle *sql.DB) (User, error) {
|
func sqlCommonValidateUserAndPass(username, password, ip, protocol string, dbHandle *sql.DB) (User, error) {
|
||||||
user, err := sqlCommonGetUserByUsername(username, dbHandle)
|
user, err := sqlCommonGetUserByUsername(username, "", dbHandle)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
providerLog(logger.LevelWarn, "error authenticating user %#v: %v", username, err)
|
providerLog(logger.LevelWarn, "error authenticating user %#v: %v", username, err)
|
||||||
return user, err
|
return user, err
|
||||||
|
@ -787,7 +908,7 @@ func sqlCommonValidateUserAndTLSCertificate(username, protocol string, tlsCert *
|
||||||
if tlsCert == nil {
|
if tlsCert == nil {
|
||||||
return user, errors.New("TLS certificate cannot be null or empty")
|
return user, errors.New("TLS certificate cannot be null or empty")
|
||||||
}
|
}
|
||||||
user, err := sqlCommonGetUserByUsername(username, dbHandle)
|
user, err := sqlCommonGetUserByUsername(username, "", dbHandle)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
providerLog(logger.LevelWarn, "error authenticating user %#v: %v", username, err)
|
providerLog(logger.LevelWarn, "error authenticating user %#v: %v", username, err)
|
||||||
return user, err
|
return user, err
|
||||||
|
@ -800,7 +921,7 @@ func sqlCommonValidateUserAndPubKey(username string, pubKey []byte, isSSHCert bo
|
||||||
if len(pubKey) == 0 {
|
if len(pubKey) == 0 {
|
||||||
return user, "", errors.New("credentials cannot be null or empty")
|
return user, "", errors.New("credentials cannot be null or empty")
|
||||||
}
|
}
|
||||||
user, err := sqlCommonGetUserByUsername(username, dbHandle)
|
user, err := sqlCommonGetUserByUsername(username, "", dbHandle)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
providerLog(logger.LevelWarn, "error authenticating user %#v: %v", username, err)
|
providerLog(logger.LevelWarn, "error authenticating user %#v: %v", username, err)
|
||||||
return user, "", err
|
return user, "", err
|
||||||
|
@ -993,12 +1114,12 @@ func sqlCommonAddUser(user *User, dbHandle *sql.DB) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
q := getAddUserQuery()
|
q := getAddUserQuery(user.Role)
|
||||||
_, err := tx.ExecContext(ctx, q, user.Username, user.Password, string(publicKeys), user.HomeDir, user.UID, user.GID,
|
_, err := tx.ExecContext(ctx, q, user.Username, user.Password, string(publicKeys), user.HomeDir, user.UID, user.GID,
|
||||||
user.MaxSessions, user.QuotaSize, user.QuotaFiles, string(permissions), user.UploadBandwidth,
|
user.MaxSessions, user.QuotaSize, user.QuotaFiles, string(permissions), user.UploadBandwidth,
|
||||||
user.DownloadBandwidth, user.Status, user.ExpirationDate, string(filters), string(fsConfig), user.AdditionalInfo,
|
user.DownloadBandwidth, user.Status, user.ExpirationDate, string(filters), string(fsConfig), user.AdditionalInfo,
|
||||||
user.Description, user.Email, util.GetTimeAsMsSinceEpoch(time.Now()), util.GetTimeAsMsSinceEpoch(time.Now()),
|
user.Description, user.Email, util.GetTimeAsMsSinceEpoch(time.Now()), util.GetTimeAsMsSinceEpoch(time.Now()),
|
||||||
user.UploadDataTransfer, user.DownloadDataTransfer, user.TotalDataTransfer)
|
user.UploadDataTransfer, user.DownloadDataTransfer, user.TotalDataTransfer, user.Role)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -1044,12 +1165,12 @@ func sqlCommonUpdateUser(user *User, dbHandle *sql.DB) error {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
return sqlCommonExecuteTx(ctx, dbHandle, func(tx *sql.Tx) error {
|
return sqlCommonExecuteTx(ctx, dbHandle, func(tx *sql.Tx) error {
|
||||||
q := getUpdateUserQuery()
|
q := getUpdateUserQuery(user.Role)
|
||||||
_, err := tx.ExecContext(ctx, q, user.Password, string(publicKeys), user.HomeDir, user.UID, user.GID, user.MaxSessions,
|
_, err := tx.ExecContext(ctx, q, user.Password, string(publicKeys), user.HomeDir, user.UID, user.GID, user.MaxSessions,
|
||||||
user.QuotaSize, user.QuotaFiles, string(permissions), user.UploadBandwidth, user.DownloadBandwidth, user.Status,
|
user.QuotaSize, user.QuotaFiles, string(permissions), user.UploadBandwidth, user.DownloadBandwidth, user.Status,
|
||||||
user.ExpirationDate, string(filters), string(fsConfig), user.AdditionalInfo, user.Description, user.Email,
|
user.ExpirationDate, string(filters), string(fsConfig), user.AdditionalInfo, user.Description, user.Email,
|
||||||
util.GetTimeAsMsSinceEpoch(time.Now()), user.UploadDataTransfer, user.DownloadDataTransfer, user.TotalDataTransfer,
|
util.GetTimeAsMsSinceEpoch(time.Now()), user.UploadDataTransfer, user.DownloadDataTransfer, user.TotalDataTransfer,
|
||||||
user.ID)
|
user.Role, user.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -1264,19 +1385,17 @@ func sqlCommonGetUsersRangeForQuotaCheck(usernames []string, dbHandle sqlQuerier
|
||||||
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var user User
|
var user User
|
||||||
var filters sql.NullString
|
var filters []byte
|
||||||
err = rows.Scan(&user.ID, &user.Username, &user.QuotaSize, &user.UsedQuotaSize, &user.TotalDataTransfer,
|
err = rows.Scan(&user.ID, &user.Username, &user.QuotaSize, &user.UsedQuotaSize, &user.TotalDataTransfer,
|
||||||
&user.UploadDataTransfer, &user.DownloadDataTransfer, &user.UsedUploadDataTransfer,
|
&user.UploadDataTransfer, &user.DownloadDataTransfer, &user.UsedUploadDataTransfer,
|
||||||
&user.UsedDownloadDataTransfer, &filters)
|
&user.UsedDownloadDataTransfer, &filters)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return users, err
|
return users, err
|
||||||
}
|
}
|
||||||
if filters.Valid {
|
var userFilters UserFilters
|
||||||
var userFilters UserFilters
|
err = json.Unmarshal(filters, &userFilters)
|
||||||
err = json.Unmarshal([]byte(filters.String), &userFilters)
|
if err == nil {
|
||||||
if err == nil {
|
user.Filters = userFilters
|
||||||
user.Filters = userFilters
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
users = append(users, user)
|
users = append(users, user)
|
||||||
}
|
}
|
||||||
|
@ -1353,13 +1472,19 @@ func sqlCommonGetActiveTransfers(from time.Time, dbHandle sqlQuerier) ([]ActiveT
|
||||||
return transfers, rows.Err()
|
return transfers, rows.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
func sqlCommonGetUsers(limit int, offset int, order string, dbHandle sqlQuerier) ([]User, error) {
|
func sqlCommonGetUsers(limit int, offset int, order, role string, dbHandle sqlQuerier) ([]User, error) {
|
||||||
users := make([]User, 0, limit)
|
users := make([]User, 0, limit)
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
q := getUsersQuery(order)
|
q := getUsersQuery(order, role)
|
||||||
rows, err := dbHandle.QueryContext(ctx, q, limit, offset)
|
var args []any
|
||||||
|
if role == "" {
|
||||||
|
args = append(args, limit, offset)
|
||||||
|
} else {
|
||||||
|
args = append(args, role, limit, offset)
|
||||||
|
}
|
||||||
|
rows, err := dbHandle.QueryContext(ctx, q, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return users, err
|
return users, err
|
||||||
}
|
}
|
||||||
|
@ -1593,7 +1718,8 @@ func sqlCommonCleanupDefenderEvents(from int64, dbHandle *sql.DB) error {
|
||||||
|
|
||||||
func getShareFromDbRow(row sqlScanner) (Share, error) {
|
func getShareFromDbRow(row sqlScanner) (Share, error) {
|
||||||
var share Share
|
var share Share
|
||||||
var description, password, allowFrom, paths sql.NullString
|
var description, password sql.NullString
|
||||||
|
var allowFrom, paths []byte
|
||||||
|
|
||||||
err := row.Scan(&share.ShareID, &share.Name, &description, &share.Scope,
|
err := row.Scan(&share.ShareID, &share.Name, &description, &share.Scope,
|
||||||
&paths, &share.Username, &share.CreatedAt, &share.UpdatedAt,
|
&paths, &share.Username, &share.CreatedAt, &share.UpdatedAt,
|
||||||
|
@ -1605,28 +1731,22 @@ func getShareFromDbRow(row sqlScanner) (Share, error) {
|
||||||
}
|
}
|
||||||
return share, err
|
return share, err
|
||||||
}
|
}
|
||||||
if paths.Valid {
|
var list []string
|
||||||
var list []string
|
err = json.Unmarshal(paths, &list)
|
||||||
err = json.Unmarshal([]byte(paths.String), &list)
|
if err != nil {
|
||||||
if err != nil {
|
return share, err
|
||||||
return share, err
|
|
||||||
}
|
|
||||||
share.Paths = list
|
|
||||||
} else {
|
|
||||||
return share, errors.New("unable to decode shared paths")
|
|
||||||
}
|
}
|
||||||
|
share.Paths = list
|
||||||
if description.Valid {
|
if description.Valid {
|
||||||
share.Description = description.String
|
share.Description = description.String
|
||||||
}
|
}
|
||||||
if password.Valid {
|
if password.Valid {
|
||||||
share.Password = password.String
|
share.Password = password.String
|
||||||
}
|
}
|
||||||
if allowFrom.Valid {
|
list = nil
|
||||||
var list []string
|
err = json.Unmarshal(allowFrom, &list)
|
||||||
err = json.Unmarshal([]byte(allowFrom.String), &list)
|
if err == nil {
|
||||||
if err == nil {
|
share.AllowFrom = list
|
||||||
share.AllowFrom = list
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return share, nil
|
return share, nil
|
||||||
}
|
}
|
||||||
|
@ -1661,10 +1781,11 @@ func getAPIKeyFromDbRow(row sqlScanner) (APIKey, error) {
|
||||||
|
|
||||||
func getAdminFromDbRow(row sqlScanner) (Admin, error) {
|
func getAdminFromDbRow(row sqlScanner) (Admin, error) {
|
||||||
var admin Admin
|
var admin Admin
|
||||||
var email, filters, additionalInfo, permissions, description sql.NullString
|
var email, additionalInfo, description, role sql.NullString
|
||||||
|
var permissions, filters []byte
|
||||||
|
|
||||||
err := row.Scan(&admin.ID, &admin.Username, &admin.Password, &admin.Status, &email, &permissions,
|
err := row.Scan(&admin.ID, &admin.Username, &admin.Password, &admin.Status, &email, &permissions,
|
||||||
&filters, &additionalInfo, &description, &admin.CreatedAt, &admin.UpdatedAt, &admin.LastLogin)
|
&filters, &additionalInfo, &description, &admin.CreatedAt, &admin.UpdatedAt, &admin.LastLogin, &role)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
@ -1673,24 +1794,21 @@ func getAdminFromDbRow(row sqlScanner) (Admin, error) {
|
||||||
return admin, err
|
return admin, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if permissions.Valid {
|
var perms []string
|
||||||
var perms []string
|
err = json.Unmarshal(permissions, &perms)
|
||||||
err = json.Unmarshal([]byte(permissions.String), &perms)
|
if err != nil {
|
||||||
if err != nil {
|
return admin, err
|
||||||
return admin, err
|
|
||||||
}
|
|
||||||
admin.Permissions = perms
|
|
||||||
}
|
}
|
||||||
|
admin.Permissions = perms
|
||||||
|
|
||||||
if email.Valid {
|
if email.Valid {
|
||||||
admin.Email = email.String
|
admin.Email = email.String
|
||||||
}
|
}
|
||||||
if filters.Valid {
|
|
||||||
var adminFilters AdminFilters
|
var adminFilters AdminFilters
|
||||||
err = json.Unmarshal([]byte(filters.String), &adminFilters)
|
err = json.Unmarshal(filters, &adminFilters)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
admin.Filters = adminFilters
|
admin.Filters = adminFilters
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if additionalInfo.Valid {
|
if additionalInfo.Valid {
|
||||||
admin.AdditionalInfo = additionalInfo.String
|
admin.AdditionalInfo = additionalInfo.String
|
||||||
|
@ -1698,6 +1816,9 @@ func getAdminFromDbRow(row sqlScanner) (Admin, error) {
|
||||||
if description.Valid {
|
if description.Valid {
|
||||||
admin.Description = description.String
|
admin.Description = description.String
|
||||||
}
|
}
|
||||||
|
if role.Valid {
|
||||||
|
admin.Role = role.String
|
||||||
|
}
|
||||||
|
|
||||||
admin.SetEmptySecretsIfNil()
|
admin.SetEmptySecretsIfNil()
|
||||||
return admin, nil
|
return admin, nil
|
||||||
|
@ -1718,11 +1839,10 @@ func getEventActionFromDbRow(row sqlScanner) (BaseEventAction, error) {
|
||||||
if description.Valid {
|
if description.Valid {
|
||||||
action.Description = description.String
|
action.Description = description.String
|
||||||
}
|
}
|
||||||
if len(options) > 0 {
|
var actionOptions BaseEventActionOptions
|
||||||
err = json.Unmarshal(options, &action.Options)
|
err = json.Unmarshal(options, &actionOptions)
|
||||||
if err != nil {
|
if err == nil {
|
||||||
return action, err
|
action.Options = actionOptions
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return action, nil
|
return action, nil
|
||||||
}
|
}
|
||||||
|
@ -1740,21 +1860,40 @@ func getEventRuleFromDbRow(row sqlScanner) (EventRule, error) {
|
||||||
}
|
}
|
||||||
return rule, err
|
return rule, err
|
||||||
}
|
}
|
||||||
if len(conditions) > 0 {
|
var ruleConditions EventConditions
|
||||||
err = json.Unmarshal(conditions, &rule.Conditions)
|
err = json.Unmarshal(conditions, &ruleConditions)
|
||||||
if err != nil {
|
if err == nil {
|
||||||
return rule, err
|
rule.Conditions = ruleConditions
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if description.Valid {
|
if description.Valid {
|
||||||
rule.Description = description.String
|
rule.Description = description.String
|
||||||
}
|
}
|
||||||
return rule, nil
|
return rule, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getRoleFromDbRow(row sqlScanner) (Role, error) {
|
||||||
|
var role Role
|
||||||
|
var description sql.NullString
|
||||||
|
|
||||||
|
err := row.Scan(&role.ID, &role.Name, &description, &role.CreatedAt, &role.UpdatedAt)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return role, util.NewRecordNotFoundError(err.Error())
|
||||||
|
}
|
||||||
|
return role, err
|
||||||
|
}
|
||||||
|
if description.Valid {
|
||||||
|
role.Description = description.String
|
||||||
|
}
|
||||||
|
|
||||||
|
return role, nil
|
||||||
|
}
|
||||||
|
|
||||||
func getGroupFromDbRow(row sqlScanner) (Group, error) {
|
func getGroupFromDbRow(row sqlScanner) (Group, error) {
|
||||||
var group Group
|
var group Group
|
||||||
var userSettings, description sql.NullString
|
var description sql.NullString
|
||||||
|
var userSettings []byte
|
||||||
|
|
||||||
err := row.Scan(&group.ID, &group.Name, &description, &group.CreatedAt, &group.UpdatedAt, &userSettings)
|
err := row.Scan(&group.ID, &group.Name, &description, &group.CreatedAt, &group.UpdatedAt, &userSettings)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -1766,12 +1905,11 @@ func getGroupFromDbRow(row sqlScanner) (Group, error) {
|
||||||
if description.Valid {
|
if description.Valid {
|
||||||
group.Description = description.String
|
group.Description = description.String
|
||||||
}
|
}
|
||||||
if userSettings.Valid {
|
|
||||||
var settings GroupUserSettings
|
var settings GroupUserSettings
|
||||||
err = json.Unmarshal([]byte(userSettings.String), &settings)
|
err = json.Unmarshal(userSettings, &settings)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
group.UserSettings = settings
|
group.UserSettings = settings
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return group, nil
|
return group, nil
|
||||||
|
@ -1779,19 +1917,16 @@ func getGroupFromDbRow(row sqlScanner) (Group, error) {
|
||||||
|
|
||||||
func getUserFromDbRow(row sqlScanner) (User, error) {
|
func getUserFromDbRow(row sqlScanner) (User, error) {
|
||||||
var user User
|
var user User
|
||||||
var permissions sql.NullString
|
|
||||||
var password sql.NullString
|
var password sql.NullString
|
||||||
var publicKey sql.NullString
|
var permissions, publicKey, filters, fsConfig []byte
|
||||||
var filters sql.NullString
|
var additionalInfo, description, email, role sql.NullString
|
||||||
var fsConfig sql.NullString
|
|
||||||
var additionalInfo, description, email sql.NullString
|
|
||||||
|
|
||||||
err := row.Scan(&user.ID, &user.Username, &password, &publicKey, &user.HomeDir, &user.UID, &user.GID, &user.MaxSessions,
|
err := row.Scan(&user.ID, &user.Username, &password, &publicKey, &user.HomeDir, &user.UID, &user.GID, &user.MaxSessions,
|
||||||
&user.QuotaSize, &user.QuotaFiles, &permissions, &user.UsedQuotaSize, &user.UsedQuotaFiles, &user.LastQuotaUpdate,
|
&user.QuotaSize, &user.QuotaFiles, &permissions, &user.UsedQuotaSize, &user.UsedQuotaFiles, &user.LastQuotaUpdate,
|
||||||
&user.UploadBandwidth, &user.DownloadBandwidth, &user.ExpirationDate, &user.LastLogin, &user.Status, &filters, &fsConfig,
|
&user.UploadBandwidth, &user.DownloadBandwidth, &user.ExpirationDate, &user.LastLogin, &user.Status, &filters, &fsConfig,
|
||||||
&additionalInfo, &description, &email, &user.CreatedAt, &user.UpdatedAt, &user.UploadDataTransfer, &user.DownloadDataTransfer,
|
&additionalInfo, &description, &email, &user.CreatedAt, &user.UpdatedAt, &user.UploadDataTransfer, &user.DownloadDataTransfer,
|
||||||
&user.TotalDataTransfer, &user.UsedUploadDataTransfer, &user.UsedDownloadDataTransfer, &user.DeletedAt, &user.FirstDownload,
|
&user.TotalDataTransfer, &user.UsedUploadDataTransfer, &user.UsedDownloadDataTransfer, &user.DeletedAt, &user.FirstDownload,
|
||||||
&user.FirstUpload)
|
&user.FirstUpload, &role)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
return user, util.NewRecordNotFoundError(err.Error())
|
return user, util.NewRecordNotFoundError(err.Error())
|
||||||
|
@ -1801,38 +1936,30 @@ func getUserFromDbRow(row sqlScanner) (User, error) {
|
||||||
if password.Valid {
|
if password.Valid {
|
||||||
user.Password = password.String
|
user.Password = password.String
|
||||||
}
|
}
|
||||||
|
perms := make(map[string][]string)
|
||||||
|
err = json.Unmarshal(permissions, &perms)
|
||||||
|
if err != nil {
|
||||||
|
providerLog(logger.LevelError, "unable to deserialize permissions for user %#v: %v", user.Username, err)
|
||||||
|
return user, fmt.Errorf("unable to deserialize permissions for user %#v: %v", user.Username, err)
|
||||||
|
}
|
||||||
|
user.Permissions = perms
|
||||||
// we can have a empty string or an invalid json in null string
|
// we can have a empty string or an invalid json in null string
|
||||||
// so we do a relaxed test if the field is optional, for example we
|
// so we do a relaxed test if the field is optional, for example we
|
||||||
// populate public keys only if unmarshal does not return an error
|
// populate public keys only if unmarshal does not return an error
|
||||||
if publicKey.Valid {
|
var pKeys []string
|
||||||
var list []string
|
err = json.Unmarshal(publicKey, &pKeys)
|
||||||
err = json.Unmarshal([]byte(publicKey.String), &list)
|
if err == nil {
|
||||||
if err == nil {
|
user.PublicKeys = pKeys
|
||||||
user.PublicKeys = list
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if permissions.Valid {
|
var userFilters UserFilters
|
||||||
perms := make(map[string][]string)
|
err = json.Unmarshal(filters, &userFilters)
|
||||||
err = json.Unmarshal([]byte(permissions.String), &perms)
|
if err == nil {
|
||||||
if err != nil {
|
user.Filters = userFilters
|
||||||
providerLog(logger.LevelError, "unable to deserialize permissions for user %#v: %v", user.Username, err)
|
|
||||||
return user, fmt.Errorf("unable to deserialize permissions for user %#v: %v", user.Username, err)
|
|
||||||
}
|
|
||||||
user.Permissions = perms
|
|
||||||
}
|
}
|
||||||
if filters.Valid {
|
var fs vfs.Filesystem
|
||||||
var userFilters UserFilters
|
err = json.Unmarshal(fsConfig, &fs)
|
||||||
err = json.Unmarshal([]byte(filters.String), &userFilters)
|
if err == nil {
|
||||||
if err == nil {
|
user.FsConfig = fs
|
||||||
user.Filters = userFilters
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if fsConfig.Valid {
|
|
||||||
var fs vfs.Filesystem
|
|
||||||
err = json.Unmarshal([]byte(fsConfig.String), &fs)
|
|
||||||
if err == nil {
|
|
||||||
user.FsConfig = fs
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if additionalInfo.Valid {
|
if additionalInfo.Valid {
|
||||||
user.AdditionalInfo = additionalInfo.String
|
user.AdditionalInfo = additionalInfo.String
|
||||||
|
@ -1843,6 +1970,9 @@ func getUserFromDbRow(row sqlScanner) (User, error) {
|
||||||
if email.Valid {
|
if email.Valid {
|
||||||
user.Email = email.String
|
user.Email = email.String
|
||||||
}
|
}
|
||||||
|
if role.Valid {
|
||||||
|
user.Role = role.String
|
||||||
|
}
|
||||||
user.SetEmptySecretsIfNil()
|
user.SetEmptySecretsIfNil()
|
||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
@ -1851,7 +1981,8 @@ func sqlCommonGetFolder(ctx context.Context, name string, dbHandle sqlQuerier) (
|
||||||
var folder vfs.BaseVirtualFolder
|
var folder vfs.BaseVirtualFolder
|
||||||
q := getFolderByNameQuery()
|
q := getFolderByNameQuery()
|
||||||
row := dbHandle.QueryRowContext(ctx, q, name)
|
row := dbHandle.QueryRowContext(ctx, q, name)
|
||||||
var mappedPath, description, fsConfig sql.NullString
|
var mappedPath, description sql.NullString
|
||||||
|
var fsConfig []byte
|
||||||
err := row.Scan(&folder.ID, &mappedPath, &folder.UsedQuotaSize, &folder.UsedQuotaFiles, &folder.LastQuotaUpdate,
|
err := row.Scan(&folder.ID, &mappedPath, &folder.UsedQuotaSize, &folder.UsedQuotaFiles, &folder.LastQuotaUpdate,
|
||||||
&folder.Name, &description, &fsConfig)
|
&folder.Name, &description, &fsConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -1866,12 +1997,10 @@ func sqlCommonGetFolder(ctx context.Context, name string, dbHandle sqlQuerier) (
|
||||||
if description.Valid {
|
if description.Valid {
|
||||||
folder.Description = description.String
|
folder.Description = description.String
|
||||||
}
|
}
|
||||||
if fsConfig.Valid {
|
var fs vfs.Filesystem
|
||||||
var fs vfs.Filesystem
|
err = json.Unmarshal(fsConfig, &fs)
|
||||||
err = json.Unmarshal([]byte(fsConfig.String), &fs)
|
if err == nil {
|
||||||
if err == nil {
|
folder.FsConfig = fs
|
||||||
folder.FsConfig = fs
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return folder, err
|
return folder, err
|
||||||
}
|
}
|
||||||
|
@ -1971,7 +2100,8 @@ func sqlCommonDumpFolders(dbHandle sqlQuerier) ([]vfs.BaseVirtualFolder, error)
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var folder vfs.BaseVirtualFolder
|
var folder vfs.BaseVirtualFolder
|
||||||
var mappedPath, description, fsConfig sql.NullString
|
var mappedPath, description sql.NullString
|
||||||
|
var fsConfig []byte
|
||||||
err = rows.Scan(&folder.ID, &mappedPath, &folder.UsedQuotaSize, &folder.UsedQuotaFiles,
|
err = rows.Scan(&folder.ID, &mappedPath, &folder.UsedQuotaSize, &folder.UsedQuotaFiles,
|
||||||
&folder.LastQuotaUpdate, &folder.Name, &description, &fsConfig)
|
&folder.LastQuotaUpdate, &folder.Name, &description, &fsConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -1983,12 +2113,10 @@ func sqlCommonDumpFolders(dbHandle sqlQuerier) ([]vfs.BaseVirtualFolder, error)
|
||||||
if description.Valid {
|
if description.Valid {
|
||||||
folder.Description = description.String
|
folder.Description = description.String
|
||||||
}
|
}
|
||||||
if fsConfig.Valid {
|
var fs vfs.Filesystem
|
||||||
var fs vfs.Filesystem
|
err = json.Unmarshal(fsConfig, &fs)
|
||||||
err = json.Unmarshal([]byte(fsConfig.String), &fs)
|
if err == nil {
|
||||||
if err == nil {
|
folder.FsConfig = fs
|
||||||
folder.FsConfig = fs
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
folders = append(folders, folder)
|
folders = append(folders, folder)
|
||||||
}
|
}
|
||||||
|
@ -2014,7 +2142,8 @@ func sqlCommonGetFolders(limit, offset int, order string, minimal bool, dbHandle
|
||||||
return folders, err
|
return folders, err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var mappedPath, description, fsConfig sql.NullString
|
var mappedPath, description sql.NullString
|
||||||
|
var fsConfig []byte
|
||||||
err = rows.Scan(&folder.ID, &mappedPath, &folder.UsedQuotaSize, &folder.UsedQuotaFiles,
|
err = rows.Scan(&folder.ID, &mappedPath, &folder.UsedQuotaSize, &folder.UsedQuotaFiles,
|
||||||
&folder.LastQuotaUpdate, &folder.Name, &description, &fsConfig)
|
&folder.LastQuotaUpdate, &folder.Name, &description, &fsConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -2026,12 +2155,10 @@ func sqlCommonGetFolders(limit, offset int, order string, minimal bool, dbHandle
|
||||||
if description.Valid {
|
if description.Valid {
|
||||||
folder.Description = description.String
|
folder.Description = description.String
|
||||||
}
|
}
|
||||||
if fsConfig.Valid {
|
var fs vfs.Filesystem
|
||||||
var fs vfs.Filesystem
|
err = json.Unmarshal(fsConfig, &fs)
|
||||||
err = json.Unmarshal([]byte(fsConfig.String), &fs)
|
if err == nil {
|
||||||
if err == nil {
|
folder.FsConfig = fs
|
||||||
folder.FsConfig = fs
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
folder.PrepareForRendering()
|
folder.PrepareForRendering()
|
||||||
|
@ -2297,7 +2424,8 @@ func getUsersWithVirtualFolders(ctx context.Context, users []User, dbHandle sqlQ
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var folder vfs.VirtualFolder
|
var folder vfs.VirtualFolder
|
||||||
var userID int64
|
var userID int64
|
||||||
var mappedPath, fsConfig, description sql.NullString
|
var mappedPath, description sql.NullString
|
||||||
|
var fsConfig []byte
|
||||||
err = rows.Scan(&folder.ID, &folder.Name, &mappedPath, &folder.UsedQuotaSize, &folder.UsedQuotaFiles,
|
err = rows.Scan(&folder.ID, &folder.Name, &mappedPath, &folder.UsedQuotaSize, &folder.UsedQuotaFiles,
|
||||||
&folder.LastQuotaUpdate, &folder.VirtualPath, &folder.QuotaSize, &folder.QuotaFiles, &userID, &fsConfig,
|
&folder.LastQuotaUpdate, &folder.VirtualPath, &folder.QuotaSize, &folder.QuotaFiles, &userID, &fsConfig,
|
||||||
&description)
|
&description)
|
||||||
|
@ -2310,12 +2438,10 @@ func getUsersWithVirtualFolders(ctx context.Context, users []User, dbHandle sqlQ
|
||||||
if description.Valid {
|
if description.Valid {
|
||||||
folder.Description = description.String
|
folder.Description = description.String
|
||||||
}
|
}
|
||||||
if fsConfig.Valid {
|
var fs vfs.Filesystem
|
||||||
var fs vfs.Filesystem
|
err = json.Unmarshal(fsConfig, &fs)
|
||||||
err = json.Unmarshal([]byte(fsConfig.String), &fs)
|
if err == nil {
|
||||||
if err == nil {
|
folder.FsConfig = fs
|
||||||
folder.FsConfig = fs
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
usersVirtualFolders[userID] = append(usersVirtualFolders[userID], folder)
|
usersVirtualFolders[userID] = append(usersVirtualFolders[userID], folder)
|
||||||
}
|
}
|
||||||
|
@ -2390,6 +2516,28 @@ func getGroupWithUsers(ctx context.Context, group Group, dbHandle sqlQuerier) (G
|
||||||
return groups[0], err
|
return groups[0], err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getRoleWithUsers(ctx context.Context, role Role, dbHandle sqlQuerier) (Role, error) {
|
||||||
|
roles, err := getRolesWithUsers(ctx, []Role{role}, dbHandle)
|
||||||
|
if err != nil {
|
||||||
|
return role, err
|
||||||
|
}
|
||||||
|
if len(roles) == 0 {
|
||||||
|
return role, errors.New("unable to associate users with role")
|
||||||
|
}
|
||||||
|
return roles[0], err
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRoleWithAdmins(ctx context.Context, role Role, dbHandle sqlQuerier) (Role, error) {
|
||||||
|
roles, err := getRolesWithAdmins(ctx, []Role{role}, dbHandle)
|
||||||
|
if err != nil {
|
||||||
|
return role, err
|
||||||
|
}
|
||||||
|
if len(roles) == 0 {
|
||||||
|
return role, errors.New("unable to associate admins with role")
|
||||||
|
}
|
||||||
|
return roles[0], err
|
||||||
|
}
|
||||||
|
|
||||||
func getGroupWithAdmins(ctx context.Context, group Group, dbHandle sqlQuerier) (Group, error) {
|
func getGroupWithAdmins(ctx context.Context, group Group, dbHandle sqlQuerier) (Group, error) {
|
||||||
groups, err := getGroupsWithAdmins(ctx, []Group{group}, dbHandle)
|
groups, err := getGroupsWithAdmins(ctx, []Group{group}, dbHandle)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -2427,7 +2575,8 @@ func getGroupsWithVirtualFolders(ctx context.Context, groups []Group, dbHandle s
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var groupID int64
|
var groupID int64
|
||||||
var folder vfs.VirtualFolder
|
var folder vfs.VirtualFolder
|
||||||
var mappedPath, fsConfig, description sql.NullString
|
var mappedPath, description sql.NullString
|
||||||
|
var fsConfig []byte
|
||||||
err = rows.Scan(&folder.ID, &folder.Name, &mappedPath, &folder.UsedQuotaSize, &folder.UsedQuotaFiles,
|
err = rows.Scan(&folder.ID, &folder.Name, &mappedPath, &folder.UsedQuotaSize, &folder.UsedQuotaFiles,
|
||||||
&folder.LastQuotaUpdate, &folder.VirtualPath, &folder.QuotaSize, &folder.QuotaFiles, &groupID, &fsConfig,
|
&folder.LastQuotaUpdate, &folder.VirtualPath, &folder.QuotaSize, &folder.QuotaFiles, &groupID, &fsConfig,
|
||||||
&description)
|
&description)
|
||||||
|
@ -2440,12 +2589,10 @@ func getGroupsWithVirtualFolders(ctx context.Context, groups []Group, dbHandle s
|
||||||
if description.Valid {
|
if description.Valid {
|
||||||
folder.Description = description.String
|
folder.Description = description.String
|
||||||
}
|
}
|
||||||
if fsConfig.Valid {
|
var fs vfs.Filesystem
|
||||||
var fs vfs.Filesystem
|
err = json.Unmarshal(fsConfig, &fs)
|
||||||
err = json.Unmarshal([]byte(fsConfig.String), &fs)
|
if err == nil {
|
||||||
if err == nil {
|
folder.FsConfig = fs
|
||||||
folder.FsConfig = fs
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
groupsVirtualFolders[groupID] = append(groupsVirtualFolders[groupID], folder)
|
groupsVirtualFolders[groupID] = append(groupsVirtualFolders[groupID], folder)
|
||||||
}
|
}
|
||||||
|
@ -2498,6 +2645,71 @@ func getGroupsWithUsers(ctx context.Context, groups []Group, dbHandle sqlQuerier
|
||||||
return groups, err
|
return groups, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getRolesWithUsers(ctx context.Context, roles []Role, dbHandle sqlQuerier) ([]Role, error) {
|
||||||
|
if len(roles) == 0 {
|
||||||
|
return roles, nil
|
||||||
|
}
|
||||||
|
rows, err := dbHandle.QueryContext(ctx, getUsersWithRolesQuery(roles))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
rolesUsers := make(map[int64][]string)
|
||||||
|
for rows.Next() {
|
||||||
|
var roleID int64
|
||||||
|
var username string
|
||||||
|
err = rows.Scan(&roleID, &username)
|
||||||
|
if err != nil {
|
||||||
|
return roles, err
|
||||||
|
}
|
||||||
|
rolesUsers[roleID] = append(rolesUsers[roleID], username)
|
||||||
|
}
|
||||||
|
err = rows.Err()
|
||||||
|
if err != nil {
|
||||||
|
return roles, err
|
||||||
|
}
|
||||||
|
if len(rolesUsers) > 0 {
|
||||||
|
for idx := range roles {
|
||||||
|
ref := &roles[idx]
|
||||||
|
ref.Users = rolesUsers[ref.ID]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return roles, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRolesWithAdmins(ctx context.Context, roles []Role, dbHandle sqlQuerier) ([]Role, error) {
|
||||||
|
if len(roles) == 0 {
|
||||||
|
return roles, nil
|
||||||
|
}
|
||||||
|
rows, err := dbHandle.QueryContext(ctx, getAdminsWithRolesQuery(roles))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
rolesAdmins := make(map[int64][]string)
|
||||||
|
for rows.Next() {
|
||||||
|
var roleID int64
|
||||||
|
var username string
|
||||||
|
err = rows.Scan(&roleID, &username)
|
||||||
|
if err != nil {
|
||||||
|
return roles, err
|
||||||
|
}
|
||||||
|
rolesAdmins[roleID] = append(rolesAdmins[roleID], username)
|
||||||
|
}
|
||||||
|
if err = rows.Err(); err != nil {
|
||||||
|
return roles, err
|
||||||
|
}
|
||||||
|
if len(rolesAdmins) > 0 {
|
||||||
|
for idx := range roles {
|
||||||
|
ref := &roles[idx]
|
||||||
|
ref.Admins = rolesAdmins[ref.ID]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return roles, nil
|
||||||
|
}
|
||||||
|
|
||||||
func getGroupsWithAdmins(ctx context.Context, groups []Group, dbHandle sqlQuerier) ([]Group, error) {
|
func getGroupsWithAdmins(ctx context.Context, groups []Group, dbHandle sqlQuerier) ([]Group, error) {
|
||||||
if len(groups) == 0 {
|
if len(groups) == 0 {
|
||||||
return groups, nil
|
return groups, nil
|
||||||
|
@ -2701,7 +2913,7 @@ func getRelatedValuesForAPIKeys(ctx context.Context, apiKeys []APIKey, dbHandle
|
||||||
func sqlCommonGetAPIKeyRelatedIDs(apiKey *APIKey) (sql.NullInt64, sql.NullInt64, error) {
|
func sqlCommonGetAPIKeyRelatedIDs(apiKey *APIKey) (sql.NullInt64, sql.NullInt64, error) {
|
||||||
var userID, adminID sql.NullInt64
|
var userID, adminID sql.NullInt64
|
||||||
if apiKey.User != "" {
|
if apiKey.User != "" {
|
||||||
u, err := provider.userExists(apiKey.User)
|
u, err := provider.userExists(apiKey.User, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return userID, adminID, util.NewValidationError(fmt.Sprintf("unable to validate user %v", apiKey.User))
|
return userID, adminID, util.NewValidationError(fmt.Sprintf("unable to validate user %v", apiKey.User))
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
// we import go-sqlite3 here to be able to disable SQLite support using a build tag
|
// we import go-sqlite3 here to be able to disable SQLite support using a build tag
|
||||||
|
@ -55,6 +56,7 @@ DROP TABLE IF EXISTS "{{rules_actions_mapping}}";
|
||||||
DROP TABLE IF EXISTS "{{events_rules}}";
|
DROP TABLE IF EXISTS "{{events_rules}}";
|
||||||
DROP TABLE IF EXISTS "{{events_actions}}";
|
DROP TABLE IF EXISTS "{{events_actions}}";
|
||||||
DROP TABLE IF EXISTS "{{tasks}}";
|
DROP TABLE IF EXISTS "{{tasks}}";
|
||||||
|
DROP TABLE IF EXISTS "{{roles}}";
|
||||||
DROP TABLE IF EXISTS "{{schema_version}}";
|
DROP TABLE IF EXISTS "{{schema_version}}";
|
||||||
`
|
`
|
||||||
sqliteInitialSQL = `CREATE TABLE "{{schema_version}}" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "version" integer NOT NULL);
|
sqliteInitialSQL = `CREATE TABLE "{{schema_version}}" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "version" integer NOT NULL);
|
||||||
|
@ -159,6 +161,19 @@ CREATE INDEX "{{prefix}}rules_actions_mapping_order_idx" ON "{{rules_actions_map
|
||||||
CREATE INDEX "{{prefix}}admins_groups_mapping_admin_id_idx" ON "{{admins_groups_mapping}}" ("admin_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");
|
CREATE INDEX "{{prefix}}admins_groups_mapping_group_id_idx" ON "{{admins_groups_mapping}}" ("group_id");
|
||||||
INSERT INTO {{schema_version}} (version) VALUES (23);
|
INSERT INTO {{schema_version}} (version) VALUES (23);
|
||||||
|
`
|
||||||
|
sqliteV24SQL = `CREATE TABLE "{{roles}}" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(255) NOT NULL UNIQUE,
|
||||||
|
"description" varchar(512) NULL, "created_at" bigint NOT NULL, "updated_at" bigint NOT NULL);
|
||||||
|
ALTER TABLE "{{users}}" ADD COLUMN "role_id" integer NULL REFERENCES "{{roles}}" ("id") ON DELETE SET NULL;
|
||||||
|
ALTER TABLE "{{admins}}" ADD COLUMN "role_id" integer NULL REFERENCES "{{roles}}" ("id") ON DELETE NO ACTION;
|
||||||
|
CREATE INDEX "{{prefix}}users_role_id_idx" ON "{{users}}" ("role_id");
|
||||||
|
CREATE INDEX "{{prefix}}admins_role_id_idx" ON "{{admins}}" ("role_id");
|
||||||
|
`
|
||||||
|
sqliteV24DownSQL = `DROP INDEX "{{prefix}}users_role_id_idx";
|
||||||
|
DROP INDEX "{{prefix}}admins_role_id_idx";
|
||||||
|
ALTER TABLE "{{users}}" DROP COLUMN role_id;
|
||||||
|
ALTER TABLE "{{admins}}" DROP COLUMN role_id;
|
||||||
|
DROP TABLE "{{roles}}";
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -239,8 +254,8 @@ func (p *SQLiteProvider) updateAdminLastLogin(username string) error {
|
||||||
return sqlCommonUpdateAdminLastLogin(username, p.dbHandle)
|
return sqlCommonUpdateAdminLastLogin(username, p.dbHandle)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *SQLiteProvider) userExists(username string) (User, error) {
|
func (p *SQLiteProvider) userExists(username, role string) (User, error) {
|
||||||
return sqlCommonGetUserByUsername(username, p.dbHandle)
|
return sqlCommonGetUserByUsername(username, role, p.dbHandle)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *SQLiteProvider) addUser(user *User) error {
|
func (p *SQLiteProvider) addUser(user *User) error {
|
||||||
|
@ -267,8 +282,8 @@ func (p *SQLiteProvider) getRecentlyUpdatedUsers(after int64) ([]User, error) {
|
||||||
return sqlCommonGetRecentlyUpdatedUsers(after, p.dbHandle)
|
return sqlCommonGetRecentlyUpdatedUsers(after, p.dbHandle)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *SQLiteProvider) getUsers(limit int, offset int, order string) ([]User, error) {
|
func (p *SQLiteProvider) getUsers(limit int, offset int, order, role string) ([]User, error) {
|
||||||
return sqlCommonGetUsers(limit, offset, order, p.dbHandle)
|
return sqlCommonGetUsers(limit, offset, order, role, p.dbHandle)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *SQLiteProvider) getUsersForQuotaCheck(toFetch map[string]bool) ([]User, error) {
|
func (p *SQLiteProvider) getUsersForQuotaCheck(toFetch map[string]bool) ([]User, error) {
|
||||||
|
@ -581,6 +596,30 @@ func (*SQLiteProvider) cleanupNodes() error {
|
||||||
return ErrNotImplemented
|
return ErrNotImplemented
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *SQLiteProvider) roleExists(name string) (Role, error) {
|
||||||
|
return sqlCommonGetRoleByName(name, p.dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SQLiteProvider) addRole(role *Role) error {
|
||||||
|
return sqlCommonAddRole(role, p.dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SQLiteProvider) updateRole(role *Role) error {
|
||||||
|
return sqlCommonUpdateRole(role, p.dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SQLiteProvider) deleteRole(role Role) error {
|
||||||
|
return sqlCommonDeleteRole(role, p.dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SQLiteProvider) getRoles(limit int, offset int, order string, minimal bool) ([]Role, error) {
|
||||||
|
return sqlCommonGetRoles(limit, offset, order, minimal, p.dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SQLiteProvider) dumpRoles() ([]Role, error) {
|
||||||
|
return sqlCommonDumpRoles(p.dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
func (p *SQLiteProvider) setFirstDownloadTimestamp(username string) error {
|
func (p *SQLiteProvider) setFirstDownloadTimestamp(username string) error {
|
||||||
return sqlCommonSetFirstDownloadTimestamp(username, p.dbHandle)
|
return sqlCommonSetFirstDownloadTimestamp(username, p.dbHandle)
|
||||||
}
|
}
|
||||||
|
@ -628,6 +667,8 @@ func (p *SQLiteProvider) migrateDatabase() error { //nolint:dupl
|
||||||
providerLog(logger.LevelError, "%v", err)
|
providerLog(logger.LevelError, "%v", err)
|
||||||
logger.ErrorToConsole("%v", err)
|
logger.ErrorToConsole("%v", err)
|
||||||
return err
|
return err
|
||||||
|
case version == 23:
|
||||||
|
return updateSQLiteDatabaseFromV23(p.dbHandle)
|
||||||
default:
|
default:
|
||||||
if version > sqlDatabaseVersion {
|
if version > sqlDatabaseVersion {
|
||||||
providerLog(logger.LevelError, "database schema version %d is newer than the supported one: %d", version,
|
providerLog(logger.LevelError, "database schema version %d is newer than the supported one: %d", version,
|
||||||
|
@ -650,6 +691,8 @@ func (p *SQLiteProvider) revertDatabase(targetVersion int) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
switch dbVersion.Version {
|
switch dbVersion.Version {
|
||||||
|
case 24:
|
||||||
|
return downgradeSQLiteDatabaseFromV24(p.dbHandle)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("database schema version not handled: %d", dbVersion.Version)
|
return fmt.Errorf("database schema version not handled: %d", dbVersion.Version)
|
||||||
}
|
}
|
||||||
|
@ -660,6 +703,34 @@ func (p *SQLiteProvider) resetDatabase() error {
|
||||||
return sqlCommonExecSQLAndUpdateDBVersion(p.dbHandle, []string{sql}, 0, false)
|
return sqlCommonExecSQLAndUpdateDBVersion(p.dbHandle, []string{sql}, 0, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateSQLiteDatabaseFromV23(dbHandle *sql.DB) error {
|
||||||
|
return updateSQLiteDatabaseFrom23To24(dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func downgradeSQLiteDatabaseFromV24(dbHandle *sql.DB) error {
|
||||||
|
return downgradeSQLiteDatabaseFrom24To23(dbHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateSQLiteDatabaseFrom23To24(dbHandle *sql.DB) error {
|
||||||
|
logger.InfoToConsole("updating database schema version: 23 -> 24")
|
||||||
|
providerLog(logger.LevelInfo, "updating database schema version: 23 -> 24")
|
||||||
|
sql := strings.ReplaceAll(sqliteV24SQL, "{{roles}}", sqlTableRoles)
|
||||||
|
sql = strings.ReplaceAll(sql, "{{admins}}", sqlTableAdmins)
|
||||||
|
sql = strings.ReplaceAll(sql, "{{users}}", sqlTableUsers)
|
||||||
|
sql = strings.ReplaceAll(sql, "{{prefix}}", config.SQLTablesPrefix)
|
||||||
|
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 24, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func downgradeSQLiteDatabaseFrom24To23(dbHandle *sql.DB) error {
|
||||||
|
logger.InfoToConsole("downgrading database schema version: 24 -> 23")
|
||||||
|
providerLog(logger.LevelInfo, "downgrading database schema version: 24 -> 23")
|
||||||
|
sql := strings.ReplaceAll(sqliteV24DownSQL, "{{roles}}", sqlTableRoles)
|
||||||
|
sql = strings.ReplaceAll(sql, "{{admins}}", sqlTableAdmins)
|
||||||
|
sql = strings.ReplaceAll(sql, "{{users}}", sqlTableUsers)
|
||||||
|
sql = strings.ReplaceAll(sql, "{{prefix}}", config.SQLTablesPrefix)
|
||||||
|
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 23, 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()
|
||||||
|
|
|
@ -23,17 +23,19 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
selectUserFields = "id,username,password,public_keys,home_dir,uid,gid,max_sessions,quota_size,quota_files,permissions,used_quota_size," +
|
selectUserFields = "u.id,u.username,u.password,u.public_keys,u.home_dir,u.uid,u.gid,u.max_sessions,u.quota_size,u.quota_files," +
|
||||||
"used_quota_files,last_quota_update,upload_bandwidth,download_bandwidth,expiration_date,last_login,status,filters,filesystem," +
|
"u.permissions,u.used_quota_size,u.used_quota_files,u.last_quota_update,u.upload_bandwidth,u.download_bandwidth," +
|
||||||
"additional_info,description,email,created_at,updated_at,upload_data_transfer,download_data_transfer,total_data_transfer," +
|
"u.expiration_date,u.last_login,u.status,u.filters,u.filesystem,u.additional_info,u.description,u.email,u.created_at," +
|
||||||
"used_upload_data_transfer,used_download_data_transfer,deleted_at,first_download,first_upload"
|
"u.updated_at,u.upload_data_transfer,u.download_data_transfer,u.total_data_transfer," +
|
||||||
|
"u.used_upload_data_transfer,u.used_download_data_transfer,u.deleted_at,u.first_download,u.first_upload,r.name"
|
||||||
selectFolderFields = "id,path,used_quota_size,used_quota_files,last_quota_update,name,description,filesystem"
|
selectFolderFields = "id,path,used_quota_size,used_quota_files,last_quota_update,name,description,filesystem"
|
||||||
selectAdminFields = "id,username,password,status,email,permissions,filters,additional_info,description,created_at,updated_at,last_login"
|
selectAdminFields = "a.id,a.username,a.password,a.status,a.email,a.permissions,a.filters,a.additional_info,a.description,a.created_at,a.updated_at,a.last_login,r.name"
|
||||||
selectAPIKeyFields = "key_id,name,api_key,scope,created_at,updated_at,last_use_at,expires_at,description,user_id,admin_id"
|
selectAPIKeyFields = "key_id,name,api_key,scope,created_at,updated_at,last_use_at,expires_at,description,user_id,admin_id"
|
||||||
selectShareFields = "s.share_id,s.name,s.description,s.scope,s.paths,u.username,s.created_at,s.updated_at,s.last_use_at," +
|
selectShareFields = "s.share_id,s.name,s.description,s.scope,s.paths,u.username,s.created_at,s.updated_at,s.last_use_at," +
|
||||||
"s.expires_at,s.password,s.max_tokens,s.used_tokens,s.allow_from"
|
"s.expires_at,s.password,s.max_tokens,s.used_tokens,s.allow_from"
|
||||||
selectGroupFields = "id,name,description,created_at,updated_at,user_settings"
|
selectGroupFields = "id,name,description,created_at,updated_at,user_settings"
|
||||||
selectEventActionFields = "id,name,description,type,options"
|
selectEventActionFields = "id,name,description,type,options"
|
||||||
|
selectRoleFields = "id,name,description,created_at,updated_at"
|
||||||
selectMinimalFields = "id,name"
|
selectMinimalFields = "id,name"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -65,6 +67,13 @@ func getSelectEventRuleFields() string {
|
||||||
return `id,name,description,created_at,updated_at,"trigger",conditions,deleted_at`
|
return `id,name,description,created_at,updated_at,"trigger",conditions,deleted_at`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getCoalesceDefaultForRole(role string) string {
|
||||||
|
if role != "" {
|
||||||
|
return "0"
|
||||||
|
}
|
||||||
|
return "NULL"
|
||||||
|
}
|
||||||
|
|
||||||
func getAddSessionQuery() string {
|
func getAddSessionQuery() string {
|
||||||
if config.Driver == MySQLDataProviderName {
|
if config.Driver == MySQLDataProviderName {
|
||||||
return fmt.Sprintf("INSERT INTO %s (`key`,`data`,`type`,`timestamp`) VALUES (%s,%s,%s,%s) "+
|
return fmt.Sprintf("INSERT INTO %s (`key`,`data`,`type`,`timestamp`) VALUES (%s,%s,%s,%s) "+
|
||||||
|
@ -170,6 +179,75 @@ func getDefenderEventsCleanupQuery() string {
|
||||||
return fmt.Sprintf(`DELETE FROM %s WHERE date_time < %s`, sqlTableDefenderEvents, sqlPlaceholders[0])
|
return fmt.Sprintf(`DELETE FROM %s WHERE date_time < %s`, sqlTableDefenderEvents, sqlPlaceholders[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getRoleByNameQuery() string {
|
||||||
|
return fmt.Sprintf(`SELECT %s FROM %s WHERE name = %s`, selectRoleFields, sqlTableRoles,
|
||||||
|
sqlPlaceholders[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRolesQuery(order string, minimal bool) string {
|
||||||
|
var fieldSelection string
|
||||||
|
if minimal {
|
||||||
|
fieldSelection = selectMinimalFields
|
||||||
|
} else {
|
||||||
|
fieldSelection = selectRoleFields
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(`SELECT %s FROM %s ORDER BY name %s LIMIT %s OFFSET %s`, fieldSelection,
|
||||||
|
sqlTableRoles, order, sqlPlaceholders[0], sqlPlaceholders[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUsersWithRolesQuery(roles []Role) string {
|
||||||
|
var sb strings.Builder
|
||||||
|
for _, r := range roles {
|
||||||
|
if sb.Len() == 0 {
|
||||||
|
sb.WriteString("(")
|
||||||
|
} else {
|
||||||
|
sb.WriteString(",")
|
||||||
|
}
|
||||||
|
sb.WriteString(strconv.FormatInt(r.ID, 10))
|
||||||
|
}
|
||||||
|
if sb.Len() > 0 {
|
||||||
|
sb.WriteString(")")
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(`SELECT r.id, u.username FROM %s u INNER JOIN %s r ON u.role_id = r.id WHERE u.role_id IN %s`,
|
||||||
|
sqlTableUsers, sqlTableRoles, sb.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAdminsWithRolesQuery(roles []Role) string {
|
||||||
|
var sb strings.Builder
|
||||||
|
for _, r := range roles {
|
||||||
|
if sb.Len() == 0 {
|
||||||
|
sb.WriteString("(")
|
||||||
|
} else {
|
||||||
|
sb.WriteString(",")
|
||||||
|
}
|
||||||
|
sb.WriteString(strconv.FormatInt(r.ID, 10))
|
||||||
|
}
|
||||||
|
if sb.Len() > 0 {
|
||||||
|
sb.WriteString(")")
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(`SELECT r.id, a.username FROM %s a INNER JOIN %s r ON a.role_id = r.id WHERE a.role_id IN %s`,
|
||||||
|
sqlTableAdmins, sqlTableRoles, sb.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDumpRolesQuery() string {
|
||||||
|
return fmt.Sprintf(`SELECT %s FROM %s`, selectRoleFields, sqlTableRoles)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAddRoleQuery() string {
|
||||||
|
return fmt.Sprintf(`INSERT INTO %s (name,description,created_at,updated_at)
|
||||||
|
VALUES (%s,%s,%s,%s)`, sqlTableRoles, sqlPlaceholders[0], sqlPlaceholders[1],
|
||||||
|
sqlPlaceholders[2], sqlPlaceholders[3])
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUpdateRoleQuery() string {
|
||||||
|
return fmt.Sprintf(`UPDATE %s SET description=%s,updated_at=%s
|
||||||
|
WHERE name = %s`, sqlTableRoles, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDeleteRoleQuery() string {
|
||||||
|
return fmt.Sprintf(`DELETE FROM %s WHERE name = %s`, sqlTableRoles, sqlPlaceholders[0])
|
||||||
|
}
|
||||||
|
|
||||||
func getGroupByNameQuery() string {
|
func getGroupByNameQuery() string {
|
||||||
return fmt.Sprintf(`SELECT %s FROM %s WHERE name = %s`, selectGroupFields, getSQLQuotedName(sqlTableGroups),
|
return fmt.Sprintf(`SELECT %s FROM %s WHERE name = %s`, selectGroupFields, getSQLQuotedName(sqlTableGroups),
|
||||||
sqlPlaceholders[0])
|
sqlPlaceholders[0])
|
||||||
|
@ -244,29 +322,33 @@ func getDeleteGroupQuery() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAdminByUsernameQuery() string {
|
func getAdminByUsernameQuery() string {
|
||||||
return fmt.Sprintf(`SELECT %s FROM %s WHERE username = %s`, selectAdminFields, sqlTableAdmins, sqlPlaceholders[0])
|
return fmt.Sprintf(`SELECT %s FROM %s a LEFT JOIN %s r on r.id = a.role_id WHERE a.username = %s`,
|
||||||
|
selectAdminFields, sqlTableAdmins, sqlTableRoles, sqlPlaceholders[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAdminsQuery(order string) string {
|
func getAdminsQuery(order string) string {
|
||||||
return fmt.Sprintf(`SELECT %s FROM %s ORDER BY username %s LIMIT %s OFFSET %s`, selectAdminFields, sqlTableAdmins,
|
return fmt.Sprintf(`SELECT %s FROM %s a LEFT JOIN %s r on r.id = a.role_id ORDER BY a.username %s LIMIT %s OFFSET %s`,
|
||||||
order, sqlPlaceholders[0], sqlPlaceholders[1])
|
selectAdminFields, sqlTableAdmins, sqlTableRoles, order, sqlPlaceholders[0], sqlPlaceholders[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDumpAdminsQuery() string {
|
func getDumpAdminsQuery() string {
|
||||||
return fmt.Sprintf(`SELECT %s FROM %s`, selectAdminFields, sqlTableAdmins)
|
return fmt.Sprintf(`SELECT %s FROM %s a LEFT JOIN %s r on r.id = a.role_id`,
|
||||||
|
selectAdminFields, sqlTableAdmins, sqlTableRoles)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAddAdminQuery() string {
|
func getAddAdminQuery(role string) string {
|
||||||
return fmt.Sprintf(`INSERT INTO %s (username,password,status,email,permissions,filters,additional_info,description,created_at,updated_at,last_login)
|
return fmt.Sprintf(`INSERT INTO %s (username,password,status,email,permissions,filters,additional_info,description,created_at,updated_at,last_login,role_id)
|
||||||
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,0)`, sqlTableAdmins, sqlPlaceholders[0], sqlPlaceholders[1],
|
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,0,COALESCE((SELECT id from %s WHERE name = %s),%s))`,
|
||||||
sqlPlaceholders[2], sqlPlaceholders[3], sqlPlaceholders[4], sqlPlaceholders[5], sqlPlaceholders[6], sqlPlaceholders[7],
|
sqlTableAdmins, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3], sqlPlaceholders[4],
|
||||||
sqlPlaceholders[8], sqlPlaceholders[9])
|
sqlPlaceholders[5], sqlPlaceholders[6], sqlPlaceholders[7], sqlPlaceholders[8], sqlPlaceholders[9],
|
||||||
|
sqlTableRoles, sqlPlaceholders[10], getCoalesceDefaultForRole(role))
|
||||||
}
|
}
|
||||||
|
|
||||||
func getUpdateAdminQuery() string {
|
func getUpdateAdminQuery(role string) string {
|
||||||
return fmt.Sprintf(`UPDATE %s SET password=%s,status=%s,email=%s,permissions=%s,filters=%s,additional_info=%s,description=%s,updated_at=%s
|
return fmt.Sprintf(`UPDATE %s SET password=%s,status=%s,email=%s,permissions=%s,filters=%s,additional_info=%s,description=%s,updated_at=%s,
|
||||||
WHERE username = %s`, sqlTableAdmins, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2],
|
role_id=COALESCE((SELECT id from %s WHERE name = %s),%s) WHERE username = %s`, sqlTableAdmins, sqlPlaceholders[0],
|
||||||
sqlPlaceholders[3], sqlPlaceholders[4], sqlPlaceholders[5], sqlPlaceholders[6], sqlPlaceholders[7], sqlPlaceholders[8])
|
sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3], sqlPlaceholders[4], sqlPlaceholders[5], sqlPlaceholders[6],
|
||||||
|
sqlPlaceholders[7], sqlTableRoles, sqlPlaceholders[8], getCoalesceDefaultForRole(role), sqlPlaceholders[9])
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDeleteAdminQuery() string {
|
func getDeleteAdminQuery() string {
|
||||||
|
@ -393,14 +475,25 @@ func getRelatedAdminsForAPIKeysQuery(apiKeys []APIKey) string {
|
||||||
return fmt.Sprintf(`SELECT id,username FROM %s WHERE id IN %s`, sqlTableAdmins, sb.String())
|
return fmt.Sprintf(`SELECT id,username FROM %s WHERE id IN %s`, sqlTableAdmins, sb.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func getUserByUsernameQuery() string {
|
func getUserByUsernameQuery(role string) string {
|
||||||
return fmt.Sprintf(`SELECT %s FROM %s WHERE username = %s AND deleted_at = 0`,
|
if role == "" {
|
||||||
selectUserFields, sqlTableUsers, sqlPlaceholders[0])
|
return fmt.Sprintf(`SELECT %s FROM %s u LEFT JOIN %s r on r.id = u.role_id WHERE u.username = %s AND u.deleted_at = 0`,
|
||||||
|
selectUserFields, sqlTableUsers, sqlTableRoles, sqlPlaceholders[0])
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(`SELECT %s FROM %s u LEFT JOIN %s r on r.id = u.role_id WHERE u.username = %s AND u.deleted_at = 0
|
||||||
|
AND u.role_id is NOT NULL AND r.name = %s`,
|
||||||
|
selectUserFields, sqlTableUsers, sqlTableRoles, sqlPlaceholders[0], sqlPlaceholders[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
func getUsersQuery(order string) string {
|
func getUsersQuery(order, role string) string {
|
||||||
return fmt.Sprintf(`SELECT %s FROM %s WHERE deleted_at = 0 ORDER BY username %s LIMIT %s OFFSET %s`,
|
if role == "" {
|
||||||
selectUserFields, sqlTableUsers, order, sqlPlaceholders[0], sqlPlaceholders[1])
|
return fmt.Sprintf(`SELECT %s FROM %s u LEFT JOIN %s r on r.id = u.role_id WHERE
|
||||||
|
u.deleted_at = 0 ORDER BY u.username %s LIMIT %s OFFSET %s`,
|
||||||
|
selectUserFields, sqlTableUsers, sqlTableRoles, order, sqlPlaceholders[0], sqlPlaceholders[1])
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(`SELECT %s FROM %s u LEFT JOIN %s r on r.id = u.role_id WHERE
|
||||||
|
u.deleted_at = 0 AND u.role_id is NOT NULL AND r.name = %s ORDER BY u.username %s LIMIT %s OFFSET %s`,
|
||||||
|
selectUserFields, sqlTableUsers, sqlTableRoles, sqlPlaceholders[0], order, sqlPlaceholders[1], sqlPlaceholders[2])
|
||||||
}
|
}
|
||||||
|
|
||||||
func getUsersForQuotaCheckQuery(numArgs int) string {
|
func getUsersForQuotaCheckQuery(numArgs int) string {
|
||||||
|
@ -422,12 +515,13 @@ func getUsersForQuotaCheckQuery(numArgs int) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func getRecentlyUpdatedUsersQuery() string {
|
func getRecentlyUpdatedUsersQuery() string {
|
||||||
return fmt.Sprintf(`SELECT %s FROM %s WHERE updated_at >= %s OR deleted_at > 0`,
|
return fmt.Sprintf(`SELECT %s FROM %s u LEFT JOIN %s r on r.id = u.role_id WHERE u.updated_at >= %s OR u.deleted_at > 0`,
|
||||||
selectUserFields, sqlTableUsers, sqlPlaceholders[0])
|
selectUserFields, sqlTableUsers, sqlTableRoles, sqlPlaceholders[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDumpUsersQuery() string {
|
func getDumpUsersQuery() string {
|
||||||
return fmt.Sprintf(`SELECT %s FROM %s WHERE deleted_at = 0`, selectUserFields, sqlTableUsers)
|
return fmt.Sprintf(`SELECT %s FROM %s u LEFT JOIN %s r on r.id = u.role_id WHERE u.deleted_at = 0`,
|
||||||
|
selectUserFields, sqlTableUsers, sqlTableRoles)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDumpFoldersQuery() string {
|
func getDumpFoldersQuery() string {
|
||||||
|
@ -490,29 +584,32 @@ func getQuotaQuery() string {
|
||||||
sqlTableUsers, sqlPlaceholders[0])
|
sqlTableUsers, sqlPlaceholders[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAddUserQuery() string {
|
func getAddUserQuery(role string) string {
|
||||||
return fmt.Sprintf(`INSERT INTO %s (username,password,public_keys,home_dir,uid,gid,max_sessions,quota_size,quota_files,permissions,
|
return fmt.Sprintf(`INSERT INTO %s (username,password,public_keys,home_dir,uid,gid,max_sessions,quota_size,quota_files,permissions,
|
||||||
used_quota_size,used_quota_files,last_quota_update,upload_bandwidth,download_bandwidth,status,last_login,expiration_date,filters,
|
used_quota_size,used_quota_files,last_quota_update,upload_bandwidth,download_bandwidth,status,last_login,expiration_date,filters,
|
||||||
filesystem,additional_info,description,email,created_at,updated_at,upload_data_transfer,download_data_transfer,total_data_transfer,
|
filesystem,additional_info,description,email,created_at,updated_at,upload_data_transfer,download_data_transfer,total_data_transfer,
|
||||||
used_upload_data_transfer,used_download_data_transfer,deleted_at,first_download,first_upload)
|
used_upload_data_transfer,used_download_data_transfer,deleted_at,first_download,first_upload,role_id)
|
||||||
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,0,0,0,%s,%s,%s,0,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,0,0,0,0,0)`,
|
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,0,0,0,%s,%s,%s,0,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,0,0,0,0,0,
|
||||||
|
COALESCE((SELECT id from %s WHERE name=%s),%s))`,
|
||||||
sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3], sqlPlaceholders[4],
|
sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3], sqlPlaceholders[4],
|
||||||
sqlPlaceholders[5], sqlPlaceholders[6], sqlPlaceholders[7], sqlPlaceholders[8], sqlPlaceholders[9],
|
sqlPlaceholders[5], sqlPlaceholders[6], sqlPlaceholders[7], sqlPlaceholders[8], sqlPlaceholders[9],
|
||||||
sqlPlaceholders[10], sqlPlaceholders[11], sqlPlaceholders[12], sqlPlaceholders[13], sqlPlaceholders[14],
|
sqlPlaceholders[10], sqlPlaceholders[11], sqlPlaceholders[12], sqlPlaceholders[13], sqlPlaceholders[14],
|
||||||
sqlPlaceholders[15], sqlPlaceholders[16], sqlPlaceholders[17], sqlPlaceholders[18], sqlPlaceholders[19],
|
sqlPlaceholders[15], sqlPlaceholders[16], sqlPlaceholders[17], sqlPlaceholders[18], sqlPlaceholders[19],
|
||||||
sqlPlaceholders[20], sqlPlaceholders[21], sqlPlaceholders[22], sqlPlaceholders[23])
|
sqlPlaceholders[20], sqlPlaceholders[21], sqlPlaceholders[22], sqlPlaceholders[23], sqlTableRoles,
|
||||||
|
sqlPlaceholders[24], getCoalesceDefaultForRole(role))
|
||||||
}
|
}
|
||||||
|
|
||||||
func getUpdateUserQuery() string {
|
func getUpdateUserQuery(role string) string {
|
||||||
return fmt.Sprintf(`UPDATE %s SET password=%s,public_keys=%s,home_dir=%s,uid=%s,gid=%s,max_sessions=%s,quota_size=%s,
|
return fmt.Sprintf(`UPDATE %s SET password=%s,public_keys=%s,home_dir=%s,uid=%s,gid=%s,max_sessions=%s,quota_size=%s,
|
||||||
quota_files=%s,permissions=%s,upload_bandwidth=%s,download_bandwidth=%s,status=%s,expiration_date=%s,filters=%s,filesystem=%s,
|
quota_files=%s,permissions=%s,upload_bandwidth=%s,download_bandwidth=%s,status=%s,expiration_date=%s,filters=%s,filesystem=%s,
|
||||||
additional_info=%s,description=%s,email=%s,updated_at=%s,upload_data_transfer=%s,download_data_transfer=%s,
|
additional_info=%s,description=%s,email=%s,updated_at=%s,upload_data_transfer=%s,download_data_transfer=%s,
|
||||||
total_data_transfer=%s WHERE id = %s`,
|
total_data_transfer=%s,role_id=COALESCE((SELECT id from %s WHERE name=%s),%s) WHERE id = %s`,
|
||||||
sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3], sqlPlaceholders[4],
|
sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3], sqlPlaceholders[4],
|
||||||
sqlPlaceholders[5], sqlPlaceholders[6], sqlPlaceholders[7], sqlPlaceholders[8], sqlPlaceholders[9],
|
sqlPlaceholders[5], sqlPlaceholders[6], sqlPlaceholders[7], sqlPlaceholders[8], sqlPlaceholders[9],
|
||||||
sqlPlaceholders[10], sqlPlaceholders[11], sqlPlaceholders[12], sqlPlaceholders[13], sqlPlaceholders[14],
|
sqlPlaceholders[10], sqlPlaceholders[11], sqlPlaceholders[12], sqlPlaceholders[13], sqlPlaceholders[14],
|
||||||
sqlPlaceholders[15], sqlPlaceholders[16], sqlPlaceholders[17], sqlPlaceholders[18], sqlPlaceholders[19],
|
sqlPlaceholders[15], sqlPlaceholders[16], sqlPlaceholders[17], sqlPlaceholders[18], sqlPlaceholders[19],
|
||||||
sqlPlaceholders[20], sqlPlaceholders[21], sqlPlaceholders[22])
|
sqlPlaceholders[20], sqlPlaceholders[21], sqlTableRoles, sqlPlaceholders[22], getCoalesceDefaultForRole(role),
|
||||||
|
sqlPlaceholders[23])
|
||||||
}
|
}
|
||||||
|
|
||||||
func getUpdateUserPasswordQuery() string {
|
func getUpdateUserPasswordQuery() string {
|
||||||
|
|
|
@ -390,7 +390,7 @@ func (u *User) setAnonymousSettings() {
|
||||||
// RenderAsJSON implements the renderer interface used within plugins
|
// RenderAsJSON implements the renderer interface used within plugins
|
||||||
func (u *User) RenderAsJSON(reload bool) ([]byte, error) {
|
func (u *User) RenderAsJSON(reload bool) ([]byte, error) {
|
||||||
if reload {
|
if reload {
|
||||||
user, err := provider.userExists(u.Username)
|
user, err := provider.userExists(u.Username, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
providerLog(logger.LevelError, "unable to reload user before rendering as json: %v", err)
|
providerLog(logger.LevelError, "unable to reload user before rendering as json: %v", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -500,7 +500,7 @@ func (u *User) getForbiddenSFTPSelfUsers(username string) ([]string, error) {
|
||||||
if allowSelfConnections == 0 {
|
if allowSelfConnections == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
sftpUser, err := UserExists(username)
|
sftpUser, err := UserExists(username, "")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = sftpUser.LoadAndApplyGroupSettings()
|
err = sftpUser.LoadAndApplyGroupSettings()
|
||||||
}
|
}
|
||||||
|
@ -1825,6 +1825,13 @@ func (u *User) removeDuplicatesAfterGroupMerge() {
|
||||||
u.groupSettingsApplied = true
|
u.groupSettingsApplied = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *User) hasRole(role string) bool {
|
||||||
|
if role == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return role == u.Role
|
||||||
|
}
|
||||||
|
|
||||||
func (u *User) getACopy() User {
|
func (u *User) getACopy() User {
|
||||||
u.SetEmptySecretsIfNil()
|
u.SetEmptySecretsIfNil()
|
||||||
pubKeys := make([]string, len(u.PublicKeys))
|
pubKeys := make([]string, len(u.PublicKeys))
|
||||||
|
@ -1899,6 +1906,7 @@ func (u *User) getACopy() User {
|
||||||
Description: u.Description,
|
Description: u.Description,
|
||||||
CreatedAt: u.CreatedAt,
|
CreatedAt: u.CreatedAt,
|
||||||
UpdatedAt: u.UpdatedAt,
|
UpdatedAt: u.UpdatedAt,
|
||||||
|
Role: u.Role,
|
||||||
},
|
},
|
||||||
Filters: filters,
|
Filters: filters,
|
||||||
VirtualFolders: virtualFolders,
|
VirtualFolders: virtualFolders,
|
||||||
|
|
|
@ -43,7 +43,7 @@ func TestBasicFTPHandlingCryptFs(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
client, err := getFTPClient(user, true, nil)
|
client, err := getFTPClient(user, true, nil)
|
||||||
if assert.NoError(t, err) {
|
if assert.NoError(t, err) {
|
||||||
assert.Len(t, common.Connections.GetStats(), 1)
|
assert.Len(t, common.Connections.GetStats(""), 1)
|
||||||
testFilePath := filepath.Join(homeBasePath, testFileName)
|
testFilePath := filepath.Join(homeBasePath, testFileName)
|
||||||
testFileSize := int64(65535)
|
testFileSize := int64(65535)
|
||||||
encryptedFileSize, err := getEncryptedFileSize(testFileSize)
|
encryptedFileSize, err := getEncryptedFileSize(testFileSize)
|
||||||
|
@ -131,7 +131,7 @@ func TestBasicFTPHandlingCryptFs(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = os.RemoveAll(user.GetHomeDir())
|
err = os.RemoveAll(user.GetHomeDir())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Eventually(t, func() bool { return len(common.Connections.GetStats()) == 0 }, 1*time.Second, 50*time.Millisecond)
|
assert.Eventually(t, func() bool { return len(common.Connections.GetStats("")) == 0 }, 1*time.Second, 50*time.Millisecond)
|
||||||
assert.Eventually(t, func() bool { return common.Connections.GetClientConnections() == 0 }, 1000*time.Millisecond,
|
assert.Eventually(t, func() bool { return common.Connections.GetClientConnections() == 0 }, 1000*time.Millisecond,
|
||||||
50*time.Millisecond)
|
50*time.Millisecond)
|
||||||
}
|
}
|
||||||
|
|
|
@ -519,9 +519,9 @@ func TestBasicFTPHandling(t *testing.T) {
|
||||||
client, err := getFTPClient(user, true, nil)
|
client, err := getFTPClient(user, true, nil)
|
||||||
if assert.NoError(t, err) {
|
if assert.NoError(t, err) {
|
||||||
if user.Username == defaultUsername {
|
if user.Username == defaultUsername {
|
||||||
assert.Len(t, common.Connections.GetStats(), 1)
|
assert.Len(t, common.Connections.GetStats(""), 1)
|
||||||
} else {
|
} else {
|
||||||
assert.Len(t, common.Connections.GetStats(), 2)
|
assert.Len(t, common.Connections.GetStats(""), 2)
|
||||||
}
|
}
|
||||||
testFilePath := filepath.Join(homeBasePath, testFileName)
|
testFilePath := filepath.Join(homeBasePath, testFileName)
|
||||||
testFileSize := int64(65535)
|
testFileSize := int64(65535)
|
||||||
|
@ -619,7 +619,7 @@ func TestBasicFTPHandling(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = os.RemoveAll(localUser.GetHomeDir())
|
err = os.RemoveAll(localUser.GetHomeDir())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Eventually(t, func() bool { return len(common.Connections.GetStats()) == 0 }, 1*time.Second, 50*time.Millisecond)
|
assert.Eventually(t, func() bool { return len(common.Connections.GetStats("")) == 0 }, 1*time.Second, 50*time.Millisecond)
|
||||||
assert.Eventually(t, func() bool { return common.Connections.GetClientConnections() == 0 }, 1000*time.Millisecond,
|
assert.Eventually(t, func() bool { return common.Connections.GetClientConnections() == 0 }, 1000*time.Millisecond,
|
||||||
50*time.Millisecond)
|
50*time.Millisecond)
|
||||||
}
|
}
|
||||||
|
@ -663,7 +663,7 @@ func TestHTTPFs(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = os.RemoveAll(user.GetHomeDir())
|
err = os.RemoveAll(user.GetHomeDir())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Eventually(t, func() bool { return len(common.Connections.GetStats()) == 0 }, 1*time.Second, 50*time.Millisecond)
|
assert.Eventually(t, func() bool { return len(common.Connections.GetStats("")) == 0 }, 1*time.Second, 50*time.Millisecond)
|
||||||
assert.Eventually(t, func() bool { return common.Connections.GetClientConnections() == 0 }, 1000*time.Millisecond,
|
assert.Eventually(t, func() bool { return common.Connections.GetClientConnections() == 0 }, 1000*time.Millisecond,
|
||||||
50*time.Millisecond)
|
50*time.Millisecond)
|
||||||
}
|
}
|
||||||
|
@ -1623,7 +1623,7 @@ func TestMaxConnections(t *testing.T) {
|
||||||
err = client.Quit()
|
err = client.Quit()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
err = dataprovider.DeleteUser(user.Username, "", "")
|
err = dataprovider.DeleteUser(user.Username, "", "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = os.RemoveAll(user.GetHomeDir())
|
err = os.RemoveAll(user.GetHomeDir())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -1653,7 +1653,7 @@ func TestMaxPerHostConnections(t *testing.T) {
|
||||||
err = client.Quit()
|
err = client.Quit()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
err = dataprovider.DeleteUser(user.Username, "", "")
|
err = dataprovider.DeleteUser(user.Username, "", "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = os.RemoveAll(user.GetHomeDir())
|
err = os.RemoveAll(user.GetHomeDir())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -1710,7 +1710,7 @@ func TestRateLimiter(t *testing.T) {
|
||||||
assert.Contains(t, err.Error(), "banned client IP")
|
assert.Contains(t, err.Error(), "banned client IP")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = dataprovider.DeleteUser(user.Username, "", "")
|
err = dataprovider.DeleteUser(user.Username, "", "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = os.RemoveAll(user.GetHomeDir())
|
err = os.RemoveAll(user.GetHomeDir())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -1752,7 +1752,7 @@ func TestDefender(t *testing.T) {
|
||||||
assert.Contains(t, err.Error(), "banned client IP")
|
assert.Contains(t, err.Error(), "banned client IP")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = dataprovider.DeleteUser(user.Username, "", "")
|
err = dataprovider.DeleteUser(user.Username, "", "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = os.RemoveAll(user.GetHomeDir())
|
err = os.RemoveAll(user.GetHomeDir())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -2387,10 +2387,10 @@ func TestClientClose(t *testing.T) {
|
||||||
if assert.NoError(t, err) {
|
if assert.NoError(t, err) {
|
||||||
err = checkBasicFTP(client)
|
err = checkBasicFTP(client)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
stats := common.Connections.GetStats()
|
stats := common.Connections.GetStats("")
|
||||||
if assert.Len(t, stats, 1) {
|
if assert.Len(t, stats, 1) {
|
||||||
common.Connections.Close(stats[0].ConnectionID)
|
common.Connections.Close(stats[0].ConnectionID, "")
|
||||||
assert.Eventually(t, func() bool { return len(common.Connections.GetStats()) == 0 },
|
assert.Eventually(t, func() bool { return len(common.Connections.GetStats("")) == 0 },
|
||||||
1*time.Second, 50*time.Millisecond)
|
1*time.Second, 50*time.Millisecond)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3671,7 +3671,7 @@ func TestNestedVirtualFolders(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = os.RemoveAll(localUser.GetHomeDir())
|
err = os.RemoveAll(localUser.GetHomeDir())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Eventually(t, func() bool { return len(common.Connections.GetStats()) == 0 }, 1*time.Second, 50*time.Millisecond)
|
assert.Eventually(t, func() bool { return len(common.Connections.GetStats("")) == 0 }, 1*time.Second, 50*time.Millisecond)
|
||||||
assert.Eventually(t, func() bool { return common.Connections.GetClientConnections() == 0 }, 1000*time.Millisecond,
|
assert.Eventually(t, func() bool { return common.Connections.GetClientConnections() == 0 }, 1000*time.Millisecond,
|
||||||
50*time.Millisecond)
|
50*time.Millisecond)
|
||||||
}
|
}
|
||||||
|
@ -3818,7 +3818,7 @@ func waitTCPListening(address string) {
|
||||||
|
|
||||||
func waitNoConnections() {
|
func waitNoConnections() {
|
||||||
time.Sleep(50 * time.Millisecond)
|
time.Sleep(50 * time.Millisecond)
|
||||||
for len(common.Connections.GetStats()) > 0 {
|
for len(common.Connections.GetStats("")) > 0 {
|
||||||
time.Sleep(50 * time.Millisecond)
|
time.Sleep(50 * time.Millisecond)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -620,12 +620,12 @@ func TestClientVersion(t *testing.T) {
|
||||||
}
|
}
|
||||||
err := common.Connections.Add(connection)
|
err := common.Connections.Add(connection)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
stats := common.Connections.GetStats()
|
stats := common.Connections.GetStats("")
|
||||||
if assert.Len(t, stats, 1) {
|
if assert.Len(t, stats, 1) {
|
||||||
assert.Equal(t, "mock version", stats[0].ClientVersion)
|
assert.Equal(t, "mock version", stats[0].ClientVersion)
|
||||||
common.Connections.Remove(connection.GetID())
|
common.Connections.Remove(connection.GetID())
|
||||||
}
|
}
|
||||||
assert.Len(t, common.Connections.GetStats(), 0)
|
assert.Len(t, common.Connections.GetStats(""), 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDriverMethodsNotImplemented(t *testing.T) {
|
func TestDriverMethodsNotImplemented(t *testing.T) {
|
||||||
|
|
|
@ -147,6 +147,10 @@ func updateAdmin(w http.ResponseWriter, r *http.Request) {
|
||||||
sendAPIResponse(w, r, errors.New("you cannot disable yourself"), "", http.StatusBadRequest)
|
sendAPIResponse(w, r, errors.New("you cannot disable yourself"), "", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if admin.Role != claims.Role {
|
||||||
|
sendAPIResponse(w, r, errors.New("you cannot add/change your role"), "", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
admin.ID = adminID
|
admin.ID = adminID
|
||||||
admin.Username = username
|
admin.Username = username
|
||||||
|
|
|
@ -40,7 +40,7 @@ func getUserConnection(w http.ResponseWriter, r *http.Request) (*Connection, err
|
||||||
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
|
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
|
||||||
return nil, fmt.Errorf("invalid token claims %w", err)
|
return nil, fmt.Errorf("invalid token claims %w", err)
|
||||||
}
|
}
|
||||||
user, err := dataprovider.GetUserWithGroupSettings(claims.Username)
|
user, err := dataprovider.GetUserWithGroupSettings(claims.Username, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendAPIResponse(w, r, nil, "Unable to retrieve your user", getRespStatus(err))
|
sendAPIResponse(w, r, nil, "Unable to retrieve your user", getRespStatus(err))
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -405,7 +405,7 @@ func getUserProfile(w http.ResponseWriter, r *http.Request) {
|
||||||
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
|
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user, err := dataprovider.UserExists(claims.Username)
|
user, err := dataprovider.UserExists(claims.Username, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
return
|
return
|
||||||
|
@ -434,7 +434,7 @@ func updateUserProfile(w http.ResponseWriter, r *http.Request) {
|
||||||
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user, userMerged, err := dataprovider.GetUserVariants(claims.Username)
|
user, userMerged, err := dataprovider.GetUserVariants(claims.Username, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
return
|
return
|
||||||
|
|
|
@ -180,6 +180,10 @@ func restoreBackup(content []byte, inputFile string, scanQuota, mode int, execut
|
||||||
return util.NewValidationError(fmt.Sprintf("unable to parse backup content: %v", err))
|
return util.NewValidationError(fmt.Sprintf("unable to parse backup content: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err = RestoreRoles(dump.Roles, inputFile, mode, executor, ipAddress); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if err = RestoreFolders(dump.Folders, inputFile, mode, scanQuota, executor, ipAddress); err != nil {
|
if err = RestoreFolders(dump.Folders, inputFile, mode, scanQuota, executor, ipAddress); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -404,6 +408,30 @@ func RestoreAdmins(admins []dataprovider.Admin, inputFile string, mode int, exec
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RestoreRoles restores the specified roles
|
||||||
|
func RestoreRoles(roles []dataprovider.Role, inputFile string, mode int, executor, ipAddress string) error {
|
||||||
|
for _, role := range roles {
|
||||||
|
role := role // pin
|
||||||
|
r, err := dataprovider.RoleExists(role.Name)
|
||||||
|
if err == nil {
|
||||||
|
if mode == 1 {
|
||||||
|
logger.Debug(logSender, "", "loaddata mode 1, existing role %q not updated", r.Name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
role.ID = r.ID
|
||||||
|
err = dataprovider.UpdateRole(&role, executor, ipAddress)
|
||||||
|
logger.Debug(logSender, "", "restoring existing role: %q, dump file: %#v, error: %v", role.Name, inputFile, err)
|
||||||
|
} else {
|
||||||
|
err = dataprovider.AddRole(&role, executor, ipAddress)
|
||||||
|
logger.Debug(logSender, "", "adding new role: %q, dump file: %q, error: %v", role.Name, inputFile, err)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to restore role %#v: %w", role.Name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// RestoreGroups restores the specified groups
|
// RestoreGroups restores the specified groups
|
||||||
func RestoreGroups(groups []dataprovider.Group, inputFile string, mode int, executor, ipAddress string) error {
|
func RestoreGroups(groups []dataprovider.Group, inputFile string, mode int, executor, ipAddress string) error {
|
||||||
for _, group := range groups {
|
for _, group := range groups {
|
||||||
|
@ -433,7 +461,7 @@ func RestoreGroups(groups []dataprovider.Group, inputFile string, mode int, exec
|
||||||
func RestoreUsers(users []dataprovider.User, inputFile string, mode, scanQuota int, executor, ipAddress string) error {
|
func RestoreUsers(users []dataprovider.User, inputFile string, mode, scanQuota int, executor, ipAddress string) error {
|
||||||
for _, user := range users {
|
for _, user := range users {
|
||||||
user := user // pin
|
user := user // pin
|
||||||
u, err := dataprovider.UserExists(user.Username)
|
u, err := dataprovider.UserExists(user.Username, "")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if mode == 1 {
|
if mode == 1 {
|
||||||
logger.Debug(logSender, "", "loaddata mode 1, existing user %#v not updated", u.Username)
|
logger.Debug(logSender, "", "loaddata mode 1, existing user %#v not updated", u.Username)
|
||||||
|
@ -454,7 +482,7 @@ func RestoreUsers(users []dataprovider.User, inputFile string, mode, scanQuota i
|
||||||
return fmt.Errorf("unable to restore user %#v: %w", user.Username, err)
|
return fmt.Errorf("unable to restore user %#v: %w", user.Username, err)
|
||||||
}
|
}
|
||||||
if scanQuota == 1 || (scanQuota == 2 && user.HasQuotaRestrictions()) {
|
if scanQuota == 1 || (scanQuota == 2 && user.HasQuotaRestrictions()) {
|
||||||
if common.QuotaScans.AddUserQuotaScan(user.Username) {
|
if common.QuotaScans.AddUserQuotaScan(user.Username, user.Role) {
|
||||||
logger.Debug(logSender, "", "starting quota scan for restored user: %#v", user.Username)
|
logger.Debug(logSender, "", "starting quota scan for restored user: %#v", user.Username)
|
||||||
go doUserQuotaScan(user) //nolint:errcheck
|
go doUserQuotaScan(user) //nolint:errcheck
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,18 +27,28 @@ import (
|
||||||
|
|
||||||
func getMetadataChecks(w http.ResponseWriter, r *http.Request) {
|
func getMetadataChecks(w http.ResponseWriter, r *http.Request) {
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
render.JSON(w, r, common.ActiveMetadataChecks.Get())
|
claims, err := getTokenClaims(r)
|
||||||
|
if err != nil || claims.Username == "" {
|
||||||
|
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
render.JSON(w, r, common.ActiveMetadataChecks.Get(claims.Role))
|
||||||
}
|
}
|
||||||
|
|
||||||
func startMetadataCheck(w http.ResponseWriter, r *http.Request) {
|
func startMetadataCheck(w http.ResponseWriter, r *http.Request) {
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
|
claims, err := getTokenClaims(r)
|
||||||
|
if err != nil || claims.Username == "" {
|
||||||
|
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
user, err := dataprovider.GetUserWithGroupSettings(getURLParam(r, "username"))
|
user, err := dataprovider.GetUserWithGroupSettings(getURLParam(r, "username"), claims.Role)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !common.ActiveMetadataChecks.Add(user.Username) {
|
if !common.ActiveMetadataChecks.Add(user.Username, user.Role) {
|
||||||
sendAPIResponse(w, r, err, fmt.Sprintf("Another check is already in progress for user %#v", user.Username),
|
sendAPIResponse(w, r, err, fmt.Sprintf("Another check is already in progress for user %#v", user.Username),
|
||||||
http.StatusConflict)
|
http.StatusConflict)
|
||||||
return
|
return
|
||||||
|
|
|
@ -152,7 +152,7 @@ func getRecoveryCodes(w http.ResponseWriter, r *http.Request) {
|
||||||
recoveryCodes := make([]recoveryCode, 0, 12)
|
recoveryCodes := make([]recoveryCode, 0, 12)
|
||||||
var accountRecoveryCodes []dataprovider.RecoveryCode
|
var accountRecoveryCodes []dataprovider.RecoveryCode
|
||||||
if claims.hasUserAudience() {
|
if claims.hasUserAudience() {
|
||||||
user, err := dataprovider.UserExists(claims.Username)
|
user, err := dataprovider.UserExists(claims.Username, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
return
|
return
|
||||||
|
@ -203,7 +203,7 @@ func generateRecoveryCodes(w http.ResponseWriter, r *http.Request) {
|
||||||
accountRecoveryCodes = append(accountRecoveryCodes, dataprovider.RecoveryCode{Secret: kms.NewPlainSecret(code)})
|
accountRecoveryCodes = append(accountRecoveryCodes, dataprovider.RecoveryCode{Secret: kms.NewPlainSecret(code)})
|
||||||
}
|
}
|
||||||
if claims.hasUserAudience() {
|
if claims.hasUserAudience() {
|
||||||
user, err := dataprovider.UserExists(claims.Username)
|
user, err := dataprovider.UserExists(claims.Username, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
return
|
return
|
||||||
|
@ -242,7 +242,7 @@ func getNewRecoveryCode() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveUserTOTPConfig(username string, r *http.Request, recoveryCodes []dataprovider.RecoveryCode) error {
|
func saveUserTOTPConfig(username string, r *http.Request, recoveryCodes []dataprovider.RecoveryCode) error {
|
||||||
user, err := dataprovider.UserExists(username)
|
user, err := dataprovider.UserExists(username, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,7 +44,12 @@ type transferQuotaUsage struct {
|
||||||
|
|
||||||
func getUsersQuotaScans(w http.ResponseWriter, r *http.Request) {
|
func getUsersQuotaScans(w http.ResponseWriter, r *http.Request) {
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
render.JSON(w, r, common.QuotaScans.GetUsersQuotaScans())
|
claims, err := getTokenClaims(r)
|
||||||
|
if err != nil || claims.Username == "" {
|
||||||
|
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
render.JSON(w, r, common.QuotaScans.GetUsersQuotaScans(claims.Role))
|
||||||
}
|
}
|
||||||
|
|
||||||
func getFoldersQuotaScans(w http.ResponseWriter, r *http.Request) {
|
func getFoldersQuotaScans(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -86,8 +91,13 @@ func startFolderQuotaScan(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
func updateUserTransferQuotaUsage(w http.ResponseWriter, r *http.Request) {
|
func updateUserTransferQuotaUsage(w http.ResponseWriter, r *http.Request) {
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
|
claims, err := getTokenClaims(r)
|
||||||
|
if err != nil || claims.Username == "" {
|
||||||
|
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
var usage transferQuotaUsage
|
var usage transferQuotaUsage
|
||||||
err := render.DecodeJSON(r.Body, &usage)
|
err = render.DecodeJSON(r.Body, &usage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
|
@ -102,7 +112,7 @@ func updateUserTransferQuotaUsage(w http.ResponseWriter, r *http.Request) {
|
||||||
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user, err := dataprovider.GetUserWithGroupSettings(getURLParam(r, "username"))
|
user, err := dataprovider.GetUserWithGroupSettings(getURLParam(r, "username"), claims.Role)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
return
|
return
|
||||||
|
@ -122,6 +132,11 @@ func updateUserTransferQuotaUsage(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func doUpdateUserQuotaUsage(w http.ResponseWriter, r *http.Request, username string, usage quotaUsage) {
|
func doUpdateUserQuotaUsage(w http.ResponseWriter, r *http.Request, username string, usage quotaUsage) {
|
||||||
|
claims, err := getTokenClaims(r)
|
||||||
|
if err != nil || claims.Username == "" {
|
||||||
|
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
if usage.UsedQuotaFiles < 0 || usage.UsedQuotaSize < 0 {
|
if usage.UsedQuotaFiles < 0 || usage.UsedQuotaSize < 0 {
|
||||||
sendAPIResponse(w, r, errors.New("invalid used quota parameters, negative values are not allowed"),
|
sendAPIResponse(w, r, errors.New("invalid used quota parameters, negative values are not allowed"),
|
||||||
"", http.StatusBadRequest)
|
"", http.StatusBadRequest)
|
||||||
|
@ -132,7 +147,7 @@ func doUpdateUserQuotaUsage(w http.ResponseWriter, r *http.Request, username str
|
||||||
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user, err := dataprovider.GetUserWithGroupSettings(username)
|
user, err := dataprovider.GetUserWithGroupSettings(username, claims.Role)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
return
|
return
|
||||||
|
@ -142,7 +157,7 @@ func doUpdateUserQuotaUsage(w http.ResponseWriter, r *http.Request, username str
|
||||||
"", http.StatusBadRequest)
|
"", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !common.QuotaScans.AddUserQuotaScan(user.Username) {
|
if !common.QuotaScans.AddUserQuotaScan(user.Username, user.Role) {
|
||||||
sendAPIResponse(w, r, err, "A quota scan is in progress for this user", http.StatusConflict)
|
sendAPIResponse(w, r, err, "A quota scan is in progress for this user", http.StatusConflict)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -189,12 +204,17 @@ func doStartUserQuotaScan(w http.ResponseWriter, r *http.Request, username strin
|
||||||
sendAPIResponse(w, r, nil, "Quota tracking is disabled!", http.StatusForbidden)
|
sendAPIResponse(w, r, nil, "Quota tracking is disabled!", http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user, err := dataprovider.GetUserWithGroupSettings(username)
|
claims, err := getTokenClaims(r)
|
||||||
|
if err != nil || claims.Username == "" {
|
||||||
|
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user, err := dataprovider.GetUserWithGroupSettings(username, claims.Role)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !common.QuotaScans.AddUserQuotaScan(user.Username) {
|
if !common.QuotaScans.AddUserQuotaScan(user.Username, user.Role) {
|
||||||
sendAPIResponse(w, r, nil, fmt.Sprintf("Another scan is already in progress for user %#v", username),
|
sendAPIResponse(w, r, nil, fmt.Sprintf("Another scan is already in progress for user %#v", username),
|
||||||
http.StatusConflict)
|
http.StatusConflict)
|
||||||
return
|
return
|
||||||
|
|
|
@ -26,13 +26,23 @@ import (
|
||||||
|
|
||||||
func getRetentionChecks(w http.ResponseWriter, r *http.Request) {
|
func getRetentionChecks(w http.ResponseWriter, r *http.Request) {
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
render.JSON(w, r, common.RetentionChecks.Get())
|
claims, err := getTokenClaims(r)
|
||||||
|
if err != nil || claims.Username == "" {
|
||||||
|
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
render.JSON(w, r, common.RetentionChecks.Get(claims.Role))
|
||||||
}
|
}
|
||||||
|
|
||||||
func startRetentionCheck(w http.ResponseWriter, r *http.Request) {
|
func startRetentionCheck(w http.ResponseWriter, r *http.Request) {
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
|
claims, err := getTokenClaims(r)
|
||||||
|
if err != nil || claims.Username == "" {
|
||||||
|
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
username := getURLParam(r, "username")
|
username := getURLParam(r, "username")
|
||||||
user, err := dataprovider.GetUserWithGroupSettings(username)
|
user, err := dataprovider.GetUserWithGroupSettings(username, claims.Role)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
return
|
return
|
||||||
|
@ -48,11 +58,6 @@ func startRetentionCheck(w http.ResponseWriter, r *http.Request) {
|
||||||
check.Notifications = getCommaSeparatedQueryParam(r, "notifications")
|
check.Notifications = getCommaSeparatedQueryParam(r, "notifications")
|
||||||
for _, notification := range check.Notifications {
|
for _, notification := range check.Notifications {
|
||||||
if notification == common.RetentionCheckNotificationEmail {
|
if notification == common.RetentionCheckNotificationEmail {
|
||||||
claims, err := getTokenClaims(r)
|
|
||||||
if err != nil {
|
|
||||||
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
admin, err := dataprovider.AdminExists(claims.Username)
|
admin, err := dataprovider.AdminExists(claims.Username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
|
|
129
internal/httpd/api_role.go
Normal file
129
internal/httpd/api_role.go
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
// Copyright (C) 2019-2022 Nicola Murino
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published
|
||||||
|
// by the Free Software Foundation, version 3.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package httpd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/go-chi/render"
|
||||||
|
|
||||||
|
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
|
||||||
|
"github.com/drakkan/sftpgo/v2/internal/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getRoles(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
|
limit, offset, order, err := getSearchFilters(w, r)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
roles, err := dataprovider.GetRoles(limit, offset, order, false)
|
||||||
|
if err != nil {
|
||||||
|
sendAPIResponse(w, r, err, "", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
render.JSON(w, r, roles)
|
||||||
|
}
|
||||||
|
|
||||||
|
func addRole(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
|
|
||||||
|
claims, err := getTokenClaims(r)
|
||||||
|
if err != nil || claims.Username == "" {
|
||||||
|
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var role dataprovider.Role
|
||||||
|
err = render.DecodeJSON(r.Body, &role)
|
||||||
|
if err != nil {
|
||||||
|
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = dataprovider.AddRole(&role, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||||
|
if err != nil {
|
||||||
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
renderRole(w, r, role.Name, http.StatusCreated)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateRole(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
|
claims, err := getTokenClaims(r)
|
||||||
|
if err != nil || claims.Username == "" {
|
||||||
|
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
name := getURLParam(r, "name")
|
||||||
|
role, err := dataprovider.RoleExists(name)
|
||||||
|
if err != nil {
|
||||||
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
roleID := role.ID
|
||||||
|
name = role.Name
|
||||||
|
err = render.DecodeJSON(r.Body, &role)
|
||||||
|
if err != nil {
|
||||||
|
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
role.ID = roleID
|
||||||
|
role.Name = name
|
||||||
|
err = dataprovider.UpdateRole(&role, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||||
|
if err != nil {
|
||||||
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sendAPIResponse(w, r, nil, "Role updated", http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderRole(w http.ResponseWriter, r *http.Request, name string, status int) {
|
||||||
|
role, err := dataprovider.RoleExists(name)
|
||||||
|
if err != nil {
|
||||||
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if status != http.StatusOK {
|
||||||
|
ctx := context.WithValue(r.Context(), render.StatusCtxKey, status)
|
||||||
|
render.JSON(w, r.WithContext(ctx), role)
|
||||||
|
} else {
|
||||||
|
render.JSON(w, r, role)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRoleByName(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
|
name := getURLParam(r, "name")
|
||||||
|
renderRole(w, r, name, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteRole(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
|
claims, err := getTokenClaims(r)
|
||||||
|
if err != nil || claims.Username == "" {
|
||||||
|
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
name := getURLParam(r, "name")
|
||||||
|
err = dataprovider.DeleteRole(name, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||||
|
if err != nil {
|
||||||
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sendAPIResponse(w, r, err, "Role deleted", http.StatusOK)
|
||||||
|
}
|
|
@ -79,7 +79,7 @@ func addShare(w http.ResponseWriter, r *http.Request) {
|
||||||
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
|
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user, err := dataprovider.GetUserWithGroupSettings(claims.Username)
|
user, err := dataprovider.GetUserWithGroupSettings(claims.Username, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendAPIResponse(w, r, err, "Unable to retrieve your user", getRespStatus(err))
|
sendAPIResponse(w, r, err, "Unable to retrieve your user", getRespStatus(err))
|
||||||
return
|
return
|
||||||
|
@ -461,7 +461,7 @@ func (s *httpdServer) checkPublicShare(w http.ResponseWriter, r *http.Request, v
|
||||||
}
|
}
|
||||||
|
|
||||||
func getUserForShare(share dataprovider.Share) (dataprovider.User, error) {
|
func getUserForShare(share dataprovider.Share) (dataprovider.User, error) {
|
||||||
user, err := dataprovider.GetUserWithGroupSettings(share.Username)
|
user, err := dataprovider.GetUserWithGroupSettings(share.Username, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return user, err
|
return user, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,8 +38,13 @@ func getUsers(w http.ResponseWriter, r *http.Request) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
claims, err := getTokenClaims(r)
|
||||||
|
if err != nil || claims.Username == "" {
|
||||||
|
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
users, err := dataprovider.GetUsers(limit, offset, order)
|
users, err := dataprovider.GetUsers(limit, offset, order, claims.Role)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
render.JSON(w, r, users)
|
render.JSON(w, r, users)
|
||||||
} else {
|
} else {
|
||||||
|
@ -49,12 +54,17 @@ func getUsers(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
func getUserByUsername(w http.ResponseWriter, r *http.Request) {
|
func getUserByUsername(w http.ResponseWriter, r *http.Request) {
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
|
claims, err := getTokenClaims(r)
|
||||||
|
if err != nil || claims.Username == "" {
|
||||||
|
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
username := getURLParam(r, "username")
|
username := getURLParam(r, "username")
|
||||||
renderUser(w, r, username, http.StatusOK)
|
renderUser(w, r, username, claims.Role, http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderUser(w http.ResponseWriter, r *http.Request, username string, status int) {
|
func renderUser(w http.ResponseWriter, r *http.Request, username, role string, status int) {
|
||||||
user, err := dataprovider.UserExists(username)
|
user, err := dataprovider.UserExists(username, role)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
return
|
return
|
||||||
|
@ -90,12 +100,19 @@ func addUser(w http.ResponseWriter, r *http.Request) {
|
||||||
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if claims.Role != "" {
|
||||||
|
user.Role = claims.Role
|
||||||
|
}
|
||||||
|
user.Filters.RecoveryCodes = nil
|
||||||
|
user.Filters.TOTPConfig = dataprovider.UserTOTPConfig{
|
||||||
|
Enabled: false,
|
||||||
|
}
|
||||||
err = dataprovider.AddUser(&user, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
err = dataprovider.AddUser(&user, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
renderUser(w, r, user.Username, http.StatusCreated)
|
renderUser(w, r, user.Username, claims.Role, http.StatusCreated)
|
||||||
}
|
}
|
||||||
|
|
||||||
func disableUser2FA(w http.ResponseWriter, r *http.Request) {
|
func disableUser2FA(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -106,7 +123,7 @@ func disableUser2FA(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
username := getURLParam(r, "username")
|
username := getURLParam(r, "username")
|
||||||
user, err := dataprovider.UserExists(username)
|
user, err := dataprovider.UserExists(username, claims.Role)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
return
|
return
|
||||||
|
@ -140,7 +157,7 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
user, err := dataprovider.UserExists(username)
|
user, err := dataprovider.UserExists(username, claims.Role)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
return
|
return
|
||||||
|
@ -188,6 +205,9 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
|
||||||
updateEncryptedSecrets(&user.FsConfig, currentS3AccessSecret, currentAzAccountKey, currentAzSASUrl,
|
updateEncryptedSecrets(&user.FsConfig, currentS3AccessSecret, currentAzAccountKey, currentAzSASUrl,
|
||||||
currentGCSCredentials, currentCryptoPassphrase, currentSFTPPassword, currentSFTPKey, currentSFTPKeyPassphrase,
|
currentGCSCredentials, currentCryptoPassphrase, currentSFTPPassword, currentSFTPKey, currentSFTPKeyPassphrase,
|
||||||
currentHTTPPassword, currentHTTPAPIKey)
|
currentHTTPPassword, currentHTTPAPIKey)
|
||||||
|
if claims.Role != "" {
|
||||||
|
user.Role = claims.Role
|
||||||
|
}
|
||||||
err = dataprovider.UpdateUser(&user, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
err = dataprovider.UpdateUser(&user, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
|
@ -207,7 +227,7 @@ func deleteUser(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
username := getURLParam(r, "username")
|
username := getURLParam(r, "username")
|
||||||
err = dataprovider.DeleteUser(username, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
err = dataprovider.DeleteUser(username, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||||
return
|
return
|
||||||
|
@ -251,9 +271,9 @@ func resetUserPassword(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func disconnectUser(username string) {
|
func disconnectUser(username string) {
|
||||||
for _, stat := range common.Connections.GetStats() {
|
for _, stat := range common.Connections.GetStats("") {
|
||||||
if stat.Username == username {
|
if stat.Username == username {
|
||||||
common.Connections.Close(stat.ConnectionID)
|
common.Connections.Close(stat.ConnectionID, "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -160,9 +160,9 @@ func getActiveConnections(w http.ResponseWriter, r *http.Request) {
|
||||||
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
|
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
stats := common.Connections.GetStats()
|
stats := common.Connections.GetStats(claims.Role)
|
||||||
if claims.NodeID == "" {
|
if claims.NodeID == "" {
|
||||||
stats = append(stats, getNodesConnections(claims.Username)...)
|
stats = append(stats, getNodesConnections(claims.Username, claims.Role)...)
|
||||||
}
|
}
|
||||||
render.JSON(w, r, stats)
|
render.JSON(w, r, stats)
|
||||||
}
|
}
|
||||||
|
@ -181,7 +181,7 @@ func handleCloseConnection(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
node := r.URL.Query().Get("node")
|
node := r.URL.Query().Get("node")
|
||||||
if node == "" || node == dataprovider.GetNodeName() {
|
if node == "" || node == dataprovider.GetNodeName() {
|
||||||
if common.Connections.Close(connectionID) {
|
if common.Connections.Close(connectionID, claims.Role) {
|
||||||
sendAPIResponse(w, r, nil, "Connection closed", http.StatusOK)
|
sendAPIResponse(w, r, nil, "Connection closed", http.StatusOK)
|
||||||
} else {
|
} else {
|
||||||
sendAPIResponse(w, r, nil, "Not Found", http.StatusNotFound)
|
sendAPIResponse(w, r, nil, "Not Found", http.StatusNotFound)
|
||||||
|
@ -195,7 +195,7 @@ func handleCloseConnection(w http.ResponseWriter, r *http.Request) {
|
||||||
sendAPIResponse(w, r, nil, http.StatusText(status), status)
|
sendAPIResponse(w, r, nil, http.StatusText(status), status)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := n.SendDeleteRequest(claims.Username, fmt.Sprintf("%s/%s", activeConnectionsPath, connectionID)); err != nil {
|
if err := n.SendDeleteRequest(claims.Username, claims.Role, fmt.Sprintf("%s/%s", activeConnectionsPath, connectionID)); err != nil {
|
||||||
logger.Warn(logSender, "", "unable to delete connection id %q from node %q: %v", connectionID, n.Name, err)
|
logger.Warn(logSender, "", "unable to delete connection id %q from node %q: %v", connectionID, n.Name, err)
|
||||||
sendAPIResponse(w, r, nil, "Not Found", http.StatusNotFound)
|
sendAPIResponse(w, r, nil, "Not Found", http.StatusNotFound)
|
||||||
return
|
return
|
||||||
|
@ -205,7 +205,7 @@ func handleCloseConnection(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
// getNodesConnections returns the active connections from other nodes.
|
// getNodesConnections returns the active connections from other nodes.
|
||||||
// Errors are silently ignored
|
// Errors are silently ignored
|
||||||
func getNodesConnections(admin string) []common.ConnectionStatus {
|
func getNodesConnections(admin, role string) []common.ConnectionStatus {
|
||||||
nodes, err := dataprovider.GetNodes()
|
nodes, err := dataprovider.GetNodes()
|
||||||
if err != nil || len(nodes) == 0 {
|
if err != nil || len(nodes) == 0 {
|
||||||
return nil
|
return nil
|
||||||
|
@ -221,7 +221,7 @@ func getNodesConnections(admin string) []common.ConnectionStatus {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
|
||||||
var stats []common.ConnectionStatus
|
var stats []common.ConnectionStatus
|
||||||
if err := node.SendGetRequest(admin, activeConnectionsPath, &stats); err != nil {
|
if err := node.SendGetRequest(admin, role, activeConnectionsPath, &stats); err != nil {
|
||||||
logger.Warn(logSender, "", "unable to get connections from node %s: %v", node.Name, err)
|
logger.Warn(logSender, "", "unable to get connections from node %s: %v", node.Name, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -647,7 +647,7 @@ func handleForgotPassword(r *http.Request, username string, isAdmin bool) error
|
||||||
email = admin.Email
|
email = admin.Email
|
||||||
subject = fmt.Sprintf("Email Verification Code for admin %#v", username)
|
subject = fmt.Sprintf("Email Verification Code for admin %#v", username)
|
||||||
} else {
|
} else {
|
||||||
user, err = dataprovider.GetUserWithGroupSettings(username)
|
user, err = dataprovider.GetUserWithGroupSettings(username, "")
|
||||||
email = user.Email
|
email = user.Email
|
||||||
subject = fmt.Sprintf("Email Verification Code for user %#v", username)
|
subject = fmt.Sprintf("Email Verification Code for user %#v", username)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
@ -719,7 +719,7 @@ func handleResetPassword(r *http.Request, code, newPassword string, isAdmin bool
|
||||||
err = resetCodesMgr.Delete(code)
|
err = resetCodesMgr.Delete(code)
|
||||||
return &admin, &user, err
|
return &admin, &user, err
|
||||||
}
|
}
|
||||||
user, err = dataprovider.GetUserWithGroupSettings(resetCode.Username)
|
user, err = dataprovider.GetUserWithGroupSettings(resetCode.Username, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &admin, &user, util.NewValidationError("Unable to associate the confirmation code with an existing user")
|
return &admin, &user, util.NewValidationError("Unable to associate the confirmation code with an existing user")
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-chi/jwtauth/v5"
|
"github.com/go-chi/jwtauth/v5"
|
||||||
"github.com/lestrrat-go/jwx/jwt"
|
"github.com/lestrrat-go/jwx/v2/jwt"
|
||||||
"github.com/rs/xid"
|
"github.com/rs/xid"
|
||||||
|
|
||||||
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
|
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
|
||||||
|
@ -51,6 +51,7 @@ const (
|
||||||
const (
|
const (
|
||||||
claimUsernameKey = "username"
|
claimUsernameKey = "username"
|
||||||
claimPermissionsKey = "permissions"
|
claimPermissionsKey = "permissions"
|
||||||
|
claimRole = "role"
|
||||||
claimAPIKey = "api_key"
|
claimAPIKey = "api_key"
|
||||||
claimNodeID = "node_id"
|
claimNodeID = "node_id"
|
||||||
claimMustSetSecondFactorKey = "2fa_required"
|
claimMustSetSecondFactorKey = "2fa_required"
|
||||||
|
@ -72,6 +73,7 @@ var (
|
||||||
type jwtTokenClaims struct {
|
type jwtTokenClaims struct {
|
||||||
Username string
|
Username string
|
||||||
Permissions []string
|
Permissions []string
|
||||||
|
Role string
|
||||||
Signature string
|
Signature string
|
||||||
Audience []string
|
Audience []string
|
||||||
APIKeyID string
|
APIKeyID string
|
||||||
|
@ -96,6 +98,9 @@ func (c *jwtTokenClaims) asMap() map[string]any {
|
||||||
|
|
||||||
claims[claimUsernameKey] = c.Username
|
claims[claimUsernameKey] = c.Username
|
||||||
claims[claimPermissionsKey] = c.Permissions
|
claims[claimPermissionsKey] = c.Permissions
|
||||||
|
if c.Role != "" {
|
||||||
|
claims[claimRole] = c.Role
|
||||||
|
}
|
||||||
if c.APIKeyID != "" {
|
if c.APIKeyID != "" {
|
||||||
claims[claimAPIKey] = c.APIKeyID
|
claims[claimAPIKey] = c.APIKeyID
|
||||||
}
|
}
|
||||||
|
@ -169,6 +174,13 @@ func (c *jwtTokenClaims) Decode(token map[string]any) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if val, ok := token[claimRole]; ok {
|
||||||
|
switch v := val.(type) {
|
||||||
|
case string:
|
||||||
|
c.Role = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
permissions := token[claimPermissionsKey]
|
permissions := token[claimPermissionsKey]
|
||||||
c.Permissions = c.decodeSliceString(permissions)
|
c.Permissions = c.decodeSliceString(permissions)
|
||||||
|
|
||||||
|
@ -350,6 +362,7 @@ func getAdminFromToken(r *http.Request) *dataprovider.Admin {
|
||||||
admin.Username = tokenClaims.Username
|
admin.Username = tokenClaims.Username
|
||||||
admin.Permissions = tokenClaims.Permissions
|
admin.Permissions = tokenClaims.Permissions
|
||||||
admin.Filters.Preferences.HideUserPageSections = tokenClaims.HideUserPageSections
|
admin.Filters.Preferences.HideUserPageSections = tokenClaims.HideUserPageSections
|
||||||
|
admin.Role = tokenClaims.Role
|
||||||
return admin
|
return admin
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,7 @@ import (
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/go-chi/jwtauth/v5"
|
"github.com/go-chi/jwtauth/v5"
|
||||||
"github.com/lestrrat-go/jwx/jwa"
|
"github.com/lestrrat-go/jwx/v2/jwa"
|
||||||
|
|
||||||
"github.com/drakkan/sftpgo/v2/internal/common"
|
"github.com/drakkan/sftpgo/v2/internal/common"
|
||||||
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
|
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
|
||||||
|
@ -91,6 +91,7 @@ const (
|
||||||
sharesPath = "/api/v2/shares"
|
sharesPath = "/api/v2/shares"
|
||||||
eventActionsPath = "/api/v2/eventactions"
|
eventActionsPath = "/api/v2/eventactions"
|
||||||
eventRulesPath = "/api/v2/eventrules"
|
eventRulesPath = "/api/v2/eventrules"
|
||||||
|
rolesPath = "/api/v2/roles"
|
||||||
healthzPath = "/healthz"
|
healthzPath = "/healthz"
|
||||||
robotsTxtPath = "/robots.txt"
|
robotsTxtPath = "/robots.txt"
|
||||||
webRootPathDefault = "/"
|
webRootPathDefault = "/"
|
||||||
|
@ -128,6 +129,8 @@ const (
|
||||||
webAdminEventRulePathDefault = "/web/admin/eventrule"
|
webAdminEventRulePathDefault = "/web/admin/eventrule"
|
||||||
webAdminEventActionsPathDefault = "/web/admin/eventactions"
|
webAdminEventActionsPathDefault = "/web/admin/eventactions"
|
||||||
webAdminEventActionPathDefault = "/web/admin/eventaction"
|
webAdminEventActionPathDefault = "/web/admin/eventaction"
|
||||||
|
webAdminRolesPathDefault = "/web/admin/roles"
|
||||||
|
webAdminRolePathDefault = "/web/admin/role"
|
||||||
webAdminTOTPGeneratePathDefault = "/web/admin/totp/generate"
|
webAdminTOTPGeneratePathDefault = "/web/admin/totp/generate"
|
||||||
webAdminTOTPValidatePathDefault = "/web/admin/totp/validate"
|
webAdminTOTPValidatePathDefault = "/web/admin/totp/validate"
|
||||||
webAdminTOTPSavePathDefault = "/web/admin/totp/save"
|
webAdminTOTPSavePathDefault = "/web/admin/totp/save"
|
||||||
|
@ -211,6 +214,8 @@ var (
|
||||||
webAdminEventRulePath string
|
webAdminEventRulePath string
|
||||||
webAdminEventActionsPath string
|
webAdminEventActionsPath string
|
||||||
webAdminEventActionPath string
|
webAdminEventActionPath string
|
||||||
|
webAdminRolesPath string
|
||||||
|
webAdminRolePath string
|
||||||
webAdminTOTPGeneratePath string
|
webAdminTOTPGeneratePath string
|
||||||
webAdminTOTPValidatePath string
|
webAdminTOTPValidatePath string
|
||||||
webAdminTOTPSavePath string
|
webAdminTOTPSavePath string
|
||||||
|
@ -1010,6 +1015,8 @@ func updateWebAdminURLs(baseURL string) {
|
||||||
webAdminEventRulePath = path.Join(baseURL, webAdminEventRulePathDefault)
|
webAdminEventRulePath = path.Join(baseURL, webAdminEventRulePathDefault)
|
||||||
webAdminEventActionsPath = path.Join(baseURL, webAdminEventActionsPathDefault)
|
webAdminEventActionsPath = path.Join(baseURL, webAdminEventActionsPathDefault)
|
||||||
webAdminEventActionPath = path.Join(baseURL, webAdminEventActionPathDefault)
|
webAdminEventActionPath = path.Join(baseURL, webAdminEventActionPathDefault)
|
||||||
|
webAdminRolesPath = path.Join(baseURL, webAdminRolesPathDefault)
|
||||||
|
webAdminRolePath = path.Join(baseURL, webAdminRolePathDefault)
|
||||||
webAdminTOTPGeneratePath = path.Join(baseURL, webAdminTOTPGeneratePathDefault)
|
webAdminTOTPGeneratePath = path.Join(baseURL, webAdminTOTPGeneratePathDefault)
|
||||||
webAdminTOTPValidatePath = path.Join(baseURL, webAdminTOTPValidatePathDefault)
|
webAdminTOTPValidatePath = path.Join(baseURL, webAdminTOTPValidatePathDefault)
|
||||||
webAdminTOTPSavePath = path.Join(baseURL, webAdminTOTPSavePathDefault)
|
webAdminTOTPSavePath = path.Join(baseURL, webAdminTOTPSavePathDefault)
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -40,8 +40,8 @@ import (
|
||||||
"github.com/go-chi/chi/v5/middleware"
|
"github.com/go-chi/chi/v5/middleware"
|
||||||
"github.com/go-chi/jwtauth/v5"
|
"github.com/go-chi/jwtauth/v5"
|
||||||
"github.com/klauspost/compress/zip"
|
"github.com/klauspost/compress/zip"
|
||||||
"github.com/lestrrat-go/jwx/jwa"
|
"github.com/lestrrat-go/jwx/v2/jwa"
|
||||||
"github.com/lestrrat-go/jwx/jwt"
|
"github.com/lestrrat-go/jwx/v2/jwt"
|
||||||
"github.com/rs/xid"
|
"github.com/rs/xid"
|
||||||
"github.com/sftpgo/sdk"
|
"github.com/sftpgo/sdk"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
@ -686,6 +686,86 @@ func TestInvalidToken(t *testing.T) {
|
||||||
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||||
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
||||||
|
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
getMetadataChecks(rr, req)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||||
|
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
||||||
|
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
startMetadataCheck(rr, req)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||||
|
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
||||||
|
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
getUsersQuotaScans(rr, req)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||||
|
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
||||||
|
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
updateUserTransferQuotaUsage(rr, req)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||||
|
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
||||||
|
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
doUpdateUserQuotaUsage(rr, req, "", quotaUsage{})
|
||||||
|
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||||
|
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
||||||
|
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
doStartUserQuotaScan(rr, req, "")
|
||||||
|
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||||
|
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
||||||
|
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
getRetentionChecks(rr, req)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||||
|
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
||||||
|
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
addRole(rr, req)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||||
|
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
||||||
|
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
updateRole(rr, req)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||||
|
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
||||||
|
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
deleteRole(rr, req)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||||
|
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
||||||
|
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
getUsers(rr, req)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||||
|
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
||||||
|
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
getUserByUsername(rr, req)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||||
|
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
||||||
|
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
server.handleGetWebUsers(rr, req)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||||
|
assert.Contains(t, rr.Body.String(), "invalid token claims")
|
||||||
|
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
server.handleWebUpdateUserGet(rr, req)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||||
|
assert.Contains(t, rr.Body.String(), "invalid token claims")
|
||||||
|
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
server.handleWebUpdateRolePost(rr, req)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||||
|
assert.Contains(t, rr.Body.String(), "invalid token claims")
|
||||||
|
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
server.handleWebAddRolePost(rr, req)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||||
|
assert.Contains(t, rr.Body.String(), "invalid token claims")
|
||||||
|
|
||||||
rr = httptest.NewRecorder()
|
rr = httptest.NewRecorder()
|
||||||
server.handleWebAddAdminPost(rr, req)
|
server.handleWebAddAdminPost(rr, req)
|
||||||
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||||
|
@ -806,7 +886,7 @@ func TestRetentionInvalidTokenClaims(t *testing.T) {
|
||||||
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||||
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
||||||
|
|
||||||
err = dataprovider.DeleteUser(username, "", "")
|
err = dataprovider.DeleteUser(username, "", "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1030,6 +1110,13 @@ func TestCreateTokenError(t *testing.T) {
|
||||||
assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
|
assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
|
||||||
assert.Contains(t, rr.Body.String(), "invalid URL escape")
|
assert.Contains(t, rr.Body.String(), "invalid URL escape")
|
||||||
|
|
||||||
|
req, _ = http.NewRequest(http.MethodPost, webAdminRolePath+"?a=a%C3%AO%JE", bytes.NewBuffer([]byte(form.Encode())))
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
server.handleWebAddRolePost(rr, req)
|
||||||
|
assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
|
||||||
|
assert.Contains(t, rr.Body.String(), "invalid URL escape")
|
||||||
|
|
||||||
req, _ = http.NewRequest(http.MethodPost, webClientResetPwdPath+"?a=a%C3%AO%JD", bytes.NewBuffer([]byte(form.Encode())))
|
req, _ = http.NewRequest(http.MethodPost, webClientResetPwdPath+"?a=a%C3%AO%JD", bytes.NewBuffer([]byte(form.Encode())))
|
||||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
rr = httptest.NewRecorder()
|
rr = httptest.NewRecorder()
|
||||||
|
@ -1074,7 +1161,7 @@ func TestCreateTokenError(t *testing.T) {
|
||||||
err = authenticateUserWithAPIKey(username, "", server.tokenAuth, req)
|
err = authenticateUserWithAPIKey(username, "", server.tokenAuth, req)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
|
|
||||||
err = dataprovider.DeleteUser(username, "", "")
|
err = dataprovider.DeleteUser(username, "", "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = os.RemoveAll(user.HomeDir)
|
err = os.RemoveAll(user.HomeDir)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -1341,13 +1428,13 @@ func TestCookieExpiration(t *testing.T) {
|
||||||
cookie = rr.Header().Get("Set-Cookie")
|
cookie = rr.Header().Get("Set-Cookie")
|
||||||
assert.Empty(t, cookie)
|
assert.Empty(t, cookie)
|
||||||
|
|
||||||
user, err = dataprovider.UserExists(user.Username)
|
user, err = dataprovider.UserExists(user.Username, "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
user.Filters.AllowedIP = []string{"172.16.4.0/24"}
|
user.Filters.AllowedIP = []string{"172.16.4.0/24"}
|
||||||
err = dataprovider.UpdateUser(&user, "", "")
|
err = dataprovider.UpdateUser(&user, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
user, err = dataprovider.UserExists(user.Username)
|
user, err = dataprovider.UserExists(user.Username, "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
claims = make(map[string]any)
|
claims = make(map[string]any)
|
||||||
claims[claimUsernameKey] = user.Username
|
claims[claimUsernameKey] = user.Username
|
||||||
|
@ -1372,7 +1459,7 @@ func TestCookieExpiration(t *testing.T) {
|
||||||
cookie = rr.Header().Get("Set-Cookie")
|
cookie = rr.Header().Get("Set-Cookie")
|
||||||
assert.NotEmpty(t, cookie)
|
assert.NotEmpty(t, cookie)
|
||||||
|
|
||||||
err = dataprovider.DeleteUser(user.Username, "", "")
|
err = dataprovider.DeleteUser(user.Username, "", "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1451,7 +1538,7 @@ func TestQuotaScanInvalidFs(t *testing.T) {
|
||||||
Provider: sdk.S3FilesystemProvider,
|
Provider: sdk.S3FilesystemProvider,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
common.QuotaScans.AddUserQuotaScan(user.Username)
|
common.QuotaScans.AddUserQuotaScan(user.Username, "")
|
||||||
err := doUserQuotaScan(user)
|
err := doUserQuotaScan(user)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
}
|
}
|
||||||
|
@ -2437,21 +2524,28 @@ func TestMetadataAPI(t *testing.T) {
|
||||||
err := dataprovider.AddUser(&user, "", "")
|
err := dataprovider.AddUser(&user, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
assert.True(t, common.ActiveMetadataChecks.Add(username))
|
assert.True(t, common.ActiveMetadataChecks.Add(username, ""))
|
||||||
|
|
||||||
|
tokenAuth := jwtauth.New(jwa.HS256.String(), util.GenerateRandomBytes(32), nil)
|
||||||
|
claims := make(map[string]any)
|
||||||
|
claims["username"] = defaultAdminUsername
|
||||||
|
claims[jwt.ExpirationKey] = time.Now().UTC().Add(1 * time.Hour)
|
||||||
|
token, _, err := tokenAuth.Encode(claims)
|
||||||
|
assert.NoError(t, err)
|
||||||
req, err := http.NewRequest(http.MethodPost, path.Join(metadataBasePath, username, "check"), nil)
|
req, err := http.NewRequest(http.MethodPost, path.Join(metadataBasePath, username, "check"), nil)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
rctx := chi.NewRouteContext()
|
rctx := chi.NewRouteContext()
|
||||||
rctx.URLParams.Add("username", username)
|
rctx.URLParams.Add("username", username)
|
||||||
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
|
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
|
||||||
|
req = req.WithContext(context.WithValue(req.Context(), jwtauth.TokenCtxKey, token))
|
||||||
|
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
startMetadataCheck(rr, req)
|
startMetadataCheck(rr, req)
|
||||||
assert.Equal(t, http.StatusConflict, rr.Code)
|
assert.Equal(t, http.StatusConflict, rr.Code, rr.Body.String())
|
||||||
|
|
||||||
assert.True(t, common.ActiveMetadataChecks.Remove(username))
|
assert.True(t, common.ActiveMetadataChecks.Remove(username))
|
||||||
assert.Len(t, common.ActiveMetadataChecks.Get(), 0)
|
assert.Len(t, common.ActiveMetadataChecks.Get(""), 0)
|
||||||
err = dataprovider.DeleteUser(username, "", "")
|
err = dataprovider.DeleteUser(username, "", "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
user.FsConfig.Provider = sdk.AzureBlobFilesystemProvider
|
user.FsConfig.Provider = sdk.AzureBlobFilesystemProvider
|
||||||
|
|
|
@ -21,7 +21,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/go-chi/jwtauth/v5"
|
"github.com/go-chi/jwtauth/v5"
|
||||||
"github.com/lestrrat-go/jwx/jwt"
|
"github.com/lestrrat-go/jwx/v2/jwt"
|
||||||
"github.com/rs/xid"
|
"github.com/rs/xid"
|
||||||
"github.com/sftpgo/sdk"
|
"github.com/sftpgo/sdk"
|
||||||
|
|
||||||
|
@ -320,7 +320,7 @@ func checkNodeToken(tokenAuth *jwtauth.JWTAuth) func(next http.Handler) http.Han
|
||||||
if len(token) > 7 && strings.ToUpper(token[0:6]) == "BEARER" {
|
if len(token) > 7 && strings.ToUpper(token[0:6]) == "BEARER" {
|
||||||
token = token[7:]
|
token = token[7:]
|
||||||
}
|
}
|
||||||
admin, err := dataprovider.AuthenticateNodeToken(token)
|
admin, role, err := dataprovider.AuthenticateNodeToken(token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Debug(logSender, "", "unable to authenticate node token %q: %v", token, err)
|
logger.Debug(logSender, "", "unable to authenticate node token %q: %v", token, err)
|
||||||
sendAPIResponse(w, r, fmt.Errorf("the provided token cannot be authenticated"), "", http.StatusUnauthorized)
|
sendAPIResponse(w, r, fmt.Errorf("the provided token cannot be authenticated"), "", http.StatusUnauthorized)
|
||||||
|
@ -330,6 +330,7 @@ func checkNodeToken(tokenAuth *jwtauth.JWTAuth) func(next http.Handler) http.Han
|
||||||
Username: admin,
|
Username: admin,
|
||||||
Permissions: []string{dataprovider.PermAdminViewConnections, dataprovider.PermAdminCloseConnections},
|
Permissions: []string{dataprovider.PermAdminViewConnections, dataprovider.PermAdminCloseConnections},
|
||||||
NodeID: dataprovider.GetNodeName(),
|
NodeID: dataprovider.GetNodeName(),
|
||||||
|
Role: role,
|
||||||
}
|
}
|
||||||
resp, err := c.createTokenResponse(tokenAuth, tokenAudienceAPI, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
resp, err := c.createTokenResponse(tokenAuth, tokenAudienceAPI, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -468,7 +469,7 @@ func authenticateUserWithAPIKey(username, keyID string, tokenAuth *jwtauth.JWTAu
|
||||||
if err := common.Config.ExecutePostConnectHook(ipAddr, protocol); err != nil {
|
if err := common.Config.ExecutePostConnectHook(ipAddr, protocol); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
user, err := dataprovider.GetUserWithGroupSettings(username)
|
user, err := dataprovider.GetUserWithGroupSettings(username, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}},
|
updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}},
|
||||||
dataprovider.LoginMethodPassword, ipAddr, err)
|
dataprovider.LoginMethodPassword, ipAddr, err)
|
||||||
|
|
|
@ -205,6 +205,7 @@ type oidcToken struct {
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Permissions []string `json:"permissions"`
|
Permissions []string `json:"permissions"`
|
||||||
HideUserPageSections int `json:"hide_user_page_sections,omitempty"`
|
HideUserPageSections int `json:"hide_user_page_sections,omitempty"`
|
||||||
|
AdminRole string `json:"admin_role,omitempty"`
|
||||||
Role any `json:"role"`
|
Role any `json:"role"`
|
||||||
CustomFields *map[string]any `json:"custom_fields,omitempty"`
|
CustomFields *map[string]any `json:"custom_fields,omitempty"`
|
||||||
Cookie string `json:"cookie"`
|
Cookie string `json:"cookie"`
|
||||||
|
@ -389,10 +390,11 @@ func (t *oidcToken) refreshUser(r *http.Request) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
t.Permissions = admin.Permissions
|
t.Permissions = admin.Permissions
|
||||||
|
t.AdminRole = admin.Role
|
||||||
t.HideUserPageSections = admin.Filters.Preferences.HideUserPageSections
|
t.HideUserPageSections = admin.Filters.Preferences.HideUserPageSections
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
user, err := dataprovider.GetUserWithGroupSettings(t.Username)
|
user, err := dataprovider.GetUserWithGroupSettings(t.Username, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -416,6 +418,7 @@ func (t *oidcToken) getUser(r *http.Request) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
t.Permissions = admin.Permissions
|
t.Permissions = admin.Permissions
|
||||||
|
t.AdminRole = admin.Role
|
||||||
t.HideUserPageSections = admin.Filters.Preferences.HideUserPageSections
|
t.HideUserPageSections = admin.Filters.Preferences.HideUserPageSections
|
||||||
dataprovider.UpdateAdminLastLogin(&admin)
|
dataprovider.UpdateAdminLastLogin(&admin)
|
||||||
return nil
|
return nil
|
||||||
|
@ -515,6 +518,7 @@ func (s *httpdServer) oidcTokenAuthenticator(audience tokenAudience) func(next h
|
||||||
jwtTokenClaims := jwtTokenClaims{
|
jwtTokenClaims := jwtTokenClaims{
|
||||||
Username: token.Username,
|
Username: token.Username,
|
||||||
Permissions: token.Permissions,
|
Permissions: token.Permissions,
|
||||||
|
Role: token.AdminRole,
|
||||||
HideUserPageSections: token.HideUserPageSections,
|
HideUserPageSections: token.HideUserPageSections,
|
||||||
}
|
}
|
||||||
_, tokenString, err := jwtTokenClaims.createToken(s.tokenAuth, audience, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
_, tokenString, err := jwtTokenClaims.createToken(s.tokenAuth, audience, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||||
|
|
|
@ -32,7 +32,7 @@ import (
|
||||||
|
|
||||||
"github.com/coreos/go-oidc/v3/oidc"
|
"github.com/coreos/go-oidc/v3/oidc"
|
||||||
"github.com/go-chi/jwtauth/v5"
|
"github.com/go-chi/jwtauth/v5"
|
||||||
"github.com/lestrrat-go/jwx/jwa"
|
"github.com/lestrrat-go/jwx/v2/jwa"
|
||||||
"github.com/rs/xid"
|
"github.com/rs/xid"
|
||||||
"github.com/sftpgo/sdk"
|
"github.com/sftpgo/sdk"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
@ -521,7 +521,7 @@ func TestOIDCLoginLogout(t *testing.T) {
|
||||||
|
|
||||||
err = os.RemoveAll(user.GetHomeDir())
|
err = os.RemoveAll(user.GetHomeDir())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = dataprovider.DeleteUser(username, "", "")
|
err = dataprovider.DeleteUser(username, "", "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -706,7 +706,7 @@ func TestOIDCRefreshUser(t *testing.T) {
|
||||||
if assert.Error(t, err) {
|
if assert.Error(t, err) {
|
||||||
assert.Contains(t, err.Error(), "is disabled")
|
assert.Contains(t, err.Error(), "is disabled")
|
||||||
}
|
}
|
||||||
user, err = dataprovider.UserExists(username)
|
user, err = dataprovider.UserExists(username, "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
user.Status = 1
|
user.Status = 1
|
||||||
err = dataprovider.UpdateUser(&user, "", "")
|
err = dataprovider.UpdateUser(&user, "", "")
|
||||||
|
@ -723,7 +723,7 @@ func TestOIDCRefreshUser(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, user.Filters.WebClient, token.Permissions)
|
assert.Equal(t, user.Filters.WebClient, token.Permissions)
|
||||||
|
|
||||||
err = dataprovider.DeleteUser(username, "", "")
|
err = dataprovider.DeleteUser(username, "", "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -877,7 +877,7 @@ func TestOIDCToken(t *testing.T) {
|
||||||
if assert.Error(t, err) {
|
if assert.Error(t, err) {
|
||||||
assert.Contains(t, err.Error(), "is disabled")
|
assert.Contains(t, err.Error(), "is disabled")
|
||||||
}
|
}
|
||||||
user, err = dataprovider.UserExists(username)
|
user, err = dataprovider.UserExists(username, "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
user.Status = 1
|
user.Status = 1
|
||||||
user.Password = "np"
|
user.Password = "np"
|
||||||
|
@ -916,7 +916,7 @@ func TestOIDCToken(t *testing.T) {
|
||||||
|
|
||||||
err = os.RemoveAll(user.GetHomeDir())
|
err = os.RemoveAll(user.GetHomeDir())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = dataprovider.DeleteUser(username, "", "")
|
err = dataprovider.DeleteUser(username, "", "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1039,7 +1039,7 @@ func TestOIDCImplicitRoles(t *testing.T) {
|
||||||
|
|
||||||
err = os.RemoveAll(user.GetHomeDir())
|
err = os.RemoveAll(user.GetHomeDir())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = dataprovider.DeleteUser(username, "", "")
|
err = dataprovider.DeleteUser(username, "", "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1164,7 +1164,7 @@ func TestOIDCPreLoginHook(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
server.initializeRouter()
|
server.initializeRouter()
|
||||||
|
|
||||||
_, err = dataprovider.UserExists(username)
|
_, err = dataprovider.UserExists(username, "")
|
||||||
_, ok = err.(*util.RecordNotFoundError)
|
_, ok = err.(*util.RecordNotFoundError)
|
||||||
assert.True(t, ok)
|
assert.True(t, ok)
|
||||||
// now login with OIDC
|
// now login with OIDC
|
||||||
|
@ -1197,10 +1197,10 @@ func TestOIDCPreLoginHook(t *testing.T) {
|
||||||
server.router.ServeHTTP(rr, r)
|
server.router.ServeHTTP(rr, r)
|
||||||
assert.Equal(t, http.StatusFound, rr.Code)
|
assert.Equal(t, http.StatusFound, rr.Code)
|
||||||
assert.Equal(t, webClientFilesPath, rr.Header().Get("Location"))
|
assert.Equal(t, webClientFilesPath, rr.Header().Get("Location"))
|
||||||
_, err = dataprovider.UserExists(username)
|
_, err = dataprovider.UserExists(username, "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
err = dataprovider.DeleteUser(username, "", "")
|
err = dataprovider.DeleteUser(username, "", "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = os.RemoveAll(u.HomeDir)
|
err = os.RemoveAll(u.HomeDir)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -1225,7 +1225,7 @@ func TestOIDCPreLoginHook(t *testing.T) {
|
||||||
server.router.ServeHTTP(rr, r)
|
server.router.ServeHTTP(rr, r)
|
||||||
assert.Equal(t, http.StatusFound, rr.Code)
|
assert.Equal(t, http.StatusFound, rr.Code)
|
||||||
assert.Equal(t, webClientLoginPath, rr.Header().Get("Location"))
|
assert.Equal(t, webClientLoginPath, rr.Header().Get("Location"))
|
||||||
_, err = dataprovider.UserExists(username)
|
_, err = dataprovider.UserExists(username, "")
|
||||||
_, ok = err.(*util.RecordNotFoundError)
|
_, ok = err.(*util.RecordNotFoundError)
|
||||||
assert.True(t, ok)
|
assert.True(t, ok)
|
||||||
if assert.Len(t, oidcMgr.tokens, 1) {
|
if assert.Len(t, oidcMgr.tokens, 1) {
|
||||||
|
|
|
@ -31,7 +31,7 @@ import (
|
||||||
"github.com/go-chi/chi/v5/middleware"
|
"github.com/go-chi/chi/v5/middleware"
|
||||||
"github.com/go-chi/jwtauth/v5"
|
"github.com/go-chi/jwtauth/v5"
|
||||||
"github.com/go-chi/render"
|
"github.com/go-chi/render"
|
||||||
"github.com/lestrrat-go/jwx/jwa"
|
"github.com/lestrrat-go/jwx/v2/jwa"
|
||||||
"github.com/rs/cors"
|
"github.com/rs/cors"
|
||||||
"github.com/rs/xid"
|
"github.com/rs/xid"
|
||||||
"github.com/sftpgo/sdk"
|
"github.com/sftpgo/sdk"
|
||||||
|
@ -329,7 +329,7 @@ func (s *httpdServer) handleWebClientTwoFactorRecoveryPost(w http.ResponseWriter
|
||||||
s.renderClientTwoFactorRecoveryPage(w, err.Error(), ipAddr)
|
s.renderClientTwoFactorRecoveryPage(w, err.Error(), ipAddr)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user, userMerged, err := dataprovider.GetUserVariants(username)
|
user, userMerged, err := dataprovider.GetUserVariants(username, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.renderClientTwoFactorRecoveryPage(w, "Invalid credentials", ipAddr)
|
s.renderClientTwoFactorRecoveryPage(w, "Invalid credentials", ipAddr)
|
||||||
return
|
return
|
||||||
|
@ -386,7 +386,7 @@ func (s *httpdServer) handleWebClientTwoFactorPost(w http.ResponseWriter, r *htt
|
||||||
s.renderClientTwoFactorPage(w, err.Error(), ipAddr)
|
s.renderClientTwoFactorPage(w, err.Error(), ipAddr)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user, err := dataprovider.GetUserWithGroupSettings(username)
|
user, err := dataprovider.GetUserWithGroupSettings(username, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.renderClientTwoFactorPage(w, "Invalid credentials", ipAddr)
|
s.renderClientTwoFactorPage(w, "Invalid credentials", ipAddr)
|
||||||
return
|
return
|
||||||
|
@ -719,6 +719,7 @@ func (s *httpdServer) loginAdmin(
|
||||||
c := jwtTokenClaims{
|
c := jwtTokenClaims{
|
||||||
Username: admin.Username,
|
Username: admin.Username,
|
||||||
Permissions: admin.Permissions,
|
Permissions: admin.Permissions,
|
||||||
|
Role: admin.Role,
|
||||||
Signature: admin.GetSignature(),
|
Signature: admin.GetSignature(),
|
||||||
HideUserPageSections: admin.Filters.Preferences.HideUserPageSections,
|
HideUserPageSections: admin.Filters.Preferences.HideUserPageSections,
|
||||||
}
|
}
|
||||||
|
@ -901,6 +902,7 @@ func (s *httpdServer) generateAndSendToken(w http.ResponseWriter, r *http.Reques
|
||||||
c := jwtTokenClaims{
|
c := jwtTokenClaims{
|
||||||
Username: admin.Username,
|
Username: admin.Username,
|
||||||
Permissions: admin.Permissions,
|
Permissions: admin.Permissions,
|
||||||
|
Role: admin.Role,
|
||||||
Signature: admin.GetSignature(),
|
Signature: admin.GetSignature(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -939,7 +941,7 @@ func (s *httpdServer) checkCookieExpiration(w http.ResponseWriter, r *http.Reque
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *httpdServer) refreshClientToken(w http.ResponseWriter, r *http.Request, tokenClaims jwtTokenClaims) {
|
func (s *httpdServer) refreshClientToken(w http.ResponseWriter, r *http.Request, tokenClaims jwtTokenClaims) {
|
||||||
user, err := dataprovider.GetUserWithGroupSettings(tokenClaims.Username)
|
user, err := dataprovider.GetUserWithGroupSettings(tokenClaims.Username, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -976,6 +978,7 @@ func (s *httpdServer) refreshAdminToken(w http.ResponseWriter, r *http.Request,
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
tokenClaims.Permissions = admin.Permissions
|
tokenClaims.Permissions = admin.Permissions
|
||||||
|
tokenClaims.Role = admin.Role
|
||||||
tokenClaims.HideUserPageSections = admin.Filters.Preferences.HideUserPageSections
|
tokenClaims.HideUserPageSections = admin.Filters.Preferences.HideUserPageSections
|
||||||
logger.Debug(logSender, "", "cookie refreshed for admin %#v", admin.Username)
|
logger.Debug(logSender, "", "cookie refreshed for admin %#v", admin.Username)
|
||||||
tokenClaims.createAndSetCookie(w, r, s.tokenAuth, tokenAudienceWebAdmin, ipAddr) //nolint:errcheck
|
tokenClaims.createAndSetCookie(w, r, s.tokenAuth, tokenAudienceWebAdmin, ipAddr) //nolint:errcheck
|
||||||
|
@ -1294,6 +1297,11 @@ func (s *httpdServer) initializeRouter() {
|
||||||
router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Post(eventRulesPath, addEventRule)
|
router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Post(eventRulesPath, addEventRule)
|
||||||
router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Put(eventRulesPath+"/{name}", updateEventRule)
|
router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Put(eventRulesPath+"/{name}", updateEventRule)
|
||||||
router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Delete(eventRulesPath+"/{name}", deleteEventRule)
|
router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Delete(eventRulesPath+"/{name}", deleteEventRule)
|
||||||
|
router.With(s.checkPerm(dataprovider.PermAdminManageRoles)).Get(rolesPath, getRoles)
|
||||||
|
router.With(s.checkPerm(dataprovider.PermAdminManageRoles)).Get(rolesPath+"/{name}", getRoleByName)
|
||||||
|
router.With(s.checkPerm(dataprovider.PermAdminManageRoles)).Post(rolesPath, addRole)
|
||||||
|
router.With(s.checkPerm(dataprovider.PermAdminManageRoles)).Put(rolesPath+"/{name}", updateRole)
|
||||||
|
router.With(s.checkPerm(dataprovider.PermAdminManageRoles)).Delete(rolesPath+"/{name}", deleteRole)
|
||||||
})
|
})
|
||||||
|
|
||||||
s.router.Get(userTokenPath, s.getUserToken)
|
s.router.Get(userTokenPath, s.getUserToken)
|
||||||
|
@ -1640,6 +1648,17 @@ func (s *httpdServer) setupWebAdminRoutes() {
|
||||||
s.handleWebUpdateEventRulePost)
|
s.handleWebUpdateEventRulePost)
|
||||||
router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), verifyCSRFHeader).
|
router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), verifyCSRFHeader).
|
||||||
Delete(webAdminEventRulePath+"/{name}", deleteEventRule)
|
Delete(webAdminEventRulePath+"/{name}", deleteEventRule)
|
||||||
|
router.With(s.checkPerm(dataprovider.PermAdminManageRoles), s.refreshCookie).
|
||||||
|
Get(webAdminRolesPath, s.handleWebGetRoles)
|
||||||
|
router.With(s.checkPerm(dataprovider.PermAdminManageRoles), s.refreshCookie).
|
||||||
|
Get(webAdminRolePath, s.handleWebAddRoleGet)
|
||||||
|
router.With(s.checkPerm(dataprovider.PermAdminManageRoles)).Post(webAdminRolePath, s.handleWebAddRolePost)
|
||||||
|
router.With(s.checkPerm(dataprovider.PermAdminManageRoles), s.refreshCookie).
|
||||||
|
Get(webAdminRolePath+"/{name}", s.handleWebUpdateRoleGet)
|
||||||
|
router.With(s.checkPerm(dataprovider.PermAdminManageRoles)).Post(webAdminRolePath+"/{name}",
|
||||||
|
s.handleWebUpdateRolePost)
|
||||||
|
router.With(s.checkPerm(dataprovider.PermAdminManageRoles), verifyCSRFHeader).
|
||||||
|
Delete(webAdminRolePath+"/{name}", deleteRole)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,6 +84,8 @@ const (
|
||||||
templateEventRule = "eventrule.html"
|
templateEventRule = "eventrule.html"
|
||||||
templateEventActions = "eventactions.html"
|
templateEventActions = "eventactions.html"
|
||||||
templateEventAction = "eventaction.html"
|
templateEventAction = "eventaction.html"
|
||||||
|
templateRoles = "roles.html"
|
||||||
|
templateRole = "role.html"
|
||||||
templateMessage = "message.html"
|
templateMessage = "message.html"
|
||||||
templateStatus = "status.html"
|
templateStatus = "status.html"
|
||||||
templateLogin = "login.html"
|
templateLogin = "login.html"
|
||||||
|
@ -101,6 +103,7 @@ const (
|
||||||
pageGroupsTitle = "Groups"
|
pageGroupsTitle = "Groups"
|
||||||
pageEventRulesTitle = "Event rules"
|
pageEventRulesTitle = "Event rules"
|
||||||
pageEventActionsTitle = "Event actions"
|
pageEventActionsTitle = "Event actions"
|
||||||
|
pageRolesTitle = "Roles"
|
||||||
pageProfileTitle = "My profile"
|
pageProfileTitle = "My profile"
|
||||||
pageChangePwdTitle = "Change password"
|
pageChangePwdTitle = "Change password"
|
||||||
pageMaintenanceTitle = "Maintenance"
|
pageMaintenanceTitle = "Maintenance"
|
||||||
|
@ -140,6 +143,8 @@ type basePage struct {
|
||||||
EventRuleURL string
|
EventRuleURL string
|
||||||
EventActionsURL string
|
EventActionsURL string
|
||||||
EventActionURL string
|
EventActionURL string
|
||||||
|
RolesURL string
|
||||||
|
RoleURL string
|
||||||
FolderQuotaScanURL string
|
FolderQuotaScanURL string
|
||||||
StatusURL string
|
StatusURL string
|
||||||
MaintenanceURL string
|
MaintenanceURL string
|
||||||
|
@ -151,6 +156,7 @@ type basePage struct {
|
||||||
GroupsTitle string
|
GroupsTitle string
|
||||||
EventRulesTitle string
|
EventRulesTitle string
|
||||||
EventActionsTitle string
|
EventActionsTitle string
|
||||||
|
RolesTitle string
|
||||||
StatusTitle string
|
StatusTitle string
|
||||||
MaintenanceTitle string
|
MaintenanceTitle string
|
||||||
DefenderTitle string
|
DefenderTitle string
|
||||||
|
@ -183,6 +189,11 @@ type groupsPage struct {
|
||||||
Groups []dataprovider.Group
|
Groups []dataprovider.Group
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type rolesPage struct {
|
||||||
|
basePage
|
||||||
|
Roles []dataprovider.Role
|
||||||
|
}
|
||||||
|
|
||||||
type eventRulesPage struct {
|
type eventRulesPage struct {
|
||||||
basePage
|
basePage
|
||||||
Rules []dataprovider.EventRule
|
Rules []dataprovider.EventRule
|
||||||
|
@ -226,6 +237,7 @@ type userPage struct {
|
||||||
Mode userPageMode
|
Mode userPageMode
|
||||||
VirtualFolders []vfs.BaseVirtualFolder
|
VirtualFolders []vfs.BaseVirtualFolder
|
||||||
Groups []dataprovider.Group
|
Groups []dataprovider.Group
|
||||||
|
Roles []dataprovider.Role
|
||||||
CanImpersonate bool
|
CanImpersonate bool
|
||||||
FsWrapper fsWrapper
|
FsWrapper fsWrapper
|
||||||
}
|
}
|
||||||
|
@ -234,6 +246,7 @@ type adminPage struct {
|
||||||
basePage
|
basePage
|
||||||
Admin *dataprovider.Admin
|
Admin *dataprovider.Admin
|
||||||
Groups []dataprovider.Group
|
Groups []dataprovider.Group
|
||||||
|
Roles []dataprovider.Role
|
||||||
Error string
|
Error string
|
||||||
IsAdd bool
|
IsAdd bool
|
||||||
}
|
}
|
||||||
|
@ -304,6 +317,13 @@ type groupPage struct {
|
||||||
FsWrapper fsWrapper
|
FsWrapper fsWrapper
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type rolePage struct {
|
||||||
|
basePage
|
||||||
|
Role *dataprovider.Role
|
||||||
|
Error string
|
||||||
|
Mode genericPageMode
|
||||||
|
}
|
||||||
|
|
||||||
type eventActionPage struct {
|
type eventActionPage struct {
|
||||||
basePage
|
basePage
|
||||||
Action dataprovider.BaseEventAction
|
Action dataprovider.BaseEventAction
|
||||||
|
@ -475,6 +495,16 @@ func loadAdminTemplates(templatesPath string) {
|
||||||
filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
|
filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
|
||||||
filepath.Join(templatesPath, templateCommonDir, templateResetPassword),
|
filepath.Join(templatesPath, templateCommonDir, templateResetPassword),
|
||||||
}
|
}
|
||||||
|
rolesPaths := []string{
|
||||||
|
filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
|
||||||
|
filepath.Join(templatesPath, templateAdminDir, templateBase),
|
||||||
|
filepath.Join(templatesPath, templateAdminDir, templateRoles),
|
||||||
|
}
|
||||||
|
rolePaths := []string{
|
||||||
|
filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
|
||||||
|
filepath.Join(templatesPath, templateAdminDir, templateBase),
|
||||||
|
filepath.Join(templatesPath, templateAdminDir, templateRole),
|
||||||
|
}
|
||||||
|
|
||||||
fsBaseTpl := template.New("fsBaseTemplate").Funcs(template.FuncMap{
|
fsBaseTpl := template.New("fsBaseTemplate").Funcs(template.FuncMap{
|
||||||
"ListFSProviders": func() []sdk.FilesystemProvider {
|
"ListFSProviders": func() []sdk.FilesystemProvider {
|
||||||
|
@ -511,6 +541,8 @@ func loadAdminTemplates(templatesPath string) {
|
||||||
setupTmpl := util.LoadTemplate(nil, setupPaths...)
|
setupTmpl := util.LoadTemplate(nil, setupPaths...)
|
||||||
forgotPwdTmpl := util.LoadTemplate(nil, forgotPwdPaths...)
|
forgotPwdTmpl := util.LoadTemplate(nil, forgotPwdPaths...)
|
||||||
resetPwdTmpl := util.LoadTemplate(nil, resetPwdPaths...)
|
resetPwdTmpl := util.LoadTemplate(nil, resetPwdPaths...)
|
||||||
|
rolesTmpl := util.LoadTemplate(nil, rolesPaths...)
|
||||||
|
roleTmpl := util.LoadTemplate(nil, rolePaths...)
|
||||||
|
|
||||||
adminTemplates[templateUsers] = usersTmpl
|
adminTemplates[templateUsers] = usersTmpl
|
||||||
adminTemplates[templateUser] = userTmpl
|
adminTemplates[templateUser] = userTmpl
|
||||||
|
@ -538,6 +570,8 @@ func loadAdminTemplates(templatesPath string) {
|
||||||
adminTemplates[templateSetup] = setupTmpl
|
adminTemplates[templateSetup] = setupTmpl
|
||||||
adminTemplates[templateForgotPassword] = forgotPwdTmpl
|
adminTemplates[templateForgotPassword] = forgotPwdTmpl
|
||||||
adminTemplates[templateResetPassword] = resetPwdTmpl
|
adminTemplates[templateResetPassword] = resetPwdTmpl
|
||||||
|
adminTemplates[templateRoles] = rolesTmpl
|
||||||
|
adminTemplates[templateRole] = roleTmpl
|
||||||
}
|
}
|
||||||
|
|
||||||
func isEventManagerResource(currentURL string) bool {
|
func isEventManagerResource(currentURL string) bool {
|
||||||
|
@ -583,6 +617,8 @@ func (s *httpdServer) getBasePageData(title, currentURL string, r *http.Request)
|
||||||
EventRuleURL: webAdminEventRulePath,
|
EventRuleURL: webAdminEventRulePath,
|
||||||
EventActionsURL: webAdminEventActionsPath,
|
EventActionsURL: webAdminEventActionsPath,
|
||||||
EventActionURL: webAdminEventActionPath,
|
EventActionURL: webAdminEventActionPath,
|
||||||
|
RolesURL: webAdminRolesPath,
|
||||||
|
RoleURL: webAdminRolePath,
|
||||||
QuotaScanURL: webQuotaScanPath,
|
QuotaScanURL: webQuotaScanPath,
|
||||||
ConnectionsURL: webConnectionsPath,
|
ConnectionsURL: webConnectionsPath,
|
||||||
StatusURL: webStatusPath,
|
StatusURL: webStatusPath,
|
||||||
|
@ -596,6 +632,7 @@ func (s *httpdServer) getBasePageData(title, currentURL string, r *http.Request)
|
||||||
GroupsTitle: pageGroupsTitle,
|
GroupsTitle: pageGroupsTitle,
|
||||||
EventRulesTitle: pageEventRulesTitle,
|
EventRulesTitle: pageEventRulesTitle,
|
||||||
EventActionsTitle: pageEventActionsTitle,
|
EventActionsTitle: pageEventActionsTitle,
|
||||||
|
RolesTitle: pageRolesTitle,
|
||||||
StatusTitle: pageStatusTitle,
|
StatusTitle: pageStatusTitle,
|
||||||
MaintenanceTitle: pageMaintenanceTitle,
|
MaintenanceTitle: pageMaintenanceTitle,
|
||||||
DefenderTitle: pageDefenderTitle,
|
DefenderTitle: pageDefenderTitle,
|
||||||
|
@ -774,6 +811,10 @@ func (s *httpdServer) renderAddUpdateAdminPage(w http.ResponseWriter, r *http.Re
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
roles, err := s.getWebRoles(w, r, 10, true)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
currentURL := webAdminPath
|
currentURL := webAdminPath
|
||||||
title := "Add a new admin"
|
title := "Add a new admin"
|
||||||
if !isAdd {
|
if !isAdd {
|
||||||
|
@ -784,6 +825,7 @@ func (s *httpdServer) renderAddUpdateAdminPage(w http.ResponseWriter, r *http.Re
|
||||||
basePage: s.getBasePageData(title, currentURL, r),
|
basePage: s.getBasePageData(title, currentURL, r),
|
||||||
Admin: admin,
|
Admin: admin,
|
||||||
Groups: groups,
|
Groups: groups,
|
||||||
|
Roles: roles,
|
||||||
Error: error,
|
Error: error,
|
||||||
IsAdd: isAdd,
|
IsAdd: isAdd,
|
||||||
}
|
}
|
||||||
|
@ -791,18 +833,7 @@ func (s *httpdServer) renderAddUpdateAdminPage(w http.ResponseWriter, r *http.Re
|
||||||
renderAdminTemplate(w, templateAdmin, data)
|
renderAdminTemplate(w, templateAdmin, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *httpdServer) renderUserPage(w http.ResponseWriter, r *http.Request, user *dataprovider.User,
|
func (s *httpdServer) getUserPageTitleAndURL(mode userPageMode, username string) (string, string) {
|
||||||
mode userPageMode, error string, admin *dataprovider.Admin,
|
|
||||||
) {
|
|
||||||
folders, err := s.getWebVirtualFolders(w, r, defaultQueryLimit, true)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
groups, err := s.getWebGroups(w, r, defaultQueryLimit, true)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
user.SetEmptySecretsIfNil()
|
|
||||||
var title, currentURL string
|
var title, currentURL string
|
||||||
switch mode {
|
switch mode {
|
||||||
case userPageModeAdd:
|
case userPageModeAdd:
|
||||||
|
@ -810,11 +841,19 @@ func (s *httpdServer) renderUserPage(w http.ResponseWriter, r *http.Request, use
|
||||||
currentURL = webUserPath
|
currentURL = webUserPath
|
||||||
case userPageModeUpdate:
|
case userPageModeUpdate:
|
||||||
title = "Update user"
|
title = "Update user"
|
||||||
currentURL = fmt.Sprintf("%v/%v", webUserPath, url.PathEscape(user.Username))
|
currentURL = fmt.Sprintf("%v/%v", webUserPath, url.PathEscape(username))
|
||||||
case userPageModeTemplate:
|
case userPageModeTemplate:
|
||||||
title = "User template"
|
title = "User template"
|
||||||
currentURL = webTemplateUser
|
currentURL = webTemplateUser
|
||||||
}
|
}
|
||||||
|
return title, currentURL
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *httpdServer) renderUserPage(w http.ResponseWriter, r *http.Request, user *dataprovider.User,
|
||||||
|
mode userPageMode, errorString string, admin *dataprovider.Admin,
|
||||||
|
) {
|
||||||
|
user.SetEmptySecretsIfNil()
|
||||||
|
title, currentURL := s.getUserPageTitleAndURL(mode, user.Username)
|
||||||
if user.Password != "" && user.IsPasswordHashed() {
|
if user.Password != "" && user.IsPasswordHashed() {
|
||||||
switch mode {
|
switch mode {
|
||||||
case userPageModeUpdate:
|
case userPageModeUpdate:
|
||||||
|
@ -833,10 +872,26 @@ func (s *httpdServer) renderUserPage(w http.ResponseWriter, r *http.Request, use
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
var roles []dataprovider.Role
|
||||||
|
if basePage.LoggedAdmin.Role == "" {
|
||||||
|
var err error
|
||||||
|
roles, err = s.getWebRoles(w, r, 10, true)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
folders, err := s.getWebVirtualFolders(w, r, defaultQueryLimit, true)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
groups, err := s.getWebGroups(w, r, defaultQueryLimit, true)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
data := userPage{
|
data := userPage{
|
||||||
basePage: basePage,
|
basePage: basePage,
|
||||||
Mode: mode,
|
Mode: mode,
|
||||||
Error: error,
|
Error: errorString,
|
||||||
User: user,
|
User: user,
|
||||||
ValidPerms: dataprovider.ValidPerms,
|
ValidPerms: dataprovider.ValidPerms,
|
||||||
ValidLoginMethods: dataprovider.ValidLoginMethods,
|
ValidLoginMethods: dataprovider.ValidLoginMethods,
|
||||||
|
@ -846,6 +901,7 @@ func (s *httpdServer) renderUserPage(w http.ResponseWriter, r *http.Request, use
|
||||||
RootDirPerms: user.GetPermissionsForPath("/"),
|
RootDirPerms: user.GetPermissionsForPath("/"),
|
||||||
VirtualFolders: folders,
|
VirtualFolders: folders,
|
||||||
Groups: groups,
|
Groups: groups,
|
||||||
|
Roles: roles,
|
||||||
CanImpersonate: os.Getuid() == 0,
|
CanImpersonate: os.Getuid() == 0,
|
||||||
FsWrapper: fsWrapper{
|
FsWrapper: fsWrapper{
|
||||||
Filesystem: user.FsConfig,
|
Filesystem: user.FsConfig,
|
||||||
|
@ -859,6 +915,27 @@ func (s *httpdServer) renderUserPage(w http.ResponseWriter, r *http.Request, use
|
||||||
renderAdminTemplate(w, templateUser, data)
|
renderAdminTemplate(w, templateUser, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *httpdServer) renderRolePage(w http.ResponseWriter, r *http.Request, role dataprovider.Role,
|
||||||
|
mode genericPageMode, error string,
|
||||||
|
) {
|
||||||
|
var title, currentURL string
|
||||||
|
switch mode {
|
||||||
|
case genericPageModeAdd:
|
||||||
|
title = "Add a new role"
|
||||||
|
currentURL = webAdminRolePath
|
||||||
|
case genericPageModeUpdate:
|
||||||
|
title = "Update role"
|
||||||
|
currentURL = fmt.Sprintf("%s/%s", webAdminRolePath, url.PathEscape(role.Name))
|
||||||
|
}
|
||||||
|
data := rolePage{
|
||||||
|
basePage: s.getBasePageData(title, currentURL, r),
|
||||||
|
Error: error,
|
||||||
|
Role: &role,
|
||||||
|
Mode: mode,
|
||||||
|
}
|
||||||
|
renderAdminTemplate(w, templateRole, data)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *httpdServer) renderGroupPage(w http.ResponseWriter, r *http.Request, group dataprovider.Group,
|
func (s *httpdServer) renderGroupPage(w http.ResponseWriter, r *http.Request, group dataprovider.Group,
|
||||||
mode genericPageMode, error string,
|
mode genericPageMode, error string,
|
||||||
) {
|
) {
|
||||||
|
@ -1577,6 +1654,7 @@ func getAdminFromPostFields(r *http.Request) (dataprovider.Admin, error) {
|
||||||
admin.Permissions = r.Form["permissions"]
|
admin.Permissions = r.Form["permissions"]
|
||||||
admin.Email = r.Form.Get("email")
|
admin.Email = r.Form.Get("email")
|
||||||
admin.Status = status
|
admin.Status = status
|
||||||
|
admin.Role = r.Form.Get("role")
|
||||||
admin.Filters.AllowList = getSliceFromDelimitedValues(r.Form.Get("allowed_ip"), ",")
|
admin.Filters.AllowList = getSliceFromDelimitedValues(r.Form.Get("allowed_ip"), ",")
|
||||||
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")
|
||||||
|
@ -1843,6 +1921,7 @@ func getUserFromPostFields(r *http.Request) (dataprovider.User, error) {
|
||||||
ExpirationDate: expirationDateMillis,
|
ExpirationDate: expirationDateMillis,
|
||||||
AdditionalInfo: r.Form.Get("additional_info"),
|
AdditionalInfo: r.Form.Get("additional_info"),
|
||||||
Description: r.Form.Get("description"),
|
Description: r.Form.Get("description"),
|
||||||
|
Role: r.Form.Get("role"),
|
||||||
},
|
},
|
||||||
Filters: dataprovider.UserFilters{
|
Filters: dataprovider.UserFilters{
|
||||||
BaseUserFilters: filters,
|
BaseUserFilters: filters,
|
||||||
|
@ -2223,6 +2302,18 @@ func getEventRuleFromPostFields(r *http.Request) (dataprovider.EventRule, error)
|
||||||
return rule, nil
|
return rule, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getRoleFromPostFields(r *http.Request) (dataprovider.Role, error) {
|
||||||
|
err := r.ParseForm()
|
||||||
|
if err != nil {
|
||||||
|
return dataprovider.Role{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return dataprovider.Role{
|
||||||
|
Name: r.Form.Get("name"),
|
||||||
|
Description: r.Form.Get("description"),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *httpdServer) handleWebAdminForgotPwd(w http.ResponseWriter, r *http.Request) {
|
func (s *httpdServer) handleWebAdminForgotPwd(w http.ResponseWriter, r *http.Request) {
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
if !smtp.IsEnabled() {
|
if !smtp.IsEnabled() {
|
||||||
|
@ -2515,10 +2606,14 @@ func (s *httpdServer) handleWebUpdateAdminPost(w http.ResponseWriter, r *http.Re
|
||||||
s.renderAddUpdateAdminPage(w, r, &updatedAdmin, "You cannot disable yourself", false)
|
s.renderAddUpdateAdminPage(w, r, &updatedAdmin, "You cannot disable yourself", false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if updatedAdmin.Role != claims.Role {
|
||||||
|
s.renderAddUpdateAdminPage(w, r, &updatedAdmin, "You cannot add/change your role", false)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
err = dataprovider.UpdateAdmin(&updatedAdmin, claims.Username, ipAddr)
|
err = dataprovider.UpdateAdmin(&updatedAdmin, claims.Username, ipAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.renderAddUpdateAdminPage(w, r, &admin, err.Error(), false)
|
s.renderAddUpdateAdminPage(w, r, &updatedAdmin, err.Error(), false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
http.Redirect(w, r, webAdminsPath, http.StatusSeeOther)
|
http.Redirect(w, r, webAdminsPath, http.StatusSeeOther)
|
||||||
|
@ -2536,6 +2631,11 @@ func (s *httpdServer) handleWebDefenderPage(w http.ResponseWriter, r *http.Reque
|
||||||
|
|
||||||
func (s *httpdServer) handleGetWebUsers(w http.ResponseWriter, r *http.Request) {
|
func (s *httpdServer) handleGetWebUsers(w http.ResponseWriter, r *http.Request) {
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
|
claims, err := getTokenClaims(r)
|
||||||
|
if err != nil || claims.Username == "" {
|
||||||
|
s.renderBadRequestPage(w, r, errors.New("invalid token claims"))
|
||||||
|
return
|
||||||
|
}
|
||||||
var limit int
|
var limit int
|
||||||
if _, ok := r.URL.Query()["qlimit"]; ok {
|
if _, ok := r.URL.Query()["qlimit"]; ok {
|
||||||
var err error
|
var err error
|
||||||
|
@ -2548,7 +2648,7 @@ func (s *httpdServer) handleGetWebUsers(w http.ResponseWriter, r *http.Request)
|
||||||
}
|
}
|
||||||
users := make([]dataprovider.User, 0, limit)
|
users := make([]dataprovider.User, 0, limit)
|
||||||
for {
|
for {
|
||||||
u, err := dataprovider.GetUsers(limit, len(users), dataprovider.OrderASC)
|
u, err := dataprovider.GetUsers(limit, len(users), dataprovider.OrderASC, claims.Role)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.renderInternalServerErrorPage(w, r, err)
|
s.renderInternalServerErrorPage(w, r, err)
|
||||||
return
|
return
|
||||||
|
@ -2657,7 +2757,7 @@ func (s *httpdServer) handleWebTemplateUserGet(w http.ResponseWriter, r *http.Re
|
||||||
}
|
}
|
||||||
if r.URL.Query().Get("from") != "" {
|
if r.URL.Query().Get("from") != "" {
|
||||||
username := r.URL.Query().Get("from")
|
username := r.URL.Query().Get("from")
|
||||||
user, err := dataprovider.UserExists(username)
|
user, err := dataprovider.UserExists(username, admin.Role)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
user.SetEmptySecrets()
|
user.SetEmptySecrets()
|
||||||
user.PublicKeys = nil
|
user.PublicKeys = nil
|
||||||
|
@ -2715,6 +2815,8 @@ func (s *httpdServer) handleWebTemplateUserPost(w http.ResponseWriter, r *http.R
|
||||||
http.StatusBadRequest, err, "")
|
http.StatusBadRequest, err, "")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// to create a template the "manage_system" permission is required, so role admins cannot use
|
||||||
|
// this method, we don't need to force the role
|
||||||
dump.Users = append(dump.Users, u)
|
dump.Users = append(dump.Users, u)
|
||||||
for _, folder := range u.VirtualFolders {
|
for _, folder := range u.VirtualFolders {
|
||||||
if !dump.HasFolder(folder.Name) {
|
if !dump.HasFolder(folder.Name) {
|
||||||
|
@ -2764,8 +2866,13 @@ func (s *httpdServer) handleWebAddUserGet(w http.ResponseWriter, r *http.Request
|
||||||
|
|
||||||
func (s *httpdServer) handleWebUpdateUserGet(w http.ResponseWriter, r *http.Request) {
|
func (s *httpdServer) handleWebUpdateUserGet(w http.ResponseWriter, r *http.Request) {
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
|
claims, err := getTokenClaims(r)
|
||||||
|
if err != nil || claims.Username == "" {
|
||||||
|
s.renderBadRequestPage(w, r, errors.New("invalid token claims"))
|
||||||
|
return
|
||||||
|
}
|
||||||
username := getURLParam(r, "username")
|
username := getURLParam(r, "username")
|
||||||
user, err := dataprovider.UserExists(username)
|
user, err := dataprovider.UserExists(username, claims.Role)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
s.renderUserPage(w, r, &user, userPageModeUpdate, "", nil)
|
s.renderUserPage(w, r, &user, userPageModeUpdate, "", nil)
|
||||||
} else if _, ok := err.(*util.RecordNotFoundError); ok {
|
} else if _, ok := err.(*util.RecordNotFoundError); ok {
|
||||||
|
@ -2797,6 +2904,13 @@ func (s *httpdServer) handleWebAddUserPost(w http.ResponseWriter, r *http.Reques
|
||||||
Password: user.Password,
|
Password: user.Password,
|
||||||
PublicKeys: user.PublicKeys,
|
PublicKeys: user.PublicKeys,
|
||||||
})
|
})
|
||||||
|
if claims.Role != "" {
|
||||||
|
user.Role = claims.Role
|
||||||
|
}
|
||||||
|
user.Filters.RecoveryCodes = nil
|
||||||
|
user.Filters.TOTPConfig = dataprovider.UserTOTPConfig{
|
||||||
|
Enabled: false,
|
||||||
|
}
|
||||||
err = dataprovider.AddUser(&user, claims.Username, ipAddr)
|
err = dataprovider.AddUser(&user, claims.Username, ipAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.renderUserPage(w, r, &user, userPageModeAdd, err.Error(), nil)
|
s.renderUserPage(w, r, &user, userPageModeAdd, err.Error(), nil)
|
||||||
|
@ -2813,7 +2927,7 @@ func (s *httpdServer) handleWebUpdateUserPost(w http.ResponseWriter, r *http.Req
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
username := getURLParam(r, "username")
|
username := getURLParam(r, "username")
|
||||||
user, err := dataprovider.UserExists(username)
|
user, err := dataprovider.UserExists(username, claims.Role)
|
||||||
if _, ok := err.(*util.RecordNotFoundError); ok {
|
if _, ok := err.(*util.RecordNotFoundError); ok {
|
||||||
s.renderNotFoundPage(w, r, err)
|
s.renderNotFoundPage(w, r, err)
|
||||||
return
|
return
|
||||||
|
@ -2849,6 +2963,9 @@ func (s *httpdServer) handleWebUpdateUserPost(w http.ResponseWriter, r *http.Req
|
||||||
Password: updatedUser.Password,
|
Password: updatedUser.Password,
|
||||||
PublicKeys: updatedUser.PublicKeys,
|
PublicKeys: updatedUser.PublicKeys,
|
||||||
})
|
})
|
||||||
|
if claims.Role != "" {
|
||||||
|
updatedUser.Role = claims.Role
|
||||||
|
}
|
||||||
|
|
||||||
err = dataprovider.UpdateUser(&updatedUser, claims.Username, ipAddr)
|
err = dataprovider.UpdateUser(&updatedUser, claims.Username, ipAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -2877,8 +2994,8 @@ func (s *httpdServer) handleWebGetConnections(w http.ResponseWriter, r *http.Req
|
||||||
s.renderBadRequestPage(w, r, errors.New("invalid token claims"))
|
s.renderBadRequestPage(w, r, errors.New("invalid token claims"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
connectionStats := common.Connections.GetStats()
|
connectionStats := common.Connections.GetStats(claims.Role)
|
||||||
connectionStats = append(connectionStats, getNodesConnections(claims.Username)...)
|
connectionStats = append(connectionStats, getNodesConnections(claims.Username, claims.Role)...)
|
||||||
data := connectionsPage{
|
data := connectionsPage{
|
||||||
basePage: s.getBasePageData(pageConnectionsTitle, webConnectionsPath, r),
|
basePage: s.getBasePageData(pageConnectionsTitle, webConnectionsPath, r),
|
||||||
Connections: connectionStats,
|
Connections: connectionStats,
|
||||||
|
@ -3400,3 +3517,110 @@ func (s *httpdServer) handleWebUpdateEventRulePost(w http.ResponseWriter, r *htt
|
||||||
}
|
}
|
||||||
http.Redirect(w, r, webAdminEventRulesPath, http.StatusSeeOther)
|
http.Redirect(w, r, webAdminEventRulesPath, http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *httpdServer) getWebRoles(w http.ResponseWriter, r *http.Request, limit int, minimal bool) ([]dataprovider.Role, error) {
|
||||||
|
roles := make([]dataprovider.Role, 0, limit)
|
||||||
|
for {
|
||||||
|
res, err := dataprovider.GetRoles(limit, len(roles), dataprovider.OrderASC, minimal)
|
||||||
|
if err != nil {
|
||||||
|
s.renderInternalServerErrorPage(w, r, err)
|
||||||
|
return roles, err
|
||||||
|
}
|
||||||
|
roles = append(roles, res...)
|
||||||
|
if len(res) < limit {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return roles, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *httpdServer) handleWebGetRoles(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
|
roles, err := s.getWebRoles(w, r, 10, false)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data := rolesPage{
|
||||||
|
basePage: s.getBasePageData(pageRolesTitle, webAdminRolesPath, r),
|
||||||
|
Roles: roles,
|
||||||
|
}
|
||||||
|
renderAdminTemplate(w, templateRoles, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *httpdServer) handleWebAddRoleGet(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
|
s.renderRolePage(w, r, dataprovider.Role{}, genericPageModeAdd, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *httpdServer) handleWebAddRolePost(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
|
role, err := getRoleFromPostFields(r)
|
||||||
|
if err != nil {
|
||||||
|
s.renderRolePage(w, r, role, genericPageModeAdd, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
claims, err := getTokenClaims(r)
|
||||||
|
if err != nil || claims.Username == "" {
|
||||||
|
s.renderBadRequestPage(w, r, errors.New("invalid token claims"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||||
|
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
|
||||||
|
s.renderForbiddenPage(w, r, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = dataprovider.AddRole(&role, claims.Username, ipAddr)
|
||||||
|
if err != nil {
|
||||||
|
s.renderRolePage(w, r, role, genericPageModeAdd, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.Redirect(w, r, webAdminRolesPath, http.StatusSeeOther)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *httpdServer) handleWebUpdateRoleGet(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
|
role, err := dataprovider.RoleExists(getURLParam(r, "name"))
|
||||||
|
if err == nil {
|
||||||
|
s.renderRolePage(w, r, role, genericPageModeUpdate, "")
|
||||||
|
} else if _, ok := err.(*util.RecordNotFoundError); ok {
|
||||||
|
s.renderNotFoundPage(w, r, err)
|
||||||
|
} else {
|
||||||
|
s.renderInternalServerErrorPage(w, r, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *httpdServer) handleWebUpdateRolePost(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||||
|
claims, err := getTokenClaims(r)
|
||||||
|
if err != nil || claims.Username == "" {
|
||||||
|
s.renderBadRequestPage(w, r, errors.New("invalid token claims"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
role, err := dataprovider.RoleExists(getURLParam(r, "name"))
|
||||||
|
if _, ok := err.(*util.RecordNotFoundError); ok {
|
||||||
|
s.renderNotFoundPage(w, r, err)
|
||||||
|
return
|
||||||
|
} else if err != nil {
|
||||||
|
s.renderInternalServerErrorPage(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedRole, err := getRoleFromPostFields(r)
|
||||||
|
if err != nil {
|
||||||
|
s.renderRolePage(w, r, role, genericPageModeAdd, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||||
|
if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil {
|
||||||
|
s.renderForbiddenPage(w, r, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updatedRole.ID = role.ID
|
||||||
|
updatedRole.Name = role.Name
|
||||||
|
err = dataprovider.UpdateRole(&updatedRole, claims.Username, ipAddr)
|
||||||
|
if err != nil {
|
||||||
|
s.renderRolePage(w, r, updatedRole, genericPageModeUpdate, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.Redirect(w, r, webAdminRolesPath, http.StatusSeeOther)
|
||||||
|
}
|
||||||
|
|
|
@ -472,7 +472,7 @@ func (s *httpdServer) renderClientMFAPage(w http.ResponseWriter, r *http.Request
|
||||||
RecCodesURL: webClientRecoveryCodesPath,
|
RecCodesURL: webClientRecoveryCodesPath,
|
||||||
Protocols: dataprovider.MFAProtocols,
|
Protocols: dataprovider.MFAProtocols,
|
||||||
}
|
}
|
||||||
user, err := dataprovider.UserExists(data.LoggedUser.Username)
|
user, err := dataprovider.UserExists(data.LoggedUser.Username, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.renderInternalServerErrorPage(w, r, err)
|
s.renderInternalServerErrorPage(w, r, err)
|
||||||
return
|
return
|
||||||
|
@ -590,7 +590,7 @@ func (s *httpdServer) renderClientProfilePage(w http.ResponseWriter, r *http.Req
|
||||||
baseClientPage: s.getBaseClientPageData(pageClientProfileTitle, webClientProfilePath, r),
|
baseClientPage: s.getBaseClientPageData(pageClientProfileTitle, webClientProfilePath, r),
|
||||||
Error: error,
|
Error: error,
|
||||||
}
|
}
|
||||||
user, userMerged, err := dataprovider.GetUserVariants(data.LoggedUser.Username)
|
user, userMerged, err := dataprovider.GetUserVariants(data.LoggedUser.Username, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.renderClientInternalServerErrorPage(w, r, err)
|
s.renderClientInternalServerErrorPage(w, r, err)
|
||||||
return
|
return
|
||||||
|
@ -620,7 +620,7 @@ func (s *httpdServer) handleWebClientDownloadZip(w http.ResponseWriter, r *http.
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := dataprovider.GetUserWithGroupSettings(claims.Username)
|
user, err := dataprovider.GetUserWithGroupSettings(claims.Username, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.renderClientMessagePage(w, r, "Unable to retrieve your user", "", getRespStatus(err), nil, "")
|
s.renderClientMessagePage(w, r, "Unable to retrieve your user", "", getRespStatus(err), nil, "")
|
||||||
return
|
return
|
||||||
|
@ -820,7 +820,7 @@ func (s *httpdServer) handleClientGetDirContents(w http.ResponseWriter, r *http.
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := dataprovider.GetUserWithGroupSettings(claims.Username)
|
user, err := dataprovider.GetUserWithGroupSettings(claims.Username, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendAPIResponse(w, r, nil, "Unable to retrieve your user", getRespStatus(err))
|
sendAPIResponse(w, r, nil, "Unable to retrieve your user", getRespStatus(err))
|
||||||
return
|
return
|
||||||
|
@ -897,7 +897,7 @@ func (s *httpdServer) handleClientGetFiles(w http.ResponseWriter, r *http.Reques
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := dataprovider.GetUserWithGroupSettings(claims.Username)
|
user, err := dataprovider.GetUserWithGroupSettings(claims.Username, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.renderClientMessagePage(w, r, "Unable to retrieve your user", "", getRespStatus(err), nil, "")
|
s.renderClientMessagePage(w, r, "Unable to retrieve your user", "", getRespStatus(err), nil, "")
|
||||||
return
|
return
|
||||||
|
@ -956,7 +956,7 @@ func (s *httpdServer) handleClientEditFile(w http.ResponseWriter, r *http.Reques
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := dataprovider.GetUserWithGroupSettings(claims.Username)
|
user, err := dataprovider.GetUserWithGroupSettings(claims.Username, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.renderClientMessagePage(w, r, "Unable to retrieve your user", "", getRespStatus(err), nil, "")
|
s.renderClientMessagePage(w, r, "Unable to retrieve your user", "", getRespStatus(err), nil, "")
|
||||||
return
|
return
|
||||||
|
@ -1025,7 +1025,7 @@ func (s *httpdServer) handleClientAddShareGet(w http.ResponseWriter, r *http.Req
|
||||||
s.renderClientForbiddenPage(w, r, "Invalid token claims")
|
s.renderClientForbiddenPage(w, r, "Invalid token claims")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user, err := dataprovider.GetUserWithGroupSettings(claims.Username)
|
user, err := dataprovider.GetUserWithGroupSettings(claims.Username, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.renderClientMessagePage(w, r, "Unable to retrieve your user", "", getRespStatus(err), nil, "")
|
s.renderClientMessagePage(w, r, "Unable to retrieve your user", "", getRespStatus(err), nil, "")
|
||||||
return
|
return
|
||||||
|
@ -1218,7 +1218,7 @@ func (s *httpdServer) handleWebClientProfilePost(w http.ResponseWriter, r *http.
|
||||||
s.renderClientForbiddenPage(w, r, "Invalid token claims")
|
s.renderClientForbiddenPage(w, r, "Invalid token claims")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user, userMerged, err := dataprovider.GetUserVariants(claims.Username)
|
user, userMerged, err := dataprovider.GetUserVariants(claims.Username, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.renderClientProfilePage(w, r, err.Error())
|
s.renderClientProfilePage(w, r, err.Error())
|
||||||
return
|
return
|
||||||
|
@ -1368,7 +1368,7 @@ func (s *httpdServer) handleClientGetPDF(w http.ResponseWriter, r *http.Request)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
name = util.CleanPath(name)
|
name = util.CleanPath(name)
|
||||||
user, err := dataprovider.GetUserWithGroupSettings(claims.Username)
|
user, err := dataprovider.GetUserWithGroupSettings(claims.Username, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.renderClientMessagePage(w, r, "Unable to retrieve your user", "", getRespStatus(err), nil, "")
|
s.renderClientMessagePage(w, r, "Unable to retrieve your user", "", getRespStatus(err), nil, "")
|
||||||
return
|
return
|
||||||
|
|
|
@ -62,6 +62,7 @@ const (
|
||||||
retentionChecksPath = "/api/v2/retention/users/checks"
|
retentionChecksPath = "/api/v2/retention/users/checks"
|
||||||
eventActionsPath = "/api/v2/eventactions"
|
eventActionsPath = "/api/v2/eventactions"
|
||||||
eventRulesPath = "/api/v2/eventrules"
|
eventRulesPath = "/api/v2/eventrules"
|
||||||
|
rolesPath = "/api/v2/roles"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -368,6 +369,115 @@ func GetGroups(limit, offset int64, expectedStatusCode int) ([]dataprovider.Grou
|
||||||
return groups, body, err
|
return groups, body, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddRole adds a new role and checks the received HTTP Status code against expectedStatusCode.
|
||||||
|
func AddRole(role dataprovider.Role, expectedStatusCode int) (dataprovider.Role, []byte, error) {
|
||||||
|
var newRole dataprovider.Role
|
||||||
|
var body []byte
|
||||||
|
asJSON, _ := json.Marshal(role)
|
||||||
|
resp, err := sendHTTPRequest(http.MethodPost, buildURLRelativeToBase(rolesPath), bytes.NewBuffer(asJSON),
|
||||||
|
"application/json", getDefaultToken())
|
||||||
|
if err != nil {
|
||||||
|
return newRole, body, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
err = checkResponse(resp.StatusCode, expectedStatusCode)
|
||||||
|
if expectedStatusCode != http.StatusCreated {
|
||||||
|
body, _ = getResponseBody(resp)
|
||||||
|
return newRole, body, err
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
err = render.DecodeJSON(resp.Body, &newRole)
|
||||||
|
} else {
|
||||||
|
body, _ = getResponseBody(resp)
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
err = checkRole(role, newRole)
|
||||||
|
}
|
||||||
|
return newRole, body, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateRole updates an existing role and checks the received HTTP Status code against expectedStatusCode
|
||||||
|
func UpdateRole(role dataprovider.Role, expectedStatusCode int) (dataprovider.Role, []byte, error) {
|
||||||
|
var newRole dataprovider.Role
|
||||||
|
var body []byte
|
||||||
|
|
||||||
|
asJSON, _ := json.Marshal(role)
|
||||||
|
resp, err := sendHTTPRequest(http.MethodPut, buildURLRelativeToBase(rolesPath, url.PathEscape(role.Name)),
|
||||||
|
bytes.NewBuffer(asJSON), "application/json", getDefaultToken())
|
||||||
|
if err != nil {
|
||||||
|
return newRole, body, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, _ = getResponseBody(resp)
|
||||||
|
err = checkResponse(resp.StatusCode, expectedStatusCode)
|
||||||
|
if expectedStatusCode != http.StatusOK {
|
||||||
|
return newRole, body, err
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
newRole, body, err = GetRoleByName(role.Name, expectedStatusCode)
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
err = checkRole(role, newRole)
|
||||||
|
}
|
||||||
|
return newRole, body, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveRole removes an existing role and checks the received HTTP Status code against expectedStatusCode.
|
||||||
|
func RemoveRole(role dataprovider.Role, expectedStatusCode int) ([]byte, error) {
|
||||||
|
var body []byte
|
||||||
|
resp, err := sendHTTPRequest(http.MethodDelete, buildURLRelativeToBase(rolesPath, url.PathEscape(role.Name)),
|
||||||
|
nil, "", getDefaultToken())
|
||||||
|
if err != nil {
|
||||||
|
return body, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, _ = getResponseBody(resp)
|
||||||
|
return body, checkResponse(resp.StatusCode, expectedStatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRoleByName gets a role by name and checks the received HTTP Status code against expectedStatusCode.
|
||||||
|
func GetRoleByName(name string, expectedStatusCode int) (dataprovider.Role, []byte, error) {
|
||||||
|
var role dataprovider.Role
|
||||||
|
var body []byte
|
||||||
|
resp, err := sendHTTPRequest(http.MethodGet, buildURLRelativeToBase(rolesPath, url.PathEscape(name)),
|
||||||
|
nil, "", getDefaultToken())
|
||||||
|
if err != nil {
|
||||||
|
return role, body, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
err = checkResponse(resp.StatusCode, expectedStatusCode)
|
||||||
|
if err == nil && expectedStatusCode == http.StatusOK {
|
||||||
|
err = render.DecodeJSON(resp.Body, &role)
|
||||||
|
} else {
|
||||||
|
body, _ = getResponseBody(resp)
|
||||||
|
}
|
||||||
|
return role, body, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRoles returns a list of roles and checks the received HTTP Status code against expectedStatusCode.
|
||||||
|
// The number of results can be limited specifying a limit.
|
||||||
|
// Some results can be skipped specifying an offset.
|
||||||
|
func GetRoles(limit, offset int64, expectedStatusCode int) ([]dataprovider.Role, []byte, error) {
|
||||||
|
var roles []dataprovider.Role
|
||||||
|
var body []byte
|
||||||
|
url, err := addLimitAndOffsetQueryParams(buildURLRelativeToBase(rolesPath), limit, offset)
|
||||||
|
if err != nil {
|
||||||
|
return roles, body, err
|
||||||
|
}
|
||||||
|
resp, err := sendHTTPRequest(http.MethodGet, url.String(), nil, "", getDefaultToken())
|
||||||
|
if err != nil {
|
||||||
|
return roles, body, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
err = checkResponse(resp.StatusCode, expectedStatusCode)
|
||||||
|
if err == nil && expectedStatusCode == http.StatusOK {
|
||||||
|
err = render.DecodeJSON(resp.Body, &roles)
|
||||||
|
} else {
|
||||||
|
body, _ = getResponseBody(resp)
|
||||||
|
}
|
||||||
|
return roles, body, err
|
||||||
|
}
|
||||||
|
|
||||||
// AddAdmin adds a new admin and checks the received HTTP Status code against expectedStatusCode.
|
// AddAdmin adds a new admin and checks the received HTTP Status code against expectedStatusCode.
|
||||||
func AddAdmin(admin dataprovider.Admin, expectedStatusCode int) (dataprovider.Admin, []byte, error) {
|
func AddAdmin(admin dataprovider.Admin, expectedStatusCode int) (dataprovider.Admin, []byte, error) {
|
||||||
var newAdmin dataprovider.Admin
|
var newAdmin dataprovider.Admin
|
||||||
|
@ -1503,6 +1613,31 @@ func checkEventRule(expected, actual dataprovider.EventRule) error {
|
||||||
return checkEventRuleActions(expected.Actions, actual.Actions)
|
return checkEventRuleActions(expected.Actions, actual.Actions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkRole(expected, actual dataprovider.Role) error {
|
||||||
|
if expected.ID <= 0 {
|
||||||
|
if actual.ID <= 0 {
|
||||||
|
return errors.New("actual role ID must be > 0")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if actual.ID != expected.ID {
|
||||||
|
return errors.New("role ID mismatch")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if dataprovider.ConvertName(expected.Name) != actual.Name {
|
||||||
|
return errors.New("name mismatch")
|
||||||
|
}
|
||||||
|
if expected.Description != actual.Description {
|
||||||
|
return errors.New("description mismatch")
|
||||||
|
}
|
||||||
|
if actual.CreatedAt == 0 {
|
||||||
|
return errors.New("created_at unset")
|
||||||
|
}
|
||||||
|
if actual.UpdatedAt == 0 {
|
||||||
|
return errors.New("updated_at unset")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func checkGroup(expected, actual dataprovider.Group) error {
|
func checkGroup(expected, actual dataprovider.Group) error {
|
||||||
if expected.ID <= 0 {
|
if expected.ID <= 0 {
|
||||||
if actual.ID <= 0 {
|
if actual.ID <= 0 {
|
||||||
|
@ -1667,6 +1802,9 @@ func compareAdminEqualFields(expected *dataprovider.Admin, actual *dataprovider.
|
||||||
if expected.AdditionalInfo != actual.AdditionalInfo {
|
if expected.AdditionalInfo != actual.AdditionalInfo {
|
||||||
return errors.New("additional info mismatch")
|
return errors.New("additional info mismatch")
|
||||||
}
|
}
|
||||||
|
if expected.Role != actual.Role {
|
||||||
|
return errors.New("role mismatch")
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2491,6 +2629,9 @@ func compareEqualsUserFields(expected *dataprovider.User, actual *dataprovider.U
|
||||||
if expected.Description != actual.Description {
|
if expected.Description != actual.Description {
|
||||||
return errors.New("description mismatch")
|
return errors.New("description mismatch")
|
||||||
}
|
}
|
||||||
|
if expected.Role != actual.Role {
|
||||||
|
return errors.New("role mismatch")
|
||||||
|
}
|
||||||
return compareQuotaUserFields(expected, actual)
|
return compareQuotaUserFields(expected, actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -350,7 +350,11 @@ func (s *Service) LoadInitialData() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) restoreDump(dump *dataprovider.BackupData) error {
|
func (s *Service) restoreDump(dump *dataprovider.BackupData) error {
|
||||||
err := httpd.RestoreFolders(dump.Folders, s.LoadDataFrom, s.LoadDataMode, s.LoadDataQuotaScan, dataprovider.ActionExecutorSystem, "")
|
err := httpd.RestoreRoles(dump.Roles, s.LoadDataFrom, s.LoadDataMode, dataprovider.ActionExecutorSystem, "")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to restore roles from file %#v: %v", s.LoadDataFrom, err)
|
||||||
|
}
|
||||||
|
err = httpd.RestoreFolders(dump.Folders, s.LoadDataFrom, s.LoadDataMode, s.LoadDataQuotaScan, dataprovider.ActionExecutorSystem, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to restore folders from file %#v: %v", s.LoadDataFrom, err)
|
return fmt.Errorf("unable to restore folders from file %#v: %v", s.LoadDataFrom, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2088,7 +2088,7 @@ func TestRecoverer(t *testing.T) {
|
||||||
}
|
}
|
||||||
err = scpCmd.handle()
|
err = scpCmd.handle()
|
||||||
assert.EqualError(t, err, common.ErrGenericFailure.Error())
|
assert.EqualError(t, err, common.ErrGenericFailure.Error())
|
||||||
assert.Len(t, common.Connections.GetStats(), 0)
|
assert.Len(t, common.Connections.GetStats(""), 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestListernerAcceptErrors(t *testing.T) {
|
func TestListernerAcceptErrors(t *testing.T) {
|
||||||
|
@ -2241,7 +2241,7 @@ func TestMaxUserSessions(t *testing.T) {
|
||||||
assert.Contains(t, err.Error(), "too many open sessions")
|
assert.Contains(t, err.Error(), "too many open sessions")
|
||||||
}
|
}
|
||||||
common.Connections.Remove(connection.GetID())
|
common.Connections.Remove(connection.GetID())
|
||||||
assert.Len(t, common.Connections.GetStats(), 0)
|
assert.Len(t, common.Connections.GetStats(""), 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCanReadSymlink(t *testing.T) {
|
func TestCanReadSymlink(t *testing.T) {
|
||||||
|
|
|
@ -237,7 +237,7 @@ func (c *Configuration) getServerConfig() *ssh.ServerConfig {
|
||||||
},
|
},
|
||||||
NextAuthMethodsCallback: func(conn ssh.ConnMetadata) []string {
|
NextAuthMethodsCallback: func(conn ssh.ConnMetadata) []string {
|
||||||
var nextMethods []string
|
var nextMethods []string
|
||||||
user, err := dataprovider.GetUserWithGroupSettings(conn.User())
|
user, err := dataprovider.GetUserWithGroupSettings(conn.User(), "")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
nextMethods = user.GetNextAuthMethods(conn.PartialSuccessMethods(), c.PasswordAuthentication)
|
nextMethods = user.GetNextAuthMethods(conn.PartialSuccessMethods(), c.PasswordAuthentication)
|
||||||
}
|
}
|
||||||
|
|
|
@ -985,7 +985,7 @@ func TestDefender(t *testing.T) {
|
||||||
_, _, err = getSftpClient(user, usePubKey)
|
_, _, err = getSftpClient(user, usePubKey)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
|
|
||||||
err = dataprovider.DeleteUser(user.Username, "", "")
|
err = dataprovider.DeleteUser(user.Username, "", "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = os.RemoveAll(user.GetHomeDir())
|
err = os.RemoveAll(user.GetHomeDir())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -1144,7 +1144,7 @@ func TestConcurrency(t *testing.T) {
|
||||||
for {
|
for {
|
||||||
servedReqs := closedConns.Load()
|
servedReqs := closedConns.Load()
|
||||||
if servedReqs > 0 {
|
if servedReqs > 0 {
|
||||||
stats := common.Connections.GetStats()
|
stats := common.Connections.GetStats("")
|
||||||
if len(stats) > maxConns {
|
if len(stats) > maxConns {
|
||||||
maxConns = len(stats)
|
maxConns = len(stats)
|
||||||
}
|
}
|
||||||
|
@ -1178,7 +1178,7 @@ func TestConcurrency(t *testing.T) {
|
||||||
}, 1*time.Second, 50*time.Millisecond)
|
}, 1*time.Second, 50*time.Millisecond)
|
||||||
|
|
||||||
assert.Eventually(t, func() bool {
|
assert.Eventually(t, func() bool {
|
||||||
return len(common.Connections.GetStats()) == 0
|
return len(common.Connections.GetStats("")) == 0
|
||||||
}, 1*time.Second, 50*time.Millisecond)
|
}, 1*time.Second, 50*time.Millisecond)
|
||||||
|
|
||||||
err = os.Remove(testFilePath)
|
err = os.Remove(testFilePath)
|
||||||
|
@ -4284,7 +4284,7 @@ func TestMaxConnections(t *testing.T) {
|
||||||
err = conn.Close()
|
err = conn.Close()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
err = dataprovider.DeleteUser(user.Username, "", "")
|
err = dataprovider.DeleteUser(user.Username, "", "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = os.RemoveAll(user.GetHomeDir())
|
err = os.RemoveAll(user.GetHomeDir())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -4319,7 +4319,7 @@ func TestMaxPerHostConnections(t *testing.T) {
|
||||||
err = conn.Close()
|
err = conn.Close()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
err = dataprovider.DeleteUser(user.Username, "", "")
|
err = dataprovider.DeleteUser(user.Username, "", "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = os.RemoveAll(user.GetHomeDir())
|
err = os.RemoveAll(user.GetHomeDir())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -4603,12 +4603,12 @@ func TestQuotaScan(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMultipleQuotaScans(t *testing.T) {
|
func TestMultipleQuotaScans(t *testing.T) {
|
||||||
res := common.QuotaScans.AddUserQuotaScan(defaultUsername)
|
res := common.QuotaScans.AddUserQuotaScan(defaultUsername, "")
|
||||||
assert.True(t, res)
|
assert.True(t, res)
|
||||||
res = common.QuotaScans.AddUserQuotaScan(defaultUsername)
|
res = common.QuotaScans.AddUserQuotaScan(defaultUsername, "")
|
||||||
assert.False(t, res, "add quota must fail if another scan is already active")
|
assert.False(t, res, "add quota must fail if another scan is already active")
|
||||||
assert.True(t, common.QuotaScans.RemoveUserQuotaScan(defaultUsername))
|
assert.True(t, common.QuotaScans.RemoveUserQuotaScan(defaultUsername))
|
||||||
activeScans := common.QuotaScans.GetUsersQuotaScans()
|
activeScans := common.QuotaScans.GetUsersQuotaScans("")
|
||||||
assert.Equal(t, 0, len(activeScans))
|
assert.Equal(t, 0, len(activeScans))
|
||||||
assert.False(t, common.QuotaScans.RemoveUserQuotaScan(defaultUsername))
|
assert.False(t, common.QuotaScans.RemoveUserQuotaScan(defaultUsername))
|
||||||
}
|
}
|
||||||
|
@ -4866,13 +4866,13 @@ func TestBandwidthAndConnections(t *testing.T) {
|
||||||
waitForActiveTransfers(t)
|
waitForActiveTransfers(t)
|
||||||
time.Sleep(100 * time.Millisecond)
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
for _, stat := range common.Connections.GetStats() {
|
for _, stat := range common.Connections.GetStats("") {
|
||||||
common.Connections.Close(stat.ConnectionID)
|
common.Connections.Close(stat.ConnectionID, "")
|
||||||
}
|
}
|
||||||
err = <-c
|
err = <-c
|
||||||
assert.Error(t, err, "connection closed while uploading: the upload must fail")
|
assert.Error(t, err, "connection closed while uploading: the upload must fail")
|
||||||
assert.Eventually(t, func() bool {
|
assert.Eventually(t, func() bool {
|
||||||
return len(common.Connections.GetStats()) == 0
|
return len(common.Connections.GetStats("")) == 0
|
||||||
}, 10*time.Second, 200*time.Millisecond)
|
}, 10*time.Second, 200*time.Millisecond)
|
||||||
err = os.Remove(testFilePath)
|
err = os.Remove(testFilePath)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -7242,7 +7242,7 @@ func TestHashedPasswords(t *testing.T) {
|
||||||
user.Password = ""
|
user.Password = ""
|
||||||
userGetInitial, _, err := httpdtest.UpdateUser(user, http.StatusOK, "")
|
userGetInitial, _, err := httpdtest.UpdateUser(user, http.StatusOK, "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
user, err = dataprovider.UserExists(user.Username)
|
user, err = dataprovider.UserExists(user.Username, "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, pwd, user.Password)
|
assert.Equal(t, pwd, user.Password)
|
||||||
user.Password = clearPwd
|
user.Password = clearPwd
|
||||||
|
@ -7259,7 +7259,7 @@ func TestHashedPasswords(t *testing.T) {
|
||||||
conn.Close()
|
conn.Close()
|
||||||
}
|
}
|
||||||
// the password must converted to bcrypt and we should still be able to login
|
// the password must converted to bcrypt and we should still be able to login
|
||||||
user, err = dataprovider.UserExists(user.Username)
|
user, err = dataprovider.UserExists(user.Username, "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.True(t, strings.HasPrefix(user.Password, "$2a$"))
|
assert.True(t, strings.HasPrefix(user.Password, "$2a$"))
|
||||||
// update the user to invalidate the cached password and force a new check
|
// update the user to invalidate the cached password and force a new check
|
||||||
|
@ -10576,7 +10576,7 @@ func TestSCPErrors(t *testing.T) {
|
||||||
time.Sleep(100 * time.Millisecond)
|
time.Sleep(100 * time.Millisecond)
|
||||||
err = cmd.Process.Kill()
|
err = cmd.Process.Kill()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Eventually(t, func() bool { return len(common.Connections.GetStats()) == 0 }, 2*time.Second, 100*time.Millisecond)
|
assert.Eventually(t, func() bool { return len(common.Connections.GetStats("")) == 0 }, 2*time.Second, 100*time.Millisecond)
|
||||||
cmd = getScpUploadCommand(testFilePath, remoteUpPath, false, false)
|
cmd = getScpUploadCommand(testFilePath, remoteUpPath, false, false)
|
||||||
go func() {
|
go func() {
|
||||||
err := cmd.Run()
|
err := cmd.Run()
|
||||||
|
@ -10588,7 +10588,7 @@ func TestSCPErrors(t *testing.T) {
|
||||||
time.Sleep(100 * time.Millisecond)
|
time.Sleep(100 * time.Millisecond)
|
||||||
err = cmd.Process.Kill()
|
err = cmd.Process.Kill()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Eventually(t, func() bool { return len(common.Connections.GetStats()) == 0 }, 2*time.Second, 100*time.Millisecond)
|
assert.Eventually(t, func() bool { return len(common.Connections.GetStats("")) == 0 }, 2*time.Second, 100*time.Millisecond)
|
||||||
err = os.Remove(testFilePath)
|
err = os.Remove(testFilePath)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
os.Remove(localPath)
|
os.Remove(localPath)
|
||||||
|
@ -11076,7 +11076,7 @@ func computeHashForFile(hasher hash.Hash, path string) (string, error) {
|
||||||
|
|
||||||
func waitForActiveTransfers(t *testing.T) {
|
func waitForActiveTransfers(t *testing.T) {
|
||||||
assert.Eventually(t, func() bool {
|
assert.Eventually(t, func() bool {
|
||||||
for _, stat := range common.Connections.GetStats() {
|
for _, stat := range common.Connections.GetStats("") {
|
||||||
if len(stat.Transfers) > 0 {
|
if len(stat.Transfers) > 0 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -181,6 +181,7 @@ func SendEmail(to []string, subject, body string, contentType EmailContentType,
|
||||||
}
|
}
|
||||||
|
|
||||||
email := mail.NewMSG()
|
email := mail.NewMSG()
|
||||||
|
email.AllowDuplicateAddress = true
|
||||||
if from != "" {
|
if from != "" {
|
||||||
email.SetFrom(from)
|
email.SetFrom(from)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -617,8 +617,8 @@ func GetSSHPublicKeyAsString(pubKey []byte) (string, error) {
|
||||||
return string(ssh.MarshalAuthorizedKey(k)), nil
|
return string(ssh.MarshalAuthorizedKey(k)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRealIP returns the ip address as result of parsing either the
|
// GetRealIP returns the ip address as result of parsing the specified
|
||||||
// X-Real-IP header or the X-Forwarded-For header
|
// header and using the specified depth
|
||||||
func GetRealIP(r *http.Request, header string, depth int) string {
|
func GetRealIP(r *http.Request, header string, depth int) string {
|
||||||
if header == "" {
|
if header == "" {
|
||||||
return ""
|
return ""
|
||||||
|
|
|
@ -17,7 +17,7 @@ package version
|
||||||
|
|
||||||
import "strings"
|
import "strings"
|
||||||
|
|
||||||
const version = "2.4.0-dev"
|
const version = "2.4.1-dev"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
commit = ""
|
commit = ""
|
||||||
|
|
|
@ -937,7 +937,7 @@ func TestTransferSeek(t *testing.T) {
|
||||||
assert.True(t, fs.IsNotExist(err))
|
assert.True(t, fs.IsNotExist(err))
|
||||||
assert.Equal(t, int64(0), res)
|
assert.Equal(t, int64(0), res)
|
||||||
|
|
||||||
assert.Len(t, common.Connections.GetStats(), 0)
|
assert.Len(t, common.Connections.GetStats(""), 0)
|
||||||
|
|
||||||
err = os.Remove(testFilePath)
|
err = os.Remove(testFilePath)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -959,7 +959,7 @@ func TestBasicUsersCache(t *testing.T) {
|
||||||
u.Permissions["/"] = []string{dataprovider.PermAny}
|
u.Permissions["/"] = []string{dataprovider.PermAny}
|
||||||
err := dataprovider.AddUser(&u, "", "")
|
err := dataprovider.AddUser(&u, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
user, err := dataprovider.UserExists(u.Username)
|
user, err := dataprovider.UserExists(u.Username, "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
c := &Configuration{
|
c := &Configuration{
|
||||||
|
@ -1054,7 +1054,7 @@ func TestBasicUsersCache(t *testing.T) {
|
||||||
_, ok = dataprovider.GetCachedWebDAVUser(username)
|
_, ok = dataprovider.GetCachedWebDAVUser(username)
|
||||||
assert.True(t, ok)
|
assert.True(t, ok)
|
||||||
// cache is invalidated after user deletion
|
// cache is invalidated after user deletion
|
||||||
err = dataprovider.DeleteUser(user.Username, "", "")
|
err = dataprovider.DeleteUser(user.Username, "", "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
_, ok = dataprovider.GetCachedWebDAVUser(username)
|
_, ok = dataprovider.GetCachedWebDAVUser(username)
|
||||||
assert.False(t, ok)
|
assert.False(t, ok)
|
||||||
|
@ -1090,7 +1090,7 @@ func TestCachedUserWithFolders(t *testing.T) {
|
||||||
})
|
})
|
||||||
err := dataprovider.AddUser(&u, "", "")
|
err := dataprovider.AddUser(&u, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
user, err := dataprovider.UserExists(u.Username)
|
user, err := dataprovider.UserExists(u.Username, "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
c := &Configuration{
|
c := &Configuration{
|
||||||
|
@ -1177,7 +1177,7 @@ func TestCachedUserWithFolders(t *testing.T) {
|
||||||
assert.False(t, cachedUser.IsExpired())
|
assert.False(t, cachedUser.IsExpired())
|
||||||
}
|
}
|
||||||
|
|
||||||
err = dataprovider.DeleteUser(user.Username, "", "")
|
err = dataprovider.DeleteUser(user.Username, "", "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
_, ok = dataprovider.GetCachedWebDAVUser(username)
|
_, ok = dataprovider.GetCachedWebDAVUser(username)
|
||||||
assert.False(t, ok)
|
assert.False(t, ok)
|
||||||
|
@ -1205,25 +1205,25 @@ func TestUsersCacheSizeAndExpiration(t *testing.T) {
|
||||||
u.Permissions["/"] = []string{dataprovider.PermAny}
|
u.Permissions["/"] = []string{dataprovider.PermAny}
|
||||||
err := dataprovider.AddUser(&u, "", "")
|
err := dataprovider.AddUser(&u, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
user1, err := dataprovider.UserExists(u.Username)
|
user1, err := dataprovider.UserExists(u.Username, "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
u.Username = username + "2"
|
u.Username = username + "2"
|
||||||
u.Password = password + "2"
|
u.Password = password + "2"
|
||||||
err = dataprovider.AddUser(&u, "", "")
|
err = dataprovider.AddUser(&u, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
user2, err := dataprovider.UserExists(u.Username)
|
user2, err := dataprovider.UserExists(u.Username, "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
u.Username = username + "3"
|
u.Username = username + "3"
|
||||||
u.Password = password + "3"
|
u.Password = password + "3"
|
||||||
err = dataprovider.AddUser(&u, "", "")
|
err = dataprovider.AddUser(&u, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
user3, err := dataprovider.UserExists(u.Username)
|
user3, err := dataprovider.UserExists(u.Username, "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
u.Username = username + "4"
|
u.Username = username + "4"
|
||||||
u.Password = password + "4"
|
u.Password = password + "4"
|
||||||
err = dataprovider.AddUser(&u, "", "")
|
err = dataprovider.AddUser(&u, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
user4, err := dataprovider.UserExists(u.Username)
|
user4, err := dataprovider.UserExists(u.Username, "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
c := &Configuration{
|
c := &Configuration{
|
||||||
|
@ -1385,13 +1385,13 @@ func TestUsersCacheSizeAndExpiration(t *testing.T) {
|
||||||
_, ok = dataprovider.GetCachedWebDAVUser(user4.Username)
|
_, ok = dataprovider.GetCachedWebDAVUser(user4.Username)
|
||||||
assert.True(t, ok)
|
assert.True(t, ok)
|
||||||
|
|
||||||
err = dataprovider.DeleteUser(user1.Username, "", "")
|
err = dataprovider.DeleteUser(user1.Username, "", "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = dataprovider.DeleteUser(user2.Username, "", "")
|
err = dataprovider.DeleteUser(user2.Username, "", "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = dataprovider.DeleteUser(user3.Username, "", "")
|
err = dataprovider.DeleteUser(user3.Username, "", "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = dataprovider.DeleteUser(user4.Username, "", "")
|
err = dataprovider.DeleteUser(user4.Username, "", "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
err = os.RemoveAll(u.GetHomeDir())
|
err = os.RemoveAll(u.GetHomeDir())
|
||||||
|
@ -1415,7 +1415,7 @@ func TestUserCacheIsolation(t *testing.T) {
|
||||||
u.Permissions["/"] = []string{dataprovider.PermAny}
|
u.Permissions["/"] = []string{dataprovider.PermAny}
|
||||||
err := dataprovider.AddUser(&u, "", "")
|
err := dataprovider.AddUser(&u, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
user, err := dataprovider.UserExists(u.Username)
|
user, err := dataprovider.UserExists(u.Username, "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
cachedUser := &dataprovider.CachedUser{
|
cachedUser := &dataprovider.CachedUser{
|
||||||
User: user,
|
User: user,
|
||||||
|
@ -1449,7 +1449,7 @@ func TestUserCacheIsolation(t *testing.T) {
|
||||||
assert.False(t, cachedUser.User.FsConfig.S3Config.AccessSecret.IsEncrypted())
|
assert.False(t, cachedUser.User.FsConfig.S3Config.AccessSecret.IsEncrypted())
|
||||||
}
|
}
|
||||||
|
|
||||||
err = dataprovider.DeleteUser(username, "", "")
|
err = dataprovider.DeleteUser(username, "", "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
_, ok = dataprovider.GetCachedWebDAVUser(username)
|
_, ok = dataprovider.GetCachedWebDAVUser(username)
|
||||||
assert.False(t, ok)
|
assert.False(t, ok)
|
||||||
|
|
|
@ -620,7 +620,7 @@ func TestBasicHandling(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = os.RemoveAll(localUser.GetHomeDir())
|
err = os.RemoveAll(localUser.GetHomeDir())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Eventually(t, func() bool { return len(common.Connections.GetStats()) == 0 },
|
assert.Eventually(t, func() bool { return len(common.Connections.GetStats("")) == 0 },
|
||||||
1*time.Second, 100*time.Millisecond)
|
1*time.Second, 100*time.Millisecond)
|
||||||
status := webdavd.GetStatus()
|
status := webdavd.GetStatus()
|
||||||
assert.True(t, status.IsActive)
|
assert.True(t, status.IsActive)
|
||||||
|
@ -702,7 +702,7 @@ func TestBasicHandlingCryptFs(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = os.RemoveAll(user.GetHomeDir())
|
err = os.RemoveAll(user.GetHomeDir())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Eventually(t, func() bool { return len(common.Connections.GetStats()) == 0 },
|
assert.Eventually(t, func() bool { return len(common.Connections.GetStats("")) == 0 },
|
||||||
1*time.Second, 100*time.Millisecond)
|
1*time.Second, 100*time.Millisecond)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -929,7 +929,7 @@ func TestPropPatch(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = os.RemoveAll(localUser.GetHomeDir())
|
err = os.RemoveAll(localUser.GetHomeDir())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Eventually(t, func() bool { return len(common.Connections.GetStats()) == 0 },
|
assert.Eventually(t, func() bool { return len(common.Connections.GetStats("")) == 0 },
|
||||||
1*time.Second, 100*time.Millisecond)
|
1*time.Second, 100*time.Millisecond)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1311,7 +1311,7 @@ func TestPreDownloadHook(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = os.RemoveAll(user.GetHomeDir())
|
err = os.RemoveAll(user.GetHomeDir())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Eventually(t, func() bool { return len(common.Connections.GetStats()) == 0 },
|
assert.Eventually(t, func() bool { return len(common.Connections.GetStats("")) == 0 },
|
||||||
1*time.Second, 100*time.Millisecond)
|
1*time.Second, 100*time.Millisecond)
|
||||||
|
|
||||||
common.Config.Actions.ExecuteOn = []string{common.OperationPreDownload}
|
common.Config.Actions.ExecuteOn = []string{common.OperationPreDownload}
|
||||||
|
@ -1361,7 +1361,7 @@ func TestPreUploadHook(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = os.RemoveAll(user.GetHomeDir())
|
err = os.RemoveAll(user.GetHomeDir())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Eventually(t, func() bool { return len(common.Connections.GetStats()) == 0 },
|
assert.Eventually(t, func() bool { return len(common.Connections.GetStats("")) == 0 },
|
||||||
1*time.Second, 100*time.Millisecond)
|
1*time.Second, 100*time.Millisecond)
|
||||||
|
|
||||||
common.Config.Actions.ExecuteOn = oldExecuteOn
|
common.Config.Actions.ExecuteOn = oldExecuteOn
|
||||||
|
@ -1424,7 +1424,7 @@ func TestMaxConnections(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = os.RemoveAll(user.GetHomeDir())
|
err = os.RemoveAll(user.GetHomeDir())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Eventually(t, func() bool { return len(common.Connections.GetStats()) == 0 },
|
assert.Eventually(t, func() bool { return len(common.Connections.GetStats("")) == 0 },
|
||||||
1*time.Second, 100*time.Millisecond)
|
1*time.Second, 100*time.Millisecond)
|
||||||
|
|
||||||
common.Config.MaxTotalConnections = oldValue
|
common.Config.MaxTotalConnections = oldValue
|
||||||
|
@ -1456,7 +1456,7 @@ func TestMaxPerHostConnections(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = os.RemoveAll(user.GetHomeDir())
|
err = os.RemoveAll(user.GetHomeDir())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Eventually(t, func() bool { return len(common.Connections.GetStats()) == 0 },
|
assert.Eventually(t, func() bool { return len(common.Connections.GetStats("")) == 0 },
|
||||||
1*time.Second, 100*time.Millisecond)
|
1*time.Second, 100*time.Millisecond)
|
||||||
|
|
||||||
common.Config.MaxPerHostConnections = oldValue
|
common.Config.MaxPerHostConnections = oldValue
|
||||||
|
@ -1482,7 +1482,7 @@ func TestMaxSessions(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = os.RemoveAll(user.GetHomeDir())
|
err = os.RemoveAll(user.GetHomeDir())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Eventually(t, func() bool { return len(common.Connections.GetStats()) == 0 },
|
assert.Eventually(t, func() bool { return len(common.Connections.GetStats("")) == 0 },
|
||||||
1*time.Second, 100*time.Millisecond)
|
1*time.Second, 100*time.Millisecond)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1894,7 +1894,7 @@ func TestClientClose(t *testing.T) {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
assert.Eventually(t, func() bool {
|
assert.Eventually(t, func() bool {
|
||||||
for _, stat := range common.Connections.GetStats() {
|
for _, stat := range common.Connections.GetStats("") {
|
||||||
if len(stat.Transfers) > 0 {
|
if len(stat.Transfers) > 0 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -1902,16 +1902,16 @@ func TestClientClose(t *testing.T) {
|
||||||
return false
|
return false
|
||||||
}, 1*time.Second, 50*time.Millisecond)
|
}, 1*time.Second, 50*time.Millisecond)
|
||||||
|
|
||||||
for _, stat := range common.Connections.GetStats() {
|
for _, stat := range common.Connections.GetStats("") {
|
||||||
common.Connections.Close(stat.ConnectionID)
|
common.Connections.Close(stat.ConnectionID, "")
|
||||||
}
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
// for the sftp user a stat is done after the failed upload and
|
// for the sftp user a stat is done after the failed upload and
|
||||||
// this triggers a new connection
|
// this triggers a new connection
|
||||||
for _, stat := range common.Connections.GetStats() {
|
for _, stat := range common.Connections.GetStats("") {
|
||||||
common.Connections.Close(stat.ConnectionID)
|
common.Connections.Close(stat.ConnectionID, "")
|
||||||
}
|
}
|
||||||
assert.Eventually(t, func() bool { return len(common.Connections.GetStats()) == 0 },
|
assert.Eventually(t, func() bool { return len(common.Connections.GetStats("")) == 0 },
|
||||||
1*time.Second, 100*time.Millisecond)
|
1*time.Second, 100*time.Millisecond)
|
||||||
|
|
||||||
err = os.Remove(testFilePath)
|
err = os.Remove(testFilePath)
|
||||||
|
@ -1929,7 +1929,7 @@ func TestClientClose(t *testing.T) {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
assert.Eventually(t, func() bool {
|
assert.Eventually(t, func() bool {
|
||||||
for _, stat := range common.Connections.GetStats() {
|
for _, stat := range common.Connections.GetStats("") {
|
||||||
if len(stat.Transfers) > 0 {
|
if len(stat.Transfers) > 0 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -1937,11 +1937,11 @@ func TestClientClose(t *testing.T) {
|
||||||
return false
|
return false
|
||||||
}, 1*time.Second, 50*time.Millisecond)
|
}, 1*time.Second, 50*time.Millisecond)
|
||||||
|
|
||||||
for _, stat := range common.Connections.GetStats() {
|
for _, stat := range common.Connections.GetStats("") {
|
||||||
common.Connections.Close(stat.ConnectionID)
|
common.Connections.Close(stat.ConnectionID, "")
|
||||||
}
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
assert.Eventually(t, func() bool { return len(common.Connections.GetStats()) == 0 },
|
assert.Eventually(t, func() bool { return len(common.Connections.GetStats("")) == 0 },
|
||||||
1*time.Second, 100*time.Millisecond)
|
1*time.Second, 100*time.Millisecond)
|
||||||
|
|
||||||
err = os.Remove(localDownloadPath)
|
err = os.Remove(localDownloadPath)
|
||||||
|
@ -2964,7 +2964,7 @@ func TestNestedVirtualFolders(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = os.RemoveAll(localUser.GetHomeDir())
|
err = os.RemoveAll(localUser.GetHomeDir())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Eventually(t, func() bool { return len(common.Connections.GetStats()) == 0 },
|
assert.Eventually(t, func() bool { return len(common.Connections.GetStats("")) == 0 },
|
||||||
1*time.Second, 100*time.Millisecond)
|
1*time.Second, 100*time.Millisecond)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ tags:
|
||||||
- name: quota
|
- name: quota
|
||||||
- name: folders
|
- name: folders
|
||||||
- name: groups
|
- name: groups
|
||||||
|
- name: roles
|
||||||
- name: users
|
- name: users
|
||||||
- name: data retention
|
- name: data retention
|
||||||
- name: events
|
- name: events
|
||||||
|
@ -27,7 +28,7 @@ info:
|
||||||
SFTPGo supports groups to simplify the administration of multiple accounts by letting you assign settings once to a group, instead of multiple times to each individual user.
|
SFTPGo supports groups to simplify the administration of multiple accounts by letting you assign settings once to a group, instead of multiple times to each individual user.
|
||||||
The SFTPGo WebClient allows end users to change their credentials, browse and manage their files in the browser and setup two-factor authentication which works with Authy, Google Authenticator and other compatible apps.
|
The SFTPGo WebClient allows end users to change their credentials, browse and manage their files in the browser and setup two-factor authentication which works with Authy, Google Authenticator and other compatible apps.
|
||||||
From the WebClient each authorized user can also create HTTP/S links to externally share files and folders securely, by setting limits to the number of downloads/uploads, protecting the share with a password, limiting access by source IP address, setting an automatic expiration date.
|
From the WebClient each authorized user can also create HTTP/S links to externally share files and folders securely, by setting limits to the number of downloads/uploads, protecting the share with a password, limiting access by source IP address, setting an automatic expiration date.
|
||||||
version: 2.4.0-dev
|
version: 2.4.1-dev
|
||||||
contact:
|
contact:
|
||||||
name: API support
|
name: API support
|
||||||
url: 'https://github.com/drakkan/sftpgo'
|
url: 'https://github.com/drakkan/sftpgo'
|
||||||
|
@ -1666,6 +1667,181 @@ paths:
|
||||||
$ref: '#/components/responses/InternalServerError'
|
$ref: '#/components/responses/InternalServerError'
|
||||||
default:
|
default:
|
||||||
$ref: '#/components/responses/DefaultResponse'
|
$ref: '#/components/responses/DefaultResponse'
|
||||||
|
/roles:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- roles
|
||||||
|
summary: Get roles
|
||||||
|
description: Returns an array with one or more roles
|
||||||
|
operationId: get_roles
|
||||||
|
parameters:
|
||||||
|
- in: query
|
||||||
|
name: offset
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
minimum: 0
|
||||||
|
default: 0
|
||||||
|
required: false
|
||||||
|
- in: query
|
||||||
|
name: limit
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
minimum: 1
|
||||||
|
maximum: 500
|
||||||
|
default: 100
|
||||||
|
required: false
|
||||||
|
description: 'The maximum number of items to return. Max value is 500, default is 100'
|
||||||
|
- in: query
|
||||||
|
name: order
|
||||||
|
required: false
|
||||||
|
description: Ordering groups by name. Default ASC
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- ASC
|
||||||
|
- DESC
|
||||||
|
example: ASC
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: successful operation
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/Role'
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/BadRequest'
|
||||||
|
'401':
|
||||||
|
$ref: '#/components/responses/Unauthorized'
|
||||||
|
'403':
|
||||||
|
$ref: '#/components/responses/Forbidden'
|
||||||
|
'500':
|
||||||
|
$ref: '#/components/responses/InternalServerError'
|
||||||
|
default:
|
||||||
|
$ref: '#/components/responses/DefaultResponse'
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- roles
|
||||||
|
summary: Add role
|
||||||
|
operationId: add_role
|
||||||
|
description: Adds a new role
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Role'
|
||||||
|
responses:
|
||||||
|
'201':
|
||||||
|
description: successful operation
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Role'
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/BadRequest'
|
||||||
|
'401':
|
||||||
|
$ref: '#/components/responses/Unauthorized'
|
||||||
|
'403':
|
||||||
|
$ref: '#/components/responses/Forbidden'
|
||||||
|
'500':
|
||||||
|
$ref: '#/components/responses/InternalServerError'
|
||||||
|
default:
|
||||||
|
$ref: '#/components/responses/DefaultResponse'
|
||||||
|
'/roles/{name}':
|
||||||
|
parameters:
|
||||||
|
- name: name
|
||||||
|
in: path
|
||||||
|
description: role name
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- roles
|
||||||
|
summary: Find roles by name
|
||||||
|
description: Returns the role with the given name if it exists.
|
||||||
|
operationId: get_role_by_name
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: successful operation
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Role'
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/BadRequest'
|
||||||
|
'401':
|
||||||
|
$ref: '#/components/responses/Unauthorized'
|
||||||
|
'403':
|
||||||
|
$ref: '#/components/responses/Forbidden'
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/NotFound'
|
||||||
|
'500':
|
||||||
|
$ref: '#/components/responses/InternalServerError'
|
||||||
|
default:
|
||||||
|
$ref: '#/components/responses/DefaultResponse'
|
||||||
|
put:
|
||||||
|
tags:
|
||||||
|
- roles
|
||||||
|
summary: Update role
|
||||||
|
description: Updates an existing role
|
||||||
|
operationId: update_role
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Role'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: successful operation
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ApiResponse'
|
||||||
|
example:
|
||||||
|
message: Group updated
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/BadRequest'
|
||||||
|
'401':
|
||||||
|
$ref: '#/components/responses/Unauthorized'
|
||||||
|
'403':
|
||||||
|
$ref: '#/components/responses/Forbidden'
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/NotFound'
|
||||||
|
'500':
|
||||||
|
$ref: '#/components/responses/InternalServerError'
|
||||||
|
default:
|
||||||
|
$ref: '#/components/responses/DefaultResponse'
|
||||||
|
delete:
|
||||||
|
tags:
|
||||||
|
- roles
|
||||||
|
summary: Delete role
|
||||||
|
description: Deletes an existing role
|
||||||
|
operationId: delete_role
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: successful operation
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ApiResponse'
|
||||||
|
example:
|
||||||
|
message: Group deleted
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/BadRequest'
|
||||||
|
'401':
|
||||||
|
$ref: '#/components/responses/Unauthorized'
|
||||||
|
'403':
|
||||||
|
$ref: '#/components/responses/Forbidden'
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/NotFound'
|
||||||
|
'500':
|
||||||
|
$ref: '#/components/responses/InternalServerError'
|
||||||
|
default:
|
||||||
|
$ref: '#/components/responses/DefaultResponse'
|
||||||
/eventactions:
|
/eventactions:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
|
@ -4269,6 +4445,7 @@ components:
|
||||||
- close_conns
|
- close_conns
|
||||||
- view_status
|
- view_status
|
||||||
- manage_admins
|
- manage_admins
|
||||||
|
- manage_groups
|
||||||
- manage_apikeys
|
- manage_apikeys
|
||||||
- quota_scans
|
- quota_scans
|
||||||
- manage_system
|
- manage_system
|
||||||
|
@ -4278,6 +4455,7 @@ components:
|
||||||
- metadata_checks
|
- metadata_checks
|
||||||
- view_events
|
- view_events
|
||||||
- manage_event_rules
|
- manage_event_rules
|
||||||
|
- manager_roles
|
||||||
description: |
|
description: |
|
||||||
Admin permissions:
|
Admin permissions:
|
||||||
* `*` - all permissions are granted
|
* `*` - all permissions are granted
|
||||||
|
@ -4289,6 +4467,7 @@ components:
|
||||||
* `close_conns` - close active connections is allowed
|
* `close_conns` - close active connections is allowed
|
||||||
* `view_status` - view the server status is allowed
|
* `view_status` - view the server status is allowed
|
||||||
* `manage_admins` - manage other admins is allowed
|
* `manage_admins` - manage other admins is allowed
|
||||||
|
* `manage_groups` - manage groups is allowed
|
||||||
* `manage_apikeys` - manage API keys is allowed
|
* `manage_apikeys` - manage API keys is allowed
|
||||||
* `quota_scans` - view and start quota scans is allowed
|
* `quota_scans` - view and start quota scans is allowed
|
||||||
* `manage_system` - backups and restores are allowed
|
* `manage_system` - backups and restores are allowed
|
||||||
|
@ -4298,6 +4477,7 @@ components:
|
||||||
* `metadata_checks` - view and start metadata checks is allowed
|
* `metadata_checks` - view and start metadata checks is allowed
|
||||||
* `view_events` - view and search filesystem and provider events is allowed
|
* `view_events` - view and search filesystem and provider events is allowed
|
||||||
* `manage_event_rules` - manage event actions and rules is allowed
|
* `manage_event_rules` - manage event actions and rules is allowed
|
||||||
|
* `manager_roles` - manage roles is allowed
|
||||||
FsProviders:
|
FsProviders:
|
||||||
type: integer
|
type: integer
|
||||||
enum:
|
enum:
|
||||||
|
@ -4538,6 +4718,7 @@ components:
|
||||||
- share
|
- share
|
||||||
- event_action
|
- event_action
|
||||||
- event_rule
|
- event_rule
|
||||||
|
- role
|
||||||
SSHAuthentications:
|
SSHAuthentications:
|
||||||
type: string
|
type: string
|
||||||
enum:
|
enum:
|
||||||
|
@ -5211,6 +5392,8 @@ components:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: true
|
additionalProperties: true
|
||||||
description: 'This field is passed to the pre-login hook if custom OIDC token fields have been configured. Field values can be of any type (this is a free form object) and depend on the type of the configured OIDC token fields'
|
description: 'This field is passed to the pre-login hook if custom OIDC token fields have been configured. Field values can be of any type (this is a free form object) and depend on the type of the configured OIDC token fields'
|
||||||
|
role:
|
||||||
|
type: string
|
||||||
AdminPreferences:
|
AdminPreferences:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
@ -5297,6 +5480,9 @@ components:
|
||||||
type: integer
|
type: integer
|
||||||
format: int64
|
format: int64
|
||||||
description: Last user login as unix timestamp in milliseconds. It is saved at most once every 10 minutes
|
description: Last user login as unix timestamp in milliseconds. It is saved at most once every 10 minutes
|
||||||
|
role:
|
||||||
|
type: string
|
||||||
|
description: 'If set the admin can only administer users with the same role. Role admins cannot have the following permissions: "manage_admins", "manage_apikeys", "manage_system", "manage_event_rules", "manage_roles", "view_events"'
|
||||||
AdminProfile:
|
AdminProfile:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
@ -5840,6 +6026,37 @@ components:
|
||||||
description: 'Maximum total data transfer as MB'
|
description: 'Maximum total data transfer as MB'
|
||||||
filters:
|
filters:
|
||||||
$ref: '#/components/schemas/BaseUserFilters'
|
$ref: '#/components/schemas/BaseUserFilters'
|
||||||
|
Role:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
format: int32
|
||||||
|
minimum: 1
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
description: name is unique
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
|
description: 'optional description'
|
||||||
|
created_at:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
description: creation time as unix timestamp in milliseconds
|
||||||
|
updated_at:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
description: last update time as unix timestamp in milliseconds
|
||||||
|
users:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
description: list of usernames associated with this group
|
||||||
|
admins:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
description: list of admins usernames associated with this group
|
||||||
Group:
|
Group:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
@ -5942,6 +6159,18 @@ components:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/Share'
|
$ref: '#/components/schemas/Share'
|
||||||
|
event_actions:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/EventAction'
|
||||||
|
event_rules:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/EventRule'
|
||||||
|
roles:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/Role'
|
||||||
version:
|
version:
|
||||||
type: integer
|
type: integer
|
||||||
PwdChange:
|
PwdChange:
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
NFPM_VERSION=2.21.0
|
NFPM_VERSION=2.22.0
|
||||||
NFPM_ARCH=${NFPM_ARCH:-amd64}
|
NFPM_ARCH=${NFPM_ARCH:-amd64}
|
||||||
if [ -z ${SFTPGO_VERSION} ]
|
if [ -z ${SFTPGO_VERSION} ]
|
||||||
then
|
then
|
||||||
|
|
|
@ -3,17 +3,17 @@
|
||||||
<package xmlns="http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd">
|
<package xmlns="http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd">
|
||||||
<metadata>
|
<metadata>
|
||||||
<id>sftpgo</id>
|
<id>sftpgo</id>
|
||||||
<version>2.4.0</version>
|
<version>2.4.1</version>
|
||||||
<packageSourceUrl>https://github.com/drakkan/sftpgo/tree/main/pkgs/choco</packageSourceUrl>
|
<packageSourceUrl>https://github.com/drakkan/sftpgo/tree/main/pkgs/choco</packageSourceUrl>
|
||||||
<owners>asheroto</owners>
|
<owners>asheroto</owners>
|
||||||
<title>SFTPGo</title>
|
<title>SFTPGo</title>
|
||||||
<authors>Nicola Murino</authors>
|
<authors>Nicola Murino</authors>
|
||||||
<projectUrl>https://github.com/drakkan/sftpgo</projectUrl>
|
<projectUrl>https://github.com/drakkan/sftpgo</projectUrl>
|
||||||
<iconUrl>https://cdn.statically.io/gh/drakkan/sftpgo/v2.4.0/static/img/logo.png</iconUrl>
|
<iconUrl>https://cdn.statically.io/gh/drakkan/sftpgo/v2.4.1/static/img/logo.png</iconUrl>
|
||||||
<licenseUrl>https://github.com/drakkan/sftpgo/blob/main/LICENSE</licenseUrl>
|
<licenseUrl>https://github.com/drakkan/sftpgo/blob/main/LICENSE</licenseUrl>
|
||||||
<requireLicenseAcceptance>false</requireLicenseAcceptance>
|
<requireLicenseAcceptance>false</requireLicenseAcceptance>
|
||||||
<projectSourceUrl>https://github.com/drakkan/sftpgo</projectSourceUrl>
|
<projectSourceUrl>https://github.com/drakkan/sftpgo</projectSourceUrl>
|
||||||
<docsUrl>https://github.com/drakkan/sftpgo/tree/v2.4.0/docs</docsUrl>
|
<docsUrl>https://github.com/drakkan/sftpgo/tree/v2.4.1/docs</docsUrl>
|
||||||
<bugTrackerUrl>https://github.com/drakkan/sftpgo/issues</bugTrackerUrl>
|
<bugTrackerUrl>https://github.com/drakkan/sftpgo/issues</bugTrackerUrl>
|
||||||
<tags>sftp sftp-server ftp webdav s3 azure-blob google-cloud-storage cloud-storage scp data-at-rest-encryption multi-factor-authentication multi-step-authentication</tags>
|
<tags>sftp sftp-server ftp webdav s3 azure-blob google-cloud-storage cloud-storage scp data-at-rest-encryption multi-factor-authentication multi-step-authentication</tags>
|
||||||
<summary>Fully featured and highly configurable SFTP server with optional HTTP/S,FTP/S and WebDAV support.</summary>
|
<summary>Fully featured and highly configurable SFTP server with optional HTTP/S,FTP/S and WebDAV support.</summary>
|
||||||
|
@ -32,7 +32,7 @@ You can find more info [here](https://github.com/drakkan/sftpgo).
|
||||||
|
|
||||||
* This package installs SFTPGo as Windows Service.
|
* This package installs SFTPGo as Windows Service.
|
||||||
* After the first installation please take a look at the [Getting Started Guide](https://github.com/drakkan/sftpgo/blob/main/docs/howto/getting-started.md).</description>
|
* After the first installation please take a look at the [Getting Started Guide](https://github.com/drakkan/sftpgo/blob/main/docs/howto/getting-started.md).</description>
|
||||||
<releaseNotes>https://github.com/drakkan/sftpgo/releases/tag/v2.4.0</releaseNotes>
|
<releaseNotes>https://github.com/drakkan/sftpgo/releases/tag/v2.4.1</releaseNotes>
|
||||||
</metadata>
|
</metadata>
|
||||||
<files>
|
<files>
|
||||||
<file src="**" exclude="**\*.md;**\icon.png;**\icon.jpg;**\icon.svg" />
|
<file src="**" exclude="**\*.md;**\icon.png;**\icon.jpg;**\icon.svg" />
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
$packageName = 'sftpgo'
|
$packageName = 'sftpgo'
|
||||||
$softwareName = 'SFTPGo'
|
$softwareName = 'SFTPGo'
|
||||||
$url = 'https://github.com/drakkan/sftpgo/releases/download/v2.4.0/sftpgo_v2.4.0_windows_x86_64.exe'
|
$url = 'https://github.com/drakkan/sftpgo/releases/download/v2.4.1/sftpgo_v2.4.1_windows_x86_64.exe'
|
||||||
$checksum = 'D8503BF3C5F606C3445D1286C8C5CA54476EC751E2CE0D4EAAE5EB4903C09412'
|
$checksum = 'AC199E8DE1F90ACE0B310FA2DEB4D84F5A8E1D592CB11F8C79DA3F4C9AC8517E'
|
||||||
$silentArgs = '/VERYSILENT'
|
$silentArgs = '/VERYSILENT'
|
||||||
$validExitCodes = @(0)
|
$validExitCodes = @(0)
|
||||||
|
|
||||||
|
@ -47,8 +47,8 @@ Write-Output ""
|
||||||
Write-Output "General information (README) location:"
|
Write-Output "General information (README) location:"
|
||||||
Write-Output "`thttps://github.com/drakkan/sftpgo"
|
Write-Output "`thttps://github.com/drakkan/sftpgo"
|
||||||
Write-Output "Getting started guide location:"
|
Write-Output "Getting started guide location:"
|
||||||
Write-Output "`thttps://github.com/drakkan/sftpgo/blob/v2.4.0/docs/howto/getting-started.md"
|
Write-Output "`thttps://github.com/drakkan/sftpgo/blob/v2.4.1/docs/howto/getting-started.md"
|
||||||
Write-Output "Detailed information (docs folder) location:"
|
Write-Output "Detailed information (docs folder) location:"
|
||||||
Write-Output "`thttps://github.com/drakkan/sftpgo/tree/v2.4.0/docs"
|
Write-Output "`thttps://github.com/drakkan/sftpgo/tree/v2.4.1/docs"
|
||||||
Write-Output ""
|
Write-Output ""
|
||||||
Write-Output "---------------------------"
|
Write-Output "---------------------------"
|
|
@ -1,3 +1,9 @@
|
||||||
|
sftpgo (2.4.1-1ppa1) bionic; urgency=medium
|
||||||
|
|
||||||
|
* New upstream release
|
||||||
|
|
||||||
|
-- Nicola Murino <nicola.murino@gmail.com> Sun, 13 Nov 2022 07:34:04 +0100
|
||||||
|
|
||||||
sftpgo (2.4.0-1ppa1) bionic; urgency=medium
|
sftpgo (2.4.0-1ppa1) bionic; urgency=medium
|
||||||
|
|
||||||
* New upstream release
|
* New upstream release
|
||||||
|
|
|
@ -96,6 +96,26 @@ 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>Role</b>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<h6 class="card-title mb-4">Setting a role limit the administrator to only manage users with the same role. Role administrators cannot have the following permissions: "manage_admins", "manage_roles", "manage_event_rules", "manage_apikeys", "manage_system"</h6>
|
||||||
|
<div class="form-group row">
|
||||||
|
<label for="idRole" class="col-sm-2 col-form-label">Role</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<select class="form-control selectpicker" data-live-search="true" id="idRole" name="role">
|
||||||
|
<option value=""></option>
|
||||||
|
{{- range .Roles}}
|
||||||
|
<option value="{{.Name}}" {{if eq $.Admin.Role .Name}}selected{{end}}>{{.Name}}</option>
|
||||||
|
{{- end}}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card bg-light mb-3">
|
<div class="card bg-light mb-3">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<b>Groups for users</b>
|
<b>Groups for users</b>
|
||||||
|
|
|
@ -55,6 +55,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
<th>Email</th>
|
<th>Email</th>
|
||||||
<th>Description</th>
|
<th>Description</th>
|
||||||
<th>Groups for users</th>
|
<th>Groups for users</th>
|
||||||
|
<th>Role</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@ -70,6 +71,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
<td>{{.Email}}</td>
|
<td>{{.Email}}</td>
|
||||||
<td>{{.Description}}</td>
|
<td>{{.Description}}</td>
|
||||||
<td>{{.GetGroupsAsString}}</td>
|
<td>{{.GetGroupsAsString}}</td>
|
||||||
|
<td>{{.Role}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
@ -233,7 +235,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
"visible": false,
|
"visible": false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"targets": [5,7,8],
|
"targets": [5,7,8,10],
|
||||||
"visible": false,
|
"visible": false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -121,6 +121,14 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
</li>
|
</li>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
{{ if .LoggedAdmin.HasPermission "manage_roles"}}
|
||||||
|
<li class="nav-item {{if eq .CurrentURL .RolesURL}}active{{end}}">
|
||||||
|
<a class="nav-link" href="{{.RolesURL}}">
|
||||||
|
<i class="fas fa-user-lock"></i>
|
||||||
|
<span>{{.RolesTitle}}</span></a>
|
||||||
|
</li>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
{{ if .LoggedAdmin.HasPermission "manage_admins"}}
|
{{ if .LoggedAdmin.HasPermission "manage_admins"}}
|
||||||
<li class="nav-item {{if eq .CurrentURL .AdminsURL}}active{{end}}">
|
<li class="nav-item {{if eq .CurrentURL .AdminsURL}}active{{end}}">
|
||||||
<a class="nav-link" href="{{.AdminsURL}}">
|
<a class="nav-link" href="{{.AdminsURL}}">
|
||||||
|
|
62
templates/webadmin/role.html
Normal file
62
templates/webadmin/role.html
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
<!--
|
||||||
|
Copyright (C) 2019-2022 Nicola Murino
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, version 3.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
{{template "base" .}}
|
||||||
|
|
||||||
|
{{define "title"}}{{.Title}}{{end}}
|
||||||
|
|
||||||
|
{{define "extra_css"}}
|
||||||
|
<link href="{{.StaticURL}}/vendor/bootstrap-select/css/bootstrap-select.min.css" rel="stylesheet">
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "page_body"}}
|
||||||
|
<!-- Page Heading -->
|
||||||
|
<div class="card shadow mb-4">
|
||||||
|
<div class="card-header py-3">
|
||||||
|
<h6 class="m-0 font-weight-bold text-primary">{{.Title}}</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{{if .Error}}
|
||||||
|
<div class="card mb-4 border-left-warning">
|
||||||
|
<div class="card-body text-form-error">{{.Error}}</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
<form id="role_form" action="{{.CurrentURL}}" method="POST" autocomplete="off">
|
||||||
|
<div class="form-group row">
|
||||||
|
<label for="idRoleName" class="col-sm-2 col-form-label">Name</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input type="text" class="form-control" id="idRoleName" name="name" placeholder=""
|
||||||
|
value="{{.Role.Name}}" maxlength="255" autocomplete="nope" required {{if eq .Mode 2}}readonly{{end}}>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<label for="idDescription" class="col-sm-2 col-form-label">Description</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input type="text" class="form-control" id="idDescription" name="description" placeholder=""
|
||||||
|
value="{{.Role.Description}}" maxlength="255" aria-describedby="descriptionHelpBlock">
|
||||||
|
<small id="descriptionHelpBlock" class="form-text text-muted">
|
||||||
|
Optional description
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
|
||||||
|
<div class="col-sm-12 text-right px-0">
|
||||||
|
<button type="submit" class="btn btn-primary mt-3 ml-3 px-5" name="form_action" value="submit">Submit</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
222
templates/webadmin/roles.html
Normal file
222
templates/webadmin/roles.html
Normal file
|
@ -0,0 +1,222 @@
|
||||||
|
<!--
|
||||||
|
Copyright (C) 2019-2022 Nicola Murino
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, version 3.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
{{template "base" .}}
|
||||||
|
|
||||||
|
{{define "title"}}{{.Title}}{{end}}
|
||||||
|
|
||||||
|
{{define "extra_css"}}
|
||||||
|
<link href="{{.StaticURL}}/vendor/datatables/dataTables.bootstrap4.min.css" rel="stylesheet">
|
||||||
|
<link href="{{.StaticURL}}/vendor/datatables/buttons.bootstrap4.min.css" rel="stylesheet">
|
||||||
|
<link href="{{.StaticURL}}/vendor/datatables/fixedHeader.bootstrap4.min.css" rel="stylesheet">
|
||||||
|
<link href="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.css" rel="stylesheet">
|
||||||
|
<link href="{{.StaticURL}}/vendor/datatables/select.bootstrap4.min.css" rel="stylesheet">
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "page_body"}}
|
||||||
|
<div id="errorMsg" class="card mb-4 border-left-warning" style="display: none;">
|
||||||
|
<div id="errorTxt" class="card-body text-form-error"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card shadow mb-4">
|
||||||
|
<div class="card-header py-3">
|
||||||
|
<h6 class="m-0 font-weight-bold text-primary">View and manage roles</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover nowrap" id="dataTable" width="100%" cellspacing="0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Members</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{range .Roles}}
|
||||||
|
<tr>
|
||||||
|
<td>{{.Name}}</td>
|
||||||
|
<td>{{.Description}}</td>
|
||||||
|
<td>{{.GetMembersAsString}}</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "dialog"}}
|
||||||
|
<div class="modal fade" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="deleteModalLabel"
|
||||||
|
aria-hidden="true">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="deleteModalLabel">
|
||||||
|
Confirmation required
|
||||||
|
</h5>
|
||||||
|
<button class="close" type="button" data-dismiss="modal" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">Do you want to delete the selected role? It is not possible to remove a role that does contain administrators</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-secondary" type="button" data-dismiss="modal">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<a class="btn btn-warning" href="#" onclick="deleteAction()">
|
||||||
|
Delete
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "extra_js"}}
|
||||||
|
<script src="{{.StaticURL}}/vendor/datatables/jquery.dataTables.min.js"></script>
|
||||||
|
<script src="{{.StaticURL}}/vendor/datatables/dataTables.bootstrap4.min.js"></script>
|
||||||
|
<script src="{{.StaticURL}}/vendor/datatables/dataTables.buttons.min.js"></script>
|
||||||
|
<script src="{{.StaticURL}}/vendor/datatables/buttons.bootstrap4.min.js"></script>
|
||||||
|
<script src="{{.StaticURL}}/vendor/datatables/buttons.colVis.min.js"></script>
|
||||||
|
<script src="{{.StaticURL}}/vendor/datatables/dataTables.fixedHeader.min.js"></script>
|
||||||
|
<script src="{{.StaticURL}}/vendor/datatables/dataTables.responsive.min.js"></script>
|
||||||
|
<script src="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.js"></script>
|
||||||
|
<script src="{{.StaticURL}}/vendor/datatables/dataTables.select.min.js"></script>
|
||||||
|
<script src="{{.StaticURL}}/vendor/datatables/ellipsis.js"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
|
||||||
|
function deleteAction() {
|
||||||
|
var table = $('#dataTable').DataTable();
|
||||||
|
table.button('delete:name').enable(false);
|
||||||
|
var roleName = table.row({ selected: true }).data()[0];
|
||||||
|
var path = '{{.RoleURL}}' + "/" + fixedEncodeURIComponent(roleName);
|
||||||
|
$('#deleteModal').modal('hide');
|
||||||
|
$.ajax({
|
||||||
|
url: path,
|
||||||
|
type: 'DELETE',
|
||||||
|
dataType: 'json',
|
||||||
|
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
|
||||||
|
timeout: 15000,
|
||||||
|
success: function (result) {
|
||||||
|
window.location.href = '{{.RolesURL}}';
|
||||||
|
},
|
||||||
|
error: function ($xhr, textStatus, errorThrown) {
|
||||||
|
var txt = "Unable to delete the selected role";
|
||||||
|
if ($xhr) {
|
||||||
|
var json = $xhr.responseJSON;
|
||||||
|
if (json) {
|
||||||
|
if (json.message){
|
||||||
|
txt += ": " + json.message;
|
||||||
|
} else {
|
||||||
|
txt += ": " + json.error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$('#errorTxt').text(txt);
|
||||||
|
$('#errorMsg').show();
|
||||||
|
setTimeout(function () {
|
||||||
|
$('#errorMsg').hide();
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).ready(function () {
|
||||||
|
$.fn.dataTable.ext.buttons.add = {
|
||||||
|
text: '<i class="fas fa-plus"></i>',
|
||||||
|
name: 'add',
|
||||||
|
titleAttr: "Add",
|
||||||
|
action: function (e, dt, node, config) {
|
||||||
|
window.location.href = '{{.RoleURL}}';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$.fn.dataTable.ext.buttons.edit = {
|
||||||
|
text: '<i class="fas fa-pen"></i>',
|
||||||
|
name: 'edit',
|
||||||
|
titleAttr: "Edit",
|
||||||
|
action: function (e, dt, node, config) {
|
||||||
|
var roleName = table.row({ selected: true }).data()[0];
|
||||||
|
var path = '{{.RoleURL}}' + "/" + fixedEncodeURIComponent(roleName);
|
||||||
|
window.location.href = path;
|
||||||
|
},
|
||||||
|
enabled: false
|
||||||
|
};
|
||||||
|
|
||||||
|
$.fn.dataTable.ext.buttons.delete = {
|
||||||
|
text: '<i class="fas fa-trash"></i>',
|
||||||
|
name: 'delete',
|
||||||
|
titleAttr: "Delete",
|
||||||
|
action: function (e, dt, node, config) {
|
||||||
|
$('#deleteModal').modal('show');
|
||||||
|
},
|
||||||
|
enabled: false
|
||||||
|
};
|
||||||
|
|
||||||
|
var table = $('#dataTable').DataTable({
|
||||||
|
"select": {
|
||||||
|
"style": "single",
|
||||||
|
"blurable": true
|
||||||
|
},
|
||||||
|
"stateSave": true,
|
||||||
|
"stateDuration": 0,
|
||||||
|
"buttons": [
|
||||||
|
{
|
||||||
|
"text": "Column visibility",
|
||||||
|
"extend": "colvis",
|
||||||
|
"columns": ":not(.noVis)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"columnDefs": [
|
||||||
|
{
|
||||||
|
"targets": [0],
|
||||||
|
"className": "noVis"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"targets": [2],
|
||||||
|
"render": $.fn.dataTable.render.ellipsis(100, true)
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"scrollX": false,
|
||||||
|
"scrollY": false,
|
||||||
|
"responsive": true,
|
||||||
|
"language": {
|
||||||
|
"emptyTable": "No role defined"
|
||||||
|
},
|
||||||
|
"order": [[0, 'asc']]
|
||||||
|
});
|
||||||
|
|
||||||
|
new $.fn.dataTable.FixedHeader( table );
|
||||||
|
|
||||||
|
{{if .LoggedAdmin.HasPermission "manage_roles"}}
|
||||||
|
table.button().add(0,'delete');
|
||||||
|
table.button().add(0,'edit');
|
||||||
|
table.button().add(0,'add');
|
||||||
|
|
||||||
|
table.buttons().container().appendTo('.col-md-6:eq(0)', table.table().container());
|
||||||
|
|
||||||
|
table.on('select deselect', function () {
|
||||||
|
var selectedRows = table.rows({ selected: true }).count();
|
||||||
|
table.button('delete:name').enable(selectedRows == 1);
|
||||||
|
table.button('edit:name').enable(selectedRows == 1);
|
||||||
|
});
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
{{end}}
|
|
@ -104,6 +104,23 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
{{if .Roles}}
|
||||||
|
<div class="form-group row">
|
||||||
|
<label for="idRole" class="col-sm-2 col-form-label">Role</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<select class="form-control selectpicker" data-live-search="true" id="idRole" name="role" aria-describedby="roleHelpBlock">
|
||||||
|
<option value=""></option>
|
||||||
|
{{- range .Roles}}
|
||||||
|
<option value="{{.Name}}" {{if eq $.User.Role .Name}}selected{{end}}>{{.Name}}</option>
|
||||||
|
{{- end}}
|
||||||
|
</select>
|
||||||
|
<small id="roleHelpBlock" class="form-text text-muted">
|
||||||
|
Users with a role can be managed by global administrators and administrators with the same role
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
{{if ne .Mode 3}}
|
{{if ne .Mode 3}}
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label for="idPassword" class="col-sm-2 col-form-label">Password</label>
|
<label for="idPassword" class="col-sm-2 col-form-label">Password</label>
|
||||||
|
|
|
@ -56,6 +56,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
<th>MFA</th>
|
<th>MFA</th>
|
||||||
<th>Bandwidth</th>
|
<th>Bandwidth</th>
|
||||||
<th>Quota</th>
|
<th>Quota</th>
|
||||||
|
<th>Role</th>
|
||||||
<th>Other</th>
|
<th>Other</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -74,6 +75,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
<td>{{.GetMFAStatusAsString}}</td>
|
<td>{{.GetMFAStatusAsString}}</td>
|
||||||
<td>{{.GetBandwidthAsString}}</td>
|
<td>{{.GetBandwidthAsString}}</td>
|
||||||
<td>{{.GetQuotaSummary}}</td>
|
<td>{{.GetQuotaSummary}}</td>
|
||||||
|
<td>{{.Role}}</td>
|
||||||
<td>{{.GetInfoString}}</td>
|
<td>{{.GetInfoString}}</td>
|
||||||
<td>{{.GetLastQuotaUpdateAsString}}</td>
|
<td>{{.GetLastQuotaUpdateAsString}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -281,7 +283,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
],
|
],
|
||||||
"columnDefs": [
|
"columnDefs": [
|
||||||
{
|
{
|
||||||
"targets": [0,12],
|
"targets": [0,13],
|
||||||
"visible": false,
|
"visible": false,
|
||||||
"searchable": false,
|
"searchable": false,
|
||||||
"className": "noVis"
|
"className": "noVis"
|
||||||
|
@ -295,7 +297,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
"render": $.fn.dataTable.render.datetime()
|
"render": $.fn.dataTable.render.datetime()
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"targets": [4,5,8],
|
"targets": [4,5,8,11],
|
||||||
"visible": false
|
"visible": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -325,7 +327,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"targets": [11],
|
"targets": [12],
|
||||||
"visible": false,
|
"visible": false,
|
||||||
"render": $.fn.dataTable.render.ellipsis(100, true)
|
"render": $.fn.dataTable.render.ellipsis(100, true)
|
||||||
}
|
}
|
||||||
|
@ -349,7 +351,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
table.button().add(0,'delete');
|
table.button().add(0,'delete');
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{if .LoggedAdmin.HasPermission "add_users"}}
|
{{if .LoggedAdmin.HasPermission "manage_system"}}
|
||||||
table.button().add(0,'template');
|
table.button().add(0,'template');
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue