Compare commits

...

469 commits

Author SHA1 Message Date
Nicola Murino
fb8a8cb791
back to development
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-03-13 21:58:54 +01:00
dependabot[bot]
5f072ad8ac
Bump golang from 1.25-trixie to 1.26-trixie (#2174)
Bumps golang from 1.25-trixie to 1.26-trixie.

---
updated-dependencies:
- dependency-name: golang
  dependency-version: 1.26-trixie
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-13 18:38:32 +01:00
dependabot[bot]
01a6bf2851
Bump actions/upload-artifact from 6 to 7 (#2177)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-13 18:38:13 +01:00
dependabot[bot]
c4c95d72b9
Bump actions/download-artifact from 7 to 8 (#2178)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 7 to 8.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v7...v8)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-13 18:38:01 +01:00
Nicola Murino
6ad1f69b2c
CI: update go version
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-03-12 19:51:34 +01:00
Nicola Murino
baedf15e0e
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-03-12 19:50:41 +01:00
Nicola Murino
c9601a8976
fix new lint warnings
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-03-07 16:51:57 +01:00
Nicola Murino
2f092d1289
fix: prevent path traversal via edge-level path normalization
Moved path sanitization (backslash conversion and path cleaning) to
the SFTP/FTP handlers before VFS routing and permission checks.

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-03-07 12:03:59 +01:00
Nicola Murino
6cef669b8d
portable mode: improve password generation and perms
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-02-16 20:17:06 +01:00
Nicola Murino
03ae0a1c84
initprovider cmd: initialize enabled commands before loading initial data
Fixes #2166

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-02-16 19:32:22 +01:00
Nicola Murino
4c00f6061c
reply to stat calls also for ongoing transfers on atomic storage backends
the check is performed only on the connection where the transfer is
initiated so it is inexpensive

Fixes #2162

Co-authored-by: Joel Studler <joel.studler@swisscom.com>
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-02-16 17:54:10 +01:00
Nicola Murino
2b4c1f32fd
sftpfs: remove a premature optimization
this may also fix a potential race condition

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-02-15 11:54:45 +01:00
Nicola Murino
7071011a5e
update js deps
include axios 1.13.5

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-02-14 18:11:13 +01:00
Nicola Murino
9b58744275
update security policy
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-02-14 18:10:55 +01:00
Nicola Murino
35d9242466
update css and js
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-02-08 09:15:17 +01:00
Nicola Murino
7a9f5eb50c
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-02-07 20:46:16 +01:00
Nicola Murino
fbbea4a1fa
vfs: fix S3 range off-by-one and part timeouts
wait for all the goroutine before retruning from multipart uploads

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-02-07 20:39:54 +01:00
Nicola Murino
96cc3dcf52
update the logo also in the img folder
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-02-05 07:59:00 +01:00
Nicola Murino
e48954f05c
update deps
replace deprecated WithCredentialsJSON with WithAuthCredentialsJSON.
This change explicitly enforces `option.ServiceAccount` as the
required credential type from JSON files

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-02-04 21:18:50 +01:00
Nicola Murino
c26cfa364f
log: remove uncessary dependency
ftpserverlib use slog now

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-02-04 19:33:40 +01:00
Nicola Murino
906b0731f1
update logo
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-02-01 10:30:23 +01:00
mmtootmm
133d2692c4
Fix/ftp active connection closed (#2164)
ftpd: avoid fresh ftp connection being closed
2026-01-27 18:48:59 +01:00
Nicola Murino
e3b2780655
webdav: ignore port for unix domain sockets
Fixes #2151

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-01-24 10:32:11 +01:00
Nicola Murino
e44ff487e5
httpd: add base URL configuration
Allow overriding the browser URL when generating share links.

Fixes #1858

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-01-23 19:38:02 +01:00
Nicola Murino
ed0c1a01ab
micro optimization to the function keepConnectionAlive
we don't need a goroutine

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-01-21 18:30:46 +01:00
Nicola Murino
f1022db5c1
Apply naming rules when validating user-associated folders
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-01-17 14:14:51 +01:00
Nicola Murino
e861f0f578
clean home dir after applying group replacement
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-01-16 19:49:06 +01:00
Nicola Murino
f90fbccb2c
ensure migration lock and transaction use the same connection
Previously, locks were session-bound but executed on a pool,
allowing race conditions.

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-01-12 18:44:22 +01:00
Nicola Murino
1815a098b6
WebAdmin: update Azure upload and download part size to match backend limits
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-01-10 14:15:27 +01:00
Nicola Murino
3bf7a85325
Update README: clarify project status and edition differences
- Add a comparison table between Community and Enterprise editions.
- Explicitly state the production-ready status of the Open Source version.
- Update feature descriptions (performance, compliance, automation).
- Refine Support and Documentation sections for better clarity.

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2026-01-01 12:14:47 +01:00
Nicola Murino
f06a2e8bd4
locales: add Spanish
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-12-31 17:10:34 +01:00
Nicola Murino
a73f8998a2
locales: add zh-CN
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-12-31 16:36:43 +01:00
Nicola Murino
0be7545d9b
docker: remove trailing whitespace from download script
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-12-29 08:55:46 +01:00
immanuwell
a8d74a311f refactor: improve script - add robust error handling for plugin downloads, make it easier to maintain, etc 2025-12-29 08:30:53 +01:00
Nicola Murino
4a091d6c24
logger: remove journald support
It was only used by the removed startsubsys mode.

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-12-27 16:41:26 +01:00
Nicola Murino
891248e7c9
update deps
added a slog/log adapter for ftpserverlib 0.28.0

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-12-27 11:48:06 +01:00
Nicola Murino
23d6e0dc3f
ftpd: add tls version, cipher and KEX to login log
Fixes #2124

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-12-24 12:52:31 +01:00
Nicola Murino
44828629c5
update nfpm to 2.44.1
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-12-24 11:26:35 +01:00
Nicola Murino
9db27aa782
remove restoreTemplateVars as it is no longer used
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-12-24 11:23:40 +01:00
Nicola Murino
3d549ce702
squash database migrations
also added shares_groups_mapping table, currently not used in the
open-source version

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-12-24 11:08:44 +01:00
Nicola Murino
1d9cc1e00f
ftpd: allow configuring a single passive port
Fixes #2146

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-12-23 07:36:06 +01:00
Nicola Murino
babdee5be1
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-12-20 17:58:57 +01:00
Nicola Murino
a1e45277dd
fix TestMemoryOIDCManager test case
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-12-20 17:19:53 +01:00
dependabot[bot]
375650e9be Bump actions/download-artifact from 6 to 7
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-20 17:03:24 +01:00
dependabot[bot]
2edd13aef6 Bump actions/upload-artifact from 5 to 6
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-20 17:03:05 +01:00
Nicola Murino
130fc8e0a2
OIDC/OAuth2: increase auth state validity to 2 minutes
Updates #2091

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-12-20 16:58:46 +01:00
Nicola Murino
0add546be3
user: fix group validation
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-12-17 20:01:16 +01:00
Nicola Murino
d0f4c6423e
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-12-11 08:44:52 +01:00
Nicola Murino
decdb187cf
Dockerfile: bump Alpine from 3.22 to 3.23
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-12-11 08:44:45 +01:00
Nicola Murino
21639b963c
OAuth2: add PKCE
Fixes #2134

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-12-11 08:44:38 +01:00
Nicola Murino
d42bbef16e
refactor: minor improvements to ResolvePath
Make ResolvePath more robust by avoiding implicit reliance on path
normalization performed by external libraries such as pkg/sftp and
ftpserverlib

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-12-11 08:44:30 +01:00
Nicola Murino
ac3e59562d
Enforce missing naming rule for actions and rules
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-12-07 10:35:13 +01:00
Nicola Murino
0cf9036f47
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-12-06 22:10:29 +01:00
Nicola Murino
8c85a722a2
apply naming rules for related groups, roles and folders
also enforce stricter validation rules for usernames/names

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-12-06 22:08:22 +01:00
Nicola Murino
e608805b13
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-11-30 12:29:42 +01:00
dependabot[bot]
cbdc48ba7c Bump actions/checkout from 5 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-30 12:26:47 +01:00
Nicola Murino
90821ffc23
cloud backends: update part size limits
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-11-23 12:56:59 +01:00
Nicola Murino
32cc426cb9
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-11-20 09:00:39 +01:00
Nicola Murino
9fa18c37f7
fix lint warning
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-11-19 19:29:04 +01:00
Nicola Murino
4230da8e7d
s3: implement multipart downloads without using the S3 Manager
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-11-19 18:53:27 +01:00
Nicola Murino
22c875c0a1
sftpd: add support for OpenPubkey SSH
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-11-19 09:16:56 +01:00
Nicola Murino
74f8539247
pre-login hook: require either a full user object or no user modification
The previous behavior was a leftover from an old refactor.
This change aligns the pre-login hook with the behavior of other hooks,
although it may break some edge cases that relied on the previous inconsistent
behavior.

Fixes #2107

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-11-18 20:09:22 +01:00
dependabot[bot]
3a42c70021 Bump golangci/golangci-lint-action from 8 to 9
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 8 to 9.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v8...v9)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-version: '9'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-18 18:54:01 +01:00
dependabot[bot]
59bd46a227 Bump actions/upload-artifact from 4 to 5
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-27 08:55:40 +01:00
dependabot[bot]
5039a0ee31 Bump actions/download-artifact from 5 to 6
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-27 08:55:02 +01:00
Nicola Murino
973b68a383
data provider: micro-optimization
skip availability check when updating the node timestamp.
If the update succeeds, the data provider is healthy

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-10-26 09:48:47 +01:00
Nicola Murino
5ce9688780
enforce group-level password strength for users and shares
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-10-26 09:44:32 +01:00
Nicola Murino
accb9703d0
back to development
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-10-26 08:14:24 +01:00
Nicola Murino
9c42ec34e8
pkgs update
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-10-26 08:12:32 +01:00
Nicola Murino
29c635a9a6
update version and deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-10-23 18:26:44 +02:00
Nicola Murino
e82d1bbef6
sftpgo.json: remove non-existent setting
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-10-16 19:40:29 +02:00
Nicola Murino
2c36077178
CI: use Go 1.25 for FreeBSD
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-10-16 19:39:49 +02:00
dependabot[bot]
4e91326124 Bump github/codeql-action from 3 to 4
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3 to 4.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-15 08:42:08 +02:00
Nicola Murino
317f14f869
Docker: remove git and rsync
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-10-15 08:41:05 +02:00
Nicola Murino
a768dac29d
jwt: increase leeway and add some tests
also export a constant for the Cookie name

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-10-11 14:14:21 +02:00
Nicola Murino
c4bc88cd2e
Docker: update to debian 13
also update nfpm to 2.43.4

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-10-10 21:30:33 +02:00
Nicola Murino
90685d8ef2
fix random failure in test cases
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-10-10 21:30:05 +02:00
Nicola Murino
f21c3d2af2
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-10-09 20:46:29 +02:00
Nicola Murino
278f522e30
sftpgo.json: cleanup unsupported configuration keys
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-10-09 20:44:43 +02:00
Nicola Murino
fa70ff35c4
Add debug log to investigate intermittent test failure in CI
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-10-08 19:22:35 +02:00
Nicola Murino
314bb5c886
update deps and nfpm
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-10-08 18:51:52 +02:00
Nicola Murino
0ae2354fed
JWT: replace jwtauth/jwx with lightweight wrapper around go-jose
We replaced the jwtauth and jwx libraries with a minimal custom wrapper
around go-jose because we don’t need the full feature set provided by jwx.
Implementing our own wrapper simplifies the codebase and improves
maintainability.

Moreover, go-jose depends only on the standard library, resulting in a
leaner dependency that still meets all our requirements.

This change also reduces the SFTPGo binary size by approximately 1MB

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-10-08 18:10:39 +02:00
Nicola Murino
9ca35c3555
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-10-04 11:42:35 +02:00
Nicola Murino
69f2c70661
CI: use windows-latest and install iscc manually
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-09-28 18:41:39 +02:00
Nicola Murino
35525e22e9
remove rsync support
rsync was executed as an external command, which means we have no insight
into or control over what it actually does.
From a security perspective, this is far from ideal.

To be clear, there's nothing inherently wrong with rsync itself. However,
if we were to support it properly within SFTPGo, we would need to implement
the low-level protocol internally rather than relying on launching an external
process. This would ensure it works seamlessly with any storage backend,
just as SFTP does, for example.
We recommend using one of the many alternatives that rely on the SFTP
protocol, such as rclone

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-09-28 18:15:15 +02:00
Nicola Murino
cc0ee9f43b
update nfpm to 2.43.1
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-09-28 10:03:45 +02:00
Nicola Murino
7dd5757a44
CI: use Windows-2022 for now
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-09-28 10:02:27 +02:00
Nicola Murino
3f21db14e4
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-09-28 09:36:28 +02:00
Nicola Murino
e892748ef4
system commands: recursively verify required permissions
If any permission is missing at any level, return a "Permission Denied"
error

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-09-28 09:36:19 +02:00
Nicola Murino
f4092b9f9e
sftpd: use VerifiedPublicKeyCallback
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-09-28 09:22:27 +02:00
Nicola Murino
cdaefbf04a
Fix flaky test case
ensure the user filter is set on the rule so notification triggers
only when expected.

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-09-22 09:16:28 +02:00
Nicola Murino
5c3aa8278b
CI: switch to Go 1.25
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-09-22 09:16:23 +02:00
Nicola Murino
255ad5f6db
remove automaxprocs: no longer required with Go 1.25
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-09-22 09:16:14 +02:00
Nicola Murino
a469dd68a2
update theme and js deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-09-22 09:15:57 +02:00
Nicola Murino
29e9d95088
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-09-21 14:21:57 +02:00
Nicola Murino
952df50a98
remove ftpserverlib fork
the correct flow is to add features to the upstream library first

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-09-21 14:21:53 +02:00
Nicola Murino
d2ee43585a
remove x/crypto fork
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-09-21 14:21:47 +02:00
dependabot[bot]
726f1fde19 Bump golang from 1.24-bookworm to 1.25-bookworm
Bumps golang from 1.24-bookworm to 1.25-bookworm.

---
updated-dependencies:
- dependency-name: golang
  dependency-version: 1.25-bookworm
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-21 14:20:25 +02:00
Nicola Murino
75a9ebcdf9
CI: remove Azure Trusted Signing action
The Azure Trusted Signing certificate is expiring soon, and renewal is no
longer available  for individuals or organizations outside of Canada and USA.

Due to this limitation, we are removing the Trusted Signing step from our
CI pipeline.

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-09-20 18:07:21 +02:00
Nicola Murino
7f03dc0fab
convert action migration: allow to import any command action
EnabledCommands are initialized after the migration so allow any
command, they will be denied if not allowed and this is temporary.
The migration will be removed in the future

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-09-20 17:59:15 +02:00
Nicola Murino
52ae36f169
README: better clarify how to select the appropriate documentation
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-09-10 20:14:35 +02:00
Nicola Murino
7ce456edef
CI release: move Azure login closer to signing step in Windows workflow
The Azure login token validity has been decreased so login just before
signing

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-09-09 19:46:06 +02:00
Nicola Murino
b1208279b7
CI: move Azure login closer to signing step in Windows workflow
Azure login tokens now appear to expire after 5 minutes.
To avoid authentication issues, the login step is now performed
immediately before signing the binaries.

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-09-09 19:08:57 +02:00
Nicola Murino
0dca906351
docs: clarify sponsor/support model and how to use versioned documentation
- Updated the "Sponsors" section to reflect the current open-core model
- Clarified that sponsorship supports the open-source edition
- Improved "Support" section to distinguish community vs. Enterprise support
- Added instructions on selecting the correct documentation version

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-09-09 18:38:27 +02:00
dependabot[bot]
20df8ba48b Bump actions/setup-go from 5 to 6
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 5 to 6.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-09 18:16:22 +02:00
Nicola Murino
b160090866
httpdtest: remove unused constant
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-09-07 18:19:45 +02:00
Nicola Murino
78d93730e0
update README and support link now that SFTPGo Enterprise is GA
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-09-07 18:18:55 +02:00
Nicola Murino
aad4de6001
html templates: update attribution
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-08-22 16:21:20 +02:00
Nicola Murino
19d1a0e0c1
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-08-22 15:57:55 +02:00
Nicola Murino
a5dd529d88
node token: embed permissions directly in JWT
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-08-22 15:57:41 +02:00
Nicola Murino
6bde42fc3f
dataprovider: prevent action execution after external authentication
As per the documentation for external authentication, provider actions
should  not be executed post-authentication.

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-08-21 07:20:06 +02:00
Nicola Murino
917d992231
CI: update FreeBSD to 14.3
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-08-19 21:29:17 +02:00
dependabot[bot]
fc111b44d9 Bump actions/download-artifact from 4 to 5
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-19 21:27:28 +02:00
dependabot[bot]
cdcea54f46 Bump actions/checkout from 4 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-19 21:26:52 +02:00
Nicola Murino
a2d3613250
dataprovider: preserve initial sort order for related resources
Folders and groups now retain their initial order, improving compatibility
and predictability when used with Terraform

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-08-19 16:11:53 +02:00
Nicola Murino
81a9813376
Windows: fix build
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-08-17 13:35:29 +02:00
Nicola Murino
63366b0007
virtual folders: fix path placeholder check
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-08-17 12:42:37 +02:00
Nicola Murino
0f6202f059
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-08-17 12:34:35 +02:00
Nicola Murino
e7a1128574
remove AWS Marketplace specific code
it is out of context for the Open-Source edition

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-08-17 12:29:57 +02:00
Nicola Murino
0dec86474e
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-08-06 20:44:09 +02:00
Nicola Murino
b48a90bce9
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-08-02 19:21:53 +02:00
Nicola Murino
75ad6346c3
removed some unused constants
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-08-02 19:00:15 +02:00
Nicola Murino
b2948a5255
sshd: removed Git support
Git integration has been removed as it is out of scope for a file transfer
solution like SFTPGo.

Maintaining Git support introduces unnecessary complexity and potential
security risks due to reliance on system commands.

In particular, allowing Git operations could enable authorized users to
upload repositories containing hooks, which might then be executed and abused.

To reduce the attack surface and simplify the codebase, Git support has been
fully dropped.

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-08-02 18:58:03 +02:00
Nicola Murino
ddbe40cefa
HTTPD, WebDAV: use http.ResponseController
backport from Enterprise edition

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-08-02 18:00:45 +02:00
Nicola Murino
9a0137befb
config: redact master key string
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-07-22 19:53:19 +02:00
Nicola Murino
0bac81816c
WebClient: add an id field to files list to simplify UI logic
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-07-22 18:59:20 +02:00
Nicola Murino
8ae6e5e486
WebUI: improve fileSizeIEC function and make it more readable
Fixes #1974

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-07-21 18:26:09 +02:00
Nicola Murino
c49d76274d
WebClient: translate "selected items" label also at bottom of page
Fixes #1979

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-07-21 18:24:49 +02:00
Nicola Murino
ae11c81bf8
Improve issue templates
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-07-20 13:18:25 +02:00
dependabot[bot]
166b87fa3c Bump azure/trusted-signing-action from 0.5.1 to 0.5.9
Bumps [azure/trusted-signing-action](https://github.com/azure/trusted-signing-action) from 0.5.1 to 0.5.9.
- [Release notes](https://github.com/azure/trusted-signing-action/releases)
- [Commits](https://github.com/azure/trusted-signing-action/compare/v0.5.1...v0.5.9)

---
updated-dependencies:
- dependency-name: azure/trusted-signing-action
  dependency-version: 0.5.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-20 10:52:22 +02:00
Nicola Murino
76f6dc06de
Log output from command hooks
Re-adds #1208 now that the CLA has been signed.

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-07-20 10:45:48 +02:00
Nicola Murino
c2835bc19d
Enable setting password change requirements in user templates
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-07-17 19:35:17 +02:00
Nicola Murino
fe78974b47
remove data retention hook
use the EventManager instead

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-07-14 18:22:32 +02:00
Nicola Murino
9d20f1744a
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-07-12 23:05:07 +02:00
Nicola Murino
7317674b41
Remove legacy data retention API
Data retention is now managed via the EventManager, introduced in v2.4.0.
This allows scheduling retention checks and sending email or HTTP notifications,
making the old API redundant.

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-07-12 22:20:54 +02:00
Nicola Murino
bdd097b1c7
s3: use multipart uploads only when multiple parts are needed
Fixes #2016

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-07-11 18:45:30 +02:00
Nicola Murino
66a20f34f8
sysv script: set mode to 755
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-07-04 11:32:07 +02:00
Nicola Murino
bb7891c196 Update and rename sftpgo to init/sftpgo
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-07-03 16:18:06 +02:00
no-systemd
1bbc56c3b9 Init script for sysv-init based systems. (#1)
Init script for sysv-init based systems.

Signed-off-by: no-systemd <saman@whitebird.cc>
2025-07-03 16:18:06 +02:00
Nicola Murino
1b95468783
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-07-03 11:53:20 +02:00
Nicola Murino
c9d361d93b
OpenAPI: add missing event statuses to rule condition options
Fixes #2011

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-07-03 11:44:06 +02:00
Nicola Murino
d0ad528135
update lint rules
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-06-29 08:43:45 +02:00
Nicola Murino
4c5d9f3a25
OpenAPI: fix logs endpoint
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-06-21 09:45:29 +02:00
Nicola Murino
fb46c28ff2
examples: update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-06-20 20:22:58 +02:00
Nicola Murino
e34c196532
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-06-20 20:12:42 +02:00
Nicola Murino
5848289756
nfpm: update to 2.43.0
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-06-16 19:09:07 +02:00
Nicola Murino
ff5ea7cd40
S3: don't use manager for uploads
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-06-16 18:48:04 +02:00
Nicola Murino
cea5dd665e
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-06-14 11:40:47 +02:00
Nicola Murino
d05250923b
Revert "GCS: allow a 10 seconds timeout for client creation"
This reverts commit b2e9935049.

Fixes #2000

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-06-10 20:17:35 +02:00
Nicola Murino
5ca3522dc0
EventManager: avoid copying user struct when updating parameters
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-06-10 20:04:59 +02:00
dependabot[bot]
d6fbe97e14
Bump alpine from 3.21 to 3.22 (#1994)
* Bump alpine from 3.21 to 3.22

Bumps alpine from 3.21 to 3.22.

---
updated-dependencies:
- dependency-name: alpine
  dependency-version: '3.22'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Update Dockerfile.alpine

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nicola Murino <nicola.murino@gmail.com>
2025-06-02 20:53:16 +02:00
Nicola Murino
0265c4c4a1
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-05-31 20:30:58 +02:00
Nicola Murino
b6873768b2
replace strings.Split with SplitSeq
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-05-31 19:03:41 +02:00
Nicola Murino
60af36813b
gcs: improve error checking
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-05-23 19:56:21 +02:00
Nicola Murino
3f7533b86a
update deps ...
... and adapt the code to the new constants I added to
golang.org/x/crypto/ssh

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-05-19 19:42:36 +02:00
Nicola Murino
e275e8a142
WebClient: prevent uploads if no file is selected
Fixes #1980

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-05-19 18:21:55 +02:00
Nicola Murino
6f9729f245
WebClient: clear file upload list on explicit cancel
Avoid clearing the file list every time the modal is opened, as
the modal might be closed unintentionally (e.g., by clicking
outside it)

Fixes #1981

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-05-19 18:20:20 +02:00
Nicola Murino
f7273ce97e
UI: add missing French and German localization for calendars
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-05-19 18:18:52 +02:00
Nicola Murino
392b22219f
Windows setup: fix copyright
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-05-13 18:42:10 +02:00
Nicola Murino
fb97b9f539
WebClient: Fix multi-page selection
removed legacy workaround code that was likely introduced to mask
a bug in the DataTable component.
This underlying issue has since been resolved and this code cause
issues now.

Fixes #1971

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-05-13 18:30:21 +02:00
Nicola Murino
c5a8d672d2
WebClient: increase contextual menu size
Some language translations may contain longer text, requiring
additional space in the menu

Fixes #1972

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-05-13 18:28:24 +02:00
dependabot[bot]
e5d2d26636
Bump golangci/golangci-lint-action from 7 to 8 (#1967)
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 7 to 8.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v7...v8)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-11 10:51:06 +02:00
Nicola Murino
09e65c8d9f
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-05-10 19:07:14 +02:00
Nicola Murino
9e2230cc33
Support leading and trailing spaces in user passwords
This improves compatibility with external authentication providers that
allow such characters in passwords.

Passwords created via the WebAdmin UI are still sanitized to prevent user
confusion.

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-04-26 14:31:13 +02:00
Nicola Murino
a709b84eef
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-04-24 19:08:02 +02:00
Nicola Murino
1c48e51384
EventManager: escape email body when content type is text/html
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-04-24 19:01:17 +02:00
Nicola Murino
5efd232809
CI: workflows improvements
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-04-24 18:01:08 +02:00
Nicola Murino
683d00caec
cmd: remove startsubsys command
SFTPGo is not designed to be used as an OpenSSH subsystem — many
features do not work correctly in subsystem mode. The functionality
was added after a user request in the pkg/sftp repository to
demonstrate that it was feasible, not for actual practical use.

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-04-20 18:51:01 +02:00
Nicola Murino
c5e76f303a
commands: initialize plugins if we have a KMS
this is necessary to be able to read KMS secrets stored within
the data provider

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-04-20 18:48:19 +02:00
Nicola Murino
513cbe3a77
CI FreeBSD: switch to Go 1.24
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-04-20 17:34:51 +02:00
Nicola Murino
11d8fffd1b
remove obsoletes build constraints
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-04-20 17:15:40 +02:00
Nicola Murino
e1472e9f97
update deps in tests and examples
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-04-14 22:24:39 +02:00
Nicola Murino
0da8adb7ac
EventManager: breaking change for placeholder names
Placeholder names must now be in the format:

{{.VirtualPath}}

instead of:

{{.VirtualPath}}

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-04-14 09:11:44 +02:00
Nicola Murino
1cf0ed5b7e
nfpm: update to 2.42.0
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-04-12 22:27:50 +02:00
Nicola Murino
a3a3d2e867
FreeBSD: disable tests until Go 1.24 is available
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-04-12 22:17:09 +02:00
Nicola Murino
17bbe3d297
update deps, add support for mlkem768x25519-sha256
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-04-12 22:10:27 +02:00
Nicola Murino
aea036715c
OIDC: ensure token username adheres to naming conventions
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-04-08 18:25:16 +02:00
Nicola Murino
f41f00fec2
httpd: allow to configure referrer policy header
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-04-07 18:48:48 +02:00
Nicola Murino
01fbf3480f
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-04-07 18:03:33 +02:00
Nicola Murino
5954d4ae20
sshconn: use a generic io.Closer instead of a net.Conn
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-04-02 18:52:06 +02:00
Nicola Murino
3cae004e6b
UI: added German and French translations
Thanks to all contributors on Crowdin.

Updates #1874

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-03-31 19:05:50 +02:00
Nicola Murino
06cd07d67a
oidc: add missing translations
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-03-29 20:51:45 +01:00
Nicola Murino
d95d773570
oidc: allow login if the password method is disabled
isLoggedInWithOIDC returns false before login so we need to add
a specific check

Fixes #1879

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-03-29 20:28:49 +01:00
Nicola Murino
cf573fc743
pre-login hook: fix loading user after update
Fixes #1890
Closes #1891

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-03-29 17:29:28 +01:00
Nicola Murino
2255c5f000
upgrade golangci-lint to v2
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-03-29 11:36:19 +01:00
Nicola Murino
6162da7636
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-03-29 10:50:25 +01:00
Nicola Murino
37d4d1c77f
added ReUI to the sponsors section
A heartfelt thank you to Sean and the KeenThemes team for their
ongoing support

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-03-29 10:44:26 +01:00
Nicola Murino
38689a71a7
migrations: fix placeholder for shared session table
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-03-23 12:06:57 +01:00
Nicola Murino
a71e53c8c8
GCS: properly check for googleapi.Error
Fixes #1936

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-03-23 11:37:21 +01:00
Nicola Murino
e590deebe0
db shared sessions: set key and type as primary key
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-03-23 11:34:10 +01:00
Nicola Murino
5a088daf97
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-03-22 14:55:40 +01:00
Nicola Murino
d4ea6adcc3
config: fix test case for slice values
this is a behaviour change in the lastest version of viper

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-03-16 14:16:04 +01:00
Nicola Murino
39ebfab693
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-03-16 12:56:47 +01:00
Nicola Murino
dfde4d45e2
update plugins bundle.js
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-03-16 12:45:01 +01:00
Nicola Murino
312902b5f5
update js deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-03-16 12:26:08 +01:00
Nicola Murino
67002ae24d
nfpm: update 2.41.3
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-03-15 20:40:06 +01:00
Nicola Murino
51a9cf79bc
azure blob fs: ensure sas url are not nil before comparing
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-03-15 20:39:42 +01:00
Nicola Murino
e3b513ccdb
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-03-13 12:00:39 +01:00
Nicola Murino
1e873ff86c
shares: show disclaimer on login page
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-03-13 12:00:33 +01:00
Nicola Murino
f096675a2b
fix log formatting
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-03-12 11:19:38 +01:00
Nicola Murino
66ec11a19f
fix typo
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-03-08 15:49:30 +01:00
Nicola Murino
15ac11b575
EventManager: add timestamp and name to scheduled event parameters
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-03-04 18:03:07 +01:00
Nicola Murino
eeee02875a
WebUI: fix draw events for datatables
sometimes the context menu was not working because the draw action
was called too early

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-03-03 18:14:19 +01:00
Nicola Murino
e409dc3100
README: update badge
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-03-02 19:35:08 +01:00
Nicola Murino
40c14607f6
WebAdmin: fix column visibility after reorder
Fixes #1899

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-03-02 18:47:10 +01:00
dependabot[bot]
c61571ea07
Bump golang from 1.23-bookworm to 1.24-bookworm (#1898)
Bumps golang from 1.23-bookworm to 1.24-bookworm.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-01 10:42:16 +01:00
Nicola Murino
cf961afe59
update CI and deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-03-01 10:11:28 +01:00
Nicola Murino
2a1374d376
back to development
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-02-24 21:23:28 +01:00
Nicola Murino
dbe31034ce
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-02-24 19:03:34 +01:00
Nicola Murino
aadd5d7d28
update js bundle
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-02-24 18:42:39 +01:00
Nicola Murino
75c45190ae
CI: disable tests on FreeBSD until Go 1.24 or 1.23.6 is available
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-02-24 11:37:21 +01:00
Nicola Murino
002e819e54
defender: don't penalize redirects to the login page
This is normal behavior

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-02-23 16:56:47 +01:00
Nicola Murino
38a6b5632a
share login page: add CheckRedirect field
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-02-22 22:28:53 +01:00
Nicola Murino
5a01ce66f1
WebUIs: fix translations for some page titles
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-02-18 18:25:52 +01:00
Nicola Murino
83cfcde9cb
OpenAPI: add email format to user's additional emails
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-02-16 18:02:45 +01:00
Nicola Murino
152448d116
dataprovider: add options to shares for future extensibility
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-02-16 12:06:00 +01:00
Nicola Murino
51e487370a
CI: update FreeBSD to 14.2
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-02-15 12:15:49 +01:00
Nicola Murino
7c6c81a841
CI: switch to Go 1.24
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-02-15 11:45:43 +01:00
Nicola Murino
0e0cfd62bb
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-02-09 11:42:32 +01:00
Nicola Murino
4dc4ccad37
back to development
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-02-07 20:26:30 +01:00
Nicola Murino
32b7fa2670
CI: macos-12 is no longer supported
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-02-07 18:41:35 +01:00
Nicola Murino
0013e35b28
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-02-06 19:23:05 +01:00
dependabot[bot]
9fcd12da2e
Bump azure/trusted-signing-action from 0.5.0 to 0.5.1 (#1863)
Bumps [azure/trusted-signing-action](https://github.com/azure/trusted-signing-action) from 0.5.0 to 0.5.1.
- [Release notes](https://github.com/azure/trusted-signing-action/releases)
- [Commits](https://github.com/azure/trusted-signing-action/compare/v0.5.0...v0.5.1)
2025-02-06 18:33:02 +01:00
Nicola Murino
b77be826e6
CI: update workflows
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-02-06 18:32:57 +01:00
Nicola Murino
519d201e74
fix rsync test case
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-02-06 11:01:12 +01:00
Nicola Murino
a3f7405a08
Sponsors: VPS2day has been rebranded to servinga
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-02-05 18:39:37 +01:00
Nicola Murino
1393cf5956
ftp: add a test for SIZE command on dirs
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-02-04 19:26:55 +01:00
Nicola Murino
69ef36b4d9
httpd: add a setting to disable login methods, deprecate the previous one
the previous enabled login methods setting is hard to extend in
a backward compatible way

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-01-25 22:00:55 +01:00
Nicola Murino
70f8b4d495
WebAdmin: allow to create admins with an unusable password
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-01-25 18:53:54 +01:00
Nicola Murino
48258f6e67
httpd: add cross origin resource and embedder policy headers
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-01-24 19:34:57 +01:00
Nicola Murino
83ee977746
ip lists: check the list size before parsing the IP
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-01-23 08:07:33 +01:00
Nicola Murino
b686da5e56
WebClient: improve error message after the session expires
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-01-22 19:54:56 +01:00
Nicola Murino
61aef41bee
WebClient: make the keep alive interval configurable
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-01-22 19:41:31 +01:00
Nicola Murino
6ab0f22d2d
update translations
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-01-21 18:10:51 +01:00
Nicola Murino
c4e80cd5b2
format config file
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-01-16 20:06:10 +01:00
Nicola Murino
ef2f3e51ea
EventManager: add more datetime placeholders
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-01-16 18:14:43 +01:00
Nicola Murino
24215dc734
remove check for cache key collisions
we use sha256 keys now

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-01-15 19:46:54 +01:00
Nicola Murino
e2b21ad946
ssh commands: fix for rsync with no arguments
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-01-15 19:23:23 +01:00
Nicola Murino
969faddeee
update nfpm to 2.41.2
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-01-13 21:11:50 +01:00
Nicola Murino
e8c5f8ed81
command actions: restrict passing env vars
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-01-13 20:58:32 +01:00
Nicola Murino
04fa242f57
azblobfs: add support for Azure Identity
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-01-13 20:58:17 +01:00
Nicola Murino
de3c987802
rsync: enforce a supported format and limit the allowed options
Many rsync options are unsafe to use in restricted environments
and may pose security risks.

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-01-13 19:41:58 +01:00
Nicola Murino
f2123b4fb0
update copyright year range
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-01-11 14:30:43 +01:00
Nicola Murino
a759789454
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-01-11 11:48:07 +01:00
Nicola Murino
da68cf3e9d
events search: remove trailing and leading space from received parameters
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-01-11 11:42:57 +01:00
Nicola Murino
5febcdca43
httpd: log csrf token duration
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-01-11 11:29:35 +01:00
Nicola Murino
b2e9935049
GCS: allow a 10 seconds timeout for client creation
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-01-11 11:25:45 +01:00
Nicola Murino
1f4cb7077a
bad host handler: return a generic error message
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-01-06 10:08:25 +01:00
Nicola Murino
fbf8b1285d
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-01-04 17:22:30 +01:00
Nicola Murino
bf0961458c
remove some unnecessary string conversions
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-01-04 11:58:37 +01:00
Nicola Murino
a4a33d4407
acme: add logger to retryable http client
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-01-04 09:27:01 +01:00
Nicola Murino
ff13be4616
zip creation: avoid stat if not strictly required
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2025-01-03 11:43:09 +01:00
Nicola Murino
37f8fb3a0e
add a link to the upgrading docs in the error message
Fixes #1854

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-12-31 10:04:43 +01:00
Nicola Murino
484bda7940
UI: fix some glitches
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-12-29 17:16:28 +01:00
Nicola Murino
deea9ff038
do not return if client IP is not allowed in login API response
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-12-28 18:47:04 +01:00
Nicola Murino
91340bbe2f
config: reset invalid rename mode
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-12-26 09:36:58 +01:00
Nicola Murino
e689d52dca
plugin: simplify notifiers queue handling
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-12-25 22:53:26 +01:00
Nicola Murino
22f80b97f0
WebClient js: add missing semicolon
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-12-25 11:34:14 +01:00
Nicola Murino
dee3f3f87a
EventManager: add placeholder for filename without extension
Fixes #1828
Fixes #1833

Co-authored-by: Per Osbeck <per.osbeck@consid.se>
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-12-23 19:08:48 +01:00
Nicola Murino
d2c5a6a914
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-12-23 08:50:51 +01:00
Nicola Murino
1a7f346b51
acme: use retryable client
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-12-21 18:28:15 +01:00
Nicola Murino
bb579e36db
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-12-19 20:01:55 +01:00
Nicola Murino
843b8c38d3
SSH: add a test case for DSA keys
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-12-19 19:55:25 +01:00
Nicola Murino
70fc00d7eb
Allow to choose enabled languages
Fixes #1835

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-12-19 19:50:19 +01:00
Nicola Murino
9f873d1059
prefer strings.EqualFold to strings.strings.ToLower where possible
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-12-19 19:44:01 +01:00
Nicola Murino
b0061f570e
WebClient: refactor preserving share password
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-12-18 19:54:39 +01:00
Nicola Murino
bfe6c58133
don't allow DSA keys
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-12-18 19:01:15 +01:00
Nicola Murino
8c5f92aeb1
dataprovider events: fix string formatting for program hook
Fixes #1845

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-12-18 18:37:44 +01:00
Nicola Murino
ec90b61bb4
allow to configure JWT tokens and cookies duration
Fixes #1839

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-12-18 18:33:37 +01:00
Nicola Murino
6a72552754
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-12-15 11:55:49 +01:00
dependabot[bot]
1ce408e673
Bump alpine from 3.20 to 3.21 (#1832)
* Bump alpine from 3.20 to 3.21

Bumps alpine from 3.20 to 3.21.

---
updated-dependencies:
- dependency-name: alpine
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Update Dockerfile.alpine

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nicola Murino <nicola.murino@gmail.com>
2024-12-15 10:01:09 +01:00
Nicola Murino
d3db80dc32
set stat: remove unecessary check
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-12-15 09:59:06 +01:00
Nicola Murino
c56be285a5
replace fnv with sha256
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-12-14 14:42:43 +01:00
Nicola Murino
599ee5a58f
EventManager: check file size for more events
Also add some defensive code

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-12-14 13:19:02 +01:00
Nicola Murino
7703f57122
rename: minor optimization
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-12-08 09:58:28 +01:00
Nicola Murino
b8a4ea50bd
CI codecov action: replace deprecated "file" attribute with "files"
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-12-07 11:46:15 +01:00
Nicola Murino
49f2555914
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-12-07 10:35:09 +01:00
Nicola Murino
e21c989038
logs: add a specific log structure for successful logins
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-12-07 10:29:33 +01:00
Nicola Murino
f8bdb84e8d
s3: metadata is not currently supported
remove useless code, we'll add it again once we support metadata

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-12-01 17:32:15 +01:00
Nicola Murino
e161015c67
upload: avoid a stat call if not strictly required
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-11-30 20:43:19 +01:00
Nicola Murino
cbd7fc917e
back to development
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-11-28 08:19:40 +01:00
Nicola Murino
6a7c8df1ef
use GenerateOpaqueString also for node secrets
this method will use rand.Text() with Go 1.24

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-11-27 19:05:19 +01:00
Nicola Murino
d3e76898cd
WebAdmin: refactor template permissions
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-11-26 20:39:36 +01:00
Nicola Murino
0f9314f900
CI: skip signing Windows binaries for pull requests
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-11-23 19:26:28 +01:00
Nicola Murino
502e3658e0
CI: update workflows to use Azure Trusted Signing
Fixes #1778

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-11-22 18:20:58 +01:00
Nicola Murino
0e77ba9546
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-11-21 07:52:57 +01:00
Nicola Murino
10b2e5671b
silence lint warning
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-11-21 07:52:53 +01:00
Nicola Murino
ebc085da77
EventManager: always close the connection filesystem
closing the user filesystem is not enough here

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-11-21 07:52:49 +01:00
Nicola Murino
4a414f0fa4
test cases: fix some random failures
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-11-21 07:52:45 +01:00
Nicola Murino
7a12db6cdb
upgrade nfpm to 2.41.1
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-11-21 07:52:42 +01:00
Nicola Murino
f30a9a2095
OIDC cookie: use a cryptographically secure random string
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-11-21 07:52:36 +01:00
Nicola Murino
ed5ff9c5cc
sftpd: remove allocator
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-11-21 07:52:24 +01:00
Nicola Murino
59833fba0d
back to development
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-11-15 20:50:54 +01:00
Nicola Murino
a79cb30cdc
CI: update codecov action to v5
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-11-15 17:28:55 +01:00
Nicola Murino
e1cd69d5ff
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-11-15 17:26:12 +01:00
Nicola Murino
85333087fa
fix license in Windows installer
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-11-15 17:06:49 +01:00
Nicola Murino
5ddac4b3b4
fix links to docs, add NOTICE
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-11-15 15:19:26 +01:00
Nicola Murino
c37b7f0493
provider rule events: allows to filter by user groups
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-11-15 14:01:08 +01:00
Nicola Murino
5896c1b7a5
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-11-13 20:22:31 +01:00
Nicola Murino
0f073a40fd
logger: add cipher suite
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-11-13 18:33:07 +01:00
Nicola Murino
618723c457
httpd: always use an opaque signing key
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-11-12 19:27:34 +01:00
Nicola Murino
4cb6acefb2
oidc/oauth2: use an opaque state
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-11-11 19:43:57 +01:00
Nicola Murino
f22ec2275f
fix new lint warnings
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-11-10 20:58:22 +01:00
Nicola Murino
7bffed712a
events: add copy action
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-11-10 15:00:11 +01:00
Nicola Murino
f30d6ad82a
update css and js deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-11-10 12:34:10 +01:00
Nicola Murino
b524da11e9
EventManager: disable commands by default
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-11-10 12:08:17 +01:00
Nicola Murino
3dd412f6e3
WebAdmin and REST API: remove too granular permissions
Our permissions system for admin users is too granular and some
permissions overlap. For example, you can define an administrator
with the "manage_system" permission and not with the "manage_admins"
or "manage_user" permission, but the "manage_system" permission
allows you to restore a backup and then create users and
administrators. The following permissions will be removed:
"manage_admins", "manage_apikeys", "manage_system", "retention_checks",
"manage_event_rules", "manage_roles", "manage_ip_lists". Now you
need to add the "*" permission to replace the removed granular
permissions because the removed permissions allow actions that
should only be allowed to super administrators.
There is no point in having separate, overlapping permissions.

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-11-10 10:46:28 +01:00
Nicola Murino
ef98ee7d11
don't allow admins to change their own permissions
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-11-09 20:24:35 +01:00
Nicola Murino
30fb1d6240
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-11-09 18:56:43 +01:00
Nicola Murino
7aac64531f
WebAdmin: check CSRF header when deleting blocked hosts
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-11-09 18:44:31 +01:00
Nicola Murino
03724d5eb1
remove fallback if rand.Reader fails
Failing to read from rand.Reader essentially can't happen, and if it
does is not possible to fallback securely, so just panic

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-11-09 18:44:25 +01:00
Nicola Murino
4eb4ff66ce
CI: switch to Go 1.23
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-11-07 20:14:16 +01:00
dependabot[bot]
0bff3e1a67
Bump golang from 1.22-bookworm to 1.23-bookworm (#1729)
Bumps golang from 1.22-bookworm to 1.23-bookworm.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-07 20:09:29 +01:00
Nicola Murino
82b437c502
plugins: fix passing additional environment variables
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-11-05 18:06:58 +01:00
Nicola Murino
88b1850b58
EventManager: allow to define the allowed system commands
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-11-01 11:37:33 +01:00
Nicola Murino
60558de728
proxy protocol: add more logs
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-10-31 18:04:55 +01:00
Nicola Murino
beff4432dc
plugin: remove invalid chars from error message
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-10-29 18:11:53 +01:00
Nicola Murino
9ae0bc4ec4
WebAdmin active connections: fix active transfer display
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-10-28 20:10:59 +01:00
Nicola Murino
21bd8c5660
node: use a plain string as key
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-10-28 18:34:36 +01:00
Nicola Murino
97bb004c12
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-10-26 21:50:21 +02:00
Nicola Murino
e4e31ec4fb
TestMaxSessionsSameConnection: make more reproducible
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-10-26 21:50:15 +02:00
Nicola Murino
259986ed1d
update nfpm to 2.41.0
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-10-26 21:26:36 +02:00
Nicola Murino
0c75d234b9
OpenAPI: document password_strength
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-10-26 21:19:28 +02:00
Nicola Murino
ae1487d733
fix connection limits
an SFTP client can start multiple transfers on a single connection

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-10-26 21:18:19 +02:00
Nicola Murino
c69fbe6bf9
tls: allow to configure all supported TLS versions and ciphers
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-10-23 19:50:37 +02:00
Nicola Murino
8d697bcc94
WebClient: enforce 2fa and password requirements also with OIDC
password and 2fa can be used with other protocols

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-10-21 20:40:44 +02:00
Nicola Murino
7e7005f5b3
README: add a section for i18n
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-10-19 10:45:02 +02:00
Nicola Murino
12a210e1f6
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-10-18 19:26:53 +02:00
Nicola Murino
169d8f6223
update README
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-10-18 19:26:19 +02:00
Nicola Murino
cd3147c654
add License NOTICE
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-10-18 19:26:11 +02:00
Nicola Murino
7feeec6941
update OpenAPI schema
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-10-16 20:51:51 +02:00
Nicola Murino
12d888f49d
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-10-16 19:19:24 +02:00
Nicola Murino
ca41b59fc4
DirLister: returns appropriate protocol errors
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-10-16 19:04:09 +02:00
Nicola Murino
77b2f8dfb3
CI FreeBSD: use Go 1.23
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-10-15 19:10:23 +02:00
Nicola Murino
d8691d1e1a
update translations
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-10-13 17:00:17 +02:00
Nicola Murino
5cb1b9c1e9
Web: add CheckRedirect to pages using baselogin.html
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-10-12 12:54:21 +02:00
Nicola Murino
b23e67ae6a
EventManager: add escaped virtual path
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-10-12 11:25:07 +02:00
Nicola Murino
8e7086ab39
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-10-11 19:36:41 +02:00
Rafał Bielawski
dc907c0ba3
Update translation.json (#1781)
Signed-off-by: Rafał Bielawski <hello@rbielawski.pl>
2024-10-11 19:30:40 +02:00
Nicola Murino
eba4c93efd
user: add additional emails
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-10-11 19:20:51 +02:00
Nicola Murino
bdd6de10a5
CI: update FreeBSD version to 14.1
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-10-08 20:55:29 +02:00
Nicola Murino
66e1e7ac2b
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-10-08 18:57:34 +02:00
Nicola Murino
4103344989
EventManager: add datetime placeholder
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-10-08 18:39:00 +02:00
Nicola Murino
0c470b9202
CI: disable GRPC modules
we don't use this feature for now

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-10-06 17:16:53 +02:00
Nicola Murino
57309dcd5f
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-10-05 19:25:36 +02:00
Nicola Murino
72ba54b5be
WebAdmin: add CSV export for users, groups, folders, admins, roles
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-10-05 19:09:46 +02:00
Nicola Murino
18bf0c6121
WebAdmin: remove max value from password_strength
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-10-04 19:20:17 +02:00
Nicola Murino
f88ce014df
WebClient: update edit and preview file extensions
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-10-04 19:18:00 +02:00
Nicola Murino
3b2f709aeb
WebClient: improve readability of upload progress
Fixes #1773

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-10-03 19:07:27 +02:00
Nicola Murino
6626c8846b
log: fix level for transfer logs
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-10-03 19:07:07 +02:00
Nicola Murino
2ecd20d444
WebClient: make sure to upload files after the queue is populated
Ugly hack to prevent to start uploading files before the upload
queue is fully populated.

We should investigate if there is a better way

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-10-03 19:06:33 +02:00
Nicola Murino
46e64706ea
CI: update Go version
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-10-03 06:54:18 +02:00
Nicola Murino
bdc5493593
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-10-02 18:34:06 +02:00
Nicola Murino
424999dacd
kms: add support for Oracle Key Vault
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-10-02 18:14:05 +02:00
Nicola Murino
2ec6aecc5d
update README
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-09-27 20:05:30 +02:00
Nicola Murino
addee12510
update issue template for bug reports
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-09-27 20:03:48 +02:00
Nicola Murino
57c0ca90e5
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-09-27 19:53:34 +02:00
Nicola Murino
85c65dcad3
update i18next
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-09-27 19:52:52 +02:00
Nicola Murino
a34d9bc916
update css and js bundle
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-09-27 18:08:54 +02:00
Nicola Murino
27e98b85ce
WebAdmin: hide certs if they cannot be used
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-09-27 15:53:12 +02:00
Nicola Murino
ae5ecbe909
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-09-27 15:53:09 +02:00
Nicola Murino
126cb1ee0d
remove some useless hooks
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-09-27 15:52:51 +02:00
Nicola Murino
eeef23139d
EventManager: filter action execution based on event status
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-09-23 19:55:03 +02:00
Nicola Murino
433d45ed87
WebUI: add a token validation mode that allows checking the signature
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-09-21 14:06:25 +02:00
Nicola Murino
5f67fcdce5
CI: use Go 1.22 until issue 68976 is fixed
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-09-18 19:23:10 +02:00
Nicola Murino
9288010636
update links to the documentation
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-09-17 19:43:26 +02:00
Nicola Murino
5162c5de87
WebUIs: add a nil check for token in refresh cookie method
token should never be null here because we have an authenticated user
however add the same check as elsewhere

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-09-16 20:11:02 +02:00
Nicola Murino
c2aed5ee92
WebUIs: fix some check conditions in js code
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-09-14 22:07:04 +02:00
Nicola Murino
44ebf2f48d
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-09-14 21:48:41 +02:00
Nicola Murino
6896d2bfb1
httpd: validate reference also for CSRF token in headers
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-09-14 21:45:25 +02:00
Nicola Murino
14cabda5c2
update shortuid to v4
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-09-08 18:01:14 +02:00
Nicola Murino
8cf0491b65
Dockerfiles: fix FromAsCasing
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-09-07 11:47:50 +02:00
Nicola Murino
1f46df0d60
Dockerfile: add go mod verify
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-09-06 20:12:37 +02:00
Nicola Murino
1b928ef6b2
sqlite: execute PRAGMA optimize on startup
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-09-06 19:35:18 +02:00
Nicola Murino
db35a55a3d
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-09-06 19:24:57 +02:00
Nicola Murino
fd6126134e
execute provider events also for plugin auth
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-09-06 19:17:31 +02:00
Nicola Murino
3b5fba2eec
update deps
and also change the ACME request limit now that it is configurable

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-09-01 13:50:34 +02:00
Nicola Murino
eb5ffb940e
update translations
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-09-01 13:44:16 +02:00
Nicola Murino
bb422ad5b9
GCS: add user agent
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-08-28 19:47:38 +02:00
Nicola Murino
53c3905ce3
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-08-25 17:36:30 +02:00
Nicola Murino
dc42680e1c
add pipeReaderAt and pipeWriterAt interfaces
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-08-25 17:35:28 +02:00
Nicola Murino
56ef9355da
add noopener noreferrer to href with target _blank
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-08-20 11:24:42 +02:00
Nicola Murino
d8e4978b61
smtp: replace deprecated method
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-08-17 09:17:22 +02:00
Nicola Murino
b9b370fbb8
add some pre-validation hooks
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-08-17 09:11:42 +02:00
Nicola Murino
dfeca3a972
update nFPM to 2.39.0
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-08-15 10:10:34 +02:00
Nicola Murino
a2934deaa6
CI: use Go 1.22.6 to build packages
We should investigate why building packages fails on archs other
than amd64 with Go 1.23.0

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-08-15 10:10:18 +02:00
Nicola Murino
2fbf608895
S3: add SSE customer key
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-08-15 10:09:06 +02:00
Nicola Murino
d783ffc13f
fix new lint warnings
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-08-14 08:46:18 +02:00
Nicola Murino
62426d25da
ip lists page: allow a missing description field
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-08-12 21:19:50 +02:00
Nicola Murino
fa710b36c2
httpd: allow to configure cache control header
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-08-12 21:19:44 +02:00
Nicola Murino
321c3f00d2
fix lint warning
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-08-11 09:26:07 +02:00
Nicola Murino
ec4bf3d76a
update deps and replace deprecated methods
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-08-11 08:59:21 +02:00
Nicola Murino
68e62d3d9b
httpd: allow to use proxy protocol
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-08-10 21:02:38 +02:00
Nicola Murino
954c36c0a2
add fs providers hook
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-08-10 15:57:05 +02:00
Nicola Murino
81433e00d1
event action: add update modtime to fs rename
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-08-09 20:18:33 +02:00
Nicola Murino
a5c5e85144
preserve metadata on copy/rename
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-08-08 18:44:26 +02:00
Nicola Murino
b94451f731
add builtin rules hook
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-08-08 07:41:39 +02:00
Nicola Murino
4edecc5c77
resetpwd: also disable two-factor authentication
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-08-04 21:27:47 +02:00
Nicola Murino
51e9a689a6
configs: fix form type
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-08-03 20:39:16 +02:00
Nicola Murino
aa920432f3
WebUIs: update theme to version 1.0.4
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-08-03 15:53:30 +02:00
Nicola Murino
ce189e5065
IDP account check: preserve user profile
Fixes #1712

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-08-02 19:25:54 +02:00
Nicola Murino
00155eaaf6
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-07-26 19:04:08 +02:00
Nicola Murino
d94f80c8da
replace utils.Contains with slices.Contains
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-07-24 18:27:13 +02:00
Nicola Murino
bd5eb03d9c
replace hand-written slice utilities with methods from slices package
SFTPGo depends on Go 1.22 so we can use slices package

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-07-24 18:17:55 +02:00
Nicola Murino
6ba1198c47
sftpd: remove unused folder prefix from Connection struct
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-07-24 16:44:25 +02:00
Nicola Murino
b5c821795a
allow to customize name and log from the WebUI
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-07-24 09:14:27 +02:00
Nicola Murino
b2926377b7
WebUI: switch favicon from ico to png
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-07-20 16:11:21 +02:00
Nicola Murino
99f47ca4e7
sftpfs: cache and reuse parsed private keys
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-07-16 19:20:28 +02:00
Nicola Murino
fef388d8cb
don't track quota for private virtual folders
they are included within the user quota.
This is a backward incompatible change.

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-07-13 21:02:40 +02:00
Nicola Murino
92849ca473
quota: move user and folder management to a common method
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-07-13 19:30:40 +02:00
Nicola Murino
0952887157
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-07-13 17:44:13 +02:00
Nicola Murino
d010b26e1c
deb packages: update copyright year
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-07-13 14:06:11 +02:00
Nicola Murino
58de410850
nt: fix unused write warnings
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-07-03 20:42:51 +02:00
Nicola Murino
54bc3ea87d
restore: fix quota scan for users with folders associated via groups
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-07-03 20:35:12 +02:00
Nicola Murino
64a2f7aa4f
oidc refresh token: validate nonce only if set
As clarified in OpenID core spec errata 2, section 12.2

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-07-01 19:06:11 +02:00
Nicola Murino
55be9f0b9c
EventManager: allow to configure the timezone to use for the scheduler
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-06-30 18:52:59 +02:00
Nicola Murino
97ffa0394f
update deps
adapt smtp configuration to changes in upstream library

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-06-30 09:18:04 +02:00
dependabot[bot]
dc91ec2056
Bump docker/build-push-action from 5 to 6 (#1668)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 5 to 6.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-24 09:29:56 +02:00
Nicola Murino
356795f8b0
add a test case for listing files with long names
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-06-22 19:23:02 +02:00
Nicola Murino
3efcd94e14
back to development
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-06-21 20:25:35 +02:00
Nicola Murino
34bc21b3b7
update deps
fixes a bug in chi compressor Handler

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-06-21 18:32:59 +02:00
Nicola Murino
37845c2936
smtp: hide commit hash in user agent
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-06-21 18:31:42 +02:00
Nicola Murino
47924716c1
back to development
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-06-19 21:04:35 +02:00
Nicola Murino
1d60505629
fix test case failure on macOS with bolt provider
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-06-19 10:45:14 +02:00
Nicola Murino
9daf0ba767
update swagger UI to 5.17.14
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-06-18 20:47:39 +02:00
Nicola Murino
bdae378569
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-06-18 19:13:21 +02:00
Nicola Murino
363770ab84
WebClient shares: add a logout button
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-06-18 19:10:32 +02:00
Nicola Murino
8bc08b25dc
sftp: limit max file list
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-06-17 19:24:03 +02:00
Nicola Murino
e0c1b974c9
add cgo to build constraints
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-06-16 09:46:17 +02:00
Nicola Murino
39cf9f6943
Web UIs: remove duplication of supported languages
Fixes #1660

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-06-15 19:35:23 +02:00
Nicola Murino
d650defa08
remove duplicated jwt tokens validation
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-06-15 16:19:37 +02:00
Nicola Murino
c5c42f072b
squash database migrations
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-06-15 16:02:09 +02:00
Nicola Murino
bd5b32101f
csrf: reuse the cookie in reset password
no need to generate a new cookie each time.

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-06-15 15:18:17 +02:00
Nicola Murino
8208ac817d
html pages: add robots meta tag
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-06-15 10:17:37 +02:00
Nicola Murino
a99c4879de
update dependencies
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-06-15 10:10:15 +02:00
Nicola Murino
01b666a78f
WebUIs: check login conditions before allowing password reset
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-06-14 19:34:42 +02:00
Nicola Murino
8294952474
WebUIs: refactor CSRF
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-06-14 18:09:32 +02:00
Nicola Murino
7fb5b1b996
reduce share token duration
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-06-08 12:13:38 +02:00
Nicola Murino
2749a98f26
CI: update workflow to 1.22.4
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-06-07 18:19:52 +02:00
Nicola Murino
08526da153
REST API: fix token invalidation after password change
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-06-07 18:19:05 +02:00
Nicola Murino
8269adf176
Windows: allow to override most of the "serve" flags from env files
The Windows specific code path was missing in 07710ad98

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-06-05 17:34:28 +02:00
Nicola Murino
0cddcba5a7
EventManager: add an action to rotate the log file
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-06-04 19:51:52 +02:00
Nicola Murino
3bd1eeacc1
make sure to return a fully populated user after plugin auth
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-06-04 18:14:09 +02:00
Nicola Murino
1698ec2eb3
EventManager: fix adding ObjectDataString for provider events
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-05-31 20:01:38 +02:00
Nicola Murino
07710ad98d
allow to override most of the "serve" flags from env files
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-05-31 18:49:23 +02:00
Nicola Murino
f63bf7093c
logs: redact plugin arguments
may contain sensitive data

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-05-30 18:10:12 +02:00
Nicola Murino
0597bf1047
Windows setup: update MinVersion
Starting from Go version 1.21, Windows 10 or Windows Server 2016 are
required

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-05-30 18:07:17 +02:00
Nicola Murino
5bde4b92a2
fix test cases
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-05-29 19:35:42 +02:00
Nicola Murino
faa994e3b3
update UI theme and dependencies
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-05-29 19:20:56 +02:00
Nicola Murino
68cc1a8e2c
fix proxy protocol policy
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-05-28 19:40:37 +02:00
Nicola Murino
9c775e2213
transfer logs: add error field
Fixes #1638

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-05-27 19:35:48 +02:00
Nicola Murino
6c94173ca1
WebUI branding: remove unused login_image_path from config
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-05-27 18:43:44 +02:00
Nicola Murino
d1e0560d28
WebAdmin status page: update the color of the labels
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-05-26 19:34:29 +02:00
Nicola Murino
52a94b2593
docker: build Alpine based image using golang:1.22-alpine3.20
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-05-25 16:53:13 +02:00
dependabot[bot]
9550fd2921
Bump alpine from 3.19 to 3.20 (#1636)
Bumps alpine from 3.19 to 3.20.

---
updated-dependencies:
- dependency-name: alpine
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-25 16:51:48 +02:00
Nicola Murino
a6549b08f9
dependabot: remove gomod
it is not really required, we update Go dependencies regularly

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-05-25 16:40:31 +02:00
Nicola Murino
ba3e2ecb5f
WebAdmin events page: fix rendering of some nullable strings
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-05-25 16:17:33 +02:00
Nicola Murino
2bd3b46e3f
update swagger ui
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-05-25 16:14:42 +02:00
Nicola Murino
7831ddaede
WebAdmin events page: set fixed sizes for potentially long fields
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-05-24 18:24:05 +02:00
Nicola Murino
613f2f1c24
WebUIs: set the lang attribute based on the chosen language
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-05-24 18:23:41 +02:00
Nicola Murino
525f33a07a
WebUIs: fix css loading order
Fixes #1628

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-05-24 18:22:58 +02:00
Nicola Murino
3f2604d33f
ssh: use 3072-bits for the auto-generated RSA key
This is the same as ssh-keygen

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-05-24 18:22:36 +02:00
Nicola Murino
b823bb04d2
WebAdmin: make the description visible in IP lists page
Fixes #1631

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-05-23 20:07:49 +02:00
Nicola Murino
9ba92d9495
WebUIs: fix datatables processing class name
was changed to dt-processing in datatables 2.0

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-05-23 19:47:45 +02:00
Nicola Murino
0127fc188b
SSH: allow to configure minimum key size for DHGEX
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-05-23 18:08:16 +02:00
Nicola Murino
3c7a651d27
plugin: don't consider file extension for env prefix
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-05-18 13:10:16 +02:00
Nicola Murino
50a3c0d911
defender: allow to impose a delay between login attempts
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-05-18 10:35:54 +02:00
Nicola Murino
b2bea85add
update README
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-05-16 10:40:48 +02:00
Nicola Murino
61bc0065f9
back to development
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-05-16 04:54:46 +02:00
310 changed files with 21673 additions and 9758 deletions

View file

@ -2,17 +2,17 @@ freebsd_task:
name: FreeBSD
matrix:
- name: FreeBSD 14.0
- name: FreeBSD 14.3
freebsd_instance:
image_family: freebsd-14-0
image_family: freebsd-14-3
pkginstall_script:
- pkg update -f
- pkg install -y go122
- pkg install -y go125
- pkg install -y git
setup_script:
- ln -s /usr/local/bin/go122 /usr/local/bin/go
- ln -s /usr/local/bin/go125 /usr/local/bin/go
- pw groupadd sftpgo
- pw useradd sftpgo -g sftpgo -w none -m
- mkdir /home/sftpgo/sftpgo
@ -20,7 +20,7 @@ freebsd_task:
- chown -R sftpgo:sftpgo /home/sftpgo/sftpgo
compile_script:
- su sftpgo -c 'cd ~/sftpgo && go build -trimpath -tags nopgxregisterdefaulttypes -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=`git describe --always --abbrev=8 --dirty` -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -o sftpgo'
- su sftpgo -c 'cd ~/sftpgo && go build -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=`git describe --always --abbrev=8 --dirty` -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -o sftpgo'
- su sftpgo -c 'cd ~/sftpgo/tests/eventsearcher && go build -trimpath -ldflags "-s -w" -o eventsearcher'
- su sftpgo -c 'cd ~/sftpgo/tests/ipfilter && go build -trimpath -ldflags "-s -w" -o ipfilter'
@ -28,4 +28,4 @@ freebsd_task:
- su sftpgo -c 'cd ~/sftpgo && ./sftpgo initprovider && ./sftpgo resetprovider --force'
test_script:
- su sftpgo -c 'cd ~/sftpgo && go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 20m ./... -coverprofile=coverage.txt -covermode=atomic'
- su sftpgo -c 'cd ~/sftpgo && go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 20m ./... -coverprofile=coverage.txt -covermode=atomic'

View file

@ -7,8 +7,10 @@ body:
attributes:
value: |
### 👍 Thank you for contributing to our project!
Before asking for help please check the [support policy](https://github.com/drakkan/sftpgo#support-policy).
If you are a commercial user or a project sponsor please contact us using the dedicated [email address](mailto:support@sftpgo.com).
Before asking for help please check our [support policy](https://github.com/drakkan/sftpgo?tab=readme-ov-file#support).
If you are a [commercial user](https://sftpgo.com/) please contact us using the dedicated [email address](mailto:support@sftpgo.com).
If you'd like to contribute code, please make sure to read and understand our [Contributor License Agreement (CLA)](https://sftpgo.com/cla.html).
Youll be asked to accept it when submitting a pull request.
- type: checkboxes
id: before-posting
attributes:

View file

@ -2,6 +2,14 @@ name: 🚀 Feature request
description: Suggest an idea for SFTPGo
labels: ["suggestion"]
body:
- type: markdown
attributes:
value: |
### 👍 Thank you for contributing to our project!
Before asking for help please check our [support policy](https://github.com/drakkan/sftpgo?tab=readme-ov-file#support).
If you are a [commercial user](https://sftpgo.com/) please contact us using the dedicated [email address](mailto:support@sftpgo.com).
If you'd like to contribute code, please make sure to read and understand our [Contributor License Agreement (CLA)](https://sftpgo.com/cla.html).
Youll be asked to accept it when submitting a pull request.
- type: textarea
attributes:
label: Is your feature request related to a problem? Please describe.

View file

@ -1,11 +1,11 @@
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 2
#- package-ecosystem: "gomod"
# directory: "/"
# schedule:
# interval: "weekly"
# open-pull-requests-limit: 2
- package-ecosystem: "docker"
directory: "/"

View file

@ -15,22 +15,22 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: '1.22'
go-version: '1.25'
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@v4
with:
languages: go
- name: Autobuild
uses: github/codeql-action/autobuild@v3
uses: github/codeql-action/autobuild@v4
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@v4

View file

@ -5,34 +5,32 @@ on:
branches: [main]
pull_request:
permissions:
id-token: write
contents: read
jobs:
test-deploy:
name: Test and deploy
runs-on: ${{ matrix.os }}
strategy:
matrix:
go: ['1.22']
go: ['1.26']
os: [ubuntu-latest, macos-latest]
upload-coverage: [true]
include:
- go: '1.22'
os: windows-latest
upload-coverage: false
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go }}
- name: Build for Linux/macOS x86_64
if: startsWith(matrix.os, 'windows-') != true
run: |
go build -trimpath -tags nopgxregisterdefaulttypes -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=`git describe --always --abbrev=8 --dirty` -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -o sftpgo
go build -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=`git describe --always --abbrev=8 --dirty` -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -o sftpgo
cd tests/eventsearcher
go build -trimpath -ldflags "-s -w" -o eventsearcher
cd -
@ -44,68 +42,35 @@ jobs:
- name: Build for macOS arm64
if: startsWith(matrix.os, 'macos-') == true
run: CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 SDKROOT=$(xcrun --sdk macosx --show-sdk-path) go build -trimpath -tags nopgxregisterdefaulttypes -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=`git describe --always --abbrev=8 --dirty` -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -o sftpgo_arm64
- name: Build for Windows
if: startsWith(matrix.os, 'windows-')
run: |
$GIT_COMMIT = (git describe --always --abbrev=8 --dirty) | Out-String
$DATE_TIME = ([datetime]::Now.ToUniversalTime().toString("yyyy-MM-ddTHH:mm:ssZ")) | Out-String
$LATEST_TAG = ((git describe --tags $(git rev-list --tags --max-count=1)) | Out-String).Trim()
$REV_LIST=$LATEST_TAG+"..HEAD"
$COMMITS_FROM_TAG= ((git rev-list $REV_LIST --count) | Out-String).Trim()
$FILE_VERSION = $LATEST_TAG.substring(1) + "." + $COMMITS_FROM_TAG
go install github.com/tc-hib/go-winres@latest
go-winres simply --arch amd64 --product-version $LATEST_TAG-dev-$GIT_COMMIT --file-version $FILE_VERSION --file-description "SFTPGo server" --product-name SFTPGo --copyright "AGPL-3.0" --original-filename sftpgo.exe --icon .\windows-installer\icon.ico
go build -trimpath -tags nopgxregisterdefaulttypes -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=$GIT_COMMIT -X github.com/drakkan/sftpgo/v2/internal/version.date=$DATE_TIME" -o sftpgo.exe
cd tests/eventsearcher
go build -trimpath -ldflags "-s -w" -o eventsearcher.exe
cd ../..
cd tests/ipfilter
go build -trimpath -ldflags "-s -w" -o ipfilter.exe
cd ../..
mkdir arm64
$Env:CGO_ENABLED='0'
$Env:GOOS='windows'
$Env:GOARCH='arm64'
go-winres simply --arch arm64 --product-version $LATEST_TAG-dev-$GIT_COMMIT --file-version $FILE_VERSION --file-description "SFTPGo server" --product-name SFTPGo --copyright "AGPL-3.0" --original-filename sftpgo.exe --icon .\windows-installer\icon.ico
go build -trimpath -tags nopgxregisterdefaulttypes,nosqlite -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=$GIT_COMMIT -X github.com/drakkan/sftpgo/v2/internal/version.date=$DATE_TIME" -o .\arm64\sftpgo.exe
mkdir x86
$Env:GOARCH='386'
go-winres simply --arch 386 --product-version $LATEST_TAG-dev-$GIT_COMMIT --file-version $FILE_VERSION --file-description "SFTPGo server" --product-name SFTPGo --copyright "AGPL-3.0" --original-filename sftpgo.exe --icon .\windows-installer\icon.ico
go build -trimpath -tags nopgxregisterdefaulttypes,nosqlite -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=$GIT_COMMIT -X github.com/drakkan/sftpgo/v2/internal/version.date=$DATE_TIME" -o .\x86\sftpgo.exe
Remove-Item Env:\CGO_ENABLED
Remove-Item Env:\GOOS
Remove-Item Env:\GOARCH
run: CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 SDKROOT=$(xcrun --sdk macosx --show-sdk-path) go build -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=`git describe --always --abbrev=8 --dirty` -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -o sftpgo_arm64
- name: Run test cases using SQLite provider
run: go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 15m ./... -coverprofile=coverage.txt -covermode=atomic
run: go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 15m ./... -coverprofile=coverage.txt -covermode=atomic
- name: Upload coverage to Codecov
if: ${{ matrix.upload-coverage }}
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
with:
file: ./coverage.txt
files: ./coverage.txt
fail_ci_if_error: false
token: ${{ secrets.CODECOV_TOKEN }}
- name: Run test cases using bolt provider
run: |
go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 2m ./internal/config -covermode=atomic
go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 5m ./internal/common -covermode=atomic
go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 5m ./internal/httpd -covermode=atomic
go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 8m ./internal/sftpd -covermode=atomic
go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 5m ./internal/ftpd -covermode=atomic
go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 5m ./internal/webdavd -covermode=atomic
go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 2m ./internal/telemetry -covermode=atomic
go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 2m ./internal/mfa -covermode=atomic
go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 2m ./internal/command -covermode=atomic
go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 2m ./internal/config -covermode=atomic
go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 5m ./internal/common -covermode=atomic
go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 5m ./internal/httpd -covermode=atomic
go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 8m ./internal/sftpd -covermode=atomic
go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 5m ./internal/ftpd -covermode=atomic
go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 5m ./internal/webdavd -covermode=atomic
go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 2m ./internal/telemetry -covermode=atomic
go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 2m ./internal/mfa -covermode=atomic
go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 2m ./internal/command -covermode=atomic
env:
SFTPGO_DATA_PROVIDER__DRIVER: bolt
SFTPGO_DATA_PROVIDER__NAME: 'sftpgo_bolt.db'
- name: Run test cases using memory provider
run: go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 15m ./... -covermode=atomic
run: go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 15m ./... -covermode=atomic
env:
SFTPGO_DATA_PROVIDER__DRIVER: memory
SFTPGO_DATA_PROVIDER__NAME: ''
@ -126,15 +91,100 @@ jobs:
./sftpgo gen man -d output/man/man1
gzip output/man/man1/*
- name: Prepare Windows installer
if: ${{ startsWith(matrix.os, 'windows-') && github.event_name != 'pull_request' }}
- name: Upload build artifact
if: startsWith(matrix.os, 'ubuntu-') != true
uses: actions/upload-artifact@v7
with:
name: sftpgo-${{ matrix.os }}-go-${{ matrix.go }}
path: output
test-deploy-windows:
name: Test and deploy Windows
runs-on: windows-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: '1.26'
- name: Run test cases using SQLite provider
run: |
cd tests/eventsearcher
go build -trimpath -ldflags "-s -w" -o eventsearcher.exe
cd ../..
cd tests/ipfilter
go build -trimpath -ldflags "-s -w" -o ipfilter.exe
cd ../..
go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 15m ./... -coverprofile=coverage.txt -covermode=atomic
- name: Run test cases using bolt provider
run: |
go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 2m ./internal/config -covermode=atomic
go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 5m ./internal/common -covermode=atomic
go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 5m ./internal/httpd -covermode=atomic
go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 8m ./internal/sftpd -covermode=atomic
go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 5m ./internal/ftpd -covermode=atomic
go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 5m ./internal/webdavd -covermode=atomic
go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 2m ./internal/telemetry -covermode=atomic
go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 2m ./internal/mfa -covermode=atomic
go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 2m ./internal/command -covermode=atomic
env:
SFTPGO_DATA_PROVIDER__DRIVER: bolt
SFTPGO_DATA_PROVIDER__NAME: 'sftpgo_bolt.db'
- name: Run test cases using memory provider
run: go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 15m ./... -covermode=atomic
env:
SFTPGO_DATA_PROVIDER__DRIVER: memory
SFTPGO_DATA_PROVIDER__NAME: ''
- name: Build
run: |
$GIT_COMMIT = (git describe --always --abbrev=8 --dirty) | Out-String
$DATE_TIME = ([datetime]::Now.ToUniversalTime().toString("yyyy-MM-ddTHH:mm:ssZ")) | Out-String
$LATEST_TAG = ((git describe --tags $(git rev-list --tags --max-count=1)) | Out-String).Trim()
$REV_LIST=$LATEST_TAG+"..HEAD"
$COMMITS_FROM_TAG= ((git rev-list $REV_LIST --count) | Out-String).Trim()
$FILE_VERSION = $LATEST_TAG.substring(1) + "." + $COMMITS_FROM_TAG
go install github.com/tc-hib/go-winres@latest
go-winres simply --arch amd64 --product-version $LATEST_TAG-dev-$GIT_COMMIT --file-version "$FILE_VERSION" --file-description "SFTPGo server" --product-name SFTPGo --copyright "2019-2025 Nicola Murino" --original-filename sftpgo.exe --icon .\windows-installer\icon.ico
go build -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=$GIT_COMMIT -X github.com/drakkan/sftpgo/v2/internal/version.date=$DATE_TIME" -o sftpgo.exe
mkdir arm64
$Env:CGO_ENABLED='0'
$Env:GOOS='windows'
$Env:GOARCH='arm64'
go-winres simply --arch arm64 --product-version $LATEST_TAG-dev-$GIT_COMMIT --file-version "$FILE_VERSION" --file-description "SFTPGo server" --product-name SFTPGo --copyright "2019-2025 Nicola Murino" --original-filename sftpgo.exe --icon .\windows-installer\icon.ico
go build -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules,nosqlite -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=$GIT_COMMIT -X github.com/drakkan/sftpgo/v2/internal/version.date=$DATE_TIME" -o .\arm64\sftpgo.exe
mkdir x86
$Env:GOARCH='386'
go-winres simply --arch 386 --product-version $LATEST_TAG-dev-$GIT_COMMIT --file-version "$FILE_VERSION" --file-description "SFTPGo server" --product-name SFTPGo --copyright "2019-2025 Nicola Murino" --original-filename sftpgo.exe --icon .\windows-installer\icon.ico
go build -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules,nosqlite -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=$GIT_COMMIT -X github.com/drakkan/sftpgo/v2/internal/version.date=$DATE_TIME" -o .\x86\sftpgo.exe
Remove-Item Env:\CGO_ENABLED
Remove-Item Env:\GOOS
Remove-Item Env:\GOARCH
- name: Initialize data provider
run: |
rm sftpgo.db
./sftpgo initprovider
shell: bash
- name: Prepare Windows installers
if: ${{ github.event_name != 'pull_request' }}
run: |
choco install innosetup
Remove-Item -LiteralPath "output" -Force -Recurse -ErrorAction Ignore
mkdir output
copy .\sftpgo.exe .\output
copy .\sftpgo.json .\output
copy .\sftpgo.db .\output
copy .\LICENSE .\output\LICENSE.txt
copy .\NOTICE .\output\NOTICE.txt
mkdir output\templates
xcopy .\templates .\output\templates\ /E
mkdir output\static
@ -145,15 +195,7 @@ jobs:
$REV_LIST=$LATEST_TAG+"..HEAD"
$COMMITS_FROM_TAG= ((git rev-list $REV_LIST --count) | Out-String).Trim()
$Env:SFTPGO_ISS_DEV_VERSION = $LATEST_TAG + "." + $COMMITS_FROM_TAG
$CERT_PATH=(Get-Location -PSProvider FileSystem).ProviderPath + "\cert.pfx"
[IO.File]::WriteAllBytes($CERT_PATH,[System.Convert]::FromBase64String($Env:CERT_DATA))
certutil -f -p "$Env:CERT_PASS" -importpfx MY "$CERT_PATH"
rm "$CERT_PATH"
& 'C:/Program Files (x86)/Windows Kits/10/bin/10.0.20348.0/x86/signtool.exe' sign /sm /tr http://timestamp.sectigo.com /td sha256 /fd sha256 /n "Nicola Murino" /d "SFTPGo" .\sftpgo.exe
& 'C:/Program Files (x86)/Windows Kits/10/bin/10.0.20348.0/x86/signtool.exe' sign /sm /tr http://timestamp.sectigo.com /td sha256 /fd sha256 /n "Nicola Murino" /d "SFTPGo" .\arm64\sftpgo.exe
& 'C:/Program Files (x86)/Windows Kits/10/bin/10.0.20348.0/x86/signtool.exe' sign /sm /tr http://timestamp.sectigo.com /td sha256 /fd sha256 /n "Nicola Murino" /d "SFTPGo" .\x86\sftpgo.exe
$INNO_S='/Ssigntool=$qC:/Program Files (x86)/Windows Kits/10/bin/10.0.20348.0/x86/signtool.exe$q sign /sm /tr http://timestamp.sectigo.com /td sha256 /fd sha256 /n $qNicola Murino$q /d $qSFTPGo$q $f'
iscc "$INNO_S" .\windows-installer\sftpgo.iss
iscc .\windows-installer\sftpgo.iss
rm .\output\sftpgo.exe
rm .\output\sftpgo.db
@ -165,40 +207,35 @@ jobs:
Remove-Item Env:\SFTPGO_DATA_PROVIDER__DRIVER
Remove-Item Env:\SFTPGO_DATA_PROVIDER__NAME
$Env:SFTPGO_ISS_ARCH='arm64'
iscc "$INNO_S" .\windows-installer\sftpgo.iss
iscc .\windows-installer\sftpgo.iss
rm .\output\sftpgo.exe
copy .\x86\sftpgo.exe .\output
$Env:SFTPGO_ISS_ARCH='x86'
iscc "$INNO_S" .\windows-installer\sftpgo.iss
certutil -delstore MY "Nicola Murino"
env:
CERT_DATA: ${{ secrets.CERT_DATA }}
CERT_PASS: ${{ secrets.CERT_PASS }}
iscc .\windows-installer\sftpgo.iss
- name: Upload Windows installer x86_64 artifact
if: ${{ startsWith(matrix.os, 'windows-') && github.event_name != 'pull_request' }}
uses: actions/upload-artifact@v4
if: ${{ github.event_name != 'pull_request' }}
uses: actions/upload-artifact@v7
with:
name: sftpgo_windows_installer_x86_64
path: ./sftpgo_windows_x86_64.exe
- name: Upload Windows installer arm64 artifact
if: ${{ startsWith(matrix.os, 'windows-') && github.event_name != 'pull_request' }}
uses: actions/upload-artifact@v4
if: ${{ github.event_name != 'pull_request' }}
uses: actions/upload-artifact@v7
with:
name: sftpgo_windows_installer_arm64
path: ./sftpgo_windows_arm64.exe
- name: Upload Windows installer x86 artifact
if: ${{ startsWith(matrix.os, 'windows-') && github.event_name != 'pull_request' }}
uses: actions/upload-artifact@v4
if: ${{ github.event_name != 'pull_request' }}
uses: actions/upload-artifact@v7
with:
name: sftpgo_windows_installer_x86
path: ./sftpgo_windows_x86.exe
- name: Prepare build artifact for Windows
if: startsWith(matrix.os, 'windows-')
run: |
Remove-Item -LiteralPath "output" -Force -Recurse -ErrorAction Ignore
mkdir output
@ -217,10 +254,9 @@ jobs:
xcopy .\openapi .\output\openapi\ /E
- name: Upload build artifact
if: startsWith(matrix.os, 'ubuntu-') != true
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: sftpgo-${{ matrix.os }}-go-${{ matrix.go }}
name: sftpgo-windows-portable
path: output
test-build-flags:
@ -228,19 +264,19 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: '1.22'
go-version: '1.26'
- name: Build
run: |
go build -trimpath -tags nopgxregisterdefaulttypes,nogcs,nos3,noportable,nobolt,nomysql,nopgsql,nosqlite,nometrics,noazblob,unixcrypt -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/internal/version.commit=`git describe --always --abbrev=8 --dirty` -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -o sftpgo
go build -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules,nogcs,nos3,noportable,nobolt,nomysql,nopgsql,nosqlite,nometrics,noazblob,unixcrypt -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/internal/version.commit=`git describe --always --abbrev=8 --dirty` -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -o sftpgo
./sftpgo -v
cp -r openapi static templates internal/bundle/
go build -trimpath -tags nopgxregisterdefaulttypes,bundle -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=`git describe --always --abbrev=8 --dirty` -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -o sftpgo
go build -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules,bundle -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=`git describe --always --abbrev=8 --dirty` -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -o sftpgo
./sftpgo -v
test-postgresql-mysql-crdb:
@ -292,16 +328,16 @@ jobs:
- 3308:3306
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: '1.22'
go-version: '1.26'
- name: Build
run: |
go build -trimpath -tags nopgxregisterdefaulttypes -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=`git describe --always --abbrev=8 --dirty` -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -o sftpgo
go build -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=`git describe --always --abbrev=8 --dirty` -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -o sftpgo
cd tests/eventsearcher
go build -trimpath -ldflags "-s -w" -o eventsearcher
cd -
@ -313,7 +349,7 @@ jobs:
run: |
./sftpgo initprovider
./sftpgo resetprovider --force
go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 15m ./... -covermode=atomic
go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 15m ./... -covermode=atomic
env:
SFTPGO_DATA_PROVIDER__DRIVER: mysql
SFTPGO_DATA_PROVIDER__NAME: sftpgo
@ -326,7 +362,7 @@ jobs:
run: |
./sftpgo initprovider
./sftpgo resetprovider --force
go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 15m ./... -covermode=atomic
go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 15m ./... -covermode=atomic
env:
SFTPGO_DATA_PROVIDER__DRIVER: postgresql
SFTPGO_DATA_PROVIDER__NAME: sftpgo
@ -339,7 +375,7 @@ jobs:
run: |
./sftpgo initprovider
./sftpgo resetprovider --force
go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 15m ./... -covermode=atomic
go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 15m ./... -covermode=atomic
env:
SFTPGO_DATA_PROVIDER__DRIVER: mysql
SFTPGO_DATA_PROVIDER__NAME: sftpgo
@ -356,7 +392,7 @@ jobs:
docker exec crdb cockroach sql --insecure -e 'create database "sftpgo"'
./sftpgo initprovider
./sftpgo resetprovider --force
go test -v -tags nopgxregisterdefaulttypes -p 1 -timeout 15m ./... -covermode=atomic
go test -v -tags nopgxregisterdefaulttypes,disable_grpc_modules -p 1 -timeout 15m ./... -covermode=atomic
docker stop crdb
env:
SFTPGO_DATA_PROVIDER__DRIVER: cockroachdb
@ -391,7 +427,7 @@ jobs:
go: latest
go-arch: arm7
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
@ -420,7 +456,7 @@ jobs:
echo 'export PATH=$PATH:/usr/local/go/bin' >> build.sh
echo 'go version' >> build.sh
echo 'cd /usr/local/src' >> build.sh
echo 'go build -buildvcs=false -trimpath -tags nopgxregisterdefaulttypes -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=${{ steps.get_commit.outputs.COMMIT }} -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -o sftpgo' >> build.sh
echo 'go build -buildvcs=false -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=${{ steps.get_commit.outputs.COMMIT }} -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -o sftpgo' >> build.sh
chmod 755 build.sh
docker run --rm --name ubuntu-build --mount type=bind,source=`pwd`,target=/usr/local/src ${{ matrix.distro }} /usr/local/src/build.sh
@ -436,7 +472,7 @@ jobs:
gzip output/man/man1/*
cp sftpgo output/
- uses: uraimo/run-on-arch-action@v2
- uses: uraimo/run-on-arch-action@v3
if: ${{ matrix.arch != 'amd64' }}
name: Build for ${{ matrix.arch }}
id: build
@ -471,7 +507,7 @@ jobs:
then
export GOARM=7
fi
go build -buildvcs=false -trimpath -tags nopgxregisterdefaulttypes -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=${{ steps.get_commit.outputs.COMMIT }} -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -o sftpgo
go build -buildvcs=false -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=${{ steps.get_commit.outputs.COMMIT }} -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -o sftpgo
mkdir -p output/{init,bash_completion,zsh_completion}
cp sftpgo.json output/
cp -r templates output/
@ -485,7 +521,7 @@ jobs:
cp sftpgo output/
- name: Upload build artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: sftpgo-linux-${{ matrix.arch }}-go-${{ matrix.go }}
path: output
@ -500,13 +536,13 @@ jobs:
echo "pkg-version=${PKG_VERSION}" >> $GITHUB_OUTPUT
- name: Upload Debian Package
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: sftpgo-${{ steps.build_linux_pkgs.outputs.pkg-version }}-${{ matrix.go-arch }}-deb
path: pkgs/dist/deb/*
- name: Upload RPM Package
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: sftpgo-${{ steps.build_linux_pkgs.outputs.pkg-version }}-${{ matrix.go-arch }}-rpm
path: pkgs/dist/rpm/*
@ -516,11 +552,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: '1.22'
- uses: actions/checkout@v4
go-version: '1.26'
- uses: actions/checkout@v6
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v6
uses: golangci/golangci-lint-action@v9
with:
version: latest

View file

@ -33,7 +33,7 @@ jobs:
optional_deps: true
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Gather image information
id: info
@ -42,7 +42,7 @@ jobs:
DOCKERFILE=Dockerfile
MINOR=""
MAJOR=""
FEATURES="nopgxregisterdefaulttypes"
FEATURES="nopgxregisterdefaulttypes,disable_grpc_modules"
if [ "${{ github.event_name }}" = "schedule" ]; then
VERSION=nightly
elif [[ $GITHUB_REF == refs/tags/* ]]; then
@ -141,21 +141,21 @@ jobs:
OPTIONAL_DEPS: ${{ matrix.optional_deps }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
- name: Set up builder
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
id: builder
- name: Login to Docker Hub
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
if: ${{ github.event_name != 'pull_request' }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@ -163,7 +163,7 @@ jobs:
if: ${{ github.event_name != 'pull_request' }}
- name: Build and push
uses: docker/build-push-action@v5
uses: docker/build-push-action@v7
with:
context: .
builder: ${{ steps.builder.outputs.name }}

View file

@ -4,17 +4,21 @@ on:
push:
tags: 'v*'
permissions:
id-token: write
contents: write
env:
GO_VERSION: 1.22.3
GO_VERSION: 1.25.8
jobs:
prepare-sources-with-deps:
name: Prepare sources with deps
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: ${{ env.GO_VERSION }}
@ -32,23 +36,20 @@ jobs:
SFTPGO_VERSION: ${{ steps.get_version.outputs.VERSION }}
- name: Upload build artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: sftpgo_${{ steps.get_version.outputs.VERSION }}_src_with_deps.tar.xz
path: ./sftpgo_${{ steps.get_version.outputs.VERSION }}_src_with_deps.tar.xz
retention-days: 1
prepare-window-mac:
name: Prepare binaries
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [macos-12, windows-2022]
prepare-windows:
name: Prepare Windows binaries
runs-on: windows-2022
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: ${{ env.GO_VERSION }}
@ -57,46 +58,24 @@ jobs:
run: echo "VERSION=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT
shell: bash
- name: Get OS name
id: get_os_name
run: |
if [[ $MATRIX_OS =~ ^macos.* ]]
then
echo "OS=macOS" >> $GITHUB_OUTPUT
else
echo "OS=windows" >> $GITHUB_OUTPUT
fi
shell: bash
env:
MATRIX_OS: ${{ matrix.os }}
- name: Build for macOS x86_64
if: startsWith(matrix.os, 'windows-') != true
run: go build -trimpath -tags nopgxregisterdefaulttypes -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=`git describe --always --abbrev=8 --dirty` -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -o sftpgo
- name: Build for macOS arm64
if: startsWith(matrix.os, 'macos-') == true
run: CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 SDKROOT=$(xcrun --sdk macosx --show-sdk-path) go build -trimpath -tags nopgxregisterdefaulttypes -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=`git describe --always --abbrev=8 --dirty` -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -o sftpgo_arm64
- name: Build for Windows
if: startsWith(matrix.os, 'windows-')
- name: Build
run: |
$GIT_COMMIT = (git describe --always --abbrev=8 --dirty) | Out-String
$DATE_TIME = ([datetime]::Now.ToUniversalTime().toString("yyyy-MM-ddTHH:mm:ssZ")) | Out-String
$FILE_VERSION = $Env:SFTPGO_VERSION.substring(1) + ".0"
go install github.com/tc-hib/go-winres@latest
go-winres simply --arch amd64 --product-version $Env:SFTPGO_VERSION-$GIT_COMMIT --file-version $FILE_VERSION --file-description "SFTPGo server" --product-name SFTPGo --copyright "AGPL-3.0" --original-filename sftpgo.exe --icon .\windows-installer\icon.ico
go build -trimpath -tags nopgxregisterdefaulttypes -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=$GIT_COMMIT -X github.com/drakkan/sftpgo/v2/internal/version.date=$DATE_TIME" -o sftpgo.exe
go-winres simply --arch amd64 --product-version $Env:SFTPGO_VERSION-$GIT_COMMIT --file-version "$FILE_VERSION" --file-description "SFTPGo server" --product-name SFTPGo --copyright "2019-2025 Nicola Murino" --original-filename sftpgo.exe --icon .\windows-installer\icon.ico
go build -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=$GIT_COMMIT -X github.com/drakkan/sftpgo/v2/internal/version.date=$DATE_TIME" -o sftpgo.exe
mkdir arm64
$Env:CGO_ENABLED='0'
$Env:GOOS='windows'
$Env:GOARCH='arm64'
go-winres simply --arch arm64 --product-version $Env:SFTPGO_VERSION-$GIT_COMMIT --file-version $FILE_VERSION --file-description "SFTPGo server" --product-name SFTPGo --copyright "AGPL-3.0" --original-filename sftpgo.exe --icon .\windows-installer\icon.ico
go build -trimpath -tags nopgxregisterdefaulttypes,nosqlite -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=$GIT_COMMIT -X github.com/drakkan/sftpgo/v2/internal/version.date=$DATE_TIME" -o .\arm64\sftpgo.exe
go-winres simply --arch arm64 --product-version $Env:SFTPGO_VERSION-$GIT_COMMIT --file-version "$FILE_VERSION" --file-description "SFTPGo server" --product-name SFTPGo --copyright "2019-2025 Nicola Murino" --original-filename sftpgo.exe --icon .\windows-installer\icon.ico
go build -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules,nosqlite -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=$GIT_COMMIT -X github.com/drakkan/sftpgo/v2/internal/version.date=$DATE_TIME" -o .\arm64\sftpgo.exe
mkdir x86
$Env:GOARCH='386'
go-winres simply --arch 386 --product-version $Env:SFTPGO_VERSION-$GIT_COMMIT --file-version $FILE_VERSION --file-description "SFTPGo server" --product-name SFTPGo --copyright "AGPL-3.0" --original-filename sftpgo.exe --icon .\windows-installer\icon.ico
go build -trimpath -tags nopgxregisterdefaulttypes,nosqlite -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=$GIT_COMMIT -X github.com/drakkan/sftpgo/v2/internal/version.date=$DATE_TIME" -o .\x86\sftpgo.exe
go-winres simply --arch 386 --product-version $Env:SFTPGO_VERSION-$GIT_COMMIT --file-version "$FILE_VERSION" --file-description "SFTPGo server" --product-name SFTPGo --copyright "2019-2025 Nicola Murino" --original-filename sftpgo.exe --icon .\windows-installer\icon.ico
go build -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules,nosqlite -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=$GIT_COMMIT -X github.com/drakkan/sftpgo/v2/internal/version.date=$DATE_TIME" -o .\x86\sftpgo.exe
Remove-Item Env:\CGO_ENABLED
Remove-Item Env:\GOOS
Remove-Item Env:\GOARCH
@ -107,14 +86,123 @@ jobs:
run: ./sftpgo initprovider
shell: bash
- name: Prepare Release for macOS
if: startsWith(matrix.os, 'macos-')
- name: Prepare Release
run: |
mkdir output
copy .\sftpgo.exe .\output
copy .\sftpgo.json .\output
copy .\sftpgo.db .\output
copy .\LICENSE .\output\LICENSE.txt
copy .\NOTICE .\output\NOTICE.txt
mkdir output\templates
xcopy .\templates .\output\templates\ /E
mkdir output\static
xcopy .\static .\output\static\ /E
mkdir output\openapi
xcopy .\openapi .\output\openapi\ /E
iscc .\windows-installer\sftpgo.iss
rm .\output\sftpgo.exe
rm .\output\sftpgo.db
copy .\arm64\sftpgo.exe .\output
(Get-Content .\output\sftpgo.json).replace('"sqlite"', '"bolt"') | Set-Content .\output\sftpgo.json
$Env:SFTPGO_DATA_PROVIDER__DRIVER='bolt'
$Env:SFTPGO_DATA_PROVIDER__NAME='.\output\sftpgo.db'
.\sftpgo.exe initprovider
Remove-Item Env:\SFTPGO_DATA_PROVIDER__DRIVER
Remove-Item Env:\SFTPGO_DATA_PROVIDER__NAME
$Env:SFTPGO_ISS_ARCH='arm64'
iscc .\windows-installer\sftpgo.iss
rm .\output\sftpgo.exe
copy .\x86\sftpgo.exe .\output
$Env:SFTPGO_ISS_ARCH='x86'
iscc .\windows-installer\sftpgo.iss
env:
SFTPGO_ISS_VERSION: ${{ steps.get_version.outputs.VERSION }}
- name: Prepare Portable Release
run: |
mkdir win-portable
copy .\sftpgo.exe .\win-portable
mkdir win-portable\arm64
copy .\arm64\sftpgo.exe .\win-portable\arm64
mkdir win-portable\x86
copy .\x86\sftpgo.exe .\win-portable\x86
copy .\sftpgo.json .\win-portable
(Get-Content .\win-portable\sftpgo.json).replace('"sqlite"', '"bolt"') | Set-Content .\win-portable\sftpgo.json
copy .\output\sftpgo.db .\win-portable
copy .\LICENSE .\win-portable\LICENSE.txt
copy .\NOTICE .\win-portable\NOTICE.txt
mkdir win-portable\templates
xcopy .\templates .\win-portable\templates\ /E
mkdir win-portable\static
xcopy .\static .\win-portable\static\ /E
mkdir win-portable\openapi
xcopy .\openapi .\win-portable\openapi\ /E
Compress-Archive .\win-portable\* sftpgo_portable.zip
- name: Upload Windows installer x86_64 artifact
uses: actions/upload-artifact@v7
with:
name: sftpgo_${{ steps.get_version.outputs.VERSION }}_windows_x86_64.exe
path: ./sftpgo_windows_x86_64.exe
retention-days: 1
- name: Upload Windows installer arm64 artifact
uses: actions/upload-artifact@v7
with:
name: sftpgo_${{ steps.get_version.outputs.VERSION }}_windows_arm64.exe
path: ./sftpgo_windows_arm64.exe
retention-days: 1
- name: Upload Windows installer x86 artifact
uses: actions/upload-artifact@v7
with:
name: sftpgo_${{ steps.get_version.outputs.VERSION }}_windows_x86.exe
path: ./sftpgo_windows_x86.exe
retention-days: 1
- name: Upload Windows portable artifact
uses: actions/upload-artifact@v7
with:
name: sftpgo_${{ steps.get_version.outputs.VERSION }}_windows_portable.zip
path: ./sftpgo_portable.zip
retention-days: 1
prepare-mac:
name: Prepare macOS binaries
runs-on: macos-14
steps:
- uses: actions/checkout@v6
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: ${{ env.GO_VERSION }}
- name: Get SFTPGo version
id: get_version
run: echo "VERSION=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT
shell: bash
- name: Build for macOS x86_64
run: go build -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=`git describe --always --abbrev=8 --dirty` -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -o sftpgo
- name: Build for macOS arm64
run: CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 SDKROOT=$(xcrun --sdk macosx --show-sdk-path) go build -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=`git describe --always --abbrev=8 --dirty` -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -o sftpgo_arm64
- name: Initialize data provider
run: ./sftpgo initprovider
shell: bash
- name: Prepare Release
run: |
mkdir -p output/{init,sqlite,bash_completion,zsh_completion}
echo "For documentation please take a look here:" > output/README.txt
echo "" >> output/README.txt
echo "https://github.com/drakkan/sftpgo/blob/${SFTPGO_VERSION}/README.md" >> output/README.txt
echo "https://docs.sftpgo.com" >> output/README.txt
cp LICENSE output/
cp NOTICE output/
cp sftpgo output/
cp sftpgo.json output/
cp sftpgo.db output/sqlite/
@ -127,130 +215,27 @@ jobs:
./sftpgo gen man -d output/man/man1
gzip output/man/man1/*
cd output
tar cJvf ../sftpgo_${SFTPGO_VERSION}_${OS}_x86_64.tar.xz *
tar cJvf ../sftpgo_${SFTPGO_VERSION}_macOS_x86_64.tar.xz *
cd ..
cp sftpgo_arm64 output/sftpgo
cd output
tar cJvf ../sftpgo_${SFTPGO_VERSION}_${OS}_arm64.tar.xz *
tar cJvf ../sftpgo_${SFTPGO_VERSION}_macOS_arm64.tar.xz *
cd ..
env:
SFTPGO_VERSION: ${{ steps.get_version.outputs.VERSION }}
OS: ${{ steps.get_os_name.outputs.OS }}
- name: Prepare Release for Windows
if: startsWith(matrix.os, 'windows-')
run: |
mkdir output
copy .\sftpgo.exe .\output
copy .\sftpgo.json .\output
copy .\sftpgo.db .\output
copy .\LICENSE .\output\LICENSE.txt
mkdir output\templates
xcopy .\templates .\output\templates\ /E
mkdir output\static
xcopy .\static .\output\static\ /E
mkdir output\openapi
xcopy .\openapi .\output\openapi\ /E
$CERT_PATH=(Get-Location -PSProvider FileSystem).ProviderPath + "\cert.pfx"
[IO.File]::WriteAllBytes($CERT_PATH,[System.Convert]::FromBase64String($Env:CERT_DATA))
certutil -f -p "$Env:CERT_PASS" -importpfx MY "$CERT_PATH"
rm "$CERT_PATH"
& 'C:/Program Files (x86)/Windows Kits/10/bin/10.0.20348.0/x86/signtool.exe' sign /sm /tr http://timestamp.sectigo.com /td sha256 /fd sha256 /n "Nicola Murino" /d "SFTPGo" .\sftpgo.exe
& 'C:/Program Files (x86)/Windows Kits/10/bin/10.0.20348.0/x86/signtool.exe' sign /sm /tr http://timestamp.sectigo.com /td sha256 /fd sha256 /n "Nicola Murino" /d "SFTPGo" .\arm64\sftpgo.exe
& 'C:/Program Files (x86)/Windows Kits/10/bin/10.0.20348.0/x86/signtool.exe' sign /sm /tr http://timestamp.sectigo.com /td sha256 /fd sha256 /n "Nicola Murino" /d "SFTPGo" .\x86\sftpgo.exe
$INNO_S='/Ssigntool=$qC:/Program Files (x86)/Windows Kits/10/bin/10.0.20348.0/x86/signtool.exe$q sign /sm /tr http://timestamp.sectigo.com /td sha256 /fd sha256 /n $qNicola Murino$q /d $qSFTPGo$q $f'
iscc "$INNO_S" .\windows-installer\sftpgo.iss
rm .\output\sftpgo.exe
rm .\output\sftpgo.db
copy .\arm64\sftpgo.exe .\output
(Get-Content .\output\sftpgo.json).replace('"sqlite"', '"bolt"') | Set-Content .\output\sftpgo.json
$Env:SFTPGO_DATA_PROVIDER__DRIVER='bolt'
$Env:SFTPGO_DATA_PROVIDER__NAME='.\output\sftpgo.db'
.\sftpgo.exe initprovider
Remove-Item Env:\SFTPGO_DATA_PROVIDER__DRIVER
Remove-Item Env:\SFTPGO_DATA_PROVIDER__NAME
$Env:SFTPGO_ISS_ARCH='arm64'
iscc "$INNO_S" .\windows-installer\sftpgo.iss
rm .\output\sftpgo.exe
copy .\x86\sftpgo.exe .\output
$Env:SFTPGO_ISS_ARCH='x86'
iscc "$INNO_S" .\windows-installer\sftpgo.iss
certutil -delstore MY "Nicola Murino"
env:
SFTPGO_ISS_VERSION: ${{ steps.get_version.outputs.VERSION }}
SFTPGO_ISS_DOC_URL: https://github.com/drakkan/sftpgo/blob/${{ steps.get_version.outputs.VERSION }}/README.md
CERT_DATA: ${{ secrets.CERT_DATA }}
CERT_PASS: ${{ secrets.CERT_PASS }}
- name: Prepare Portable Release for Windows
if: startsWith(matrix.os, 'windows-')
run: |
mkdir win-portable
copy .\sftpgo.exe .\win-portable
mkdir win-portable\arm64
copy .\arm64\sftpgo.exe .\win-portable\arm64
mkdir win-portable\x86
copy .\x86\sftpgo.exe .\win-portable\x86
copy .\sftpgo.json .\win-portable
(Get-Content .\win-portable\sftpgo.json).replace('"sqlite"', '"bolt"') | Set-Content .\win-portable\sftpgo.json
copy .\output\sftpgo.db .\win-portable
copy .\LICENSE .\win-portable\LICENSE.txt
mkdir win-portable\templates
xcopy .\templates .\win-portable\templates\ /E
mkdir win-portable\static
xcopy .\static .\win-portable\static\ /E
mkdir win-portable\openapi
xcopy .\openapi .\win-portable\openapi\ /E
Compress-Archive .\win-portable\* sftpgo_portable.zip
- name: Upload macOS x86_64 artifact
if: startsWith(matrix.os, 'macos-')
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: sftpgo_${{ steps.get_version.outputs.VERSION }}_${{ steps.get_os_name.outputs.OS }}_x86_64.tar.xz
path: ./sftpgo_${{ steps.get_version.outputs.VERSION }}_${{ steps.get_os_name.outputs.OS }}_x86_64.tar.xz
name: sftpgo_${{ steps.get_version.outputs.VERSION }}_macOS_x86_64.tar.xz
path: ./sftpgo_${{ steps.get_version.outputs.VERSION }}_macOS_x86_64.tar.xz
retention-days: 1
- name: Upload macOS arm64 artifact
if: startsWith(matrix.os, 'macos-')
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: sftpgo_${{ steps.get_version.outputs.VERSION }}_${{ steps.get_os_name.outputs.OS }}_arm64.tar.xz
path: ./sftpgo_${{ steps.get_version.outputs.VERSION }}_${{ steps.get_os_name.outputs.OS }}_arm64.tar.xz
retention-days: 1
- name: Upload Windows installer x86_64 artifact
if: startsWith(matrix.os, 'windows-')
uses: actions/upload-artifact@v4
with:
name: sftpgo_${{ steps.get_version.outputs.VERSION }}_${{ steps.get_os_name.outputs.OS }}_x86_64.exe
path: ./sftpgo_windows_x86_64.exe
retention-days: 1
- name: Upload Windows installer arm64 artifact
if: startsWith(matrix.os, 'windows-')
uses: actions/upload-artifact@v4
with:
name: sftpgo_${{ steps.get_version.outputs.VERSION }}_${{ steps.get_os_name.outputs.OS }}_arm64.exe
path: ./sftpgo_windows_arm64.exe
retention-days: 1
- name: Upload Windows installer x86 artifact
if: startsWith(matrix.os, 'windows-')
uses: actions/upload-artifact@v4
with:
name: sftpgo_${{ steps.get_version.outputs.VERSION }}_${{ steps.get_os_name.outputs.OS }}_x86.exe
path: ./sftpgo_windows_x86.exe
retention-days: 1
- name: Upload Windows portable artifact
if: startsWith(matrix.os, 'windows-')
uses: actions/upload-artifact@v4
with:
name: sftpgo_${{ steps.get_version.outputs.VERSION }}_${{ steps.get_os_name.outputs.OS }}_portable.zip
path: ./sftpgo_portable.zip
name: sftpgo_${{ steps.get_version.outputs.VERSION }}_macOS_arm64.tar.xz
path: ./sftpgo_${{ steps.get_version.outputs.VERSION }}_macOS_arm64.tar.xz
retention-days: 1
prepare-linux:
@ -285,7 +270,7 @@ jobs:
tar-arch: armv7
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Get versions
id: get_version
@ -310,7 +295,7 @@ jobs:
echo 'export PATH=$PATH:/usr/local/go/bin' >> build.sh
echo 'go version' >> build.sh
echo 'cd /usr/local/src' >> build.sh
echo 'go build -buildvcs=false -trimpath -tags nopgxregisterdefaulttypes -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=${{ steps.get_version.outputs.COMMIT }} -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -o sftpgo' >> build.sh
echo 'go build -buildvcs=false -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=${{ steps.get_version.outputs.COMMIT }} -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -o sftpgo' >> build.sh
chmod 755 build.sh
docker run --rm --name ubuntu-build --mount type=bind,source=`pwd`,target=/usr/local/src ${{ matrix.distro }} /usr/local/src/build.sh
@ -319,6 +304,7 @@ jobs:
echo "" >> output/README.txt
echo "https://github.com/drakkan/sftpgo/blob/${SFTPGO_VERSION}/README.md" >> output/README.txt
cp LICENSE output/
cp NOTICE output/
cp sftpgo.json output/
cp -r templates output/
cp -r static output/
@ -337,7 +323,7 @@ jobs:
env:
SFTPGO_VERSION: ${{ steps.get_version.outputs.SFTPGO_VERSION }}
- uses: uraimo/run-on-arch-action@v2
- uses: uraimo/run-on-arch-action@v3
if: ${{ matrix.arch != 'amd64' }}
name: Build for ${{ matrix.arch }}
id: build
@ -362,12 +348,13 @@ jobs:
run: |
export PATH=$PATH:/usr/local/go/bin
go version
go build -buildvcs=false -trimpath -tags nopgxregisterdefaulttypes -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=${{ steps.get_version.outputs.COMMIT }} -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -o sftpgo
go build -buildvcs=false -trimpath -tags nopgxregisterdefaulttypes,disable_grpc_modules -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=${{ steps.get_version.outputs.COMMIT }} -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -o sftpgo
mkdir -p output/{init,sqlite,bash_completion,zsh_completion}
echo "For documentation please take a look here:" > output/README.txt
echo "" >> output/README.txt
echo "https://github.com/drakkan/sftpgo/blob/${{ steps.get_version.outputs.SFTPGO_VERSION }}/README.md" >> output/README.txt
cp LICENSE output/
cp NOTICE output/
cp sftpgo.json output/
cp -r templates output/
cp -r static output/
@ -385,7 +372,7 @@ jobs:
cd ..
- name: Upload build artifact for ${{ matrix.arch }}
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_${{ matrix.tar-arch }}.tar.xz
path: ./output/sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_${{ matrix.tar-arch }}.tar.xz
@ -403,14 +390,14 @@ jobs:
SFTPGO_VERSION: ${{ steps.get_version.outputs.SFTPGO_VERSION }}
- name: Upload Deb Package
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: sftpgo_${{ steps.build_linux_pkgs.outputs.pkg-version }}-1_${{ matrix.deb-arch}}.deb
path: ./pkgs/dist/deb/sftpgo_${{ steps.build_linux_pkgs.outputs.pkg-version }}-1_${{ matrix.deb-arch}}.deb
retention-days: 1
- name: Upload RPM Package
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: sftpgo-${{ steps.build_linux_pkgs.outputs.pkg-version }}-1.${{ matrix.rpm-arch}}.rpm
path: ./pkgs/dist/rpm/sftpgo-${{ steps.build_linux_pkgs.outputs.pkg-version }}-1.${{ matrix.rpm-arch}}.rpm
@ -429,22 +416,22 @@ jobs:
shell: bash
- name: Download amd64 artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_x86_64.tar.xz
- name: Download arm64 artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_arm64.tar.xz
- name: Download ppc64le artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_ppc64le.tar.xz
- name: Download armv7 artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_armv7.tar.xz
@ -467,7 +454,7 @@ jobs:
SFTPGO_VERSION: ${{ steps.get_version.outputs.SFTPGO_VERSION }}
- name: Upload Linux bundle
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_bundle.tar.xz
path: ./bundle/sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_bundle.tar.xz
@ -475,11 +462,11 @@ jobs:
create-release:
name: Release
needs: [prepare-linux-bundle, prepare-sources-with-deps, prepare-window-mac]
needs: [prepare-linux-bundle, prepare-sources-with-deps, prepare-mac, prepare-windows]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Get versions
id: get_version
run: |
@ -490,102 +477,102 @@ jobs:
shell: bash
- name: Download amd64 artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_x86_64.tar.xz
- name: Download arm64 artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_arm64.tar.xz
- name: Download ppc64le artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_ppc64le.tar.xz
- name: Download armv7 artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_armv7.tar.xz
- name: Download Linux bundle artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_linux_bundle.tar.xz
- name: Download Deb amd64 artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: sftpgo_${{ steps.get_version.outputs.PKG_VERSION }}-1_amd64.deb
- name: Download Deb arm64 artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: sftpgo_${{ steps.get_version.outputs.PKG_VERSION }}-1_arm64.deb
- name: Download Deb ppc64le artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: sftpgo_${{ steps.get_version.outputs.PKG_VERSION }}-1_ppc64el.deb
- name: Download Deb armv7 artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: sftpgo_${{ steps.get_version.outputs.PKG_VERSION }}-1_armhf.deb
- name: Download RPM x86_64 artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: sftpgo-${{ steps.get_version.outputs.PKG_VERSION }}-1.x86_64.rpm
- name: Download RPM aarch64 artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: sftpgo-${{ steps.get_version.outputs.PKG_VERSION }}-1.aarch64.rpm
- name: Download RPM ppc64le artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: sftpgo-${{ steps.get_version.outputs.PKG_VERSION }}-1.ppc64le.rpm
- name: Download RPM armv7 artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: sftpgo-${{ steps.get_version.outputs.PKG_VERSION }}-1.armv7hl.rpm
- name: Download macOS x86_64 artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_macOS_x86_64.tar.xz
- name: Download macOS arm64 artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_macOS_arm64.tar.xz
- name: Download Windows installer x86_64 artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_windows_x86_64.exe
- name: Download Windows installer arm64 artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_windows_arm64.exe
- name: Download Windows installer x86 artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_windows_x86.exe
- name: Download Windows portable artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_windows_portable.zip
- name: Download source with deps artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: sftpgo_${{ steps.get_version.outputs.SFTPGO_VERSION }}_src_with_deps.tar.xz

View file

@ -1,52 +1,66 @@
version: "2"
run:
timeout: 10m
issues-exit-code: 1
tests: true
linters-settings:
dupl:
threshold: 150
errcheck:
check-type-assertions: false
check-blank: false
goconst:
min-len: 3
min-occurrences: 3
gocyclo:
min-complexity: 15
gofmt:
simplify: true
goimports:
local-prefixes: github.com/drakkan/sftpgo
#govet:
# report about shadowed variables
#check-shadowing: true
#enable:
# - fieldalignment
issues:
include:
- EXC0002
- EXC0012
- EXC0013
- EXC0014
- EXC0015
linters:
enable:
- goconst
- errcheck
- gofmt
- goimports
- revive
- unconvert
- unparam
- bodyclose
- dogsled
- dupl
- goconst
- gocyclo
- misspell
- whitespace
- dupl
- revive
- rowserrcheck
- dogsled
- govet
- unconvert
- unparam
- whitespace
settings:
dupl:
threshold: 150
errcheck:
check-type-assertions: false
check-blank: false
goconst:
min-len: 3
min-occurrences: 3
gocyclo:
min-complexity: 15
# https://golangci-lint.run/usage/linters/#revive
revive:
rules:
- name: var-naming
severity: warning
disabled: true
exclude: [""]
arguments:
- ["ID"] # AllowList
- ["VM"] # DenyList
- - upper-case-const: true
- - skip-package-name-checks: true
exclusions:
generated: lax
presets:
- common-false-positives
- legacy
- std-error-handling
paths:
- third_party$
- builtin$
- examples$
formatters:
enable:
- gofmt
- goimports
settings:
gofmt:
simplify: true
goimports:
local-prefixes:
- github.com/drakkan/sftpgo
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$

View file

@ -1,4 +1,4 @@
FROM golang:1.22-bookworm as builder
FROM golang:1.26-trixie AS builder
ENV GOFLAGS="-mod=readonly"
@ -10,7 +10,7 @@ WORKDIR /workspace
ARG GOPROXY
COPY go.mod go.sum ./
RUN go mod download
RUN go mod download && go mod verify
ARG COMMIT_SHA
@ -30,14 +30,14 @@ ARG DOWNLOAD_PLUGINS=false
RUN if [ "${DOWNLOAD_PLUGINS}" = "true" ]; then apt-get update && apt-get install --no-install-recommends -y curl && ./docker/scripts/download-plugins.sh; fi
FROM debian:bookworm-slim
FROM debian:trixie-slim
# Set to "true" to install jq and the optional git and rsync dependencies
# Set to "true" to install jq
ARG INSTALL_OPTIONAL_PACKAGES=false
RUN apt-get update && apt-get -y upgrade && apt-get install --no-install-recommends -y ca-certificates media-types && rm -rf /var/lib/apt/lists/*
RUN if [ "${INSTALL_OPTIONAL_PACKAGES}" = "true" ]; then apt-get update && apt-get install --no-install-recommends -y jq git rsync && rm -rf /var/lib/apt/lists/*; fi
RUN if [ "${INSTALL_OPTIONAL_PACKAGES}" = "true" ]; then apt-get update && apt-get install --no-install-recommends -y jq && rm -rf /var/lib/apt/lists/*; fi
RUN mkdir -p /etc/sftpgo /var/lib/sftpgo /usr/share/sftpgo /srv/sftpgo/data /srv/sftpgo/backups

View file

@ -1,4 +1,4 @@
FROM golang:1.22-alpine3.19 AS builder
FROM golang:1.26-alpine3.23 AS builder
ENV GOFLAGS="-mod=readonly"
@ -10,7 +10,7 @@ WORKDIR /workspace
ARG GOPROXY
COPY go.mod go.sum ./
RUN go mod download
RUN go mod download && go mod verify
ARG COMMIT_SHA
@ -25,14 +25,14 @@ RUN set -xe && \
export COMMIT_SHA=${COMMIT_SHA:-$(git describe --always --abbrev=8 --dirty)} && \
go build $(if [ -n "${FEATURES}" ]; then echo "-tags ${FEATURES}"; fi) -trimpath -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=${COMMIT_SHA} -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -v -o sftpgo
FROM alpine:3.19
FROM alpine:3.23
# Set to "true" to install jq and the optional git and rsync dependencies
# Set to "true" to install jq
ARG INSTALL_OPTIONAL_PACKAGES=false
RUN apk -U upgrade --no-cache && 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; fi
RUN mkdir -p /etc/sftpgo /var/lib/sftpgo /usr/share/sftpgo /srv/sftpgo/data /srv/sftpgo/backups

View file

@ -1,4 +1,4 @@
FROM golang:1.22-bookworm as builder
FROM golang:1.26-trixie AS builder
ENV CGO_ENABLED=0 GOFLAGS="-mod=readonly"
@ -10,7 +10,7 @@ WORKDIR /workspace
ARG GOPROXY
COPY go.mod go.sum ./
RUN go mod download
RUN go mod download && go mod verify
ARG COMMIT_SHA
@ -32,7 +32,7 @@ RUN sed -i 's|"users_base_dir": "",|"users_base_dir": "/srv/sftpgo/data",|' sftp
RUN mkdir /etc/sftpgo /var/lib/sftpgo /srv/sftpgo
FROM gcr.io/distroless/static-debian12
FROM gcr.io/distroless/static-debian13
COPY --from=builder --chown=1000:1000 /etc/sftpgo /etc/sftpgo
COPY --from=builder --chown=1000:1000 /srv/sftpgo /srv/sftpgo

12
NOTICE Normal file
View file

@ -0,0 +1,12 @@
Additional terms under GNU AGPL version 3 section 7.3(b) and 13.1:
If you have included SFTPGo so that it is offered through any network
interactions, including by means of an external user interface, or
any other integration, even without modifying its source code and then
SFTPGo is partially, fully or optionally configured via your frontend,
you must provide reasonable but clear attribution to the SFTPGo project
and its author(s), not imply any endorsement by or affiliation with the
SFTPGo project, and you must prominently offer all users interacting
with it remotely through a computer network an opportunity to receive
the Corresponding Source of the SFTPGo version you include by providing
a link to the Corresponding Source in the SFTPGo source code repository.

View file

@ -1,31 +1,51 @@
# SFTPGo
[![CI Status](https://github.com/drakkan/sftpgo/workflows/CI/badge.svg?branch=main&event=push)](https://github.com/drakkan/sftpgo/workflows/CI/badge.svg?branch=main&event=push)
[![Code Coverage](https://codecov.io/gh/drakkan/sftpgo/branch/main/graph/badge.svg)](https://codecov.io/gh/drakkan/sftpgo/branch/main)
[![CI Status](https://github.com/drakkan/sftpgo/workflows/CI/badge.svg)](https://github.com/drakkan/sftpgo/workflows/CI/badge.svg)
[![License: AGPL-3.0-only](https://img.shields.io/badge/License-AGPLv3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0)
[![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go)
Full-featured and highly configurable event-driven file transfer solution.
Server protocols: SFTP, HTTP/S, FTP/S, WebDAV.
Storage backends: local filesystem, encrypted local filesystem, S3 (compatible) Object Storage, Google Cloud Storage, Azure Blob Storage, other SFTP servers.
Full-featured and highly configurable event-driven file transfer solution. Server protocols: SFTP, HTTP/S, FTP/S, WebDAV. Storage backends: local filesystem, encrypted local filesystem, S3 (compatible) Object Storage, Google Cloud Storage, Azure Blob Storage, other SFTP servers.
With SFTPGo you can leverage local and cloud storage backends for exchanging and storing files internally or with business partners using the same tools and processes you are already familiar with.
The WebAdmin UI allows to easily create and manage your users, folders, groups and other resources.
## Project Status & Editions
The WebClient UI allows end users to change their credentials, browse and manage their files in the browser and setup two-factor authentication which works with Microsoft Authenticator, Google Authenticator, Authy and other compatible apps.
SFTPGo is an open-source project with a sustainable business model. We offer two editions to suit different requirements, ensuring the project remains healthy and maintained for everyone.
### Open Source (Community)
Free, Copyleft (AGPLv3), Community Supported. The Community edition is a fully functional, production-ready solution widely adopted worldwide. It includes all the core protocols, storage backends, and the WebAdmin/WebClient UIs. It is ideal for:
- Standard file transfer needs.
- Integrating storage backends (S3, GCS, Azure Blob) with legacy protocols.
- Projects that are comfortable with AGPLv3 licensing.
### SFTPGo Enterprise
Commercial License, Professional Support, ISO 27001 Vendor. The Enterprise edition is built on the same core but extends it for mission-critical environments, compliance-heavy industries, and advanced workflows. It is a drop-in replacement (seamless upgrade).
| Feature | Open Source (Community) | Enterprise Edition |
| :--- | :--- | :--- |
| **License Type** | AGPLv3 (Copyleft) | **Commercial License**<br/>Proprietary/No Copyleft |
| **Vendor Compliance** | Not Applicable<br/>Community Project | **Certified Vendor**<br/>ISO 27001 & Supply Chain Validation |
| **Support** | Community (GitHub) | **Direct from Authors** |
| **Cloud Storage Engine** | Standard | **High Performance & Scalable**<br/>In-memory streaming (no local temp files) and up to 70% faster |
| **High Availability (HA)** | Standard<br/>Shared DB & Storage | **Advanced**<br/>Enhanced event handling and optimized instance coordination |
| **Automation Logic** | Simple Placeholders | **Dynamic Logic & Virtual Folders**<br/>Conditions, loops, route data across storage backends |
| **Data Lifecycle** | Delete / Retain | **Smart Archiving**<br/>Move data to external Cloud/SFTP storage via Virtual Folders |
| **Email Data Ingestion** | - | **Native IMAP Integration**<br/>Auto-extract attachments from email to storage |
| **Public Sharing** | Standard Links | **Advanced & Collaborative**<br/>Email Authentication & Group Delegation |
| **Data Protection** | - | **Encryption & Scanning**<br/>Automated PGP, Antivirus & DLP via ICAP |
| **Advanced Identity (SSO)** | Standard | **Extended Controls**<br/>Advanced Single Sign-On parameters |
| **Document Editing** | - | **Included**<br/>View, edit, and co-author in browser |
**Note**: We are committed to keeping the Open Source edition powerful and maintained. The Enterprise edition helps fund the development of the entire SFTPGo ecosystem.
## Sponsors
We strongly believe in Open Source software model, so we decided to make SFTPGo available to everyone, but maintaining and evolving SFTPGo takes a lot of time and work. To make development and maintenance sustainable you should consider to support the project with a [sponsorship](https://github.com/sponsors/drakkan).
If you rely on SFTPGo in your projects, consider becoming a [sponsor](https://github.com/sponsors/drakkan).
We also provide [professional services](https://sftpgo.com/#pricing) to support you in using SFTPGo to the fullest.
The open source license grant you freedom but not assurance of help. So why would you rely on free software without support or any guarantee it will stay healthy and maintained for the upcoming years?
Supporting the project benefit businesses and the community because if the project is financially sustainable, using this business model, we don't have to restrict features and/or switch to an [Open-core](https://en.wikipedia.org/wiki/Open-core_model) model. The technology stays truly open source. Everyone wins.
You should support the project for its ongoing maintenance, even if you don't have any questions or need new features. If SFTPGo is no longer maintained you will have troubles and your company will lose money: bugs and security vulnerabilities will no longer be fixed, new algorithms will not be added to support newer clients, and so on. You will be forced to switch to a similar proprietary product and pay for its license and the migration cost.
Your sponsorship helps cover maintenance, security updates and ongoing development of the open-source edition.
### Thank you to our sponsors
@ -45,23 +65,39 @@ You should support the project for its ongoing maintenance, even if you don't ha
[<img src="./img/7digital.png" alt="7digital logo" width="178" height="56">](https://www.7digital.com/)
</br></br>
[<img src="./img/vps2day.png" alt="VPS2day logo" width="234" height="56">](https://www.vps2day.com/)
## Support policy
You can use SFTPGo for free, respecting the obligations of the Open Source license, but please do not ask or expect free support as well.
Use [discussions](https://github.com/drakkan/sftpgo/discussions) to ask questions and get support from the community.
If you report an invalid issue and/or ask for step-by-step support, your issue will be closed as invalid without further explanation. Invalid bug reports left open may confuse other users. Thanks for understanding.
[<img src="./img/servinga.png" alt="servinga logo" width="258" height="56">](https://servinga.com/)
</br></br>
[<img src="./img/reui.png" alt="ReUI logo" width="151" height="56">](https://www.reui.io/)
## Documentation
You can read more about supported features and documentation at [sftpgo.github.io](https://sftpgo.github.io/).
You can explore all supported features and configuration options at [docs.sftpgo.com](https://docs.sftpgo.com/latest/).
**Note:** The link above refers to the **Community Edition**.
For details on **Enterprise Edition**, please refer to the [Enterprise Documentation](https://docs.sftpgo.com/enterprise/).
## Support
- **Community Support**: use [GitHub Discussions](https://github.com/drakkan/sftpgo/discussions) to ask questions, share feedback, and engage with other users.
- **Commercial Support**: If you require guaranteed SLAs, expert guidance, or the advanced features listed above, check out [SFTPGo Enterprise](https://sftpgo.com).
SFTPGo Enterprise is available as:
- On-premises: Full control on your infrastructure. More details: [sftpgo.com/on-premises](https://sftpgo.com/on-premises)
- Fully managed SaaS: We handle the infrastructure. More details: [sftpgo.com/saas](https://sftpgo.com/saas)
## Internationalization
The translations are available via [Crowdin](https://crowdin.com/project/sftpgo), who have granted us an open source license.
Before translating please take a look at our contribution [guidelines](https://docs.sftpgo.com/latest/web-interfaces/#internationalization).
## Release Cadence
SFTPGo releases are feature-driven, we don't have a fixed time based schedule. As a rough estimate, you can expect 1 or 2 new releases per year.
SFTPGo follows a feature-driven release cycle.
- Enterprise Edition: Receives major new features first and follows a faster [release cadence](https://docs.sftpgo.com/enterprise/changelog/).
- Community Edition: Remains maintained, receiving bug fixes, security updates, and updates to core features.
## Acknowledgements
@ -71,7 +107,7 @@ We are very grateful to all the people who contributed with ideas and/or pull re
Thank you to [ysura](https://www.ysura.com/) for granting us stable access to a test AWS S3 account.
Thank you to [KeenThemes](https://keenthemes.com/) for granting us a custom license to use their amazing [Mega Bundle](https://keenthemes.com/products/templates-mega-bundle) for SFTPGo UI.
Thank you to [KeenThemes](https://keenthemes.com/) for granting us a custom license to use their amazing [themes](https://keenthemes.com/bootstrap-templates) for the SFTPGo WebAdmin and WebClient user interfaces, across both the Open Source and Open Core versions.
Thank you to [Crowdin](https://crowdin.com/) for granting us an Open Source License.
@ -79,15 +115,17 @@ Thank you to [Incode](https://www.incode.it/) for helping us to improve the UI/U
## License
SFTPGo source code is licensed under the GNU AGPL-3.0-only.
SFTPGo source code is licensed under the GNU AGPL-3.0-only with [additional terms](./NOTICE).
The [theme](https://keenthemes.com/products/templates-mega-bundle) used in WebAdmin and WebClient user interfaces is proprietary, this means:
The [theme](https://keenthemes.com/bootstrap-templates) used in WebAdmin and WebClient user interfaces is proprietary, this means:
- KeenThemes HTML/CSS/JS components are allowed for use only within the SFTPGo product and restricted to be used in a resealable HTML template that can compete with KeenThemes products anyhow.
- The SFTPGo WebAdmin and WebClient user interfaces (HTML, CSS and JS components) based on this theme are allowed for use only within the SFTPGo product and therefore cannot be used in derivative works/products without an explicit grant from the [SFTPGo Team](mailto:support@sftpgo.com).
More information about [compliance](https://sftpgo.com/compliance.html).
**Note:** We do not provide legal advice. If you have questions about license compliance or whether your use case is permitted under the license terms, please consult your legal team.
## Copyright
Copyright (C) 2019 Nicola Murino
Copyright (C) 2019 - 2026 Nicola Murino

View file

@ -2,8 +2,18 @@
## Supported Versions
Only the current release of the software is actively supported.
[Contact us](mailto:support@sftpgo.com) if you need early security patches and enterprise-grade security.
We actively maintain the latest stable release of SFTPGo. While we strive to keep the Open Source version secure and up-to-date, maintenance is performed on a best-effort basis by the community and contributors.
## Scope and Dependency Policy
Our security advisories focus on vulnerabilities found within the **SFTPGo codebase itself**.
To ensure the long-term sustainability of the project, we handle upstream dependencies (like the Go standard library, external packages, or Docker base images) as follows:
- Community Updates: For the Open Source version, vulnerabilities in upstream components (such as the Go standard library or third-party packages) are addressed during our **regular release cycles**. We generally do not provide immediate, out-of-band or ad-hoc releases to address dependency-only CVEs.
- Empowering Users: One of the strengths of SFTPGo being open-source is that you have full control. If your security scanners require an immediate fix, you can always rebuild the project using the latest patched Go toolchain or updated dependencies.
- Compatibility: We are committed to keeping SFTPGo compatible with the latest stable Go compiler. If an upstream fix breaks SFTPGo, fixing that becomes a priority for us.
- Professional Needs: We understand that some organizations have strict compliance requirements or internal SLAs that require guaranteed, immediate response times and out-of-band patches. For these cases, we offer [SFTPGo Enterprise](https://sftpgo.com/on-premises) to cover the additional maintenance and support overhead.
## Reporting a Vulnerability

View file

@ -1,13 +1,13 @@
#!/usr/bin/env bash
set -e
set -euo pipefail
ARCH=`uname -m`
ARCH=$(uname -m)
case ${ARCH} in
"x86_64")
x86_64)
SUFFIX=amd64
;;
"aarch64")
aarch64)
SUFFIX=arm64
;;
*)
@ -15,11 +15,21 @@ case ${ARCH} in
;;
esac
echo "download plugins for arch ${SUFFIX}"
echo "Downloading plugins for arch ${SUFFIX}"
for PLUGIN in geoipfilter kms pubsub eventstore eventsearch auth
do
echo "download plugin from https://github.com/sftpgo/sftpgo-plugin-${PLUGIN}/releases/latest/download/sftpgo-plugin-${PLUGIN}-linux-${SUFFIX}"
curl -L "https://github.com/sftpgo/sftpgo-plugin-${PLUGIN}/releases/latest/download/sftpgo-plugin-${PLUGIN}-linux-${SUFFIX}" --output "/usr/local/bin/sftpgo-plugin-${PLUGIN}"
chmod 755 "/usr/local/bin/sftpgo-plugin-${PLUGIN}"
PLUGINS=(geoipfilter kms pubsub eventstore eventsearch auth)
for PLUGIN in "${PLUGINS[@]}"; do
URL="https://github.com/sftpgo/sftpgo-plugin-${PLUGIN}/releases/latest/download/sftpgo-plugin-${PLUGIN}-linux-${SUFFIX}"
DEST="/usr/local/bin/sftpgo-plugin-${PLUGIN}"
echo "Downloading ${PLUGIN}..."
if curl --fail --silent --show-error -L "${URL}" --output "${DEST}"; then
chmod 755 "${DEST}"
else
echo "Error: Failed to download ${PLUGIN}" >&2
exit 1
fi
done
echo "All plugins downloaded successfully"

View file

@ -1,6 +1,6 @@
# Data Backup
:warning: Since v2.4.0 you can use the [EventManager](https://sftpgo.github.io/latest/eventmanager/) to schedule backups.
:warning: Since v2.4.0 you can use the [EventManager](https://docs.sftpgo.com/latest/eventmanager/) to schedule backups.
The `backup` example script shows how to use the SFTPGo REST API to backup your data.

View file

@ -1,36 +0,0 @@
# File retention policies
:warning: Since v2.4.0 you can use the [EventManager](https://sftpgo.github.io/latest/eventmanager/) to schedule data retention checks.
The `checkretention` example script shows how to use the SFTPGo REST API to manage data retention.
:warning: Deleting files is an irreversible action, please make sure you fully understand what you are doing before using this feature, you may have users with overlapping home directories or virtual folders shared between multiple users, it is relatively easy to inadvertently delete files you need.
The example shows how to setup a really simple retention policy, for each user it sends this request:
```json
[
{
"path": "/",
"retention": 168,
"delete_empty_dirs": true,
"ignore_user_permissions": false
}
]
```
so alls files with modification time older than 168 hours (7 days) will be deleted. Empty directories will be removed and the check will respect user's permissions, so if the user cannot delete a file/folder it will be skipped.
You can define different retention policies per-user and per-folder and you can exclude a folder setting the retention to `0`.
You can use this script as a starting point, please edit it according to your needs.
The script is written in Python and has the following requirements:
- python3 or python2
- python [Requests](https://requests.readthedocs.io/en/master/) module
The provided example tries to connect to an SFTPGo instance running on `127.0.0.1:8080` using the following credentials:
- username: `admin`
- password: `password`

View file

@ -1,115 +0,0 @@
#!/usr/bin/env python
from datetime import datetime
import sys
import time
import pytz
import requests
try:
import urllib.parse as urlparse
except ImportError:
import urlparse
# change base_url to point to your SFTPGo installation
base_url = "http://127.0.0.1:8080"
# set to False if you want to skip TLS certificate validation
verify_tls_cert = True
# set the credentials for a valid admin here
admin_user = "admin"
admin_password = "password"
class CheckRetention:
def __init__(self):
self.limit = 100
self.offset = 0
self.access_token = ""
self.access_token_expiration = None
def printLog(self, message):
print("{} - {}".format(datetime.now(), message))
def checkAccessToken(self):
if self.access_token != "" and self.access_token_expiration:
expire_diff = self.access_token_expiration - datetime.now(tz=pytz.UTC)
# we don't use total_seconds to be python 2 compatible
seconds_to_expire = expire_diff.days * 86400 + expire_diff.seconds
if seconds_to_expire > 180:
return
auth = requests.auth.HTTPBasicAuth(admin_user, admin_password)
r = requests.get(urlparse.urljoin(base_url, "api/v2/token"), auth=auth, verify=verify_tls_cert, timeout=10)
if r.status_code != 200:
self.printLog("error getting access token: {}".format(r.text))
sys.exit(1)
self.access_token = r.json()["access_token"]
self.access_token_expiration = pytz.timezone("UTC").localize(datetime.strptime(r.json()["expires_at"],
"%Y-%m-%dT%H:%M:%SZ"))
def getAuthHeader(self):
self.checkAccessToken()
return {"Authorization": "Bearer " + self.access_token}
def waitForRentionCheck(self, username):
while True:
auth_header = self.getAuthHeader()
r = requests.get(urlparse.urljoin(base_url, "api/v2/retention/users/checks"), headers=auth_header, verify=verify_tls_cert,
timeout=10)
if r.status_code != 200:
self.printLog("error getting retention checks while waiting for {}: {}".format(username, r.text))
sys.exit(1)
checking = False
for check in r.json():
if check["username"] == username:
checking = True
if not checking:
break
self.printLog("waiting for the retention check to complete for user {}".format(username))
time.sleep(2)
self.printLog("retention check for user {} finished".format(username))
def checkUserRetention(self, username):
self.printLog("starting retention check for user {}".format(username))
auth_header = self.getAuthHeader()
retention = [
{
"path": "/",
"retention": 168,
"delete_empty_dirs": True,
"ignore_user_permissions": False
}
]
r = requests.post(urlparse.urljoin(base_url, "api/v2/retention/users/" + username + "/check"), headers=auth_header,
json=retention, verify=verify_tls_cert, timeout=10)
if r.status_code != 202:
self.printLog("error starting retention check for user {}: {}".format(username, r.text))
sys.exit(1)
self.waitForRentionCheck(username)
def checkUsersRetention(self):
while True:
self.printLog("get users, limit {} offset {}".format(self.limit, self.offset))
auth_header = self.getAuthHeader()
payload = {"limit":self.limit, "offset":self.offset}
r = requests.get(urlparse.urljoin(base_url, "api/v2/users"), headers=auth_header, params=payload,
verify=verify_tls_cert, timeout=10)
if r.status_code != 200:
self.printLog("error getting users: {}".format(r.text))
sys.exit(1)
users = r.json()
for user in users:
self.checkUserRetention(user["username"])
self.offset += len(users)
if len(users) < self.limit:
break
if __name__ == '__main__':
c = CheckRetention()
c.checkUsersRetention()

View file

@ -1,15 +1,15 @@
module github.com/drakkan/ldapauth
go 1.22.2
go 1.25.0
require (
github.com/go-ldap/ldap/v3 v3.4.8
golang.org/x/crypto v0.23.0
github.com/go-ldap/ldap/v3 v3.4.12
golang.org/x/crypto v0.45.0
)
require (
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
github.com/Azure/go-ntlmssp v0.1.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
github.com/google/uuid v1.6.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/sys v0.38.0 // indirect
)

View file

@ -1,20 +1,15 @@
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/Azure/go-ntlmssp v0.1.0 h1:DjFo6YtWzNqNvQdrwEyr/e4nhU3vRiwenz5QX7sFz+A=
github.com/Azure/go-ntlmssp v0.1.0/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
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/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk=
github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-ldap/ldap/v3 v3.4.8 h1:loKJyspcRezt2Q3ZRMq2p/0v8iOurlmeXDPw6fikSvQ=
github.com/go-ldap/ldap/v3 v3.4.8/go.mod h1:qS3Sjlu76eHfHGpUdWkAXQTw4beih+cHsco2jXlIXrk=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4=
github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
@ -31,69 +26,15 @@ github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZ
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -1,43 +1,37 @@
module github.com/drakkan/sftpgo/ldapauthserver
go 1.22.2
go 1.25.0
require (
github.com/go-chi/chi/v5 v5.0.12
github.com/go-chi/chi/v5 v5.2.3
github.com/go-chi/render v1.0.3
github.com/go-ldap/ldap/v3 v3.4.8
github.com/go-ldap/ldap/v3 v3.4.12
github.com/nathanaelle/password/v2 v2.0.1
github.com/rs/zerolog v1.32.0
github.com/spf13/cobra v1.8.0
github.com/spf13/viper v1.18.2
golang.org/x/crypto v0.23.0
github.com/rs/zerolog v1.34.0
github.com/spf13/cobra v1.10.1
github.com/spf13/viper v1.21.0
golang.org/x/crypto v0.45.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
)
require (
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
github.com/Azure/go-ntlmssp v0.1.0 // indirect
github.com/ajg/form v1.5.1 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/sagikazarmark/locafero v0.12.0 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
)

View file

@ -1,40 +1,34 @@
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/Azure/go-ntlmssp v0.1.0 h1:DjFo6YtWzNqNvQdrwEyr/e4nhU3vRiwenz5QX7sFz+A=
github.com/Azure/go-ntlmssp v0.1.0/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
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.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk=
github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
github.com/go-ldap/ldap/v3 v3.4.8 h1:loKJyspcRezt2Q3ZRMq2p/0v8iOurlmeXDPw6fikSvQ=
github.com/go-ldap/ldap/v3 v3.4.8/go.mod h1:qS3Sjlu76eHfHGpUdWkAXQTw4beih+cHsco2jXlIXrk=
github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4=
github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
@ -53,132 +47,68 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/nathanaelle/password/v2 v2.0.1 h1:ItoCTdsuIWzilYmllQPa3DR3YoCXcpfxScWLqr8Ii2s=
github.com/nathanaelle/password/v2 v2.0.1/go.mod h1:eaoT+ICQEPNtikBRIAatN8ThWwMhVG+r1jTw60BvPJk=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0=
github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
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/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200311171314-f7b00557c8c4/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/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-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -1,6 +1,6 @@
# Update user quota
:warning: Since v2.4.0 you can use the [EventManager](https://sftpgo.github.io/latest/eventmanager/) to schedule quota scans.
:warning: Since v2.4.0 you can use the [EventManager](https://docs.sftpgo.com/latest/eventmanager/) to schedule quota scans.
The `scanuserquota` example script shows how to use the SFTPGo REST API to update the users' quota.

283
go.mod
View file

@ -1,190 +1,185 @@
module github.com/drakkan/sftpgo/v2
go 1.22.2
go 1.25.0
require (
cloud.google.com/go/storage v1.41.0
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.2
cloud.google.com/go/storage v1.60.0
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5
github.com/alexedwards/argon2id v1.0.0
github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964
github.com/aws/aws-sdk-go-v2 v1.26.1
github.com/aws/aws-sdk-go-v2/config v1.27.13
github.com/aws/aws-sdk-go-v2/credentials v1.17.13
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.18
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.21.5
github.com/aws/aws-sdk-go-v2/service/s3 v1.54.0
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.28.7
github.com/aws/aws-sdk-go-v2/service/sts v1.28.7
github.com/bmatcuk/doublestar/v4 v4.6.1
github.com/cockroachdb/cockroach-go/v2 v2.3.8
github.com/coreos/go-oidc/v3 v3.10.0
github.com/drakkan/webdav v0.0.0-20240503091431-218ec83910bb
github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001
github.com/fclairamb/ftpserverlib v0.24.0
github.com/fclairamb/go-log v0.5.0
github.com/go-acme/lego/v4 v4.16.1
github.com/go-chi/chi/v5 v5.0.12
github.com/go-chi/jwtauth/v5 v5.3.1
github.com/aws/aws-sdk-go-v2 v1.41.3
github.com/aws/aws-sdk-go-v2/config v1.32.11
github.com/aws/aws-sdk-go-v2/credentials v1.19.11
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.4
github.com/aws/aws-sdk-go-v2/service/sts v1.41.8
github.com/bmatcuk/doublestar/v4 v4.10.0
github.com/cockroachdb/cockroach-go/v2 v2.4.3
github.com/coreos/go-oidc/v3 v3.17.0
github.com/drakkan/webdav v0.0.0-20241026165615-b8b8f74ae71b
github.com/eikenb/pipeat v0.0.0-20251030185646-385cd3c3e07b
github.com/fclairamb/ftpserverlib v0.30.0
github.com/go-acme/lego/v4 v4.32.0
github.com/go-chi/chi/v5 v5.2.5
github.com/go-chi/render v1.0.3
github.com/go-sql-driver/mysql v1.8.1
github.com/go-jose/go-jose/v4 v4.1.3
github.com/go-sql-driver/mysql v1.9.3
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/google/uuid v1.6.0
github.com/hashicorp/go-hclog v1.6.3
github.com/hashicorp/go-plugin v1.6.1
github.com/hashicorp/go-retryablehttp v0.7.6
github.com/jackc/pgx/v5 v5.5.5
github.com/hashicorp/go-plugin v1.7.0
github.com/hashicorp/go-retryablehttp v0.7.8
github.com/jackc/pgx/v5 v5.8.0
github.com/jlaffaye/ftp v0.2.0
github.com/klauspost/compress v1.17.8
github.com/lestrrat-go/jwx/v2 v2.0.21
github.com/lithammer/shortuuid/v3 v3.0.7
github.com/mattn/go-sqlite3 v1.14.22
github.com/klauspost/compress v1.18.4
github.com/lithammer/shortuuid/v4 v4.2.0
github.com/mattn/go-sqlite3 v1.14.34
github.com/mhale/smtpd v0.8.3
github.com/minio/sio v0.3.1
github.com/otiai10/copy v1.14.0
github.com/pires/go-proxyproto v0.7.0
github.com/pkg/sftp v1.13.7-0.20240410063531-637088883317
github.com/pquerna/otp v1.4.0
github.com/prometheus/client_golang v1.19.1
github.com/minio/sio v0.4.3
github.com/otiai10/copy v1.14.1
github.com/pires/go-proxyproto v0.11.0
github.com/pkg/sftp v1.13.10
github.com/pquerna/otp v1.5.0
github.com/prometheus/client_golang v1.23.2
github.com/robfig/cron/v3 v3.0.1
github.com/rs/cors v1.11.0
github.com/rs/xid v1.5.0
github.com/rs/zerolog v1.32.0
github.com/sftpgo/sdk v0.1.7
github.com/shirou/gopsutil/v3 v3.24.4
github.com/spf13/afero v1.11.0
github.com/spf13/cobra v1.8.0
github.com/spf13/viper v1.18.2
github.com/stretchr/testify v1.9.0
github.com/studio-b12/gowebdav v0.9.0
github.com/rs/cors v1.11.1
github.com/rs/xid v1.6.0
github.com/rs/zerolog v1.34.0
github.com/sftpgo/sdk v0.1.9
github.com/shirou/gopsutil/v3 v3.24.5
github.com/spf13/afero v1.15.0
github.com/spf13/cobra v1.10.2
github.com/spf13/viper v1.21.0
github.com/stretchr/testify v1.11.1
github.com/studio-b12/gowebdav v0.12.0
github.com/subosito/gotenv v1.6.0
github.com/unrolled/secure v1.14.0
github.com/unrolled/secure v1.17.0
github.com/wagslane/go-password-validator v0.3.0
github.com/wneessen/go-mail v0.4.1
github.com/wneessen/go-mail v0.7.2
github.com/yl2chen/cidranger v1.0.3-0.20210928021809-d1cb2c52f37a
go.etcd.io/bbolt v1.3.10
go.uber.org/automaxprocs v1.5.3
gocloud.dev v0.37.0
golang.org/x/crypto v0.23.0
golang.org/x/net v0.25.0
golang.org/x/oauth2 v0.20.0
golang.org/x/sys v0.20.0
golang.org/x/term v0.20.0
golang.org/x/time v0.5.0
google.golang.org/api v0.180.0
go.etcd.io/bbolt v1.4.3
gocloud.dev v0.45.0
golang.org/x/crypto v0.49.0
golang.org/x/net v0.52.0
golang.org/x/oauth2 v0.36.0
golang.org/x/sys v0.42.0
golang.org/x/term v0.41.0
golang.org/x/time v0.15.0
google.golang.org/api v0.271.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
)
require (
cloud.google.com/go v0.113.0 // indirect
cloud.google.com/go/auth v0.4.1 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect
cloud.google.com/go/compute/metadata v0.3.0 // indirect
cloud.google.com/go/iam v1.1.8 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0 // indirect
github.com/ajg/form v1.5.1 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.5 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.5 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.20.6 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.0 // indirect
github.com/aws/smithy-go v1.20.2 // indirect
cel.dev/expr v0.25.1 // indirect
cloud.google.com/go v0.123.0 // indirect
cloud.google.com/go/auth v0.18.2 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
cloud.google.com/go/iam v1.5.3 // indirect
cloud.google.com/go/monitoring v1.24.3 // indirect
filippo.io/edwards25519 v1.2.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.7.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect
github.com/ajg/form v1.7.1 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.20 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 // indirect
github.com/aws/smithy-go v1.24.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/boombuler/barcode v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/boombuler/barcode v1.1.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
github.com/fatih/color v1.17.0 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-jose/go-jose/v4 v4.0.2 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/s2a-go v0.1.7 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/googleapis/gax-go/v2 v2.12.4 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect
github.com/googleapis/gax-go/v2 v2.18.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hashicorp/yamux v0.1.1 // indirect
github.com/hashicorp/yamux v0.1.2 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/kr/fs v0.1.0 // indirect
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/httprc v1.0.5 // indirect
github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/miekg/dns v1.1.59 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/oklog/run v1.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/miekg/dns v1.1.72 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/oklog/run v1.2.0 // indirect
github.com/otiai10/mint v1.6.3 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.53.0 // indirect
github.com/prometheus/procfs v0.15.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.20.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/segmentio/asm v1.2.0 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/tklauser/go-sysconf v0.3.14 // indirect
github.com/tklauser/numcpus v0.8.0 // indirect
github.com/sagikazarmark/locafero v0.12.0 // indirect
github.com/shoenig/go-m1cpu v0.2.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
github.com/tklauser/go-sysconf v0.3.16 // indirect
github.com/tklauser/numcpus v0.11.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 // indirect
go.opentelemetry.io/otel v1.26.0 // indirect
go.opentelemetry.io/otel/metric v1.26.0 // indirect
go.opentelemetry.io/otel/trace v1.26.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/text v0.15.0 // indirect
golang.org/x/tools v0.21.0 // indirect
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
google.golang.org/genproto v0.0.0-20240513163218-0867130af1f8 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240513163218-0867130af1f8 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240513163218-0867130af1f8 // indirect
google.golang.org/grpc v1.64.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.42.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect
go.opentelemetry.io/otel v1.42.0 // indirect
go.opentelemetry.io/otel/metric v1.42.0 // indirect
go.opentelemetry.io/otel/sdk v1.42.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.42.0 // indirect
go.opentelemetry.io/otel/trace v1.42.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/mod v0.34.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/text v0.35.0 // indirect
golang.org/x/tools v0.43.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
google.golang.org/genproto v0.0.0-20260311181403-84a4fc48630c // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260311181403-84a4fc48630c // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect
google.golang.org/grpc v1.79.2 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace (
github.com/fclairamb/ftpserverlib => github.com/drakkan/ftpserverlib v0.0.0-20240510125431-4617586dfa1c
github.com/jlaffaye/ftp => github.com/drakkan/ftp v0.0.0-20240430173938-7ba8270c8e7f
github.com/robfig/cron/v3 => github.com/drakkan/cron/v3 v3.0.0-20230222140221-217a1e4d96c0
golang.org/x/crypto => github.com/drakkan/crypto v0.0.0-20240509175024-33071fb6437f
)

747
go.sum
View file

@ -1,222 +1,198 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.113.0 h1:g3C70mn3lWfckKBiCVsAshabrDg01pQ0pnX1MNtnMkA=
cloud.google.com/go v0.113.0/go.mod h1:glEqlogERKYeePz6ZdkcLJ28Q2I6aERgDDErBg9GzO8=
cloud.google.com/go/auth v0.4.1 h1:Z7YNIhlWRtrnKlZke7z3GMqzvuYzdc2z98F9D1NV5Hg=
cloud.google.com/go/auth v0.4.1/go.mod h1:QVBuVEKpCn4Zp58hzRGvL0tjRGU0YqdRTdCHM1IHnro=
cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4=
cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q=
cloud.google.com/go/compute v1.26.0 h1:uHf0NN2nvxl1Gh4QO83yRCOdMK4zivtMS5gv0dEX0hg=
cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
cloud.google.com/go/iam v1.1.8 h1:r7umDwhj+BQyz0ScZMp4QrGXjSTI3ZINnpgU2nlB/K0=
cloud.google.com/go/iam v1.1.8/go.mod h1:GvE6lyMmfxXauzNq8NbgJbeVQNspG+tcdL/W8QO1+zE=
cloud.google.com/go/kms v1.16.0 h1:1yZsRPhmargZOmY+fVAh8IKiR9HzCb0U1zsxb5g2nRY=
cloud.google.com/go/kms v1.16.0/go.mod h1:olQUXy2Xud+1GzYfiBO9N0RhjsJk5IJLU6n/ethLXVc=
cloud.google.com/go/longrunning v0.5.7 h1:WLbHekDbjK1fVFD3ibpFFVoyizlLRl73I7YKuAKilhU=
cloud.google.com/go/longrunning v0.5.7/go.mod h1:8GClkudohy1Fxm3owmBGid8W0pSgodEMwEAztp38Xng=
cloud.google.com/go/storage v1.41.0 h1:RusiwatSu6lHeEXe3kglxakAmAbfV+rhtPqA6i8RBx0=
cloud.google.com/go/storage v1.41.0/go.mod h1:J1WCa/Z2FcgdEDuPUY8DxT5I+d9mFKsCepp5vR6Sq80=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=
cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=
cloud.google.com/go/kms v1.26.0 h1:cK9mN2cf+9V63D3H1f6koxTatWy39aTI/hCjz1I+adU=
cloud.google.com/go/kms v1.26.0/go.mod h1:pHKOdFJm63hxBsiPkYtowZPltu9dW0MWvBa6IA4HM58=
cloud.google.com/go/logging v1.13.2 h1:qqlHCBvieJT9Cdq4QqYx1KPadCQ2noD4FK02eNqHAjA=
cloud.google.com/go/logging v1.13.2/go.mod h1:zaybliM3yun1J8mU2dVQ1/qDzjbOqEijZCn6hSBtKak=
cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8=
cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk=
cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE=
cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI=
cloud.google.com/go/storage v1.60.0 h1:oBfZrSOCimggVNz9Y/bXY35uUcts7OViubeddTTVzQ8=
cloud.google.com/go/storage v1.60.0/go.mod h1:q+5196hXfejkctrnx+VYU8RKQr/L3c0cBIlrjmiAKE0=
cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U=
cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s=
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 h1:E+OJmp2tPvt1W+amx48v1eqbjDYsgN+RzP4q16yV5eM=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1 h1:sO0/P7g68FrryJzljemN+6GTssUXdANk6aJ7T1ZxnsQ=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1/go.mod h1:h8hyGFDsU5HMivxiS2iYFZsgDbU9OnnJ163x5UGVKYo=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0 h1:jBQA3cKT4L2rWMpgE7Yt3Hwh2aUj8KXjIGLxjHeYNNo=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.5.0 h1:AifHbc4mg0x9zW52WOpKbsHaDKuRhlI7TVl47thgQ70=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.5.0/go.mod h1:T5RfihdXtBDxt1Ch2wobif3TvzTdumDy29kahv6AV9A=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.2 h1:YUUxeiOWgdAQE3pXt2H7QXzZs0q8UBjgRbl56qo8GYM=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.2/go.mod h1:dmXQgZuiSubAecswZE+Sm8jkvEa7kQgTPVRvwL/nd0E=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1 h1:/Zt+cDPnpC3OVDm/JKLOs7M2DKmLRIIp3XIx9pHHiig=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1/go.mod h1:Ng3urmn6dYe8gnbCMoHHVl5APYz2txho3koEkV2o2HA=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4 h1:jWQK1GI+LeGGUKBADtcH2rRqPxYB1Ljwms5gFA2LqrM=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4/go.mod h1:8mwH4klAm9DUgR2EEHyEEAQlRDvLPyg5fQry3y+cDew=
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
github.com/AzureAD/microsoft-authentication-library-for-go v1.7.0 h1:4iB+IesclUXdP0ICgAabvq2FYLXrJWKx1fJQ+GxSo3Y=
github.com/AzureAD/microsoft-authentication-library-for-go v1.7.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 h1:IEjq88XO4PuBDcvmjQJcQGg+w+UaafSy8G5Kcb5tBhI=
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5/go.mod h1:exZ0C/1emQJAw5tHOaUDyY1ycttqBAPcxuzf7QbY6ec=
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0 h1:7t/qx5Ost0s0wbA/VDrByOooURhp+ikYwv20i9Y07TQ=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 h1:0s6TxfCu2KHkkZPnBfsQ2y5qia0jl3MMrmBhu3nCOYk=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/ajg/form v1.7.1 h1:OsnBDzTkrWdrxvEnO68I72ZVGJGNaMwPhoAm0V+llgc=
github.com/ajg/form v1.7.1/go.mod h1:HL757PzLyNkj5AIfptT6L+iGNeXTlnrr/oDePGc/y7Q=
github.com/alexedwards/argon2id v1.0.0 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHcQQP0w=
github.com/alexedwards/argon2id v1.0.0/go.mod h1:tYKkqIjzXvZdzPvADMWOEZ+l6+BD6CtBXMj5fnJppiw=
github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964 h1:I9YN9WMo3SUh7p/4wKeNvD/IQla3U3SUa61U7ul+xM4=
github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964/go.mod h1:eFiR01PwTcpbzXtdMces7zxg6utvFM5puiWHpWB8D/k=
github.com/aws/aws-sdk-go-v2 v1.26.1 h1:5554eUqIYVWpU0YmeeYZ0wU64H2VLBs8TlhRB2L+EkA=
github.com/aws/aws-sdk-go-v2 v1.26.1/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2 h1:x6xsQXGSmW6frevwDA+vi/wqhp1ct18mVXYN08/93to=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2/go.mod h1:lPprDr1e6cJdyYeGXnRaJoP4Md+cDBvi2eOj00BlGmg=
github.com/aws/aws-sdk-go-v2/config v1.27.13 h1:WbKW8hOzrWoOA/+35S5okqO/2Ap8hkkFUzoW8Hzq24A=
github.com/aws/aws-sdk-go-v2/config v1.27.13/go.mod h1:XLiyiTMnguytjRER7u5RIkhIqS8Nyz41SwAWb4xEjxs=
github.com/aws/aws-sdk-go-v2/credentials v1.17.13 h1:XDCJDzk/u5cN7Aple7D/MiAhx1Rjo/0nueJ0La8mRuE=
github.com/aws/aws-sdk-go-v2/credentials v1.17.13/go.mod h1:FMNcjQrmuBYvOTZDtOLCIu0esmxjF7RuA/89iSXWzQI=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1 h1:FVJ0r5XTHSmIHJV6KuDmdYhEpvlHpiSd38RQWhut5J4=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1/go.mod h1:zusuAeqezXzAB24LGuzuekqMAEgWkVYukBec3kr3jUg=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.17 h1:9b1Os1s11mF5qTIKLgSsyPG810di2+ySSLIIt9bwe9I=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.17/go.mod h1:9Wp7tDOMhv0+sb/FTRAkbHNQ7abYDnoJRzm5AAtCnTc=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.18 h1:fUHit8Pe+2dWEHtxpOVDTOSQR257iH24HjT17DAz6qs=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.18/go.mod h1:IX1n1o870YYxzqN56w26s7FrO5Zaw/hdatxhJDiEf2U=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 h1:aw39xVGeRWlWx9EzGVnhOR4yOjQDHPQ6o6NmBlscyQg=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5/go.mod h1:FSaRudD0dXiMPK2UjknVwwTYyZMRsHv3TtkabsZih5I=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 h1:PG1F3OD1szkuQPzDw3CIQsRIrtTlUC3lP84taWzHlq0=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5/go.mod h1:jU1li6RFryMz+so64PpKtudI+QzbKoIEivqdf6LNpOc=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.5 h1:81KE7vaZzrl7yHBYHVEzYB8sypz11NMOZ40YlWvPxsU=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.5/go.mod h1:LIt2rg7Mcgn09Ygbdh/RdIm0rQ+3BNkbP1gyVMFtRK0=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 h1:Ji0DY1xUsUr3I8cHps0G+XM3WWU16lP6yG8qu1GAZAs=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2/go.mod h1:5CsjAbs3NlGQyZNFACh+zztPDI7fU6eW9QsxjfnuBKg=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.7 h1:ZMeFZ5yk+Ek+jNr1+uwCd2tG89t6oTS5yVWpa6yy2es=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.7/go.mod h1:mxV05U+4JiHqIpGqqYXOHLPKUC6bDXC44bsUhNjOEwY=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 h1:ogRAwT1/gxJBcSWDMZlgyFUM962F51A5CRhDLbxLdmo=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7/go.mod h1:YCsIZhXfRPLFFCl5xxY+1T9RKzOKjCut+28JSX2DnAk=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.5 h1:f9RyWNtS8oH7cZlbn+/JNPpjUk5+5fLd5lM9M0i49Ys=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.5/go.mod h1:h5CoMZV2VF297/VLhRhO1WF+XYWOzXo+4HsObA4HjBQ=
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.21.5 h1:p2PxN+OO28p2bCCXE79sJfFBaSohwxa24bQdjuyPZCs=
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.21.5/go.mod h1:Q01yJLephuOzv6IYzcknrpVAriOqB66+qtGnpqgw9UE=
github.com/aws/aws-sdk-go-v2/service/s3 v1.53.2 h1:rq2hglTQM3yHZvOPVMtNvLS5x6hijx7JvRDgKiTNDGQ=
github.com/aws/aws-sdk-go-v2/service/s3 v1.53.2/go.mod h1:qmdkIIAC+GCLASF7R2whgNrJADz0QZPX+Seiw/i4S3o=
github.com/aws/aws-sdk-go-v2/service/s3 v1.54.0 h1:Ls94RY3P6HtB88JkzXo1lHrXzonHPpNR//OSAV63mSE=
github.com/aws/aws-sdk-go-v2/service/s3 v1.54.0/go.mod h1:qmdkIIAC+GCLASF7R2whgNrJADz0QZPX+Seiw/i4S3o=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.28.7 h1:4cziOtpDwtgcb+wTYRzz8C+GoH1XySy0p7j4oBbqPQE=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.28.7/go.mod h1:3Ba++UwWd154xtP4FRX5pUK3Gt4up5sDHCve6kVfE+g=
github.com/aws/aws-sdk-go-v2/service/sso v1.20.6 h1:o5cTaeunSpfXiLTIBx5xo2enQmiChtu1IBbzXnfU9Hs=
github.com/aws/aws-sdk-go-v2/service/sso v1.20.6/go.mod h1:qGzynb/msuZIE8I75DVRCUXw3o3ZyBmUvMwQ2t/BrGM=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.0 h1:Qe0r0lVURDDeBQJ4yP+BOrJkvkiCo/3FH/t+wY11dmw=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.0/go.mod h1:mUYPBhaF2lGiukDEjJX2BLRRKTmoUSitGDUgM4tRxak=
github.com/aws/aws-sdk-go-v2/service/sts v1.28.7 h1:et3Ta53gotFR4ERLXXHIHl/Uuk1qYpP5uU7cvNql8ns=
github.com/aws/aws-sdk-go-v2/service/sts v1.28.7/go.mod h1:FZf1/nKNEkHdGGJP/cI2MoIMquumuRK6ol3QQJNDxmw=
github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q=
github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
github.com/aws/aws-sdk-go-v2 v1.41.3 h1:4kQ/fa22KjDt13QCy1+bYADvdgcxpfH18f0zP542kZA=
github.com/aws/aws-sdk-go-v2 v1.41.3/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6 h1:N4lRUXZpZ1KVEUn6hxtco/1d2lgYhNn1fHkkl8WhlyQ=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI=
github.com/aws/aws-sdk-go-v2/config v1.32.11 h1:ftxI5sgz8jZkckuUHXfC/wMUc8u3fG1vQS0plr2F2Zs=
github.com/aws/aws-sdk-go-v2/config v1.32.11/go.mod h1:twF11+6ps9aNRKEDimksp923o44w/Thk9+8YIlzWMmo=
github.com/aws/aws-sdk-go-v2/credentials v1.19.11 h1:NdV8cwCcAXrCWyxArt58BrvZJ9pZ9Fhf9w6Uh5W3Uyc=
github.com/aws/aws-sdk-go-v2/credentials v1.19.11/go.mod h1:30yY2zqkMPdrvxBqzI9xQCM+WrlrZKSOpSJEsylVU+8=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 h1:INUvJxmhdEbVulJYHI061k4TVuS3jzzthNvjqvVvTKM=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19/go.mod h1:FpZN2QISLdEBWkayloda+sZjVJL+e9Gl0k1SyTgcswU=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 h1:/sECfyq2JTifMI2JPyZ4bdRN77zJmr6SrS1eL3augIA=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19/go.mod h1:dMf8A5oAqr9/oxOfLkC/c2LU/uMcALP0Rgn2BD5LWn0=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 h1:AWeJMk33GTBf6J20XJe6qZoRSJo0WfUhsMdUKhoODXE=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19/go.mod h1:+GWrYoaAsV7/4pNHpwh1kiNLXkKaSoppxQq9lbH8Ejw=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 h1:clHU5fm//kWS1C2HgtgWxfQbFbx4b6rx+5jzhgX9HrI=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.20 h1:qi3e/dmpdONhj1RyIZdi6DKKpDXS5Lb8ftr3p7cyHJc=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.20/go.mod h1:V1K+TeJVD5JOk3D9e5tsX2KUdL7BlB+FV6cBhdobN8c=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 h1:XAq62tBTJP/85lFD5oqOOe7YYgWxY9LvWq8plyDvDVg=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11 h1:BYf7XNsJMzl4mObARUBUib+j2tf0U//JAAtTnYqvqCw=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11/go.mod h1:aEUS4WrNk/+FxkBZZa7tVgp4pGH+kFGW40Y8rCPqt5g=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 h1:X1Tow7suZk9UCJHE1Iw9GMZJJl0dAnKXXP1NaSDHwmw=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19/go.mod h1:/rARO8psX+4sfjUQXp5LLifjUt8DuATZ31WptNJTyQA=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19 h1:JnQeStZvPHFHeyky/7LbMlyQjUa+jIBj36OlWm0pzIk=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19/go.mod h1:HGyasyHvYdFQeJhvDHfH7HXkHh57htcJGKDZ+7z+I24=
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.4 h1:4ExZyubQ6LQQVuF2Qp9OsfEvsTdAWh5Gfwf6PgIdLdk=
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.4/go.mod h1:NF3JcMGOiARAss1ld3WGORCw71+4ExDD2cbbdKS5PpA=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 h1:Y2cAXlClHsXkkOvWZFXATr34b0hxxloeQu/pAZz2row=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.7/go.mod h1:idzZ7gmDeqeNrSPkdbtMp9qWMgcBwykA7P7Rzh5DXVU=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 h1:iSsvB9EtQ09YrsmIc44Heqlx5ByGErqhPK1ZQLppias=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.12/go.mod h1:fEWYKTRGoZNl8tZ77i61/ccwOMJdGxwOhWCkp6TXAr0=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 h1:EnUdUqRP1CNzt2DkV67tJx6XDN4xlfBFm+bzeNOQVb0=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16/go.mod h1:Jic/xv0Rq/pFNCh3WwpH4BEqdbSAl+IyHro8LbibHD8=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 h1:XQTQTF75vnug2TXS8m7CVJfC2nniYPZnO1D4Np761Oo=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.8/go.mod h1:Xgx+PR1NUOjNmQY+tRMnouRp83JRM8pRMw/vCaVhPkI=
github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I=
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=
github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs=
github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA=
github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo=
github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cockroachdb/cockroach-go/v2 v2.3.8 h1:53yoUo4+EtrC1NrAEgnnad4AS3ntNvGup1PAXZ7UmpE=
github.com/cockroachdb/cockroach-go/v2 v2.3.8/go.mod h1:9uH5jK4yQ3ZQUT9IXe4I2fHzMIF5+JC/oOdzTRgJYJk=
github.com/coreos/go-oidc/v3 v3.10.0 h1:tDnXHnLyiTVyT/2zLDGj09pFPkhND8Gl8lnTRhoEaJU=
github.com/coreos/go-oidc/v3 v3.10.0/go.mod h1:5j11xcw0D3+SGxn6Z/WFADsgcWVMyNAlSQupk0KK3ac=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik=
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4=
github.com/cockroachdb/cockroach-go/v2 v2.4.3 h1:LJO3K3jC5WXvMePRQSJE1NsIGoFGcEx1LW83W6RAlhw=
github.com/cockroachdb/cockroach-go/v2 v2.4.3/go.mod h1:9U179XbCx4qFWtNhc7BiWLPfuyMVQ7qdAhfrwLz1vH0=
github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/drakkan/cron/v3 v3.0.0-20230222140221-217a1e4d96c0 h1:EW9gIJRmt9lzk66Fhh4S8VEtURA6QHZqGeSRE9Nb2/U=
github.com/drakkan/cron/v3 v3.0.0-20230222140221-217a1e4d96c0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/drakkan/crypto v0.0.0-20240509175024-33071fb6437f h1:4+0I7deWH0/8dTS1xVgFrNSq7aaNvKrfaqLlfFBNV64=
github.com/drakkan/crypto v0.0.0-20240509175024-33071fb6437f/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
github.com/drakkan/ftp v0.0.0-20240430173938-7ba8270c8e7f h1:S9JUlrOzjK58UKoLqqb40YLyVlt0bcIFtYrvnanV3zc=
github.com/drakkan/ftp v0.0.0-20240430173938-7ba8270c8e7f/go.mod h1:4p8lUl4vQ80L598CygL+3IFtm+3nggvvW/palOlViwE=
github.com/drakkan/ftpserverlib v0.0.0-20240510125431-4617586dfa1c h1:cO3eqB2Bjv8WM8HUDfajAt3bFFGj6FUQ2eIxsxVvyC8=
github.com/drakkan/ftpserverlib v0.0.0-20240510125431-4617586dfa1c/go.mod h1:+9afJRWESpCq4/O8Vr00Q2jfinRxP6PiCpXph6CgGuc=
github.com/drakkan/webdav v0.0.0-20240503091431-218ec83910bb h1:067/Uo8cfeY7QC0yzWCr/RImuNcM0rLWAsBUyMks59o=
github.com/drakkan/webdav v0.0.0-20240503091431-218ec83910bb/go.mod h1:zOVb1QDhwwqWn2L2qZ0U3swMSO4GTSNyIwXCGO/UGWE=
github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001 h1:/ZshrfQzayqRSBDodmp3rhNCHJCff+utvgBuWRbiqu4=
github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001/go.mod h1:kltMsfRMTHSFdMbK66XdS8mfMW77+FZA1fGY1xYMF84=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/drakkan/webdav v0.0.0-20241026165615-b8b8f74ae71b h1:Y1tLiQ8fnxM5f3wiBjAXsHzHNwiY9BR+mXZA75nZwrs=
github.com/drakkan/webdav v0.0.0-20241026165615-b8b8f74ae71b/go.mod h1:zOVb1QDhwwqWn2L2qZ0U3swMSO4GTSNyIwXCGO/UGWE=
github.com/eikenb/pipeat v0.0.0-20251030185646-385cd3c3e07b h1:G2Mm3YhlyjkFrNnvu5E6LtNcPJtggXL1i5ekDV4hDD4=
github.com/eikenb/pipeat v0.0.0-20251030185646-385cd3c3e07b/go.mod h1:XccPiThW83W5pzeOCsJAylEUtWeH+3zQVwiO402FXXc=
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU=
github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ=
github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds=
github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
github.com/fclairamb/go-log v0.5.0 h1:Gz9wSamEaA6lta4IU2cjJc2xSq5sV5VYSB5w/SUHhVc=
github.com/fclairamb/go-log v0.5.0/go.mod h1:XoRO1dYezpsGmLLkZE9I+sHqpqY65p8JA+Vqblb7k40=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fclairamb/ftpserverlib v0.30.0 h1:caB9sDn1Au//q0j2ev/icPn388qPuk4k1ajSvglDcMQ=
github.com/fclairamb/ftpserverlib v0.30.0/go.mod h1:QmogtltTOgkihyKza0GNo37Mu4AEzbJ+sH6W9Y0MBIQ=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/go-acme/lego/v4 v4.16.1 h1:JxZ93s4KG0jL27rZ30UsIgxap6VGzKuREsSkkyzeoCQ=
github.com/go-acme/lego/v4 v4.16.1/go.mod h1:AVvwdPned/IWpD/ihHhMsKnveF7HHYAz/CmtXi7OZoE=
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/jwtauth/v5 v5.3.1 h1:1ePWrjVctvp1tyBq5b/2ER8Th/+RbYc7x4qNsc5rh5A=
github.com/go-chi/jwtauth/v5 v5.3.1/go.mod h1:6Fl2RRmWXs3tJYE1IQGX81FsPoGqDwq9c15j52R5q80=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-acme/lego/v4 v4.32.0 h1:z7Ss7aa1noabhKj+DBzhNCO2SM96xhE3b0ucVW3x8Tc=
github.com/go-acme/lego/v4 v4.32.0/go.mod h1:lI2fZNdgeM/ymf9xQ9YKbgZm6MeDuf91UrohMQE4DhI=
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk=
github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU=
github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA=
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=
github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/wire v0.6.0 h1:HBkoIh4BdSxoyo9PveV8giw7ZsaBOvzWKfcg/6MrVwI=
github.com/google/wire v0.6.0/go.mod h1:F4QhpQ9EDIdJ1Mbop/NZBRB+5yrR6qg3BnctaoUk6NA=
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
github.com/googleapis/gax-go/v2 v2.12.4 h1:9gWcmF85Wvq4ryPFvGFaOgPIs1AQX0d0bcbGw4Z96qg=
github.com/googleapis/gax-go/v2 v2.12.4/go.mod h1:KYEYLorsnIGDi/rPC8b5TdlB9kbKoFubselGIoBMCwI=
github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4=
github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18=
github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8=
github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
github.com/googleapis/gax-go/v2 v2.18.0 h1:jxP5Uuo3bxm3M6gGtV94P4lliVetoCB4Wk2x8QA86LI=
github.com/googleapis/gax-go/v2 v2.18.0/go.mod h1:uSzZN4a356eRG985CzJ3WfbFSpqkLTjsnhWGJR6EwrE=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@ -226,33 +202,28 @@ github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB1
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-plugin v1.6.1 h1:P7MR2UP6gNKGPp+y7EZw2kOiq4IR9WiqLvp0XOsVdwI=
github.com/hashicorp/go-plugin v1.6.1/go.mod h1:XPHFku2tFo3o3QKFgSYo+cghcUhw1NA1hZyMK0PWAw0=
github.com/hashicorp/go-retryablehttp v0.7.6 h1:TwRYfx2z2C4cLbXmT8I5PgP/xmuqASDyiVuGYfs9GZM=
github.com/hashicorp/go-retryablehttp v0.7.6/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
github.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA=
github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8=
github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=
github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=
github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA=
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
github.com/jackc/puddle v1.3.0 h1:eHK/5clGOatcjX3oWGBO/MpxpbHzSwud5EWTSCI+MX0=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=
github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94=
github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8=
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@ -261,217 +232,185 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
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/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k=
github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
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/httprc v1.0.5 h1:bsTfiH8xaKOJPrg1R+E3iE/AWZr/x0Phj9PBTG/OLUk=
github.com/lestrrat-go/httprc v1.0.5/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
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/jwx/v2 v2.0.21 h1:jAPKupy4uHgrHFEdjVjNkUgoBKtVDgrQPB/h55FHrR0=
github.com/lestrrat-go/jwx/v2 v2.0.21/go.mod h1:09mLW8zto6bWL9GbwnqAli+ArLf+5M33QLQPDggkUWM=
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=
github.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae h1:dIZY4ULFcto4tAFlj1FYZl8ztUZ13bdq+PLY+NOfbyI=
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/lithammer/shortuuid/v4 v4.2.0 h1:LMFOzVB3996a7b8aBuEXxqOBflbfPQAiVzkIcHO0h8c=
github.com/lithammer/shortuuid/v4 v4.2.0/go.mod h1:D5noHZ2oFw/YaKCfGy0YxyE7M0wMbezmMjPdhyEFe6Y=
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 h1:PTw+yKnXcOFCR6+8hHTyWBeQ/P4Nb7dd4/0ohEcWQuM=
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk=
github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mhale/smtpd v0.8.3 h1:8j8YNXajksoSLZja3HdwvYVZPuJSqAxFsib3adzRRt8=
github.com/mhale/smtpd v0.8.3/go.mod h1:MQl+y2hwIEQCXtNhe5+55n0GZOjSmeqORDIXbqUL3x4=
github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs=
github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk=
github.com/minio/sio v0.3.1 h1:d59r5RTHb1OsQaSl1EaTWurzMMDRLA5fgNmjzD4eVu4=
github.com/minio/sio v0.3.1/go.mod h1:S0ovgVgc+sTlQyhiXA1ppBLv7REM7TYi5yyq2qL/Y6o=
github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU=
github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU=
github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w=
github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks=
github.com/otiai10/mint v1.5.1/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs=
github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4=
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
github.com/minio/sio v0.4.3 h1:JqyID1XM86KwBZox5RAdLD4MLPIDoCY2cke2CXCJCkg=
github.com/minio/sio v0.4.3/go.mod h1:4ANoe4CCXqnt1FCiLM0+vlBUhhWZzVOhYCz0069KtFc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/oklog/run v1.2.0 h1:O8x3yXwah4A73hJdlrwo/2X6J62gE5qTMusH0dvz60E=
github.com/oklog/run v1.2.0/go.mod h1:mgDbKRSwPhJfesJ4PntqFUbKQRZ50NgmZTSPlFA0YFk=
github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8=
github.com/otiai10/copy v1.14.1/go.mod h1:oQwrEDDOci3IM8dJF0d8+jnbfPDllW6vUjNc3DoZm9I=
github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs=
github.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4=
github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.7-0.20240410063531-637088883317 h1:kupFhKi4R3XqKmUmqGSHWn/WZbC9CnwSoW421tL1gGw=
github.com/pkg/sftp v1.13.7-0.20240410063531-637088883317/go.mod h1:KMKI0t3T6hfA+lTR/ssZdunHo+uwq7ghoN09/FSu3DY=
github.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU=
github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.53.0 h1:U2pL9w9nmJwJDa4qqLQ3ZaePJ6ZTwt7cMD3AG3+aLCE=
github.com/prometheus/common v0.53.0/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U=
github.com/prometheus/procfs v0.15.0 h1:A82kmvXJq2jTu5YUhSGNlYoxh85zLnKgPz4bMZgI5Ek=
github.com/prometheus/procfs v0.15.0/go.mod h1:Y0RJ/Y5g5wJpkTisOtqwDSo4HwhGmLB4VQSw2sQJLHk=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po=
github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0=
github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4 h1:PT+ElG/UUFMfqy5HrxJxNzj3QBOf7dZwupeVC+mG1Lo=
github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4/go.mod h1:MnkX001NG75g3p8bhFycnyIjeQoOjGL6CEIsdE/nKSY=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/sftpgo/sdk v0.1.7 h1:lzOKBDnKb1PpSMlskqCPxBYKxVWz34uMBhT78r/13iA=
github.com/sftpgo/sdk v0.1.7/go.mod h1:ler/KG6kMLlsOs/8s6dVN3oom+z+NkbXBVWO//Cv/WA=
github.com/shirou/gopsutil/v3 v3.24.4 h1:dEHgzZXt4LMNm+oYELpzl9YCqV65Yr/6SfrvgRBtXeU=
github.com/shirou/gopsutil/v3 v3.24.4/go.mod h1:lTd2mdiOspcqLgAnr9/nGi71NkeMpWKdmhuxm9GusH8=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
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/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
github.com/sftpgo/sdk v0.1.9 h1:onBWfibCt34xHeKC2KFYPZ1DBqXGl9um/cAw+AVdgzY=
github.com/sftpgo/sdk v0.1.9/go.mod h1:ehimvlTP+XTEiE3t1CPwWx9n7+6A6OGvMGlZ7ouvKFk=
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
github.com/shoenig/go-m1cpu v0.2.0 h1:t4GNqvPZ84Vjtpboo/kT3pIkbaK3vc+JIlD/Wz1zSFY=
github.com/shoenig/go-m1cpu v0.2.0/go.mod h1:KkDOw6m3ZJQAPHbrzkZki4hnx+pDRR1Lo+ldA56wD5w=
github.com/shoenig/test v1.7.0 h1:eWcHtTXa6QLnBvm0jgEabMRN/uJ4DMV3M8xUGgRkZmk=
github.com/shoenig/test v1.7.0/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsBHFoI=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=
github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/studio-b12/gowebdav v0.9.0 h1:1j1sc9gQnNxbXXM4M/CebPOX4aXYtr7MojAVcN4dHjU=
github.com/studio-b12/gowebdav v0.9.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/studio-b12/gowebdav v0.12.0 h1:kFRtQECt8jmVAvA6RHBz3geXUGJHUZA6/IKpOVUs5kM=
github.com/studio-b12/gowebdav v0.12.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY=
github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE=
github.com/unrolled/secure v1.14.0 h1:u9vJTU/pR4Bny0ntLUMxdfLtmIRGvQf2sEFuA0TG9AE=
github.com/unrolled/secure v1.14.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40=
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
github.com/unrolled/secure v1.17.0 h1:Io7ifFgo99Bnh0J7+Q+qcMzWM6kaDPCA5FroFZEdbWU=
github.com/unrolled/secure v1.17.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40=
github.com/wagslane/go-password-validator v0.3.0 h1:vfxOPzGHkz5S146HDpavl0cw1DSVP061Ry2PX0/ON6I=
github.com/wagslane/go-password-validator v0.3.0/go.mod h1:TI1XJ6T5fRdRnHqHt14pvy1tNVnrwe7m3/f1f2fDphQ=
github.com/wneessen/go-mail v0.4.1 h1:m2rSg/sc8FZQCdtrV5M8ymHYOFrC6KJAQAIcgrXvqoo=
github.com/wneessen/go-mail v0.4.1/go.mod h1:zxOlafWCP/r6FEhAaRgH4IC1vg2YXxO0Nar9u0IScZ8=
github.com/wneessen/go-mail v0.7.2 h1:xxPnhZ6IZLSgxShebmZ6DPKh1b6OJcoHfzy7UjOkzS8=
github.com/wneessen/go-mail v0.7.2/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k=
github.com/yl2chen/cidranger v1.0.3-0.20210928021809-d1cb2c52f37a h1:XfF01GyP+0eWCaVp0y6rNN+kFp7pt9Da4UUYrJ5XPWA=
github.com/yl2chen/cidranger v1.0.3-0.20210928021809-d1cb2c52f37a/go.mod h1:aXb8yZQEWo1XHGMf1qQfnb83GR/EJ2EBlwtUgAaNBoE=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0=
go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0 h1:A3SayB3rNyt+1S6qpI9mHPkeHTZbD7XILEqWnYZb2l0=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0/go.mod h1:27iA5uvhuRNmalO+iEUdVn5ZMj2qy10Mm+XRIpRmyuU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 h1:Xs2Ncz0gNihqu9iosIZ5SkBbWo5T8JhhLJFMQL1qmLI=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0/go.mod h1:vy+2G/6NvVMpwGX/NyLqcC41fxepnuKHk16E6IZUcJc=
go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs=
go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4=
go.opentelemetry.io/otel/metric v1.26.0 h1:7S39CLuY5Jgg9CrnA9HHiEjGMF/X2VHvoXGgSllRz30=
go.opentelemetry.io/otel/metric v1.26.0/go.mod h1:SY+rHOI4cEawI9a7N1A4nIg/nTQXe1ccCNWYOJUrpX4=
go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw=
go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg=
go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA=
go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0=
go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8=
go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
gocloud.dev v0.37.0 h1:XF1rN6R0qZI/9DYjN16Uy0durAmSlf58DHOcb28GPro=
gocloud.dev v0.37.0/go.mod h1:7/O4kqdInCNsc6LqgmuFnS0GRew4XNNYWpA44yQnwco=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/detectors/gcp v1.42.0 h1:kpt2PEJuOuqYkPcktfJqWWDjTEd/FNgrxcniL7kQrXQ=
go.opentelemetry.io/contrib/detectors/gcp v1.42.0/go.mod h1:W9zQ439utxymRrXsUOzZbFX4JhLxXU4+ZnCt8GG7yA8=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0 h1:5gn2urDL/FBnK8OkCfD1j3/ER79rUuTYmCvlXBKeYL8=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0/go.mod h1:0fBG6ZJxhqByfFZDwSwpZGzJU671HkwpWaNe2t4VUPI=
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=
go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=
go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=
go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
gocloud.dev v0.45.0 h1:WknIK8IbRdmynDvara3Q7G6wQhmEiOGwpgJufbM39sY=
gocloud.dev v0.45.0/go.mod h1:0kXKmkCLG6d31N7NyLZWzt7jDSQura9zD/mWgiB6THI=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
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-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo=
golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
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/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210601080250-7ecdf8ef093b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -483,86 +422,56 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw=
golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
google.golang.org/api v0.180.0 h1:M2D87Yo0rGBPWpo1orwfCLehUUL6E7/TYe5gvMQWDh4=
google.golang.org/api v0.180.0/go.mod h1:51AiyoEg1MJPSZ9zvklA8VnRILPXxn1iVen9v25XHAE=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20240513163218-0867130af1f8 h1:XpH03M6PDRKTo1oGfZBXu2SzwcbfxUokgobVinuUZoU=
google.golang.org/genproto v0.0.0-20240513163218-0867130af1f8/go.mod h1:OLh2Ylz+WlYAJaSBRpJIJLP8iQP+8da+fpxbwNEAV/o=
google.golang.org/genproto/googleapis/api v0.0.0-20240513163218-0867130af1f8 h1:W5Xj/70xIA4x60O/IFyXivR5MGqblAb8R3w26pnD6No=
google.golang.org/genproto/googleapis/api v0.0.0-20240513163218-0867130af1f8/go.mod h1:vPrPUTsDCYxXWjP7clS81mZ6/803D8K4iM9Ma27VKas=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240513163218-0867130af1f8 h1:mxSlqyb8ZAHsYDCfiXN1EDdNTdvjUJSLY+OnAUtYNYA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240513163218-0867130af1f8/go.mod h1:I7Y+G38R2bu5j1aLzfFmQfTcU/WnFuqDwLZAbvKTKpM=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM=
google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA=
google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY=
google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/api v0.271.0 h1:cIPN4qcUc61jlh7oXu6pwOQqbJW2GqYh5PS6rB2C/JY=
google.golang.org/api v0.271.0/go.mod h1:CGT29bhwkbF+i11qkRUJb2KMKqcJ1hdFceEIRd9u64Q=
google.golang.org/genproto v0.0.0-20260311181403-84a4fc48630c h1:ZhFDeBMmFc/4g8/GwxnJ4rzB3O4GwQVNr+8Mh7Y5z4g=
google.golang.org/genproto v0.0.0-20260311181403-84a4fc48630c/go.mod h1:hf4r/rBuzaTkLUWRO03771Xvcs6P5hwdQK3UUEJjqo0=
google.golang.org/genproto/googleapis/api v0.0.0-20260311181403-84a4fc48630c h1:OyQPd6I3pN/9gDxz6L13kYGJgqkpdrAohJRBeXyxlgI=
google.golang.org/genproto/googleapis/api v0.0.0-20260311181403-84a4fc48630c/go.mod h1:X2gu9Qwng7Nn009s/r3RUxqkzQNqOrAy79bluY7ojIg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Before After
Before After

BIN
img/reui.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
img/servinga.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8 KiB

102
init/sftpgo Executable file
View file

@ -0,0 +1,102 @@
#! /bin/sh
### BEGIN INIT INFO
# Provides: SFTPGo
# Required-Start: $remote_fs $syslog
# Required-Stop: $remote_fs $syslog
# Default-Start: 2 3 4 5
# Default-Stop:
# Short-Description: SFTPGo server
### END INIT INFO
set -e
# /etc/init.d/sftpgo: start and stop the SFTPGo "server" daemon
SFTPGO_USER="sftpgo"
SFTPGO_GROUP="sftpgo"
SFTPGO_BIN_NAME="sftpgo"
SFTPGO_BIN="/usr/bin/sftpgo"
SFTPGO_PID="/run/sftpgo.pid"
SFTPGO_CONF_DIR="/etc/sftpgo"
SFTPGO_CONF_FILE="sftpgo.json"
SFTPGO_OPTS="serve -c $SFTPGO_CONF_DIR --config-file $SFTPGO_CONF_FILE"
umask 022
test -x $SFTPGO_BIN || exit 0
if test -f /etc/default/$SFTPGO_BIN_NAME; then
. /etc/default/$SFTPGO_BIN_NAME
fi
. /lib/lsb/init-functions
if [ -n "$2" ]; then
SFTPGO_OPTS="$SFTPGO_OPTS $2"
fi
# Are we running from init?
run_by_init() {
([ "$previous" ] && [ "$runlevel" ]) || [ "$runlevel" = S ]
}
check_dev_null() {
if [ ! -c /dev/null ]; then
if [ "$1" = log_end_msg ]; then
log_end_msg 1 || true
fi
if ! run_by_init; then
log_action_msg "/dev/null is not a character device!" || true
fi
exit 1
fi
}
write_pid() {
sleep 0.25
echo $(/bin/pidof $SFTPGO_BIN_NAME) > $SFTPGO_PID
}
export PATH="${PATH:+$PATH:}/usr/sbin:/sbin"
case "$1" in
start)
check_dev_null
log_daemon_msg "Starting SFTPGo server" "$SFTPGO_BIN_NAME" || true
if start-stop-daemon --start --background --quiet --oknodo --chuid $SFTPGO_USER:$SFTPGO_GROUP --pidfile $SFTPGO_PID --exec $SFTPGO_BIN -- $SFTPGO_OPTS; then
log_end_msg 0 || true
write_pid
else
log_end_msg 1 || true
fi
;;
stop)
log_daemon_msg "Stopping SFTPGo server" "$SFTPGO_BIN_NAME" || true
if start-stop-daemon --stop --quiet --oknodo --pidfile $SFTPGO_PID --exec $SFTPGO_BIN; then
log_end_msg 0 || true
else
log_end_msg 1 || true
fi
;;
reload)
log_daemon_msg "Reloading SFTPGo server" "$SFTPGO_BIN_NAME" || true
if start-stop-daemon --stop --signal 1 --quiet --oknodo --pidfile $SFTPGO_PID --exec $SFTPGO_BIN; then
log_end_msg 0 || true
else
log_end_msg 1 || true
fi
;;
status)
status_of_proc -p $SFTPGO_PID $SFTPGO_BIN $SFTPGO_BIN_NAME && exit 0 || exit $?
;;
*)
log_action_msg "Usage: /etc/init.d/$SFTPGO_BIN_NAME {start|stop|reload|status}" || true
exit 1
esac
exit 0

View file

@ -30,6 +30,7 @@ import (
"net/url"
"os"
"path/filepath"
"slices"
"strconv"
"strings"
"time"
@ -43,6 +44,7 @@ import (
"github.com/go-acme/lego/v4/log"
"github.com/go-acme/lego/v4/providers/http/webroot"
"github.com/go-acme/lego/v4/registration"
"github.com/hashicorp/go-retryablehttp"
"github.com/robfig/cron/v3"
"github.com/drakkan/sftpgo/v2/internal/common"
@ -249,7 +251,7 @@ func (c *Configuration) Initialize(configDir string) error {
if c.RenewDays < 1 {
return fmt.Errorf("invalid number of days remaining before renewal: %d", c.RenewDays)
}
if !util.Contains(supportedKeyTypes, c.KeyType) {
if !slices.Contains(supportedKeyTypes, c.KeyType) {
return fmt.Errorf("invalid key type %q", c.KeyType)
}
caURL, err := url.Parse(c.CAEndpoint)
@ -489,7 +491,16 @@ func (c *Configuration) setup() (*account, *lego.Client, error) {
config := lego.NewConfig(&account)
config.CADirURL = c.CAEndpoint
config.Certificate.KeyType = certcrypto.KeyType(c.KeyType)
config.Certificate.OverallRequestLimit = 6
config.UserAgent = version.GetServerVersion("/", false)
retryClient := retryablehttp.NewClient()
retryClient.Logger = &logger.LeveledLogger{Sender: "RetryableHTTPClient"}
retryClient.RetryMax = 5
retryClient.HTTPClient = config.HTTPClient
config.HTTPClient = retryClient.StandardClient()
client, err := lego.NewClient(config)
if err != nil {
acmeLog(logger.LevelError, "unable to get ACME client: %v", err)
@ -557,6 +568,13 @@ func (c *Configuration) tryRecoverRegistration(privateKey crypto.PrivateKey) (*r
config.CADirURL = c.CAEndpoint
config.UserAgent = version.GetServerVersion("/", false)
retryClient := retryablehttp.NewClient()
retryClient.Logger = &logger.LeveledLogger{Sender: "RetryableHTTPClient"}
retryClient.RetryMax = 5
retryClient.HTTPClient = config.HTTPClient
config.HTTPClient = retryClient.StandardClient()
client, err := lego.NewClient(config)
if err != nil {
acmeLog(logger.LevelError, "unable to get the ACME client: %v", err)
@ -671,7 +689,7 @@ func (c *Configuration) notifyCertificateRenewal(domain string, err error) {
params := common.EventParams{
Name: domain,
Event: "Certificate renewal",
Timestamp: time.Now().UnixNano(),
Timestamp: time.Now(),
}
if err != nil {
params.Status = 2
@ -749,7 +767,7 @@ func (c *Configuration) renewCertificates() error {
func isDomainValid(domain string) (string, bool) {
isValid := false
for _, d := range strings.Split(domain, ",") {
for d := range strings.SplitSeq(domain, ",") {
d = strings.TrimSpace(d)
if d != "" {
isValid = true
@ -767,7 +785,7 @@ func getDomains(domain string) []string {
delimiter = " "
}
for _, d := range strings.Split(domain, delimiter) {
for d := range strings.SplitSeq(domain, delimiter) {
d = strings.TrimSpace(d)
if d != "" {
domains = append(domains, d)

View file

@ -13,7 +13,6 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//go:build bundle
// +build bundle
package bundle

View file

@ -24,6 +24,7 @@ import (
"github.com/drakkan/sftpgo/v2/internal/config"
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
"github.com/drakkan/sftpgo/v2/internal/logger"
"github.com/drakkan/sftpgo/v2/internal/plugin"
"github.com/drakkan/sftpgo/v2/internal/util"
)
@ -56,6 +57,15 @@ renewed by the SFTPGo service
logger.ErrorToConsole("unable to initialize KMS: %v", err)
os.Exit(1)
}
if config.HasKMSPlugin() {
if err := plugin.Initialize(config.GetPluginsConfig(), "debug"); err != nil {
logger.ErrorToConsole("unable to initialize plugin system: %v", err)
os.Exit(1)
}
registerSignals()
defer plugin.Handler.Cleanup()
}
mfaConfig := config.GetMFAConfig()
err = mfaConfig.Initialize()
if err != nil {

View file

@ -1,34 +0,0 @@
// Copyright (C) 2019 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/>.
//go:build awscontainer
// +build awscontainer
package cmd
import (
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func addAWSContainerFlags(cmd *cobra.Command) {
viper.SetDefault("disable_aws_installation_code", false)
viper.BindEnv("disable_aws_installation_code", "SFTPGO_DISABLE_AWS_INSTALLATION_CODE") //nolint:errcheck
cmd.Flags().BoolVar(&disableAWSInstallationCode, "disable-aws-installation-code", viper.GetBool("disable_aws_installation_code"),
`Disable installation code for the AWS container.
This flag can be set using
SFTPGO_DISABLE_AWS_INSTALLATION_CODE env var too.
`)
viper.BindPFlag("disable_aws_installation_code", cmd.Flags().Lookup("disable-aws-installation-code")) //nolint:errcheck
}

View file

@ -21,9 +21,11 @@ import (
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/drakkan/sftpgo/v2/internal/common"
"github.com/drakkan/sftpgo/v2/internal/config"
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
"github.com/drakkan/sftpgo/v2/internal/logger"
"github.com/drakkan/sftpgo/v2/internal/plugin"
"github.com/drakkan/sftpgo/v2/internal/service"
"github.com/drakkan/sftpgo/v2/internal/util"
)
@ -65,6 +67,15 @@ Please take a look at the usage below to customize the options.`,
logger.ErrorToConsole("Unable to initialize KMS: %v", err)
os.Exit(1)
}
if config.HasKMSPlugin() {
if err := plugin.Initialize(config.GetPluginsConfig(), "debug"); err != nil {
logger.ErrorToConsole("unable to initialize plugin system: %v", err)
os.Exit(1)
}
registerSignals()
defer plugin.Handler.Cleanup()
}
mfaConfig := config.GetMFAConfig()
err = mfaConfig.Initialize()
if err != nil {
@ -78,15 +89,20 @@ Please take a look at the usage below to customize the options.`,
providerConf.Actions.ExecuteOn = nil
logger.InfoToConsole("Initializing provider: %q config file: %q", providerConf.Driver, viper.ConfigFileUsed())
err = dataprovider.InitializeDatabase(providerConf, configDir)
if err == nil {
switch err {
case nil:
logger.InfoToConsole("Data provider successfully initialized/updated")
} else if err == dataprovider.ErrNoInitRequired {
case dataprovider.ErrNoInitRequired:
logger.InfoToConsole("%v", err.Error())
} else {
default:
logger.ErrorToConsole("Unable to initialize/update the data provider: %v", err)
os.Exit(1)
}
if providerConf.Driver != dataprovider.MemoryDataProviderName && loadDataFrom != "" {
if err := common.Initialize(config.GetCommonConfig(), providerConf.GetShared()); err != nil {
logger.ErrorToConsole("%v", err)
os.Exit(1)
}
service := service.Service{
LoadDataFrom: loadDataFrom,
LoadDataMode: loadDataMode,

View file

@ -13,7 +13,6 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//go:build !noportable
// +build !noportable
package cmd
@ -497,7 +496,7 @@ func getPatternsFilterValues(value string) (string, []string) {
if len(dirExts) > 1 {
dir := strings.TrimSpace(dirExts[0])
exts := []string{}
for _, e := range strings.Split(dirExts[1], ",") {
for e := range strings.SplitSeq(dirExts[1], ",") {
cleanedExt := strings.TrimSpace(e)
if cleanedExt != "" {
exts = append(exts, cleanedExt)

View file

@ -13,7 +13,6 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//go:build noportable
// +build noportable
package cmd

View file

@ -27,6 +27,7 @@ import (
"github.com/drakkan/sftpgo/v2/internal/config"
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
"github.com/drakkan/sftpgo/v2/internal/logger"
"github.com/drakkan/sftpgo/v2/internal/plugin"
"github.com/drakkan/sftpgo/v2/internal/util"
)
@ -37,6 +38,7 @@ var (
Short: "Reset the password for the specified administrator",
Long: `This command reads the data provider connection details from the specified
configuration file and resets the password for the specified administrator.
Two-factor authentication is also disabled.
This command is not supported for the memory provider.
For embedded providers like bolt and SQLite you should stop the running SFTPGo
instance to avoid database corruption.
@ -57,6 +59,15 @@ Please take a look at the usage below to customize the options.`,
logger.ErrorToConsole("unable to initialize KMS: %v", err)
os.Exit(1)
}
if config.HasKMSPlugin() {
if err := plugin.Initialize(config.GetPluginsConfig(), "debug"); err != nil {
logger.ErrorToConsole("unable to initialize plugin system: %v", err)
os.Exit(1)
}
registerSignals()
defer plugin.Handler.Cleanup()
}
mfaConfig := config.GetMFAConfig()
err = mfaConfig.Initialize()
if err != nil {
@ -98,6 +109,7 @@ Please take a look at the usage below to customize the options.`,
os.Exit(1)
}
admin.Password = string(pwd)
admin.Filters.TOTPConfig.Enabled = false
if err := dataprovider.UpdateAdmin(&admin, dataprovider.ActionExecutorSystem, "", ""); err != nil {
logger.ErrorToConsole("Unable to update password: %v", err)
os.Exit(1)

View file

@ -24,6 +24,7 @@ import (
"github.com/drakkan/sftpgo/v2/internal/config"
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
"github.com/drakkan/sftpgo/v2/internal/logger"
"github.com/drakkan/sftpgo/v2/internal/plugin"
"github.com/drakkan/sftpgo/v2/internal/util"
)
@ -40,8 +41,8 @@ Please take a look at the usage below to customize the options.`,
Run: func(_ *cobra.Command, _ []string) {
logger.DisableLogger()
logger.EnableConsoleLogger(zerolog.DebugLevel)
if revertProviderTargetVersion != 28 {
logger.WarnToConsole("Unsupported target version, 28 is the only supported one")
if revertProviderTargetVersion != 33 {
logger.WarnToConsole("Unsupported target version, 33 is the only supported one")
os.Exit(1)
}
configDir = util.CleanDirInput(configDir)
@ -56,6 +57,21 @@ Please take a look at the usage below to customize the options.`,
logger.ErrorToConsole("unable to initialize KMS: %v", err)
os.Exit(1)
}
if config.HasKMSPlugin() {
if err := plugin.Initialize(config.GetPluginsConfig(), "debug"); err != nil {
logger.ErrorToConsole("unable to initialize plugin system: %v", err)
os.Exit(1)
}
registerSignals()
defer plugin.Handler.Cleanup()
}
mfaConfig := config.GetMFAConfig()
err = mfaConfig.Initialize()
if err != nil {
logger.ErrorToConsole("Unable to initialize MFA: %v", err)
os.Exit(1)
}
providerConf := config.GetProviderConf()
logger.InfoToConsole("Reverting provider: %q config file: %q target version %d", providerConf.Driver,
viper.ConfigFileUsed(), revertProviderTargetVersion)
@ -71,7 +87,7 @@ Please take a look at the usage below to customize the options.`,
func init() {
addConfigFlags(revertProviderCmd)
revertProviderCmd.Flags().IntVar(&revertProviderTargetVersion, "to-version", 28, `28 means the version supported in v2.5.x`)
revertProviderCmd.Flags().IntVar(&revertProviderTargetVersion, "to-version", 33, `33 means the version supported in v2.7.x`)
rootCmd.AddCommand(revertProviderCmd)
}

View file

@ -85,8 +85,6 @@ var (
loadDataQuotaScan int
loadDataClean bool
graceTime int
// used if awscontainer build tag is enabled
disableAWSInstallationCode bool
rootCmd = &cobra.Command{
Use: "sftpgo",

View file

@ -16,13 +16,21 @@ package cmd
import (
"os"
"path/filepath"
"strconv"
"strings"
"github.com/spf13/cobra"
"github.com/subosito/gotenv"
"github.com/drakkan/sftpgo/v2/internal/service"
"github.com/drakkan/sftpgo/v2/internal/util"
)
const (
envFileMaxSize = 1048576
)
var (
serveCmd = &cobra.Command{
Use: "serve",
@ -34,9 +42,11 @@ $ sftpgo serve
Please take a look at the usage below to customize the startup options`,
Run: func(_ *cobra.Command, _ []string) {
configDir := util.CleanDirInput(configDir)
checkServeParamsFromEnvFiles(configDir)
service.SetGraceTime(graceTime)
service := service.Service{
ConfigDir: util.CleanDirInput(configDir),
ConfigDir: configDir,
ConfigFile: configFile,
LogFilePath: logFilePath,
LogMaxSize: logMaxSize,
@ -51,7 +61,7 @@ Please take a look at the usage below to customize the startup options`,
LoadDataClean: loadDataClean,
Shutdown: make(chan bool),
}
if err := service.Start(disableAWSInstallationCode); err == nil {
if err := service.Start(); err == nil {
service.Wait()
if service.Error == nil {
os.Exit(0)
@ -62,8 +72,76 @@ Please take a look at the usage below to customize the startup options`,
}
)
func setIntFromEnv(receiver *int, val string) {
converted, err := strconv.Atoi(val)
if err == nil {
*receiver = converted
}
}
func setBoolFromEnv(receiver *bool, val string) {
converted, err := strconv.ParseBool(strings.TrimSpace(val))
if err == nil {
*receiver = converted
}
}
func checkServeParamsFromEnvFiles(configDir string) { //nolint:gocyclo
// The logger is not yet initialized here, we have no way to report errors.
envd := filepath.Join(configDir, "env.d")
entries, err := os.ReadDir(envd)
if err != nil {
return
}
for _, entry := range entries {
info, err := entry.Info()
if err == nil && info.Mode().IsRegular() {
envFile := filepath.Join(envd, entry.Name())
if info.Size() > envFileMaxSize {
continue
}
envVars, err := gotenv.Read(envFile)
if err != nil {
return
}
for k, v := range envVars {
if _, isSet := os.LookupEnv(k); isSet {
continue
}
switch k {
case "SFTPGO_LOG_FILE_PATH":
logFilePath = v
case "SFTPGO_LOG_MAX_SIZE":
setIntFromEnv(&logMaxSize, v)
case "SFTPGO_LOG_MAX_BACKUPS":
setIntFromEnv(&logMaxBackups, v)
case "SFTPGO_LOG_MAX_AGE":
setIntFromEnv(&logMaxAge, v)
case "SFTPGO_LOG_COMPRESS":
setBoolFromEnv(&logCompress, v)
case "SFTPGO_LOG_LEVEL":
logLevel = v
case "SFTPGO_LOG_UTC_TIME":
setBoolFromEnv(&logUTCTime, v)
case "SFTPGO_CONFIG_FILE":
configFile = v
case "SFTPGO_LOADDATA_FROM":
loadDataFrom = v
case "SFTPGO_LOADDATA_MODE":
setIntFromEnv(&loadDataMode, v)
case "SFTPGO_LOADDATA_CLEAN":
setBoolFromEnv(&loadDataClean, v)
case "SFTPGO_LOADDATA_QUOTA_SCAN":
setIntFromEnv(&loadDataQuotaScan, v)
case "SFTPGO_GRACE_TIME":
setIntFromEnv(&graceTime, v)
}
}
}
}
}
func init() {
rootCmd.AddCommand(serveCmd)
addServeFlags(serveCmd)
addAWSContainerFlags(serveCmd)
}

View file

@ -1,4 +1,4 @@
// Copyright (C) 2019 Nicola Murino
// Copyright (C) 2025 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
@ -13,26 +13,29 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//go:build !windows
// +build !windows
package sftpd
package cmd
import (
"os"
"os/exec"
"os/signal"
"syscall"
"github.com/drakkan/sftpgo/v2/internal/logger"
"github.com/drakkan/sftpgo/v2/internal/plugin"
)
var (
processUID = os.Geteuid()
processGID = os.Getegid()
)
func wrapCmd(cmd *exec.Cmd, uid, gid int) *exec.Cmd {
isCurrentUser := processUID == uid && processGID == gid
if (uid > 0 || gid > 0) && !isCurrentUser {
cmd.SysProcAttr = &syscall.SysProcAttr{}
cmd.SysProcAttr.Credential = &syscall.Credential{Uid: uint32(uid), Gid: uint32(gid)}
}
return cmd
func registerSignals() {
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
go func() {
for sig := range c {
switch sig {
case syscall.SIGINT, syscall.SIGTERM:
logger.DebugToConsole("Received interrupt request")
plugin.Handler.Cleanup()
os.Exit(0)
}
}
}()
}

View file

@ -1,4 +1,4 @@
// Copyright (C) 2019 Nicola Murino
// Copyright (C) 2025 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
@ -12,13 +12,25 @@
// 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/>.
//go:build !awscontainer
// +build !awscontainer
package cmd
import (
"github.com/spf13/cobra"
"os"
"os/signal"
"github.com/drakkan/sftpgo/v2/internal/logger"
"github.com/drakkan/sftpgo/v2/internal/plugin"
)
func addAWSContainerFlags(_ *cobra.Command) {}
func registerSignals() {
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
go func() {
for range c {
logger.DebugToConsole("Received interrupt request")
plugin.Handler.Cleanup()
os.Exit(0)
}
}()
}

View file

@ -17,7 +17,6 @@ package cmd
import (
"fmt"
"os"
"path/filepath"
"github.com/spf13/cobra"
@ -31,21 +30,23 @@ var (
Short: "Start the SFTPGo Windows Service",
Run: func(_ *cobra.Command, _ []string) {
configDir = util.CleanDirInput(configDir)
if !filepath.IsAbs(logFilePath) && util.IsFileInputValid(logFilePath) {
logFilePath = filepath.Join(configDir, logFilePath)
}
checkServeParamsFromEnvFiles(configDir)
service.SetGraceTime(graceTime)
s := service.Service{
ConfigDir: configDir,
ConfigFile: configFile,
LogFilePath: logFilePath,
LogMaxSize: logMaxSize,
LogMaxBackups: logMaxBackups,
LogMaxAge: logMaxAge,
LogCompress: logCompress,
LogLevel: logLevel,
LogUTCTime: logUTCTime,
Shutdown: make(chan bool),
ConfigDir: configDir,
ConfigFile: configFile,
LogFilePath: logFilePath,
LogMaxSize: logMaxSize,
LogMaxBackups: logMaxBackups,
LogMaxAge: logMaxAge,
LogCompress: logCompress,
LogLevel: logLevel,
LogUTCTime: logUTCTime,
LoadDataFrom: loadDataFrom,
LoadDataMode: loadDataMode,
LoadDataQuotaScan: loadDataQuotaScan,
LoadDataClean: loadDataClean,
Shutdown: make(chan bool),
}
winService := service.WindowsService{
Service: s,

View file

@ -1,227 +0,0 @@
// Copyright (C) 2019 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 cmd
import (
"io"
"os"
"os/user"
"path/filepath"
"github.com/rs/xid"
"github.com/rs/zerolog"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/drakkan/sftpgo/v2/internal/common"
"github.com/drakkan/sftpgo/v2/internal/config"
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
"github.com/drakkan/sftpgo/v2/internal/logger"
"github.com/drakkan/sftpgo/v2/internal/plugin"
"github.com/drakkan/sftpgo/v2/internal/sftpd"
"github.com/drakkan/sftpgo/v2/internal/version"
)
var (
logJournalD = false
preserveHomeDir = false
baseHomeDir = ""
subsystemCmd = &cobra.Command{
Use: "startsubsys",
Short: "Use sftpgo as SFTP file transfer subsystem",
Long: `In this mode SFTPGo speaks the server side of SFTP protocol to stdout and
expects client requests from stdin.
This mode is not intended to be called directly, but from sshd using the
Subsystem option.
For example adding a line like this one in "/etc/ssh/sshd_config":
Subsystem sftp sftpgo startsubsys
Command-line flags should be specified in the Subsystem declaration.
`,
Run: func(_ *cobra.Command, _ []string) {
logSender := "startsubsys"
connectionID := xid.New().String()
var zeroLogLevel zerolog.Level
switch logLevel {
case "info":
zeroLogLevel = zerolog.InfoLevel
case "warn":
zeroLogLevel = zerolog.WarnLevel
case "error":
zeroLogLevel = zerolog.ErrorLevel
default:
zeroLogLevel = zerolog.DebugLevel
}
logger.SetLogTime(logUTCTime)
if logJournalD {
logger.InitJournalDLogger(zeroLogLevel)
} else {
logger.InitStdErrLogger(zeroLogLevel)
}
osUser, err := user.Current()
if err != nil {
logger.Error(logSender, connectionID, "unable to get the current user: %v", err)
os.Exit(1)
}
username := osUser.Username
homedir := osUser.HomeDir
logger.Info(logSender, connectionID, "starting SFTPGo %v as subsystem, user %q home dir %q config dir %q base home dir %q",
version.Get(), username, homedir, configDir, baseHomeDir)
err = config.LoadConfig(configDir, configFile)
if err != nil {
logger.Error(logSender, connectionID, "unable to load configuration: %v", err)
os.Exit(1)
}
kmsConfig := config.GetKMSConfig()
if err := kmsConfig.Initialize(); err != nil {
logger.Error(logSender, connectionID, "unable to initialize KMS: %v", err)
os.Exit(1)
}
mfaConfig := config.GetMFAConfig()
err = mfaConfig.Initialize()
if err != nil {
logger.Error(logSender, "", "unable to initialize MFA: %v", err)
os.Exit(1)
}
dataProviderConf := config.GetProviderConf()
if dataProviderConf.Driver == dataprovider.SQLiteDataProviderName || dataProviderConf.Driver == dataprovider.BoltDataProviderName {
logger.Debug(logSender, connectionID, "data provider %q not supported in subsystem mode, using %q provider",
dataProviderConf.Driver, dataprovider.MemoryDataProviderName)
dataProviderConf.Driver = dataprovider.MemoryDataProviderName
dataProviderConf.Name = ""
}
config.SetProviderConf(dataProviderConf)
err = dataprovider.Initialize(dataProviderConf, configDir, false)
if err != nil {
logger.Error(logSender, connectionID, "unable to initialize the data provider: %v", err)
os.Exit(1)
}
if err := plugin.Initialize(config.GetPluginsConfig(), logLevel); err != nil {
logger.Error(logSender, connectionID, "unable to initialize plugin system: %v", err)
os.Exit(1)
}
smtpConfig := config.GetSMTPConfig()
err = smtpConfig.Initialize(configDir, false)
if err != nil {
logger.Error(logSender, connectionID, "unable to initialize SMTP configuration: %v", err)
os.Exit(1)
}
commonConfig := config.GetCommonConfig()
// idle connection are managed externally
commonConfig.IdleTimeout = 0
config.SetCommonConfig(commonConfig)
if err := common.Initialize(config.GetCommonConfig(), dataProviderConf.GetShared()); err != nil {
logger.Error(logSender, connectionID, "%v", err)
os.Exit(1)
}
httpConfig := config.GetHTTPConfig()
if err := httpConfig.Initialize(configDir); err != nil {
logger.Error(logSender, connectionID, "unable to initialize http client: %v", err)
os.Exit(1)
}
commandConfig := config.GetCommandConfig()
if err := commandConfig.Initialize(); err != nil {
logger.Error(logSender, connectionID, "unable to initialize commands configuration: %v", err)
os.Exit(1)
}
user, err := dataprovider.UserExists(username, "")
if err == nil {
if user.HomeDir != filepath.Clean(homedir) && !preserveHomeDir {
// update the user
user.HomeDir = filepath.Clean(homedir)
err = dataprovider.UpdateUser(&user, dataprovider.ActionExecutorSystem, "", "")
if err != nil {
logger.Error(logSender, connectionID, "unable to update user %q: %v", username, err)
os.Exit(1)
}
}
} else {
user.Username = username
if baseHomeDir != "" && filepath.IsAbs(baseHomeDir) {
user.HomeDir = filepath.Join(baseHomeDir, username)
} else {
user.HomeDir = filepath.Clean(homedir)
}
logger.Debug(logSender, connectionID, "home dir for new user %q", user.HomeDir)
user.Password = connectionID
user.Permissions = make(map[string][]string)
user.Permissions["/"] = []string{dataprovider.PermAny}
err = dataprovider.AddUser(&user, dataprovider.ActionExecutorSystem, "", "")
if err != nil {
logger.Error(logSender, connectionID, "unable to add user %q: %v", username, err)
os.Exit(1)
}
}
err = user.LoadAndApplyGroupSettings()
if err != nil {
logger.Error(logSender, connectionID, "unable to apply group settings for user %q: %v", username, err)
os.Exit(1)
}
err = sftpd.ServeSubSystemConnection(&user, connectionID, os.Stdin, os.Stdout)
if err != nil && err != io.EOF {
logger.Warn(logSender, connectionID, "serving subsystem finished with error: %v", err)
os.Exit(1)
}
logger.Info(logSender, connectionID, "serving subsystem finished")
plugin.Handler.Cleanup()
os.Exit(0)
},
}
)
func init() {
subsystemCmd.Flags().BoolVarP(&preserveHomeDir, "preserve-home", "p", false, `If the user already exists, the existing home
directory will not be changed`)
subsystemCmd.Flags().StringVarP(&baseHomeDir, "base-home-dir", "d", "", `If the user does not exist specify an alternate
starting directory. The home directory for a new
user will be:
[base-home-dir]/[username]
base-home-dir must be an absolute path.`)
subsystemCmd.Flags().BoolVarP(&logJournalD, "log-to-journald", "j", false, `Send logs to journald. Only available on Linux.
Use:
$ journalctl -o verbose -f
To see full logs.
If not set, the logs will be sent to the standard
error`)
addConfigFlags(subsystemCmd)
viper.SetDefault(logLevelKey, defaultLogLevel)
viper.BindEnv(logLevelKey, "SFTPGO_LOG_LEVEL") //nolint:errcheck
subsystemCmd.Flags().StringVar(&logLevel, logLevelFlag, viper.GetString(logLevelKey),
`Set the log level. Supported values:
debug, info, warn, error.
This flag can be set
using SFTPGO_LOG_LEVEL env var too.
`)
viper.BindPFlag(logLevelKey, subsystemCmd.Flags().Lookup(logLevelFlag)) //nolint:errcheck
viper.SetDefault(logUTCTimeKey, defaultLogUTCTime)
viper.BindEnv(logUTCTimeKey, "SFTPGO_LOG_UTC_TIME") //nolint:errcheck
subsystemCmd.Flags().BoolVar(&logUTCTime, logUTCTimeFlag, viper.GetBool(logUTCTimeKey),
`Use UTC time for logging. This flag can be set
using SFTPGO_LOG_UTC_TIME env var too.
`)
viper.BindPFlag(logUTCTimeKey, subsystemCmd.Flags().Lookup(logUTCTimeFlag)) //nolint:errcheck
rootCmd.AddCommand(subsystemCmd)
}

View file

@ -17,10 +17,9 @@ package command
import (
"fmt"
"slices"
"strings"
"time"
"github.com/drakkan/sftpgo/v2/internal/util"
)
const (
@ -36,7 +35,6 @@ const (
HookStartup = "startup"
HookPostConnect = "post_connect"
HookPostDisconnect = "post_disconnect"
HookDataRetention = "data_retention"
HookCheckPassword = "check_password"
HookPreLogin = "pre_login"
HookPostLogin = "post_login"
@ -47,7 +45,7 @@ const (
var (
config Config
supportedHooks = []string{HookFsActions, HookProviderActions, HookStartup, HookPostConnect, HookPostDisconnect,
HookDataRetention, HookCheckPassword, HookPreLogin, HookPostLogin, HookExternalAuth, HookKeyboardInteractive}
HookCheckPassword, HookPreLogin, HookPostLogin, HookExternalAuth, HookKeyboardInteractive}
)
// Command define the configuration for a specific commands
@ -117,7 +115,7 @@ func (c Config) Initialize() error {
}
// don't validate args, we allow to pass empty arguments
if cmd.Hook != "" {
if !util.Contains(supportedHooks, cmd.Hook) {
if !slices.Contains(supportedHooks, cmd.Hook) {
return fmt.Errorf("invalid hook name %q, supported values: %+v", cmd.Hook, supportedHooks)
}
}

View file

@ -25,6 +25,7 @@ import (
"os/exec"
"path"
"path/filepath"
"slices"
"strings"
"sync/atomic"
"time"
@ -37,7 +38,6 @@ import (
"github.com/drakkan/sftpgo/v2/internal/httpclient"
"github.com/drakkan/sftpgo/v2/internal/logger"
"github.com/drakkan/sftpgo/v2/internal/plugin"
"github.com/drakkan/sftpgo/v2/internal/util"
)
var (
@ -86,13 +86,14 @@ func InitializeActionHandler(handler ActionHandler) {
func ExecutePreAction(conn *BaseConnection, operation, filePath, virtualPath string, fileSize int64, openFlags int) (int, error) {
var event *notifier.FsEvent
hasNotifiersPlugin := plugin.Handler.HasNotifiers()
hasHook := util.Contains(Config.Actions.ExecuteOn, operation)
hasHook := slices.Contains(Config.Actions.ExecuteOn, operation)
hasRules := eventManager.hasFsRules()
if !hasHook && !hasNotifiersPlugin && !hasRules {
return 0, nil
}
dateTime := time.Now()
event = newActionNotification(&conn.User, operation, filePath, virtualPath, "", "", "",
conn.protocol, conn.GetRemoteIP(), conn.ID, fileSize, openFlags, conn.getNotificationStatus(nil), 0, nil)
conn.protocol, conn.GetRemoteIP(), conn.ID, fileSize, openFlags, conn.getNotificationStatus(nil), 0, dateTime, nil)
if hasNotifiersPlugin {
plugin.Handler.NotifyFsEvent(event)
}
@ -112,7 +113,7 @@ func ExecutePreAction(conn *BaseConnection, operation, filePath, virtualPath str
Protocol: event.Protocol,
IP: event.IP,
Role: event.Role,
Timestamp: event.Timestamp,
Timestamp: dateTime,
Email: conn.User.Email,
Object: nil,
}
@ -132,13 +133,14 @@ func ExecuteActionNotification(conn *BaseConnection, operation, filePath, virtua
fileSize int64, err error, elapsed int64, metadata map[string]string,
) error {
hasNotifiersPlugin := plugin.Handler.HasNotifiers()
hasHook := util.Contains(Config.Actions.ExecuteOn, operation)
hasHook := slices.Contains(Config.Actions.ExecuteOn, operation)
hasRules := eventManager.hasFsRules()
if !hasHook && !hasNotifiersPlugin && !hasRules {
return nil
}
dateTime := time.Now()
notification := newActionNotification(&conn.User, operation, filePath, virtualPath, target, virtualTarget, sshCmd,
conn.protocol, conn.GetRemoteIP(), conn.ID, fileSize, 0, conn.getNotificationStatus(err), elapsed, metadata)
conn.protocol, conn.GetRemoteIP(), conn.ID, fileSize, 0, conn.getNotificationStatus(err), elapsed, dateTime, metadata)
if hasNotifiersPlugin {
plugin.Handler.NotifyFsEvent(notification)
}
@ -159,7 +161,7 @@ func ExecuteActionNotification(conn *BaseConnection, operation, filePath, virtua
Protocol: notification.Protocol,
IP: notification.IP,
Role: notification.Role,
Timestamp: notification.Timestamp,
Timestamp: dateTime,
Email: conn.User.Email,
Object: nil,
Metadata: metadata,
@ -173,7 +175,7 @@ func ExecuteActionNotification(conn *BaseConnection, operation, filePath, virtua
}
}
if hasHook {
if util.Contains(Config.Actions.ExecuteSync, operation) {
if slices.Contains(Config.Actions.ExecuteSync, operation) {
_, err := actionHandler.Handle(notification)
return err
}
@ -197,6 +199,7 @@ func newActionNotification(
operation, filePath, virtualPath, target, virtualTarget, sshCmd, protocol, ip, sessionID string,
fileSize int64,
openFlags, status int, elapsed int64,
datetime time.Time,
metadata map[string]string,
) *notifier.FsEvent {
var bucket, endpoint string
@ -238,7 +241,7 @@ func newActionNotification(
SessionID: sessionID,
OpenFlags: openFlags,
Role: user.Role,
Timestamp: time.Now().UnixNano(),
Timestamp: datetime.UnixNano(),
Elapsed: elapsed,
Metadata: metadata,
}
@ -247,7 +250,7 @@ func newActionNotification(
type defaultActionHandler struct{}
func (h *defaultActionHandler) Handle(event *notifier.FsEvent) (int, error) {
if !util.Contains(Config.Actions.ExecuteOn, event.Action) {
if !slices.Contains(Config.Actions.ExecuteOn, event.Action) {
return 0, nil
}
@ -345,7 +348,7 @@ func notificationAsEnvVars(event *notifier.FsEvent) []string {
if len(event.Metadata) > 0 {
data, err := json.Marshal(event.Metadata)
if err == nil {
result = append(result, fmt.Sprintf("SFTPGO_ACTION_METADATA=%s", util.BytesToString(data)))
result = append(result, fmt.Sprintf("SFTPGO_ACTION_METADATA=%s", data))
}
}
return result

View file

@ -22,8 +22,9 @@ import (
"path/filepath"
"runtime"
"testing"
"time"
"github.com/lithammer/shortuuid/v3"
"github.com/lithammer/shortuuid/v4"
"github.com/rs/xid"
"github.com/sftpgo/sdk"
"github.com/sftpgo/sdk/plugin/notifier"
@ -71,7 +72,7 @@ func TestNewActionNotification(t *testing.T) {
c := NewBaseConnection("id", ProtocolSSH, "", "", user)
sessionID := xid.New().String()
a := newActionNotification(&user, operationDownload, "path", "vpath", "target", "", "", ProtocolSFTP, "", sessionID,
123, 0, c.getNotificationStatus(errors.New("fake error")), 0, nil)
123, 0, c.getNotificationStatus(errors.New("fake error")), 0, time.Now(), nil)
assert.Equal(t, user.Username, a.Username)
assert.Equal(t, 0, len(a.Bucket))
assert.Equal(t, 0, len(a.Endpoint))
@ -79,38 +80,38 @@ func TestNewActionNotification(t *testing.T) {
user.FsConfig.Provider = sdk.S3FilesystemProvider
a = newActionNotification(&user, operationDownload, "path", "vpath", "target", "", "", ProtocolSSH, "", sessionID,
123, 0, c.getNotificationStatus(nil), 0, nil)
123, 0, c.getNotificationStatus(nil), 0, time.Now(), nil)
assert.Equal(t, "s3bucket", a.Bucket)
assert.Equal(t, "endpoint", a.Endpoint)
assert.Equal(t, 1, a.Status)
user.FsConfig.Provider = sdk.GCSFilesystemProvider
a = newActionNotification(&user, operationDownload, "path", "vpath", "target", "", "", ProtocolSCP, "", sessionID,
123, 0, c.getNotificationStatus(ErrQuotaExceeded), 0, nil)
123, 0, c.getNotificationStatus(ErrQuotaExceeded), 0, time.Now(), nil)
assert.Equal(t, "gcsbucket", a.Bucket)
assert.Equal(t, 0, len(a.Endpoint))
assert.Equal(t, 3, a.Status)
a = newActionNotification(&user, operationDownload, "path", "vpath", "target", "", "", ProtocolSCP, "", sessionID,
123, 0, c.getNotificationStatus(fmt.Errorf("wrapper quota error: %w", ErrQuotaExceeded)), 0, nil)
123, 0, c.getNotificationStatus(fmt.Errorf("wrapper quota error: %w", ErrQuotaExceeded)), 0, time.Now(), nil)
assert.Equal(t, "gcsbucket", a.Bucket)
assert.Equal(t, 0, len(a.Endpoint))
assert.Equal(t, 3, a.Status)
user.FsConfig.Provider = sdk.HTTPFilesystemProvider
a = newActionNotification(&user, operationDownload, "path", "vpath", "target", "", "", ProtocolSSH, "", sessionID,
123, 0, c.getNotificationStatus(nil), 0, nil)
123, 0, c.getNotificationStatus(nil), 0, time.Now(), nil)
assert.Equal(t, "httpendpoint", a.Endpoint)
assert.Equal(t, 1, a.Status)
user.FsConfig.Provider = sdk.AzureBlobFilesystemProvider
a = newActionNotification(&user, operationDownload, "path", "vpath", "target", "", "", ProtocolSCP, "", sessionID,
123, 0, c.getNotificationStatus(nil), 0, nil)
123, 0, c.getNotificationStatus(nil), 0, time.Now(), nil)
assert.Equal(t, "azcontainer", a.Bucket)
assert.Equal(t, "azendpoint", a.Endpoint)
assert.Equal(t, 1, a.Status)
a = newActionNotification(&user, operationDownload, "path", "vpath", "target", "", "", ProtocolSCP, "", sessionID,
123, os.O_APPEND, c.getNotificationStatus(nil), 0, nil)
123, os.O_APPEND, c.getNotificationStatus(nil), 0, time.Now(), nil)
assert.Equal(t, "azcontainer", a.Bucket)
assert.Equal(t, "azendpoint", a.Endpoint)
assert.Equal(t, 1, a.Status)
@ -118,7 +119,7 @@ func TestNewActionNotification(t *testing.T) {
user.FsConfig.Provider = sdk.SFTPFilesystemProvider
a = newActionNotification(&user, operationDownload, "path", "vpath", "target", "", "", ProtocolSFTP, "", sessionID,
123, 0, c.getNotificationStatus(nil), 0, nil)
123, 0, c.getNotificationStatus(nil), 0, time.Now(), nil)
assert.Equal(t, "sftpendpoint", a.Endpoint)
}
@ -135,7 +136,7 @@ func TestActionHTTP(t *testing.T) {
},
}
a := newActionNotification(user, operationDownload, "path", "vpath", "target", "", "", ProtocolSFTP, "",
xid.New().String(), 123, 0, 1, 0, nil)
xid.New().String(), 123, 0, 1, 0, time.Now(), nil)
status, err := actionHandler.Handle(a)
assert.NoError(t, err)
assert.Equal(t, 1, status)
@ -175,7 +176,7 @@ func TestActionCMD(t *testing.T) {
}
sessionID := shortuuid.New()
a := newActionNotification(user, operationDownload, "path", "vpath", "target", "", "", ProtocolSFTP, "", sessionID,
123, 0, 1, 0, map[string]string{"key": "value"})
123, 0, 1, 0, time.Now(), map[string]string{"key": "value"})
status, err := actionHandler.Handle(a)
assert.NoError(t, err)
assert.Equal(t, 1, status)
@ -208,7 +209,7 @@ func TestWrongActions(t *testing.T) {
}
a := newActionNotification(user, operationUpload, "", "", "", "", "", ProtocolSFTP, "", xid.New().String(),
123, 0, 1, 0, nil)
123, 0, 1, 0, time.Now(), nil)
status, err := actionHandler.Handle(a)
assert.Error(t, err, "action with bad command must fail")
assert.Equal(t, 1, status)

View file

@ -19,12 +19,14 @@ import (
"context"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
"slices"
"strconv"
"strings"
"sync"
@ -124,6 +126,9 @@ func init() {
Connections.clients = clientsMap{
clients: make(map[string]int),
}
Connections.transfers = clientsMap{
clients: make(map[string]int),
}
Connections.perUserConns = make(map[string]int)
Connections.mapping = make(map[string]int)
Connections.sshMapping = make(map[string]int)
@ -163,13 +168,20 @@ var (
rateLimiters map[string][]*rateLimiter
isShuttingDown atomic.Bool
ftpLoginCommands = []string{"PASS", "USER"}
fnUpdateBranding func(*dataprovider.BrandingConfigs)
)
// SetUpdateBrandingFn sets the function to call to update branding configs.
func SetUpdateBrandingFn(fn func(*dataprovider.BrandingConfigs)) {
fnUpdateBranding = fn
}
// Initialize sets the common configuration
func Initialize(c Configuration, isShared int) error {
isShuttingDown.Store(false)
util.SetUmask(c.Umask)
version.SetConfig(c.ServerVersion)
dataprovider.SetTZ(c.TZ)
Config = c
Config.Actions.ExecuteOn = util.RemoveDuplicates(Config.Actions.ExecuteOn, true)
Config.Actions.ExecuteSync = util.RemoveDuplicates(Config.Actions.ExecuteSync, true)
@ -200,7 +212,7 @@ func Initialize(c Configuration, isShared int) error {
Config.rateLimitersList = rateLimitersList
}
if c.DefenderConfig.Enabled {
if !util.Contains(supportedDefenderDrivers, c.DefenderConfig.Driver) {
if !slices.Contains(supportedDefenderDrivers, c.DefenderConfig.Driver) {
return fmt.Errorf("unsupported defender driver %q", c.DefenderConfig.Driver)
}
var defender Defender
@ -228,6 +240,9 @@ func Initialize(c Configuration, isShared int) error {
if err := c.initializeProxyProtocol(); err != nil {
return err
}
if err := c.EventManager.validate(); err != nil {
return err
}
vfs.SetTempPath(c.TempPath)
dataprovider.SetTempPath(c.TempPath)
vfs.SetAllowSelfConnections(c.AllowSelfConnections)
@ -236,6 +251,7 @@ func Initialize(c Configuration, isShared int) error {
vfs.SetResumeMaxSize(c.ResumeMaxSize)
vfs.SetUploadMode(c.UploadMode)
dataprovider.SetAllowSelfConnections(c.AllowSelfConnections)
dataprovider.EnabledActionCommands = c.EventManager.EnabledCommands
transfersChecker = getTransfersChecker(isShared)
return nil
}
@ -327,6 +343,13 @@ func Reload() error {
return nil
}
// DelayLogin applies the configured login delay
func DelayLogin(err error) {
if Config.defender != nil {
Config.defender.DelayLogin(err)
}
}
// IsBanned returns true if the specified IP address is banned
func IsBanned(ip, protocol string) bool {
if plugin.Handler.IsIPBanned(ip, protocol) {
@ -395,6 +418,23 @@ func AddDefenderEvent(ip, protocol string, event HostEvent) bool {
return Config.defender.AddEvent(ip, protocol, event)
}
func reloadProviderConfigs() {
configs, err := dataprovider.GetConfigs()
if err != nil {
logger.Error(logSender, "", "unable to load config from provider: %v", err)
return
}
configs.SetNilsToEmpty()
if fnUpdateBranding != nil {
fnUpdateBranding(configs.Branding)
}
if err := configs.SMTP.TryDecrypt(); err != nil {
logger.Error(logSender, "", "unable to decrypt smtp config: %v", err)
return
}
smtp.Activate(configs.SMTP)
}
func startPeriodicChecks(duration time.Duration, isShared int) {
startEventScheduler()
spec := fmt.Sprintf("@every %s", duration)
@ -403,7 +443,7 @@ func startPeriodicChecks(duration time.Duration, isShared int) {
logger.Info(logSender, "", "scheduled overquota transfers check, schedule %q", spec)
if isShared == 1 {
logger.Info(logSender, "", "add reload configs task")
_, err := eventScheduler.AddFunc("@every 10m", smtp.ReloadProviderConf)
_, err := eventScheduler.AddFunc("@every 10m", reloadProviderConfigs)
util.PanicOnError(err)
}
if Config.IdleTimeout > 0 {
@ -423,6 +463,7 @@ type ActiveTransfer interface {
GetDownloadedSize() int64
GetUploadedSize() int64
GetVirtualPath() string
GetFsPath() string
GetStartTime() time.Time
SignalClose(err error)
Truncate(fsPath string, size int64) (int64, error)
@ -477,6 +518,23 @@ type ConnectionTransfer struct {
DLSize int64 `json:"-"`
}
// EventManagerConfig defines the configuration for the EventManager
type EventManagerConfig struct {
// EnabledCommands defines the system commands that can be executed via EventManager,
// an empty list means that any command is allowed to be executed.
// Commands must be set as an absolute path
EnabledCommands []string `json:"enabled_commands" mapstructure:"enabled_commands"`
}
func (c *EventManagerConfig) validate() error {
for _, c := range c.EnabledCommands {
if !filepath.IsAbs(c) {
return fmt.Errorf("invalid command %q: it must be an absolute path", c)
}
}
return nil
}
// MetadataConfig defines how to handle metadata for cloud storage backends
type MetadataConfig struct {
// If not zero the metadata will be read before downloads and will be
@ -558,9 +616,6 @@ type Configuration struct {
// Absolute path to an external program or an HTTP URL to invoke after an SSH/FTP connection ends.
// Leave empty do disable.
PostDisconnectHook string `json:"post_disconnect_hook" mapstructure:"post_disconnect_hook"`
// Absolute path to an external program or an HTTP URL to invoke after a data retention check completes.
// Leave empty do disable.
DataRetentionHook string `json:"data_retention_hook" mapstructure:"data_retention_hook"`
// Maximum number of concurrent client connections. 0 means unlimited
MaxTotalConnections int `json:"max_total_connections" mapstructure:"max_total_connections"`
// Maximum number of concurrent client connections from the same host (IP). 0 means unlimited
@ -581,8 +636,14 @@ type Configuration struct {
Umask string `json:"umask" mapstructure:"umask"`
// Defines the server version
ServerVersion string `json:"server_version" mapstructure:"server_version"`
// TZ defines the time zone to use for the EventManager scheduler and to
// control time-based access restrictions. Set to "local" to use the
// server's local time, otherwise UTC will be used.
TZ string `json:"tz" mapstructure:"tz"`
// Metadata configuration
Metadata MetadataConfig `json:"metadata" mapstructure:"metadata"`
Metadata MetadataConfig `json:"metadata" mapstructure:"metadata"`
// EventManager configuration
EventManager EventManagerConfig `json:"event_manager" mapstructure:"event_manager"`
idleTimeoutAsDuration time.Duration
idleLoginTimeout time.Duration
defender Defender
@ -615,7 +676,7 @@ func (c *Configuration) initializeProxyProtocol() error {
// GetProxyListener returns a wrapper for the given listener that supports the
// HAProxy Proxy Protocol
func (c *Configuration) GetProxyListener(listener net.Listener) (*proxyproto.Listener, error) {
func (c *Configuration) GetProxyListener(listener net.Listener) (net.Listener, error) {
if c.ProxyProtocol > 0 {
defaultPolicy := proxyproto.REQUIRE
if c.ProxyProtocol == 1 {
@ -624,7 +685,7 @@ func (c *Configuration) GetProxyListener(listener net.Listener) (*proxyproto.Lis
return &proxyproto.Listener{
Listener: listener,
Policy: getProxyPolicy(c.proxyAllowed, c.proxySkipped, defaultPolicy),
ConnPolicy: getProxyPolicy(c.proxyAllowed, c.proxySkipped, defaultPolicy),
ReadHeaderTimeout: 10 * time.Second,
}, nil
}
@ -742,7 +803,7 @@ func (c *Configuration) checkPostDisconnectHook(remoteAddr, protocol, username,
if c.PostDisconnectHook == "" {
return
}
if !util.Contains(disconnHookProtocols, protocol) {
if !slices.Contains(disconnHookProtocols, protocol) {
return
}
go c.executePostDisconnectHook(remoteAddr, protocol, username, connID, connectionTime)
@ -799,12 +860,14 @@ func (c *Configuration) ExecutePostConnectHook(ipAddr, protocol string) error {
return nil
}
func getProxyPolicy(allowed, skipped []func(net.IP) bool, def proxyproto.Policy) proxyproto.PolicyFunc {
return func(upstream net.Addr) (proxyproto.Policy, error) {
upstreamIP, err := util.GetIPFromNetAddr(upstream)
func getProxyPolicy(allowed, skipped []func(net.IP) bool, def proxyproto.Policy) proxyproto.ConnPolicyFunc {
return func(connPolicyOptions proxyproto.ConnPolicyOptions) (proxyproto.Policy, error) {
upstreamIP, err := util.GetIPFromNetAddr(connPolicyOptions.Upstream)
if err != nil {
// something is wrong with the source IP, better reject the connection
return proxyproto.REJECT, err
// Something is wrong with the source IP, better reject the
// connection.
logger.Error(logSender, "", "reject connection from ip %q, err: %v", connPolicyOptions.Upstream, err)
return proxyproto.REJECT, proxyproto.ErrInvalidUpstream
}
for _, skippedFrom := range skipped {
@ -822,6 +885,11 @@ func getProxyPolicy(allowed, skipped []func(net.IP) bool, def proxyproto.Policy)
}
}
if def == proxyproto.REQUIRE {
logger.Debug(logSender, "", "reject connection from ip %q: proxy protocol signature required and not set",
upstreamIP)
return proxyproto.REJECT, proxyproto.ErrInvalidUpstream
}
return def, nil
}
}
@ -830,12 +898,12 @@ func getProxyPolicy(allowed, skipped []func(net.IP) bool, def proxyproto.Policy)
// Each SSH connection can open several channels for SFTP or SSH commands
type SSHConnection struct {
id string
conn net.Conn
conn io.Closer
lastActivity atomic.Int64
}
// NewSSHConnection returns a new SSHConnection
func NewSSHConnection(id string, conn net.Conn) *SSHConnection {
func NewSSHConnection(id string, conn io.Closer) *SSHConnection {
c := &SSHConnection{
id: id,
conn: conn,
@ -868,7 +936,9 @@ func (c *SSHConnection) Close() error {
type ActiveConnections struct {
// clients contains both authenticated and estabilished connections and the ones waiting
// for authentication
clients clientsMap
clients clientsMap
// transfers contains active transfers, total and per-user
transfers clientsMap
transfersCheckStatus atomic.Bool
sync.RWMutex
connections []ActiveConnection
@ -919,6 +989,9 @@ func (conns *ActiveConnections) Add(c ActiveConnection) error {
if val := conns.perUserConns[username]; val >= maxSessions {
return fmt.Errorf("too many open sessions: %d/%d", val, maxSessions)
}
if val := conns.transfers.getTotalFrom(username); val >= maxSessions {
return fmt.Errorf("too many open transfers: %d/%d", val, maxSessions)
}
}
conns.addUserConnection(username)
}
@ -980,7 +1053,7 @@ func (conns *ActiveConnections) Remove(connectionID string) {
metric.UpdateActiveConnectionsSize(lastIdx)
logger.Debug(conn.GetProtocol(), conn.GetID(), "connection removed, local address %q, remote address %q close fs error: %v, num open connections: %d",
conn.GetLocalAddress(), conn.GetRemoteAddress(), err, lastIdx)
if conn.GetProtocol() == ProtocolFTP && conn.GetUsername() == "" && !util.Contains(ftpLoginCommands, conn.GetCommand()) {
if conn.GetProtocol() == ProtocolFTP && conn.GetUsername() == "" && !slices.Contains(ftpLoginCommands, conn.GetCommand()) {
ip := util.GetIPFromRemoteAddress(conn.GetRemoteAddress())
logger.ConnectionFailedLog("", ip, dataprovider.LoginMethodNoAuthTried, ProtocolFTP,
dataprovider.ErrNoAuthTried.Error())
@ -1089,7 +1162,7 @@ func (conns *ActiveConnections) checkIdles() {
logger.Debug(conn.GetProtocol(), conn.GetID(), "close idle connection, idle time: %s, username: %q close err: %v",
time.Since(conn.GetLastActivity()), conn.GetUsername(), err)
}(c)
} else if !c.isAccessAllowed() {
} else if !isUnauthenticatedFTPUser && !c.isAccessAllowed() {
defer func(conn ActiveConnection) {
err := conn.Disconnect()
logger.Info(conn.GetProtocol(), conn.GetID(), "access conditions not met for user: %q close connection err: %v",
@ -1179,6 +1252,35 @@ func (conns *ActiveConnections) GetClientConnections() int32 {
return conns.clients.getTotal()
}
// GetTotalTransfers returns the total number of active transfers
func (conns *ActiveConnections) GetTotalTransfers() int32 {
return conns.transfers.getTotal()
}
// IsNewTransferAllowed returns an error if the maximum number of concurrent allowed
// transfers is exceeded
func (conns *ActiveConnections) IsNewTransferAllowed(username string) error {
if isShuttingDown.Load() {
return ErrShuttingDown
}
if Config.MaxTotalConnections == 0 && Config.MaxPerHostConnections == 0 {
return nil
}
if Config.MaxPerHostConnections > 0 {
if transfers := conns.transfers.getTotalFrom(username); transfers >= Config.MaxPerHostConnections {
logger.Info(logSender, "", "active transfers from user %q: %d/%d", username, transfers, Config.MaxPerHostConnections)
return ErrConnectionDenied
}
}
if Config.MaxTotalConnections > 0 {
if transfers := conns.transfers.getTotal(); transfers >= int32(Config.MaxTotalConnections) {
logger.Info(logSender, "", "active transfers %d/%d", transfers, Config.MaxTotalConnections)
return ErrConnectionDenied
}
}
return nil
}
// IsNewConnectionAllowed returns an error if the maximum number of concurrent allowed
// connections is exceeded or a whitelist is defined and the specified ipAddr is not listed
// or the service is shutting down
@ -1219,7 +1321,11 @@ func (conns *ActiveConnections) IsNewConnectionAllowed(ipAddr, protocol string)
}
// on a single SFTP connection we could have multiple SFTP channels or commands
// so we check the estabilished connections too
// so we check the estabilished connections and active uploads too
if transfers := conns.transfers.getTotal(); transfers >= int32(Config.MaxTotalConnections) {
logger.Info(logSender, "", "active transfers %d/%d", transfers, Config.MaxTotalConnections)
return ErrConnectionDenied
}
conns.RLock()
defer conns.RUnlock()

View file

@ -23,6 +23,7 @@ import (
"os/exec"
"path/filepath"
"runtime"
"slices"
"sync"
"testing"
"time"
@ -63,7 +64,7 @@ func (c *fakeConnection) AddUser(user dataprovider.User) error {
if err != nil {
return err
}
c.BaseConnection.User = user
c.User = user
return nil
}
@ -216,6 +217,33 @@ func TestConnections(t *testing.T) {
Connections.RUnlock()
}
func TestEventManagerCommandsInitialization(t *testing.T) {
configCopy := Config
c := Configuration{
EventManager: EventManagerConfig{
EnabledCommands: []string{"ls"}, // not an absolute path
},
}
err := Initialize(c, 0)
assert.ErrorContains(t, err, "invalid command")
var commands []string
if runtime.GOOS == osWindows {
commands = []string{"C:\\command"}
} else {
commands = []string{"/bin/ls"}
}
c.EventManager.EnabledCommands = commands
err = Initialize(c, 0)
assert.NoError(t, err)
assert.Equal(t, commands, dataprovider.EnabledActionCommands)
dataprovider.EnabledActionCommands = configCopy.EventManager.EnabledCommands
Config = configCopy
}
func TestInitializationProxyErrors(t *testing.T) {
configCopy := Config
@ -419,6 +447,9 @@ func TestDefenderIntegration(t *testing.T) {
ObservationTime: 15,
EntriesSoftLimit: 100,
EntriesHardLimit: 150,
LoginDelay: LoginDelay{
PasswordFailed: 200,
},
}
err = Initialize(Config, 0)
// ScoreInvalid cannot be greater than threshold
@ -477,6 +508,16 @@ func TestDefenderIntegration(t *testing.T) {
assert.Nil(t, banTime)
assert.False(t, DeleteDefenderHost(ip))
startTime := time.Now()
DelayLogin(nil)
elapsed := time.Since(startTime)
assert.Less(t, elapsed, time.Millisecond*50)
startTime = time.Now()
DelayLogin(ErrInternalFailure)
elapsed = time.Since(startTime)
assert.Greater(t, elapsed, time.Millisecond*150)
Config = configCopy
}
@ -612,11 +653,17 @@ func TestMaxConnections(t *testing.T) {
ipAddr := "192.168.7.8"
assert.NoError(t, Connections.IsNewConnectionAllowed(ipAddr, ProtocolFTP))
assert.NoError(t, Connections.IsNewTransferAllowed(userTestUsername))
Config.MaxTotalConnections = 1
Config.MaxPerHostConnections = perHost
assert.NoError(t, Connections.IsNewConnectionAllowed(ipAddr, ProtocolHTTP))
assert.NoError(t, Connections.IsNewTransferAllowed(userTestUsername))
isShuttingDown.Store(true)
assert.ErrorIs(t, Connections.IsNewTransferAllowed(userTestUsername), ErrShuttingDown)
isShuttingDown.Store(false)
c := NewBaseConnection("id", ProtocolSFTP, "", "", dataprovider.User{})
fakeConn := &fakeConnection{
BaseConnection: c,
@ -625,6 +672,10 @@ func TestMaxConnections(t *testing.T) {
assert.NoError(t, err)
assert.Len(t, Connections.GetStats(""), 1)
assert.Error(t, Connections.IsNewConnectionAllowed(ipAddr, ProtocolSSH))
Connections.transfers.add(userTestUsername)
assert.Error(t, Connections.IsNewTransferAllowed(userTestUsername))
Connections.transfers.remove(userTestUsername)
assert.Equal(t, int32(0), Connections.GetTotalTransfers())
res := Connections.Close(fakeConn.GetID(), "")
assert.True(t, res)
@ -636,6 +687,9 @@ func TestMaxConnections(t *testing.T) {
assert.Error(t, Connections.IsNewConnectionAllowed(ipAddr, ProtocolSSH))
Connections.RemoveClientConnection(ipAddr)
assert.NoError(t, Connections.IsNewConnectionAllowed(ipAddr, ProtocolWebDAV))
Connections.transfers.add(userTestUsername)
assert.Error(t, Connections.IsNewConnectionAllowed(ipAddr, ProtocolSSH))
Connections.transfers.remove(userTestUsername)
Connections.RemoveClientConnection(ipAddr)
Config.MaxTotalConnections = oldValue
@ -774,11 +828,7 @@ func TestIdleConnections(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, Connections.GetActiveSessions(username), 2)
cFTP := NewBaseConnection("id2", ProtocolFTP, "", "", dataprovider.User{
BaseUser: sdk.BaseUser{
Status: 1,
},
})
cFTP := NewBaseConnection("id2", ProtocolFTP, "", "", dataprovider.User{})
cFTP.lastActivity.Store(time.Now().UnixNano())
fakeConn = &fakeConnection{
BaseConnection: cFTP,
@ -945,9 +995,10 @@ func TestConnectionStatus(t *testing.T) {
assert.Len(t, stats, 3)
for _, stat := range stats {
assert.Equal(t, stat.Username, username)
if stat.ConnectionID == "SFTP_id1" {
switch stat.ConnectionID {
case "SFTP_id1":
assert.Len(t, stat.Transfers, 2)
} else if stat.ConnectionID == "DAV_id3" {
case "DAV_id3":
assert.Len(t, stat.Transfers, 1)
}
}
@ -1028,9 +1079,13 @@ func TestQuotaScansRole(t *testing.T) {
func TestProxyPolicy(t *testing.T) {
addr := net.TCPAddr{}
downstream := net.TCPAddr{IP: net.ParseIP("1.1.1.1")}
p := getProxyPolicy(nil, nil, proxyproto.IGNORE)
policy, err := p(&addr)
assert.Error(t, err)
policy, err := p(proxyproto.ConnPolicyOptions{
Upstream: &addr,
Downstream: &downstream,
})
assert.ErrorIs(t, err, proxyproto.ErrInvalidUpstream)
assert.Equal(t, proxyproto.REJECT, policy)
ip1 := net.ParseIP("10.8.1.1")
ip2 := net.ParseIP("10.8.1.2")
@ -1040,31 +1095,55 @@ func TestProxyPolicy(t *testing.T) {
skipped, err := util.ParseAllowedIPAndRanges([]string{ip2.String(), ip3.String()})
assert.NoError(t, err)
p = getProxyPolicy(allowed, skipped, proxyproto.IGNORE)
policy, err = p(&net.TCPAddr{IP: ip1})
policy, err = p(proxyproto.ConnPolicyOptions{
Upstream: &net.TCPAddr{IP: ip1},
Downstream: &downstream,
})
assert.NoError(t, err)
assert.Equal(t, proxyproto.USE, policy)
policy, err = p(&net.TCPAddr{IP: ip2})
policy, err = p(proxyproto.ConnPolicyOptions{
Upstream: &net.TCPAddr{IP: ip2},
Downstream: &downstream,
})
assert.NoError(t, err)
assert.Equal(t, proxyproto.SKIP, policy)
policy, err = p(&net.TCPAddr{IP: ip3})
policy, err = p(proxyproto.ConnPolicyOptions{
Upstream: &net.TCPAddr{IP: ip3},
Downstream: &downstream,
})
assert.NoError(t, err)
assert.Equal(t, proxyproto.SKIP, policy)
policy, err = p(&net.TCPAddr{IP: net.ParseIP("10.8.1.4")})
policy, err = p(proxyproto.ConnPolicyOptions{
Upstream: &net.TCPAddr{IP: net.ParseIP("10.8.1.4")},
Downstream: &downstream,
})
assert.NoError(t, err)
assert.Equal(t, proxyproto.IGNORE, policy)
p = getProxyPolicy(allowed, skipped, proxyproto.REQUIRE)
policy, err = p(&net.TCPAddr{IP: ip1})
policy, err = p(proxyproto.ConnPolicyOptions{
Upstream: &net.TCPAddr{IP: ip1},
Downstream: &downstream,
})
assert.NoError(t, err)
assert.Equal(t, proxyproto.REQUIRE, policy)
policy, err = p(&net.TCPAddr{IP: ip2})
policy, err = p(proxyproto.ConnPolicyOptions{
Upstream: &net.TCPAddr{IP: ip2},
Downstream: &downstream,
})
assert.NoError(t, err)
assert.Equal(t, proxyproto.SKIP, policy)
policy, err = p(&net.TCPAddr{IP: ip3})
policy, err = p(proxyproto.ConnPolicyOptions{
Upstream: &net.TCPAddr{IP: ip3},
Downstream: &downstream,
})
assert.NoError(t, err)
assert.Equal(t, proxyproto.SKIP, policy)
policy, err = p(&net.TCPAddr{IP: net.ParseIP("10.8.1.5")})
assert.NoError(t, err)
assert.Equal(t, proxyproto.REQUIRE, policy)
policy, err = p(proxyproto.ConnPolicyOptions{
Upstream: &net.TCPAddr{IP: net.ParseIP("10.8.1.5")},
Downstream: &downstream,
})
assert.ErrorIs(t, err, proxyproto.ErrInvalidUpstream)
assert.Equal(t, proxyproto.REJECT, policy)
}
func TestProxyProtocolVersion(t *testing.T) {
@ -1076,14 +1155,18 @@ func TestProxyProtocolVersion(t *testing.T) {
assert.Contains(t, err.Error(), "proxy protocol not configured")
}
c.ProxyProtocol = 1
proxyListener, err := c.GetProxyListener(nil)
listener, err := c.GetProxyListener(nil)
assert.NoError(t, err)
assert.NotNil(t, proxyListener.Policy)
proxyListener, ok := listener.(*proxyproto.Listener)
require.True(t, ok)
assert.NotNil(t, proxyListener.ConnPolicy)
c.ProxyProtocol = 2
proxyListener, err = c.GetProxyListener(nil)
listener, err = c.GetProxyListener(nil)
assert.NoError(t, err)
assert.NotNil(t, proxyListener.Policy)
proxyListener, ok = listener.(*proxyproto.Listener)
require.True(t, ok)
assert.NotNil(t, proxyListener.ConnPolicy)
}
func TestStartupHook(t *testing.T) {
@ -1213,8 +1296,8 @@ func TestFolderCopy(t *testing.T) {
folder.ID = 2
folder.Users = []string{"user3"}
require.Len(t, folderCopy.Users, 2)
require.True(t, util.Contains(folderCopy.Users, "user1"))
require.True(t, util.Contains(folderCopy.Users, "user2"))
require.True(t, slices.Contains(folderCopy.Users, "user1"))
require.True(t, slices.Contains(folderCopy.Users, "user2"))
require.Equal(t, int64(1), folderCopy.ID)
require.Equal(t, folder.Name, folderCopy.Name)
require.Equal(t, folder.MappedPath, folderCopy.MappedPath)
@ -1230,7 +1313,7 @@ func TestFolderCopy(t *testing.T) {
folderCopy = folder.GetACopy()
folder.FsConfig.CryptConfig.Passphrase = kms.NewEmptySecret()
require.Len(t, folderCopy.Users, 1)
require.True(t, util.Contains(folderCopy.Users, "user3"))
require.True(t, slices.Contains(folderCopy.Users, "user3"))
require.Equal(t, int64(2), folderCopy.ID)
require.Equal(t, folder.Name, folderCopy.Name)
require.Equal(t, folder.MappedPath, folderCopy.MappedPath)

View file

@ -21,6 +21,7 @@ import (
"io/fs"
"os"
"path"
"slices"
"strings"
"sync"
"sync/atomic"
@ -62,7 +63,7 @@ type BaseConnection struct {
// NewBaseConnection returns a new BaseConnection
func NewBaseConnection(id, protocol, localAddr, remoteAddr string, user dataprovider.User) *BaseConnection {
connID := id
if util.Contains(supportedProtocols, protocol) {
if slices.Contains(supportedProtocols, protocol) {
connID = fmt.Sprintf("%s_%s", protocol, id)
}
user.UploadBandwidth, user.DownloadBandwidth = user.GetBandwidthForIP(util.GetIPFromRemoteAddress(remoteAddr), connID)
@ -131,7 +132,7 @@ func (c *BaseConnection) GetRemoteIP() string {
// SetProtocol sets the protocol for this connection
func (c *BaseConnection) SetProtocol(protocol string) {
c.protocol = protocol
if util.Contains(supportedProtocols, c.protocol) {
if slices.Contains(supportedProtocols, c.protocol) {
c.ID = fmt.Sprintf("%v_%v", c.protocol, c.ID)
}
}
@ -158,6 +159,8 @@ func (c *BaseConnection) CloseFS() error {
// AddTransfer associates a new transfer to this connection
func (c *BaseConnection) AddTransfer(t ActiveTransfer) {
Connections.transfers.add(c.User.Username)
c.Lock()
defer c.Unlock()
@ -189,6 +192,8 @@ func (c *BaseConnection) AddTransfer(t ActiveTransfer) {
// RemoveTransfer removes the specified transfer from the active ones
func (c *BaseConnection) RemoveTransfer(t ActiveTransfer) {
Connections.transfers.remove(c.User.Username)
c.Lock()
defer c.Unlock()
@ -291,6 +296,20 @@ func (c *BaseConnection) setTimes(fsPath string, atime time.Time, mtime time.Tim
return false
}
// getInfoForOngoingUpload returns upload statistics for an upload currently in
// progress on this connection.
func (c *BaseConnection) getInfoForOngoingUpload(fsPath string) (os.FileInfo, error) {
c.RLock()
defer c.RUnlock()
for _, t := range c.activeTransfers {
if t.GetType() == TransferUpload && t.GetFsPath() == fsPath {
return vfs.NewFileInfo(t.GetVirtualPath(), false, t.GetSize(), t.GetStartTime(), false), nil
}
}
return nil, os.ErrNotExist
}
func (c *BaseConnection) truncateOpenHandle(fsPath string, size int64) (int64, error) {
c.RLock()
defer c.RUnlock()
@ -321,10 +340,9 @@ func (c *BaseConnection) ListDir(virtualPath string) (*DirListerAt, error) {
}
return &DirListerAt{
virtualPath: virtualPath,
user: &c.User,
conn: c,
fs: fs,
info: c.User.GetVirtualFoldersInfo(virtualPath),
id: c.ID,
protocol: c.protocol,
lister: lister,
}, nil
}
@ -449,10 +467,7 @@ func (c *BaseConnection) RemoveFile(fs vfs.Fs, fsPath, virtualPath string, info
if updateQuota && info.Mode()&os.ModeSymlink == 0 {
vfolder, err := c.User.GetVirtualFolderForPath(path.Dir(virtualPath))
if err == nil {
dataprovider.UpdateVirtualFolderQuota(&vfolder.BaseVirtualFolder, -1, -size, false) //nolint:errcheck
if vfolder.IsIncludedInUserQuota() {
dataprovider.UpdateUserQuota(&c.User, -1, -size, false) //nolint:errcheck
}
dataprovider.UpdateUserFolderQuota(&vfolder, &c.User, -1, -size, false)
} else {
dataprovider.UpdateUserQuota(&c.User, -1, -size, false) //nolint:errcheck
}
@ -616,13 +631,15 @@ func (c *BaseConnection) checkCopy(srcInfo, dstInfo os.FileInfo, virtualSource,
if dstInfo != nil && dstInfo.IsDir() {
return fmt.Errorf("cannot overwrite file %q with dir %q: %w", virtualSource, virtualTarget, c.GetOpUnsupportedError())
}
if fsSourcePath == fsTargetPath {
return fmt.Errorf("the copy source and target cannot be the same: %w", c.GetOpUnsupportedError())
if c.IsSameResource(virtualSource, virtualTarget) {
if fsSourcePath == fsTargetPath {
return fmt.Errorf("the copy source and target cannot be the same: %w", c.GetOpUnsupportedError())
}
}
return nil
}
func (c *BaseConnection) copyFile(virtualSourcePath, virtualTargetPath string, srcSize int64) error {
func (c *BaseConnection) copyFile(virtualSourcePath, virtualTargetPath string, srcInfo os.FileInfo) error {
if !c.User.HasPerm(dataprovider.PermCopy, virtualSourcePath) || !c.User.HasPerm(dataprovider.PermCopy, virtualTargetPath) {
return c.GetPermissionDeniedError()
}
@ -640,12 +657,12 @@ func (c *BaseConnection) copyFile(virtualSourcePath, virtualTargetPath string, s
return err
}
startTime := time.Now()
numFiles, sizeDiff, err := copier.CopyFile(fsSourcePath, fsTargetPath, srcSize)
numFiles, sizeDiff, err := copier.CopyFile(fsSourcePath, fsTargetPath, srcInfo)
elapsed := time.Since(startTime).Nanoseconds() / 1000000
updateUserQuotaAfterFileWrite(c, virtualTargetPath, numFiles, sizeDiff)
logger.CommandLog(copyLogSender, fsSourcePath, fsTargetPath, c.User.Username, "", c.ID, c.protocol, -1, -1,
"", "", "", srcSize, c.localAddr, c.remoteAddr, elapsed)
ExecuteActionNotification(c, operationCopy, fsSourcePath, virtualSourcePath, fsTargetPath, virtualTargetPath, "", srcSize, err, elapsed, nil) //nolint:errcheck
"", "", "", srcInfo.Size(), c.localAddr, c.remoteAddr, elapsed)
ExecuteActionNotification(c, operationCopy, fsSourcePath, virtualSourcePath, fsTargetPath, virtualTargetPath, "", srcInfo.Size(), err, elapsed, nil) //nolint:errcheck
return err
}
}
@ -657,7 +674,7 @@ func (c *BaseConnection) copyFile(virtualSourcePath, virtualTargetPath string, s
defer rCancelFn()
defer reader.Close()
writer, numFiles, truncatedSize, wCancelFn, err := getFileWriter(c, virtualTargetPath, srcSize)
writer, numFiles, truncatedSize, wCancelFn, err := getFileWriter(c, virtualTargetPath, srcInfo.Size())
if err != nil {
return fmt.Errorf("unable to get writer for path %q: %w", virtualTargetPath, err)
}
@ -708,7 +725,7 @@ func (c *BaseConnection) doRecursiveCopy(virtualSourcePath, virtualTargetPath st
return nil
}
return c.copyFile(virtualSourcePath, virtualTargetPath, srcInfo.Size())
return c.copyFile(virtualSourcePath, virtualTargetPath, srcInfo)
}
func (c *BaseConnection) recursiveCopyEntries(virtualSourcePath, virtualTargetPath string, entries []os.FileInfo, recursion int) error {
@ -770,29 +787,27 @@ func (c *BaseConnection) Copy(virtualSourcePath, virtualTargetPath string) error
return err
}
}
createTargetDir := true
if dstInfo != nil && dstInfo.IsDir() {
createTargetDir = false
}
createTargetDir := dstInfo == nil || !dstInfo.IsDir()
if err := c.checkCopy(srcInfo, dstInfo, virtualSourcePath, destPath); err != nil {
return err
}
if err := c.CheckParentDirs(path.Dir(destPath)); err != nil {
return err
}
done := make(chan bool)
defer close(done)
go keepConnectionAlive(c, done, 2*time.Minute)
stopKeepAlive := keepConnectionAlive(c, 2*time.Minute)
defer stopKeepAlive()
return c.doRecursiveCopy(virtualSourcePath, destPath, srcInfo, createTargetDir, 0)
}
// Rename renames (moves) virtualSourcePath to virtualTargetPath
func (c *BaseConnection) Rename(virtualSourcePath, virtualTargetPath string) error {
return c.renameInternal(virtualSourcePath, virtualTargetPath, false)
return c.renameInternal(virtualSourcePath, virtualTargetPath, false, vfs.CheckParentDir)
}
func (c *BaseConnection) renameInternal(virtualSourcePath, virtualTargetPath string, checkParentDestination bool) error {
func (c *BaseConnection) renameInternal(virtualSourcePath, virtualTargetPath string, //nolint:gocyclo
checkParentDestination bool, checks int,
) error {
if virtualSourcePath == virtualTargetPath {
return fmt.Errorf("the rename source and target cannot be the same: %w", c.GetOpUnsupportedError())
}
@ -813,7 +828,11 @@ func (c *BaseConnection) renameInternal(virtualSourcePath, virtualTargetPath str
return c.GetPermissionDeniedError()
}
initialSize := int64(-1)
if dstInfo, err := fsDst.Lstat(fsTargetPath); err == nil {
dstInfo, err := fsDst.Lstat(fsTargetPath)
if err != nil && !fsDst.IsNotExist(err) {
return err
}
if err == nil {
checkParentDestination = false
if dstInfo.IsDir() {
c.Log(logger.LevelWarn, "attempted to rename %q overwriting an existing directory %q",
@ -835,18 +854,17 @@ func (c *BaseConnection) renameInternal(virtualSourcePath, virtualTargetPath str
return err
}
}
if !c.hasSpaceForRename(fsSrc, virtualSourcePath, virtualTargetPath, initialSize, fsSourcePath) {
if !c.hasSpaceForRename(fsSrc, virtualSourcePath, virtualTargetPath, initialSize, fsSourcePath, srcInfo) {
c.Log(logger.LevelInfo, "denying cross rename due to space limit")
return c.GetGenericError(ErrQuotaExceeded)
}
if checkParentDestination {
c.CheckParentDirs(path.Dir(virtualTargetPath)) //nolint:errcheck
}
done := make(chan bool)
defer close(done)
go keepConnectionAlive(c, done, 2*time.Minute)
stopKeepAlive := keepConnectionAlive(c, 2*time.Minute)
defer stopKeepAlive()
files, size, err := fsDst.Rename(fsSourcePath, fsTargetPath)
files, size, err := fsDst.Rename(fsSourcePath, fsTargetPath, checks)
if err != nil {
c.Log(logger.LevelError, "failed to rename %q -> %q: %+v", fsSourcePath, fsTargetPath, err)
return c.GetFsError(fsSrc, err)
@ -918,16 +936,6 @@ func (c *BaseConnection) CreateSymlink(virtualSourcePath, virtualTargetPath stri
return nil
}
func (c *BaseConnection) getPathForSetStatPerms(fs vfs.Fs, fsPath, virtualPath string) string {
pathForPerms := virtualPath
if fi, err := fs.Lstat(fsPath); err == nil {
if fi.IsDir() {
pathForPerms = path.Dir(virtualPath)
}
}
return pathForPerms
}
func (c *BaseConnection) doStatInternal(virtualPath string, mode int, checkFilePatterns,
convertResult bool,
) (os.FileInfo, error) {
@ -958,7 +966,19 @@ func (c *BaseConnection) doStatInternal(virtualPath string, mode int, checkFileP
info, err = fs.Stat(c.getRealFsPath(fsPath))
}
if err != nil {
if !fs.IsNotExist(err) {
isNotExist := fs.IsNotExist(err)
if isNotExist {
// This is primarily useful for atomic storage backends, where files
// become visible only after they are closed. However, since we may
// be proxying (for example) an SFTP server backed by atomic
// storage, and this search only inspects transfers active on the
// current connection (typically just one), the check is inexpensive
// and safe to perform unconditionally.
if info, err := c.getInfoForOngoingUpload(fsPath); err == nil {
return info, nil
}
}
if !isNotExist {
c.Log(logger.LevelWarn, "stat error for path %q: %+v", virtualPath, err)
}
return nil, c.GetFsError(fs, err)
@ -1064,7 +1084,7 @@ func (c *BaseConnection) SetStat(virtualPath string, attributes *StatAttributes)
if err != nil {
return err
}
pathForPerms := c.getPathForSetStatPerms(fs, fsPath, virtualPath)
pathForPerms := path.Dir(virtualPath)
if attributes.Flags&StatAttrTimes != 0 {
if err = c.handleChtimes(fs, fsPath, pathForPerms, attributes); err != nil {
@ -1121,10 +1141,7 @@ func (c *BaseConnection) truncateFile(fs vfs.Fs, fsPath, virtualPath string, siz
sizeDiff := initialSize - size
vfolder, err := c.User.GetVirtualFolderForPath(path.Dir(virtualPath))
if err == nil {
dataprovider.UpdateVirtualFolderQuota(&vfolder.BaseVirtualFolder, 0, -sizeDiff, false) //nolint:errcheck
if vfolder.IsIncludedInUserQuota() {
dataprovider.UpdateUserQuota(&c.User, 0, -sizeDiff, false) //nolint:errcheck
}
dataprovider.UpdateUserFolderQuota(&vfolder, &c.User, 0, -sizeDiff, false)
} else {
dataprovider.UpdateUserQuota(&c.User, 0, -sizeDiff, false) //nolint:errcheck
}
@ -1133,11 +1150,11 @@ func (c *BaseConnection) truncateFile(fs vfs.Fs, fsPath, virtualPath string, siz
}
func (c *BaseConnection) checkRecursiveRenameDirPermissions(fsSrc, fsDst vfs.Fs, sourcePath, targetPath,
virtualSourcePath, virtualTargetPath string, fi os.FileInfo,
virtualSourcePath, virtualTargetPath string, srcInfo os.FileInfo,
) error {
if !c.User.HasPermissionsInside(virtualSourcePath) &&
!c.User.HasPermissionsInside(virtualTargetPath) {
if !c.isRenamePermitted(fsSrc, fsDst, sourcePath, targetPath, virtualSourcePath, virtualTargetPath, fi) {
if !c.isRenamePermitted(fsSrc, fsDst, sourcePath, targetPath, virtualSourcePath, virtualTargetPath, srcInfo) {
c.Log(logger.LevelInfo, "rename %q -> %q is not allowed, virtual destination path: %q",
sourcePath, targetPath, virtualTargetPath)
return c.GetPermissionDeniedError()
@ -1197,7 +1214,7 @@ func (c *BaseConnection) hasRenamePerms(virtualSourcePath, virtualTargetPath str
}
func (c *BaseConnection) checkFolderRename(fsSrc, fsDst vfs.Fs, fsSourcePath, fsTargetPath, virtualSourcePath,
virtualTargetPath string, fi os.FileInfo) error {
virtualTargetPath string, srcInfo os.FileInfo) error {
if util.IsDirOverlapped(virtualSourcePath, virtualTargetPath, true, "/") {
c.Log(logger.LevelDebug, "renaming the folder %q->%q is not supported: nested folders",
virtualSourcePath, virtualTargetPath)
@ -1221,7 +1238,7 @@ func (c *BaseConnection) checkFolderRename(fsSrc, fsDst vfs.Fs, fsSourcePath, fs
return fmt.Errorf("folder %q has virtual folders inside it: %w", virtualTargetPath, c.GetOpUnsupportedError())
}
if err := c.checkRecursiveRenameDirPermissions(fsSrc, fsDst, fsSourcePath, fsTargetPath,
virtualSourcePath, virtualTargetPath, fi); err != nil {
virtualSourcePath, virtualTargetPath, srcInfo); err != nil {
c.Log(logger.LevelDebug, "error checking recursive permissions before renaming %q: %+v", fsSourcePath, err)
return err
}
@ -1229,7 +1246,7 @@ func (c *BaseConnection) checkFolderRename(fsSrc, fsDst vfs.Fs, fsSourcePath, fs
}
func (c *BaseConnection) isRenamePermitted(fsSrc, fsDst vfs.Fs, fsSourcePath, fsTargetPath, virtualSourcePath,
virtualTargetPath string, fi os.FileInfo,
virtualTargetPath string, srcInfo os.FileInfo,
) bool {
if !c.IsSameResource(virtualSourcePath, virtualTargetPath) {
c.Log(logger.LevelInfo, "rename %q->%q is not allowed: the paths must be on the same resource",
@ -1259,11 +1276,11 @@ func (c *BaseConnection) isRenamePermitted(fsSrc, fsDst vfs.Fs, fsSourcePath, fs
virtualTargetPath)
return false
}
return c.hasRenamePerms(virtualSourcePath, virtualTargetPath, fi)
return c.hasRenamePerms(virtualSourcePath, virtualTargetPath, srcInfo)
}
func (c *BaseConnection) hasSpaceForRename(fs vfs.Fs, virtualSourcePath, virtualTargetPath string, initialSize int64,
fsSourcePath string) bool {
sourcePath string, srcInfo os.FileInfo) bool {
if dataprovider.GetQuotaTracking() == 0 {
return true
}
@ -1293,30 +1310,28 @@ func (c *BaseConnection) hasSpaceForRename(fs vfs.Fs, virtualSourcePath, virtual
// no quota restrictions
return true
}
return c.hasSpaceForCrossRename(fs, quotaResult, initialSize, fsSourcePath)
return c.hasSpaceForCrossRename(fs, quotaResult, initialSize, sourcePath, srcInfo)
}
// hasSpaceForCrossRename checks the quota after a rename between different folders
func (c *BaseConnection) hasSpaceForCrossRename(fs vfs.Fs, quotaResult vfs.QuotaCheckResult, initialSize int64, sourcePath string) bool {
func (c *BaseConnection) hasSpaceForCrossRename(fs vfs.Fs, quotaResult vfs.QuotaCheckResult, initialSize int64,
sourcePath string, srcInfo os.FileInfo,
) bool {
if !quotaResult.HasSpace && initialSize == -1 {
// we are over quota and this is not a file replace
return false
}
fi, err := fs.Lstat(sourcePath)
if err != nil {
c.Log(logger.LevelError, "cross rename denied, stat error for path %q: %v", sourcePath, err)
return false
}
var sizeDiff int64
var filesDiff int
if fi.Mode().IsRegular() {
sizeDiff = fi.Size()
var err error
if srcInfo.Mode().IsRegular() {
sizeDiff = srcInfo.Size()
filesDiff = 1
if initialSize != -1 {
sizeDiff -= initialSize
filesDiff = 0
}
} else if fi.IsDir() {
} else if srcInfo.IsDir() {
filesDiff, sizeDiff, err = fs.GetDirSize(sourcePath)
if err != nil {
c.Log(logger.LevelError, "cross rename denied, error getting size for directory %q: %v", sourcePath, err)
@ -1343,7 +1358,7 @@ func (c *BaseConnection) hasSpaceForCrossRename(fs vfs.Fs, quotaResult vfs.Quota
}
if quotaResult.QuotaSize > 0 {
remainingSize := quotaResult.GetRemainingSize()
c.Log(logger.LevelDebug, "cross rename, source %q remaining size %d to add %d", sourcePath,
c.Log(logger.LevelDebug, "cross rename, source %q remaining size %d to add %d", srcInfo.Name(),
remainingSize, sizeDiff)
if remainingSize < sizeDiff {
return false
@ -1518,61 +1533,40 @@ func (c *BaseConnection) updateQuotaMoveBetweenVFolders(sourceFolder, dstFolder
if sourceFolder.Name == dstFolder.Name {
// both files are inside the same virtual folder
if initialSize != -1 {
dataprovider.UpdateVirtualFolderQuota(&dstFolder.BaseVirtualFolder, -numFiles, -initialSize, false) //nolint:errcheck
if dstFolder.IsIncludedInUserQuota() {
dataprovider.UpdateUserQuota(&c.User, -numFiles, -initialSize, false) //nolint:errcheck
}
dataprovider.UpdateUserFolderQuota(dstFolder, &c.User, -numFiles, -initialSize, false)
}
return
}
// files are inside different virtual folders
dataprovider.UpdateVirtualFolderQuota(&sourceFolder.BaseVirtualFolder, -numFiles, -filesSize, false) //nolint:errcheck
if sourceFolder.IsIncludedInUserQuota() {
dataprovider.UpdateUserQuota(&c.User, -numFiles, -filesSize, false) //nolint:errcheck
}
dataprovider.UpdateUserFolderQuota(sourceFolder, &c.User, -numFiles, -filesSize, false)
if initialSize == -1 {
dataprovider.UpdateVirtualFolderQuota(&dstFolder.BaseVirtualFolder, numFiles, filesSize, false) //nolint:errcheck
if dstFolder.IsIncludedInUserQuota() {
dataprovider.UpdateUserQuota(&c.User, numFiles, filesSize, false) //nolint:errcheck
}
} else {
// we cannot have a directory here, initialSize != -1 only for files
dataprovider.UpdateVirtualFolderQuota(&dstFolder.BaseVirtualFolder, 0, filesSize-initialSize, false) //nolint:errcheck
if dstFolder.IsIncludedInUserQuota() {
dataprovider.UpdateUserQuota(&c.User, 0, filesSize-initialSize, false) //nolint:errcheck
}
dataprovider.UpdateUserFolderQuota(dstFolder, &c.User, numFiles, filesSize, false)
return
}
// we cannot have a directory here, initialSize != -1 only for files
dataprovider.UpdateUserFolderQuota(dstFolder, &c.User, 0, filesSize-initialSize, false)
}
func (c *BaseConnection) updateQuotaMoveFromVFolder(sourceFolder *vfs.VirtualFolder, initialSize, filesSize int64, numFiles int) {
// move between a virtual folder and the user home dir
dataprovider.UpdateVirtualFolderQuota(&sourceFolder.BaseVirtualFolder, -numFiles, -filesSize, false) //nolint:errcheck
if sourceFolder.IsIncludedInUserQuota() {
dataprovider.UpdateUserQuota(&c.User, -numFiles, -filesSize, false) //nolint:errcheck
}
dataprovider.UpdateUserFolderQuota(sourceFolder, &c.User, -numFiles, -filesSize, false)
if initialSize == -1 {
dataprovider.UpdateUserQuota(&c.User, numFiles, filesSize, false) //nolint:errcheck
} else {
// we cannot have a directory here, initialSize != -1 only for files
dataprovider.UpdateUserQuota(&c.User, 0, filesSize-initialSize, false) //nolint:errcheck
return
}
// we cannot have a directory here, initialSize != -1 only for files
dataprovider.UpdateUserQuota(&c.User, 0, filesSize-initialSize, false) //nolint:errcheck
}
func (c *BaseConnection) updateQuotaMoveToVFolder(dstFolder *vfs.VirtualFolder, initialSize, filesSize int64, numFiles int) {
// move between the user home dir and a virtual folder
dataprovider.UpdateUserQuota(&c.User, -numFiles, -filesSize, false) //nolint:errcheck
if initialSize == -1 {
dataprovider.UpdateVirtualFolderQuota(&dstFolder.BaseVirtualFolder, numFiles, filesSize, false) //nolint:errcheck
if dstFolder.IsIncludedInUserQuota() {
dataprovider.UpdateUserQuota(&c.User, numFiles, filesSize, false) //nolint:errcheck
}
} else {
// we cannot have a directory here, initialSize != -1 only for files
dataprovider.UpdateVirtualFolderQuota(&dstFolder.BaseVirtualFolder, 0, filesSize-initialSize, false) //nolint:errcheck
if dstFolder.IsIncludedInUserQuota() {
dataprovider.UpdateUserQuota(&c.User, 0, filesSize-initialSize, false) //nolint:errcheck
}
dataprovider.UpdateUserFolderQuota(dstFolder, &c.User, numFiles, filesSize, false)
return
}
// we cannot have a directory here, initialSize != -1 only for files
dataprovider.UpdateUserFolderQuota(dstFolder, &c.User, 0, filesSize-initialSize, false)
}
func (c *BaseConnection) updateQuotaAfterRename(fs vfs.Fs, virtualSourcePath, virtualTargetPath, targetPath string,
@ -1814,20 +1808,19 @@ func (c *BaseConnection) GetFsAndResolvedPath(virtualPath string) (vfs.Fs, strin
// DirListerAt defines a directory lister implementing the ListAt method.
type DirListerAt struct {
virtualPath string
user *dataprovider.User
conn *BaseConnection
fs vfs.Fs
info []os.FileInfo
id string
protocol string
mu sync.Mutex
lister vfs.DirLister
}
// Add adds the given os.FileInfo to the internal cache
func (l *DirListerAt) Add(fi os.FileInfo) {
// Prepend adds the given os.FileInfo as first element of the internal cache
func (l *DirListerAt) Prepend(fi os.FileInfo) {
l.mu.Lock()
defer l.mu.Unlock()
l.info = append(l.info, fi)
l.info = slices.Insert(l.info, 0, fi)
}
// ListAt implements sftp.ListerAt
@ -1840,10 +1833,10 @@ func (l *DirListerAt) ListAt(f []os.FileInfo, _ int64) (int, error) {
}
if len(f) <= len(l.info) {
files := make([]os.FileInfo, 0, len(f))
for idx := len(l.info) - 1; idx >= 0; idx-- {
for idx := range l.info {
files = append(files, l.info[idx])
if len(files) == len(f) {
l.info = l.info[:idx]
l.info = l.info[idx+1:]
n := copy(f, files)
return n, nil
}
@ -1860,14 +1853,12 @@ func (l *DirListerAt) Next(limit int) ([]os.FileInfo, error) {
for {
files, err := l.lister.Next(limit)
if err != nil && !errors.Is(err, io.EOF) {
logger.Debug(l.protocol, l.id, "error retrieving directory entries: %+v", err)
return files, err
l.conn.Log(logger.LevelDebug, "error retrieving directory entries: %+v", err)
return files, l.conn.GetFsError(l.fs, err)
}
files = l.user.FilterListDir(files, l.virtualPath)
files = l.conn.User.FilterListDir(files, l.virtualPath)
if len(l.info) > 0 {
for _, fi := range l.info {
files = util.PrependFileInfo(files, fi)
}
files = slices.Concat(l.info, files)
l.info = nil
}
if err != nil || len(files) > 0 {
@ -1902,18 +1893,22 @@ func getPermissionDeniedError(protocol string) error {
}
}
func keepConnectionAlive(c *BaseConnection, done chan bool, interval time.Duration) {
ticker := time.NewTicker(interval)
defer func() {
ticker.Stop()
}()
func keepConnectionAlive(c *BaseConnection, interval time.Duration) func() {
var timer *time.Timer
var closed atomic.Bool
for {
select {
case <-done:
return
case <-ticker.C:
c.UpdateLastActivity()
task := func() {
c.UpdateLastActivity()
if !closed.Load() {
timer.Reset(interval)
}
}
timer = time.AfterFunc(interval, task)
return func() {
closed.Store(true)
timer.Stop()
}
}

View file

@ -22,6 +22,7 @@ import (
"path"
"path/filepath"
"runtime"
"slices"
"strconv"
"testing"
"time"
@ -197,25 +198,24 @@ func TestRecursiveRenameWalkError(t *testing.T) {
}
func TestCrossRenameFsErrors(t *testing.T) {
if runtime.GOOS == osWindows {
t.Skip("this test is not available on Windows")
}
fs := vfs.NewOsFs("", os.TempDir(), "", nil)
conn := NewBaseConnection("", ProtocolWebDAV, "", "", dataprovider.User{})
res := conn.hasSpaceForCrossRename(fs, vfs.QuotaCheckResult{}, 1, "missingsource")
dirPath := filepath.Join(os.TempDir(), "d")
err := os.Mkdir(dirPath, os.ModePerm)
assert.NoError(t, err)
err = os.Chmod(dirPath, 0001)
assert.NoError(t, err)
srcInfo := vfs.NewFileInfo(filepath.Base(dirPath), true, 0, time.Now(), false)
res := conn.hasSpaceForCrossRename(fs, vfs.QuotaCheckResult{}, 1, dirPath, srcInfo)
assert.False(t, res)
if runtime.GOOS != osWindows {
dirPath := filepath.Join(os.TempDir(), "d")
err := os.Mkdir(dirPath, os.ModePerm)
assert.NoError(t, err)
err = os.Chmod(dirPath, 0001)
assert.NoError(t, err)
res = conn.hasSpaceForCrossRename(fs, vfs.QuotaCheckResult{}, 1, dirPath)
assert.False(t, res)
err = os.Chmod(dirPath, os.ModePerm)
assert.NoError(t, err)
err = os.Remove(dirPath)
assert.NoError(t, err)
}
err = os.Chmod(dirPath, os.ModePerm)
assert.NoError(t, err)
err = os.Remove(dirPath)
assert.NoError(t, err)
}
func TestRenameVirtualFolders(t *testing.T) {
@ -389,7 +389,7 @@ func TestErrorsMapping(t *testing.T) {
err := conn.GetFsError(fs, os.ErrNotExist)
if protocol == ProtocolSFTP {
assert.ErrorIs(t, err, sftp.ErrSSHFxNoSuchFile)
} else if util.Contains(osErrorsProtocols, protocol) {
} else if slices.Contains(osErrorsProtocols, protocol) {
assert.EqualError(t, err, os.ErrNotExist.Error())
} else {
assert.EqualError(t, err, ErrNotExist.Error())
@ -627,12 +627,11 @@ func TestErrorResolvePath(t *testing.T) {
func TestConnectionKeepAlive(t *testing.T) {
conn := NewBaseConnection("", ProtocolWebDAV, "", "", dataprovider.User{})
lastActivity := conn.GetLastActivity()
done := make(chan bool)
go func() {
time.Sleep(200 * time.Millisecond)
close(done)
}()
keepConnectionAlive(conn, done, 50*time.Millisecond)
stop := keepConnectionAlive(conn, 50*time.Millisecond)
defer stop()
time.Sleep(200 * time.Millisecond)
assert.Greater(t, conn.GetLastActivity(), lastActivity)
}
@ -1047,6 +1046,37 @@ func TestFilePatterns(t *testing.T) {
require.Len(t, filtered, 1)
}
func TestStatForOngoingTransfers(t *testing.T) {
user := dataprovider.User{
BaseUser: sdk.BaseUser{
Username: xid.New().String(),
Password: xid.New().String(),
HomeDir: filepath.Clean(os.TempDir()),
Status: 1,
Permissions: map[string][]string{
"/": {"*"},
},
},
}
fileName := "file.txt"
conn := NewBaseConnection(xid.New().String(), ProtocolSFTP, "", "", user)
fs := vfs.NewOsFs("", os.TempDir(), "", nil)
tr := NewBaseTransfer(nil, conn, nil, filepath.Join(os.TempDir(), fileName), filepath.Join(os.TempDir(), fileName),
fileName, TransferUpload, 0, 0, 0, 0, true, fs, dataprovider.TransferQuota{})
_, err := conn.DoStat("/file.txt", 0, false)
assert.NoError(t, err)
err = tr.Close()
assert.NoError(t, err)
tr = NewBaseTransfer(nil, conn, nil, filepath.Join(os.TempDir(), fileName), filepath.Join(os.TempDir(), fileName),
fileName, TransferDownload, 0, 0, 0, 0, true, fs, dataprovider.TransferQuota{})
_, err = conn.DoStat("/file.txt", 0, false)
assert.Error(t, err)
err = tr.Close()
assert.NoError(t, err)
err = conn.CloseFS()
assert.NoError(t, err)
}
func TestListerAt(t *testing.T) {
dir := t.TempDir()
user := dataprovider.User{
@ -1090,7 +1120,7 @@ func TestListerAt(t *testing.T) {
require.ErrorIs(t, err, io.EOF)
require.Len(t, files, 0)
_, err = lister.Next(-1)
require.ErrorContains(t, err, "invalid limit")
require.ErrorContains(t, err, conn.GetGenericError(err).Error())
err = lister.Close()
require.NoError(t, err)
@ -1134,8 +1164,8 @@ func TestListerAt(t *testing.T) {
require.Equal(t, 0, n)
lister, err = conn.ListDir("/")
require.NoError(t, err)
lister.Add(vfs.NewFileInfo("..", true, 0, time.Unix(0, 0), false))
lister.Add(vfs.NewFileInfo(".", true, 0, time.Unix(0, 0), false))
lister.Prepend(vfs.NewFileInfo("..", true, 0, time.Unix(0, 0), false))
lister.Prepend(vfs.NewFileInfo(".", true, 0, time.Unix(0, 0), false))
files = make([]os.FileInfo, 1)
n, err = lister.ListAt(files, 0)
require.NoError(t, err)
@ -1159,3 +1189,348 @@ func TestListerAt(t *testing.T) {
err = lister.Close()
require.NoError(t, err)
}
func TestGetFsAndResolvedPath(t *testing.T) {
homeDir := filepath.Join(os.TempDir(), "home_test")
localVdir := filepath.Join(os.TempDir(), "local_mount_test")
err := os.MkdirAll(homeDir, 0777)
require.NoError(t, err)
err = os.MkdirAll(localVdir, 0777)
require.NoError(t, err)
t.Cleanup(func() {
os.RemoveAll(homeDir)
os.RemoveAll(localVdir)
})
user := dataprovider.User{
BaseUser: sdk.BaseUser{
Username: xid.New().String(),
Status: 1,
HomeDir: homeDir,
},
VirtualFolders: []vfs.VirtualFolder{
{
BaseVirtualFolder: vfs.BaseVirtualFolder{
Name: "s3",
MappedPath: "",
FsConfig: vfs.Filesystem{
Provider: sdk.S3FilesystemProvider,
S3Config: vfs.S3FsConfig{
BaseS3FsConfig: sdk.BaseS3FsConfig{
Bucket: "my-test-bucket",
Region: "us-east-1",
},
},
},
},
VirtualPath: "/s3",
},
{
BaseVirtualFolder: vfs.BaseVirtualFolder{
Name: "local",
MappedPath: localVdir,
FsConfig: vfs.Filesystem{
Provider: sdk.LocalFilesystemProvider,
},
},
VirtualPath: "/local",
},
},
}
conn := NewBaseConnection(xid.New().String(), ProtocolSFTP, "", "", user)
tests := []struct {
name string
inputVirtualPath string
expectedFsType string
expectedPhyPath string // The resolved path on the target FS
expectedRelativePath string
}{
{
name: "Root File",
inputVirtualPath: "/file.txt",
expectedFsType: "osfs",
expectedPhyPath: filepath.Join(homeDir, "file.txt"),
expectedRelativePath: "/file.txt",
},
{
name: "Standard S3 File",
inputVirtualPath: "/s3/image.png",
expectedFsType: "S3Fs",
expectedPhyPath: "image.png",
expectedRelativePath: "/s3/image.png",
},
{
name: "Standard Local Mount File",
inputVirtualPath: "/local/config.json",
expectedFsType: "osfs",
expectedPhyPath: filepath.Join(localVdir, "config.json"),
expectedRelativePath: "/local/config.json",
},
{
name: "Backslash Separator -> Should hit S3",
inputVirtualPath: "\\s3\\doc.txt",
expectedFsType: "S3Fs",
expectedPhyPath: "doc.txt",
expectedRelativePath: "/s3/doc.txt",
},
{
name: "Mixed Separators -> Should hit Local Mount",
inputVirtualPath: "/local\\subdir/test.txt",
expectedFsType: "osfs",
expectedPhyPath: filepath.Join(localVdir, "subdir", "test.txt"),
expectedRelativePath: "/local/subdir/test.txt",
},
{
name: "Double Slash -> Should normalize and hit S3",
inputVirtualPath: "//s3//dir @1/data.csv",
expectedFsType: "S3Fs",
expectedPhyPath: "dir @1/data.csv",
expectedRelativePath: "/s3/dir @1/data.csv",
},
{
name: "Local Mount Traversal (Attempt to escape)",
inputVirtualPath: "/local/../../etc/passwd",
expectedFsType: "osfs",
expectedPhyPath: filepath.Join(homeDir, "/etc/passwd"),
expectedRelativePath: "/etc/passwd",
},
{
name: "Traversal Out of S3 (Valid)",
inputVirtualPath: "/s3/../../secret.txt",
expectedFsType: "osfs",
expectedPhyPath: filepath.Join(homeDir, "secret.txt"),
expectedRelativePath: "/secret.txt",
},
{
name: "Traversal Inside S3",
inputVirtualPath: "/s3/subdir/../image.png",
expectedFsType: "S3Fs",
expectedPhyPath: "image.png",
expectedRelativePath: "/s3/image.png",
},
{
name: "Mount Point Bypass -> Target Local Mount",
inputVirtualPath: "/s3\\..\\local\\secret.txt",
expectedFsType: "osfs",
expectedPhyPath: filepath.Join(localVdir, "secret.txt"),
expectedRelativePath: "/local/secret.txt",
},
{
name: "Dirty Relative Path (Your Case)",
inputVirtualPath: "test\\..\\..\\oops/file.txt",
expectedFsType: "osfs",
expectedPhyPath: filepath.Join(homeDir, "oops", "file.txt"),
expectedRelativePath: "/oops/file.txt",
},
{
name: "Relative Path targeting S3 (No leading slash)",
inputVirtualPath: "s3//sub/../image.png",
expectedFsType: "S3Fs",
expectedPhyPath: "image.png",
expectedRelativePath: "/s3/image.png",
},
{
name: "Windows Path starting with Backslash",
inputVirtualPath: "\\s3\\doc/dir\\doc.txt",
expectedFsType: "S3Fs",
expectedPhyPath: "doc/dir/doc.txt",
expectedRelativePath: "/s3/doc/dir/doc.txt",
},
{
name: "Filesystem Juggling (Relative)",
inputVirtualPath: "local/../s3/file.txt",
expectedFsType: "S3Fs",
expectedPhyPath: "file.txt",
expectedRelativePath: "/s3/file.txt",
},
{
name: "Triple Dot Filename (Valid Name)",
inputVirtualPath: "/...hidden/secret",
expectedFsType: "osfs",
expectedPhyPath: filepath.Join(homeDir, "...hidden", "secret"),
expectedRelativePath: "/...hidden/secret",
},
{
name: "Dot Slash Prefix",
inputVirtualPath: "./local/file.txt",
expectedFsType: "osfs",
expectedPhyPath: filepath.Join(localVdir, "file.txt"),
expectedRelativePath: "/local/file.txt",
},
{
name: "Root of Local Mount Exactly",
inputVirtualPath: "/local/",
expectedFsType: "osfs",
expectedPhyPath: localVdir,
expectedRelativePath: "/local",
},
{
name: "Root of S3 Mount Exactly",
inputVirtualPath: "/s3/",
expectedFsType: "S3Fs",
expectedPhyPath: "",
expectedRelativePath: "/s3",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// The input path is sanitized by the protocol handler
// implementations before reaching GetFsAndResolvedPath.
cleanInput := util.CleanPath(tc.inputVirtualPath)
fs, resolvedPath, err := conn.GetFsAndResolvedPath(cleanInput)
if assert.NoError(t, err, "did not expect error for path: %q, got: %v", tc.inputVirtualPath, err) {
assert.Contains(t, fs.Name(), tc.expectedFsType,
"routing error: input %q but expected fs %q, got %q", tc.inputVirtualPath, tc.expectedFsType, fs.Name())
assert.Equal(t, tc.expectedPhyPath, resolvedPath,
"resolution error: input %q resolved to %q expected %q", tc.inputVirtualPath, resolvedPath, tc.expectedPhyPath)
relativePath := fs.GetRelativePath(resolvedPath)
assert.Equal(t, tc.expectedRelativePath, relativePath,
"relative path error, input %q, got %q, expected %q", tc.inputVirtualPath, tc.expectedRelativePath, relativePath)
}
})
}
}
func TestOsFsGetRelativePath(t *testing.T) {
homeDir := filepath.Join(os.TempDir(), "home_test")
localVdir := filepath.Join(os.TempDir(), "local_mount_test")
err := os.MkdirAll(homeDir, 0777)
require.NoError(t, err)
err = os.MkdirAll(localVdir, 0777)
require.NoError(t, err)
t.Cleanup(func() {
os.RemoveAll(homeDir)
os.RemoveAll(localVdir)
})
user := dataprovider.User{
BaseUser: sdk.BaseUser{
Username: xid.New().String(),
Status: 1,
HomeDir: homeDir,
},
VirtualFolders: []vfs.VirtualFolder{
{
BaseVirtualFolder: vfs.BaseVirtualFolder{
Name: "local",
MappedPath: localVdir,
FsConfig: vfs.Filesystem{
Provider: sdk.LocalFilesystemProvider,
},
},
VirtualPath: "/local",
},
},
}
connID := xid.New().String()
rootFs, err := user.GetFilesystemForPath("/", connID)
require.NoError(t, err)
localFs, err := user.GetFilesystemForPath("/local", connID)
require.NoError(t, err)
tests := []struct {
name string
fs vfs.Fs
inputPath string // The physical path to reverse-map
expectedRel string // The expected virtual path
}{
{
name: "Root FS - Inside root",
fs: rootFs,
inputPath: filepath.Join(homeDir, "docs", "file.txt"),
expectedRel: "/docs/file.txt",
},
{
name: "Root FS - Exact root directory",
fs: rootFs,
inputPath: homeDir,
expectedRel: "/",
},
{
name: "Root FS - External absolute path (Jail to /)",
fs: rootFs,
inputPath: "/etc/passwd",
expectedRel: "/",
},
{
name: "Root FS - Traversal escape (Jail to /)",
fs: rootFs,
inputPath: filepath.Join(homeDir, "..", "escaped.txt"),
expectedRel: "/",
},
{
name: "Root FS - Valid file named with triple dots",
fs: rootFs,
inputPath: filepath.Join(homeDir, "..."),
expectedRel: "/...",
},
{
name: "Local FS - Up path in dir",
fs: rootFs,
inputPath: homeDir + "/../" + filepath.Base(homeDir) + "/dir/test.txt",
expectedRel: "/dir/test.txt",
},
{
name: "Local FS - Inside mount",
fs: localFs,
inputPath: filepath.Join(localVdir, "data", "config.json"),
expectedRel: "/local/data/config.json",
},
{
name: "Local FS - Exact mount directory",
fs: localFs,
inputPath: localVdir,
expectedRel: "/local",
},
{
name: "Local FS - External absolute path (Jail to /local)",
fs: localFs,
inputPath: "/var/log/syslog",
expectedRel: "/local",
},
{
name: "Local FS - Traversal escape (Jail to /local)",
fs: localFs,
inputPath: filepath.Join(localVdir, "..", "..", "etc", "passwd"),
expectedRel: "/local",
},
{
name: "Local FS - Partial prefix (Jail to /local)",
fs: localFs,
inputPath: localVdir + "_backup",
expectedRel: "/local",
},
{
name: "Local FS - Relative traversal matching virual dir",
fs: localFs,
inputPath: localVdir + "/../" + filepath.Base(localVdir) + "/dir/test.txt",
expectedRel: "/local/dir/test.txt",
},
{
name: "Local FS - Valid file starting with two dots",
fs: localFs,
inputPath: filepath.Join(localVdir, "..hidden_file.txt"),
expectedRel: "/local/..hidden_file.txt",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
actualRel := tc.fs.GetRelativePath(tc.inputPath)
assert.Equal(t, tc.expectedRel, actualRel,
"Failed mapping physical path %q on FS %q", tc.inputPath, tc.fs.Name())
})
}
}

View file

@ -15,44 +15,20 @@
package common
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"sync"
"time"
"github.com/wneessen/go-mail"
"github.com/drakkan/sftpgo/v2/internal/command"
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
"github.com/drakkan/sftpgo/v2/internal/httpclient"
"github.com/drakkan/sftpgo/v2/internal/logger"
"github.com/drakkan/sftpgo/v2/internal/smtp"
"github.com/drakkan/sftpgo/v2/internal/util"
"github.com/drakkan/sftpgo/v2/internal/vfs"
)
// RetentionCheckNotification defines the supported notification methods for a retention check result
type RetentionCheckNotification = string
// Supported notification methods
const (
// notify results using the defined "data_retention_hook"
RetentionCheckNotificationHook = "Hook"
// notify results by email
RetentionCheckNotificationEmail = "Email"
)
var (
// RetentionChecks is the list of active retention checks
RetentionChecks ActiveRetentionChecks
@ -74,14 +50,10 @@ func (c *ActiveRetentionChecks) Get(role string) []RetentionCheck {
if role == "" || role == check.Role {
foldersCopy := make([]dataprovider.FolderRetention, len(check.Folders))
copy(foldersCopy, check.Folders)
notificationsCopy := make([]string, len(check.Notifications))
copy(notificationsCopy, check.Notifications)
checks = append(checks, RetentionCheck{
Username: check.Username,
StartTime: check.StartTime,
Notifications: notificationsCopy,
Email: check.Email,
Folders: foldersCopy,
Username: check.Username,
StartTime: check.StartTime,
Folders: foldersCopy,
})
}
}
@ -150,54 +122,10 @@ type RetentionCheck struct {
StartTime int64 `json:"start_time"`
// affected folders
Folders []dataprovider.FolderRetention `json:"folders"`
// how cleanup results will be notified
Notifications []RetentionCheckNotification `json:"notifications,omitempty"`
// email to use if the notification method is set to email
Email string `json:"email,omitempty"`
Role string `json:"-"`
Role string `json:"-"`
// Cleanup results
results []folderRetentionCheckResult `json:"-"`
conn *BaseConnection
}
// Validate returns an error if the specified folders are not valid
func (c *RetentionCheck) Validate() error {
folderPaths := make(map[string]bool)
nothingToDo := true
for idx := range c.Folders {
f := &c.Folders[idx]
if err := f.Validate(); err != nil {
return err
}
if f.Retention > 0 {
nothingToDo = false
}
if _, ok := folderPaths[f.Path]; ok {
return util.NewValidationError(fmt.Sprintf("duplicated folder path %q", f.Path))
}
folderPaths[f.Path] = true
}
if nothingToDo {
return util.NewValidationError("nothing to delete!")
}
for _, notification := range c.Notifications {
switch notification {
case RetentionCheckNotificationEmail:
if !smtp.IsEnabled() {
return util.NewValidationError("in order to notify results via email you must configure an SMTP server")
}
if c.Email == "" {
return util.NewValidationError("in order to notify results via email you must add a valid email address to your profile")
}
case RetentionCheckNotificationHook:
if Config.DataRetentionHook == "" {
return util.NewValidationError("in order to notify results via hook you must define a data_retention_hook")
}
default:
return util.NewValidationError(fmt.Sprintf("invalid notification %q", notification))
}
}
return nil
conn *BaseConnection `json:"-"`
}
func (c *RetentionCheck) updateUserPermissions() {
@ -269,7 +197,7 @@ func (c *RetentionCheck) cleanupFolder(folderPath string, recursion int) error {
return nil
}
result.Error = fmt.Sprintf("unable to get lister for directory %q", folderPath)
c.conn.Log(logger.LevelError, result.Error)
c.conn.Log(logger.LevelError, "%s", result.Error)
return err
}
defer lister.Close()
@ -359,130 +287,13 @@ func (c *RetentionCheck) Start() error {
for _, folder := range c.Folders {
if folder.Retention > 0 {
if err := c.cleanupFolder(folder.Path, 0); err != nil {
c.conn.Log(logger.LevelError, "retention check failed, unable to cleanup folder %q", folder.Path)
c.sendNotifications(time.Since(startTime), err)
c.conn.Log(logger.LevelError, "retention check failed, unable to cleanup folder %q, elapsed: %s",
folder.Path, time.Since(startTime))
return err
}
}
}
c.conn.Log(logger.LevelInfo, "retention check completed")
c.sendNotifications(time.Since(startTime), nil)
c.conn.Log(logger.LevelInfo, "retention check completed, elapsed: %s", time.Since(startTime))
return nil
}
func (c *RetentionCheck) sendNotifications(elapsed time.Duration, err error) {
for _, notification := range c.Notifications {
switch notification {
case RetentionCheckNotificationEmail:
c.sendEmailNotification(err) //nolint:errcheck
case RetentionCheckNotificationHook:
c.sendHookNotification(elapsed, err) //nolint:errcheck
}
}
}
func (c *RetentionCheck) sendEmailNotification(errCheck error) error {
params := EventParams{}
if len(c.results) > 0 || errCheck != nil {
params.retentionChecks = append(params.retentionChecks, executedRetentionCheck{
Username: c.conn.User.Username,
ActionName: "Retention check",
Results: c.results,
})
}
var files []*mail.File
f, err := params.getRetentionReportsAsMailAttachment()
if err != nil {
c.conn.Log(logger.LevelError, "unable to get retention report as mail attachment: %v", err)
return err
}
f.Name = "retention-report.zip"
files = append(files, f)
startTime := time.Now()
var subject string
if errCheck == nil {
subject = fmt.Sprintf("Successful retention check for user %q", c.conn.User.Username)
} else {
subject = fmt.Sprintf("Retention check failed for user %q", c.conn.User.Username)
}
body := "Further details attached."
err = smtp.SendEmail([]string{c.Email}, nil, subject, body, smtp.EmailContentTypeTextPlain, files...)
if err != nil {
c.conn.Log(logger.LevelError, "unable to notify retention check result via email: %v, elapsed: %s", err,
time.Since(startTime))
return err
}
c.conn.Log(logger.LevelInfo, "retention check result successfully notified via email, elapsed: %s", time.Since(startTime))
return nil
}
func (c *RetentionCheck) sendHookNotification(elapsed time.Duration, errCheck error) error {
startNewHook()
defer hookEnded()
data := make(map[string]any)
totalDeletedFiles := 0
totalDeletedSize := int64(0)
for _, result := range c.results {
totalDeletedFiles += result.DeletedFiles
totalDeletedSize += result.DeletedSize
}
data["username"] = c.conn.User.Username
data["start_time"] = c.StartTime
data["elapsed"] = elapsed.Milliseconds()
if errCheck == nil {
data["status"] = 1
} else {
data["status"] = 0
}
data["total_deleted_files"] = totalDeletedFiles
data["total_deleted_size"] = totalDeletedSize
data["details"] = c.results
jsonData, _ := json.Marshal(data)
startTime := time.Now()
if strings.HasPrefix(Config.DataRetentionHook, "http") {
var url *url.URL
url, err := url.Parse(Config.DataRetentionHook)
if err != nil {
c.conn.Log(logger.LevelError, "invalid data retention hook %q: %v", Config.DataRetentionHook, err)
return err
}
respCode := 0
resp, err := httpclient.RetryablePost(url.String(), "application/json", bytes.NewBuffer(jsonData))
if err == nil {
respCode = resp.StatusCode
resp.Body.Close()
if respCode != http.StatusOK {
err = errUnexpectedHTTResponse
}
}
c.conn.Log(logger.LevelDebug, "notified result to URL: %q, status code: %v, elapsed: %v err: %v",
url.Redacted(), respCode, time.Since(startTime), err)
return err
}
if !filepath.IsAbs(Config.DataRetentionHook) {
err := fmt.Errorf("invalid data retention hook %q", Config.DataRetentionHook)
c.conn.Log(logger.LevelError, "%v", err)
return err
}
timeout, env, args := command.GetConfig(Config.DataRetentionHook, command.HookDataRetention)
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
cmd := exec.CommandContext(ctx, Config.DataRetentionHook, args...)
cmd.Env = append(env,
fmt.Sprintf("SFTPGO_DATA_RETENTION_RESULT=%s", util.BytesToString(jsonData)))
err := cmd.Run()
c.conn.Log(logger.LevelDebug, "notified result using command: %q, elapsed: %s err: %v",
Config.DataRetentionHook, time.Since(startTime), err)
return err
}

View file

@ -15,228 +15,17 @@
package common
import (
"errors"
"fmt"
"os/exec"
"runtime"
"testing"
"time"
"github.com/sftpgo/sdk"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
"github.com/drakkan/sftpgo/v2/internal/smtp"
"github.com/drakkan/sftpgo/v2/internal/util"
)
func TestRetentionValidation(t *testing.T) {
check := RetentionCheck{}
check.Folders = []dataprovider.FolderRetention{
{
Path: "/",
Retention: -1,
},
}
err := check.Validate()
require.Error(t, err)
assert.Contains(t, err.Error(), "invalid folder retention")
check.Folders = []dataprovider.FolderRetention{
{
Path: "/ab/..",
Retention: 0,
},
}
err = check.Validate()
require.Error(t, err)
assert.Contains(t, err.Error(), "nothing to delete")
assert.Equal(t, "/", check.Folders[0].Path)
check.Folders = append(check.Folders, dataprovider.FolderRetention{
Path: "/../..",
Retention: 24,
})
err = check.Validate()
require.Error(t, err)
assert.Contains(t, err.Error(), `duplicated folder path "/"`)
check.Folders = []dataprovider.FolderRetention{
{
Path: "/dir1",
Retention: 48,
},
{
Path: "/dir2",
Retention: 96,
},
}
err = check.Validate()
assert.NoError(t, err)
assert.Len(t, check.Notifications, 0)
assert.Empty(t, check.Email)
check.Notifications = []RetentionCheckNotification{RetentionCheckNotificationEmail}
err = check.Validate()
require.Error(t, err)
assert.Contains(t, err.Error(), "you must configure an SMTP server")
smtpCfg := smtp.Config{
Host: "mail.example.com",
Port: 25,
From: "notification@example.com",
TemplatesPath: "templates",
}
err = smtpCfg.Initialize(configDir, true)
require.NoError(t, err)
err = check.Validate()
require.Error(t, err)
assert.Contains(t, err.Error(), "you must add a valid email address")
check.Email = "admin@example.com"
err = check.Validate()
assert.NoError(t, err)
smtpCfg = smtp.Config{}
err = smtpCfg.Initialize(configDir, true)
require.NoError(t, err)
check.Notifications = []RetentionCheckNotification{RetentionCheckNotificationHook}
err = check.Validate()
require.Error(t, err)
assert.Contains(t, err.Error(), "data_retention_hook")
check.Notifications = []string{"not valid"}
err = check.Validate()
require.Error(t, err)
assert.Contains(t, err.Error(), "invalid notification")
}
func TestRetentionEmailNotifications(t *testing.T) {
smtpCfg := smtp.Config{
Host: "127.0.0.1",
Port: 2525,
From: "notification@example.com",
TemplatesPath: "templates",
}
err := smtpCfg.Initialize(configDir, true)
require.NoError(t, err)
user := dataprovider.User{
BaseUser: sdk.BaseUser{
Username: "user1",
},
}
user.Permissions = make(map[string][]string)
user.Permissions["/"] = []string{dataprovider.PermAny}
check := RetentionCheck{
Notifications: []RetentionCheckNotification{RetentionCheckNotificationEmail},
Email: "notification@example.com",
results: []folderRetentionCheckResult{
{
Path: "/",
Retention: 24,
DeletedFiles: 10,
DeletedSize: 32657,
Elapsed: 10 * time.Second,
},
},
}
conn := NewBaseConnection("", "", "", "", user)
conn.SetProtocol(ProtocolDataRetention)
conn.ID = fmt.Sprintf("data_retention_%v", user.Username)
check.conn = conn
check.sendNotifications(1*time.Second, nil)
err = check.sendEmailNotification(nil)
assert.NoError(t, err)
err = check.sendEmailNotification(errors.New("test error"))
assert.NoError(t, err)
check.results = nil
err = check.sendEmailNotification(nil)
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "no data retention report available")
}
smtpCfg.Port = 2626
err = smtpCfg.Initialize(configDir, true)
require.NoError(t, err)
err = check.sendEmailNotification(nil)
assert.Error(t, err)
check.results = []folderRetentionCheckResult{
{
Path: "/",
Retention: 24,
DeletedFiles: 20,
DeletedSize: 456789,
Elapsed: 12 * time.Second,
},
}
smtpCfg = smtp.Config{}
err = smtpCfg.Initialize(configDir, true)
require.NoError(t, err)
err = check.sendEmailNotification(nil)
assert.Error(t, err)
}
func TestRetentionHookNotifications(t *testing.T) {
dataRetentionHook := Config.DataRetentionHook
Config.DataRetentionHook = fmt.Sprintf("http://%v", httpAddr)
user := dataprovider.User{
BaseUser: sdk.BaseUser{
Username: "user2",
},
}
user.Permissions = make(map[string][]string)
user.Permissions["/"] = []string{dataprovider.PermAny}
check := RetentionCheck{
Notifications: []RetentionCheckNotification{RetentionCheckNotificationHook},
results: []folderRetentionCheckResult{
{
Path: "/",
Retention: 24,
DeletedFiles: 10,
DeletedSize: 32657,
Elapsed: 10 * time.Second,
},
},
}
conn := NewBaseConnection("", "", "", "", user)
conn.SetProtocol(ProtocolDataRetention)
conn.ID = fmt.Sprintf("data_retention_%v", user.Username)
check.conn = conn
check.sendNotifications(1*time.Second, nil)
err := check.sendHookNotification(1*time.Second, nil)
assert.NoError(t, err)
Config.DataRetentionHook = fmt.Sprintf("http://%v/404", httpAddr)
err = check.sendHookNotification(1*time.Second, nil)
assert.ErrorIs(t, err, errUnexpectedHTTResponse)
Config.DataRetentionHook = "http://foo\x7f.com/retention"
err = check.sendHookNotification(1*time.Second, err)
assert.Error(t, err)
Config.DataRetentionHook = "relativepath"
err = check.sendHookNotification(1*time.Second, err)
assert.Error(t, err)
if runtime.GOOS != osWindows {
hookCmd, err := exec.LookPath("true")
assert.NoError(t, err)
Config.DataRetentionHook = hookCmd
err = check.sendHookNotification(1*time.Second, err)
assert.NoError(t, err)
}
Config.DataRetentionHook = dataRetentionHook
}
func TestRetentionPermissionsAndGetFolder(t *testing.T) {
user := dataprovider.User{
BaseUser: sdk.BaseUser{
@ -311,7 +100,6 @@ func TestRetentionCheckAddRemove(t *testing.T) {
Retention: 48,
},
},
Notifications: []RetentionCheckNotification{RetentionCheckNotificationHook},
}
assert.NotNil(t, RetentionChecks.Add(check, &user))
checks := RetentionChecks.Get("")
@ -321,8 +109,6 @@ func TestRetentionCheckAddRemove(t *testing.T) {
require.Len(t, checks[0].Folders, 1)
assert.Equal(t, check.Folders[0].Path, checks[0].Folders[0].Path)
assert.Equal(t, check.Folders[0].Retention, checks[0].Folders[0].Retention)
require.Len(t, checks[0].Notifications, 1)
assert.Equal(t, RetentionCheckNotificationHook, checks[0].Notifications[0])
assert.Nil(t, RetentionChecks.Add(check, &user))
assert.True(t, RetentionChecks.remove(username))
@ -349,7 +135,6 @@ func TestRetentionCheckRole(t *testing.T) {
Retention: 48,
},
},
Notifications: []RetentionCheckNotification{RetentionCheckNotificationHook},
}
assert.NotNil(t, RetentionChecks.Add(check, &user))
checks := RetentionChecks.Get("")

View file

@ -53,6 +53,7 @@ type Defender interface {
GetBanTime(ip string) (*time.Time, error)
GetScore(ip string) (int, error)
DeleteHost(ip string) bool
DelayLogin(err error)
}
// DefenderConfig defines the "defender" configuration
@ -90,6 +91,16 @@ type DefenderConfig struct {
// to return when you request for the entire host list from the defender
EntriesSoftLimit int `json:"entries_soft_limit" mapstructure:"entries_soft_limit"`
EntriesHardLimit int `json:"entries_hard_limit" mapstructure:"entries_hard_limit"`
// Configuration to impose a delay between login attempts
LoginDelay LoginDelay `json:"login_delay" mapstructure:"login_delay"`
}
// LoginDelay defines the delays to impose between login attempts.
type LoginDelay struct {
// The number of milliseconds to pause prior to allowing a successful login
Success int `json:"success" mapstructure:"success"`
// The number of milliseconds to pause prior to reporting a failed login
PasswordFailed int `json:"password_failed" mapstructure:"password_failed"`
}
type baseDefender struct {
@ -163,6 +174,19 @@ func (d *baseDefender) logBan(ip, protocol string) {
Send()
}
// DelayLogin applies the configured login delay.
func (d *baseDefender) DelayLogin(err error) {
if err == nil {
if d.config.LoginDelay.Success > 0 {
time.Sleep(time.Duration(d.config.LoginDelay.Success) * time.Millisecond)
}
return
}
if d.config.LoginDelay.PasswordFailed > 0 {
time.Sleep(time.Duration(d.config.LoginDelay.PasswordFailed) * time.Millisecond)
}
}
type hostEvent struct {
dateTime time.Time
score int

View file

@ -435,6 +435,31 @@ func TestDefenderCleanup(t *testing.T) {
assert.Equal(t, 0, score)
}
func TestDefenderDelay(t *testing.T) {
d := memoryDefender{
baseDefender: baseDefender{
config: &DefenderConfig{
ObservationTime: 1,
EntriesSoftLimit: 2,
EntriesHardLimit: 3,
LoginDelay: LoginDelay{
Success: 50,
PasswordFailed: 200,
},
},
},
}
startTime := time.Now()
d.DelayLogin(nil)
elapsed := time.Since(startTime)
assert.Less(t, elapsed, time.Millisecond*100)
startTime = time.Now()
d.DelayLogin(ErrInternalFailure)
elapsed = time.Since(startTime)
assert.Greater(t, elapsed, time.Millisecond*150)
}
func TestDefenderConfig(t *testing.T) {
c := DefenderConfig{}
err := c.validate()

View file

@ -62,7 +62,7 @@ func (d *dbDefender) GetHost(ip string) (dataprovider.DefenderEntry, error) {
// and increase ban time if the IP is found.
// This method must be called as soon as the client connects
func (d *dbDefender) IsBanned(ip, protocol string) bool {
if d.baseDefender.isBanned(ip, protocol) {
if d.isBanned(ip, protocol) {
return true
}
@ -95,22 +95,22 @@ func (d *dbDefender) AddEvent(ip, protocol string, event HostEvent) bool {
return true
}
score := d.baseDefender.getScore(event)
score := d.getScore(event)
host, err := dataprovider.AddDefenderEvent(ip, score, d.getStartObservationTime())
if err != nil {
return false
}
d.baseDefender.logEvent(ip, protocol, event, host.Score)
d.logEvent(ip, protocol, event, host.Score)
if host.Score > d.config.Threshold {
d.baseDefender.logBan(ip, protocol)
d.logBan(ip, protocol)
banTime := time.Now().Add(time.Duration(d.config.BanTime) * time.Minute)
err = dataprovider.SetDefenderBanTime(ip, util.GetTimeAsMsSinceEpoch(banTime))
if err == nil {
eventManager.handleIPBlockedEvent(EventParams{
Event: ipBlockedEventName,
IP: ip,
Timestamp: time.Now().UnixNano(),
Timestamp: time.Now(),
Status: 1,
})
}

View file

@ -148,7 +148,7 @@ func (d *memoryDefender) IsBanned(ip, protocol string) bool {
defer d.RUnlock()
return d.baseDefender.isBanned(ip, protocol)
return d.isBanned(ip, protocol)
}
// DeleteHost removes the specified IP from the defender lists
@ -188,7 +188,7 @@ func (d *memoryDefender) AddEvent(ip, protocol string, event HostEvent) bool {
delete(d.banned, ip)
}
score := d.baseDefender.getScore(event)
score := d.getScore(event)
ev := hostEvent{
dateTime: time.Now(),
@ -207,25 +207,25 @@ func (d *memoryDefender) AddEvent(ip, protocol string, event HostEvent) bool {
idx++
}
}
d.baseDefender.logEvent(ip, protocol, event, hs.TotalScore)
d.logEvent(ip, protocol, event, hs.TotalScore)
hs.Events = hs.Events[:idx]
if hs.TotalScore >= d.config.Threshold {
d.baseDefender.logBan(ip, protocol)
d.logBan(ip, protocol)
d.banned[ip] = time.Now().Add(time.Duration(d.config.BanTime) * time.Minute)
delete(d.hosts, ip)
d.cleanupBanned()
eventManager.handleIPBlockedEvent(EventParams{
Event: ipBlockedEventName,
IP: ip,
Timestamp: time.Now().UnixNano(),
Timestamp: time.Now(),
Status: 1,
})
} else {
d.hosts[ip] = hs
}
} else {
d.baseDefender.logEvent(ip, protocol, event, ev.score)
d.logEvent(ip, protocol, event, ev.score)
d.hosts[ip] = hostScore{
TotalScore: ev.score,
Events: []hostEvent{ev},

View file

@ -21,6 +21,7 @@ import (
"encoding/json"
"errors"
"fmt"
"html"
"io"
"mime"
"mime/multipart"
@ -31,6 +32,7 @@ import (
"os/exec"
"path"
"path/filepath"
"slices"
"strconv"
"strings"
"sync"
@ -55,8 +57,9 @@ import (
const (
ipBlockedEventName = "IP Blocked"
maxAttachmentsSize = int64(10 * 1024 * 1024)
objDataPlaceholder = "{{ObjectData}}"
objDataPlaceholderString = "{{ObjectDataString}}"
objDataPlaceholder = "{{.ObjectData}}"
objDataPlaceholderString = "{{.ObjectDataString}}"
dateTimeMillisFormat = "2006-01-02T15:04:05.000"
)
// Supported IDP login events
@ -69,6 +72,9 @@ var (
// eventManager handle the supported event rules actions
eventManager eventRulesContainer
multipartQuoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
fsEventsWithSize = []string{operationPreDelete, OperationPreUpload, operationDelete,
operationCopy, operationDownload, operationFirstUpload, operationFirstDownload,
operationUpload}
)
func init() {
@ -88,11 +94,12 @@ func init() {
ObjectType: objectType,
IP: ip,
Role: role,
Timestamp: time.Now().UnixNano(),
Timestamp: time.Now(),
Object: object,
}
if u, ok := object.(*dataprovider.User); ok {
p.Email = u.Email
p.Groups = u.Groups
} else if a, ok := object.(*dataprovider.Admin); ok {
p.Email = a.Email
}
@ -271,7 +278,8 @@ func (r *eventRulesContainer) addUpdateRuleInternal(rule dataprovider.EventRule)
func (r *eventRulesContainer) loadRules() {
eventManagerLog(logger.LevelDebug, "loading updated rules")
modTime := util.GetTimeAsMsSinceEpoch(time.Now())
rules, err := dataprovider.GetRecentlyUpdatedRules(r.getLastLoadTime())
lastLoadTime := r.getLastLoadTime()
rules, err := dataprovider.GetRecentlyUpdatedRules(lastLoadTime)
if err != nil {
eventManagerLog(logger.LevelError, "unable to load event rules: %v", err)
return
@ -307,23 +315,26 @@ func (*eventRulesContainer) checkIPDLoginEventMatch(conditions *dataprovider.Eve
}
func (*eventRulesContainer) checkProviderEventMatch(conditions *dataprovider.EventConditions, params *EventParams) bool {
if !util.Contains(conditions.ProviderEvents, params.Event) {
if !slices.Contains(conditions.ProviderEvents, params.Event) {
return false
}
if !checkEventConditionPatterns(params.Name, conditions.Options.Names) {
return false
}
if !checkEventGroupConditionPatterns(params.Groups, conditions.Options.GroupNames) {
return false
}
if !checkEventConditionPatterns(params.Role, conditions.Options.RoleNames) {
return false
}
if len(conditions.Options.ProviderObjects) > 0 && !util.Contains(conditions.Options.ProviderObjects, params.ObjectType) {
if len(conditions.Options.ProviderObjects) > 0 && !slices.Contains(conditions.Options.ProviderObjects, params.ObjectType) {
return false
}
return true
}
func (*eventRulesContainer) checkFsEventMatch(conditions *dataprovider.EventConditions, params *EventParams) bool {
if !util.Contains(conditions.FsEvents, params.Event) {
if !slices.Contains(conditions.FsEvents, params.Event) {
return false
}
if !checkEventConditionPatterns(params.Name, conditions.Options.Names) {
@ -338,10 +349,10 @@ func (*eventRulesContainer) checkFsEventMatch(conditions *dataprovider.EventCond
if !checkEventConditionPatterns(params.VirtualPath, conditions.Options.FsPaths) {
return false
}
if len(conditions.Options.Protocols) > 0 && !util.Contains(conditions.Options.Protocols, params.Protocol) {
if len(conditions.Options.Protocols) > 0 && !slices.Contains(conditions.Options.Protocols, params.Protocol) {
return false
}
if params.Event == operationUpload || params.Event == operationDownload {
if slices.Contains(fsEventsWithSize, params.Event) {
if conditions.Options.MinFileSize > 0 {
if params.FileSize < conditions.Options.MinFileSize {
return false
@ -555,7 +566,7 @@ type EventParams struct {
IP string
Role string
Email string
Timestamp int64
Timestamp time.Time
UID string
IDPCustomFields *map[string]string
Object plugin.Renderer
@ -639,7 +650,7 @@ func (p *EventParams) setBackupParams(backupPath string) {
p.FsPath = backupPath
p.ObjectName = filepath.Base(backupPath)
p.VirtualPath = "/" + p.ObjectName
p.Timestamp = time.Now().UnixNano()
p.Timestamp = time.Now()
info, err := os.Stat(backupPath)
if err == nil {
p.FileSize = info.Size()
@ -765,46 +776,70 @@ func (p *EventParams) getRetentionReportsAsMailAttachment() (*mail.File, error)
}, nil
}
func (*EventParams) getStringReplacement(val string, jsonEscaped bool) string {
if jsonEscaped {
func (*EventParams) getStringReplacement(val string, escapeMode int) string {
switch escapeMode {
case 1:
return util.JSONEscape(val)
case 2:
return html.EscapeString(val)
default:
return val
}
return val
}
func (p *EventParams) getStringReplacements(addObjectData, jsonEscaped bool) []string {
func (p *EventParams) getStringReplacements(addObjectData bool, escapeMode int) []string {
var dateTimeString string
if Config.TZ == "local" {
dateTimeString = p.Timestamp.Local().Format(dateTimeMillisFormat)
} else {
dateTimeString = p.Timestamp.UTC().Format(dateTimeMillisFormat)
}
year := dateTimeString[0:4]
month := dateTimeString[5:7]
day := dateTimeString[8:10]
hour := dateTimeString[11:13]
minute := dateTimeString[14:16]
replacements := []string{
"{{Name}}", p.getStringReplacement(p.Name, jsonEscaped),
"{{Event}}", p.Event,
"{{Status}}", fmt.Sprintf("%d", p.Status),
"{{VirtualPath}}", p.getStringReplacement(p.VirtualPath, jsonEscaped),
"{{FsPath}}", p.getStringReplacement(p.FsPath, jsonEscaped),
"{{VirtualTargetPath}}", p.getStringReplacement(p.VirtualTargetPath, jsonEscaped),
"{{FsTargetPath}}", p.getStringReplacement(p.FsTargetPath, jsonEscaped),
"{{ObjectName}}", p.getStringReplacement(p.ObjectName, jsonEscaped),
"{{ObjectType}}", p.ObjectType,
"{{FileSize}}", strconv.FormatInt(p.FileSize, 10),
"{{Elapsed}}", strconv.FormatInt(p.Elapsed, 10),
"{{Protocol}}", p.Protocol,
"{{IP}}", p.IP,
"{{Role}}", p.getStringReplacement(p.Role, jsonEscaped),
"{{Email}}", p.getStringReplacement(p.Email, jsonEscaped),
"{{Timestamp}}", strconv.FormatInt(p.Timestamp, 10),
"{{StatusString}}", p.getStatusString(),
"{{UID}}", p.getStringReplacement(p.UID, jsonEscaped),
"{{Ext}}", p.getStringReplacement(p.Extension, jsonEscaped),
"{{.Name}}", p.getStringReplacement(p.Name, escapeMode),
"{{.Event}}", p.Event,
"{{.Status}}", fmt.Sprintf("%d", p.Status),
"{{.VirtualPath}}", p.getStringReplacement(p.VirtualPath, escapeMode),
"{{.EscapedVirtualPath}}", p.getStringReplacement(url.QueryEscape(p.VirtualPath), escapeMode),
"{{.FsPath}}", p.getStringReplacement(p.FsPath, escapeMode),
"{{.VirtualTargetPath}}", p.getStringReplacement(p.VirtualTargetPath, escapeMode),
"{{.FsTargetPath}}", p.getStringReplacement(p.FsTargetPath, escapeMode),
"{{.ObjectName}}", p.getStringReplacement(p.ObjectName, escapeMode),
"{{.ObjectBaseName}}", p.getStringReplacement(strings.TrimSuffix(p.ObjectName, p.Extension), escapeMode),
"{{.ObjectType}}", p.ObjectType,
"{{.FileSize}}", strconv.FormatInt(p.FileSize, 10),
"{{.Elapsed}}", strconv.FormatInt(p.Elapsed, 10),
"{{.Protocol}}", p.Protocol,
"{{.IP}}", p.IP,
"{{.Role}}", p.getStringReplacement(p.Role, escapeMode),
"{{.Email}}", p.getStringReplacement(p.Email, escapeMode),
"{{.Timestamp}}", strconv.FormatInt(p.Timestamp.UnixNano(), 10),
"{{.DateTime}}", dateTimeString,
"{{.Year}}", year,
"{{.Month}}", month,
"{{.Day}}", day,
"{{.Hour}}", hour,
"{{.Minute}}", minute,
"{{.StatusString}}", p.getStatusString(),
"{{.UID}}", p.getStringReplacement(p.UID, escapeMode),
"{{.Ext}}", p.getStringReplacement(p.Extension, escapeMode),
}
if p.VirtualPath != "" {
replacements = append(replacements, "{{VirtualDirPath}}", p.getStringReplacement(path.Dir(p.VirtualPath), jsonEscaped))
replacements = append(replacements, "{{.VirtualDirPath}}", p.getStringReplacement(path.Dir(p.VirtualPath), escapeMode))
}
if p.VirtualTargetPath != "" {
replacements = append(replacements, "{{VirtualTargetDirPath}}", p.getStringReplacement(path.Dir(p.VirtualTargetPath), jsonEscaped))
replacements = append(replacements, "{{TargetName}}", p.getStringReplacement(path.Base(p.VirtualTargetPath), jsonEscaped))
replacements = append(replacements, "{{.VirtualTargetDirPath}}", p.getStringReplacement(path.Dir(p.VirtualTargetPath), escapeMode))
replacements = append(replacements, "{{.TargetName}}", p.getStringReplacement(path.Base(p.VirtualTargetPath), escapeMode))
}
if len(p.errors) > 0 {
replacements = append(replacements, "{{ErrorString}}", p.getStringReplacement(strings.Join(p.errors, ", "), jsonEscaped))
replacements = append(replacements, "{{.ErrorString}}", p.getStringReplacement(strings.Join(p.errors, ", "), escapeMode))
} else {
replacements = append(replacements, "{{ErrorString}}", "")
replacements = append(replacements, "{{.ErrorString}}", "")
}
replacements = append(replacements, objDataPlaceholder, "{}")
replacements = append(replacements, objDataPlaceholderString, "")
@ -812,23 +847,23 @@ func (p *EventParams) getStringReplacements(addObjectData, jsonEscaped bool) []s
data, err := p.Object.RenderAsJSON(p.Event != operationDelete)
if err == nil {
dataString := util.BytesToString(data)
replacements[len(replacements)-3] = p.getStringReplacement(dataString, false)
replacements[len(replacements)-1] = p.getStringReplacement(dataString, true)
replacements[len(replacements)-3] = p.getStringReplacement(dataString, 0)
replacements[len(replacements)-1] = p.getStringReplacement(dataString, 1)
}
}
if p.IDPCustomFields != nil {
for k, v := range *p.IDPCustomFields {
replacements = append(replacements, fmt.Sprintf("{{IDPField%s}}", k), p.getStringReplacement(v, jsonEscaped))
replacements = append(replacements, fmt.Sprintf("{{.IDPField%s}}", k), p.getStringReplacement(v, escapeMode))
}
}
replacements = append(replacements, "{{Metadata}}", "{}")
replacements = append(replacements, "{{MetadataString}}", "")
replacements = append(replacements, "{{.Metadata}}", "{}")
replacements = append(replacements, "{{.MetadataString}}", "")
if len(p.Metadata) > 0 {
data, err := json.Marshal(p.Metadata)
if err == nil {
dataString := util.BytesToString(data)
replacements[len(replacements)-3] = p.getStringReplacement(dataString, false)
replacements[len(replacements)-1] = p.getStringReplacement(dataString, true)
replacements[len(replacements)-3] = p.getStringReplacement(dataString, 0)
replacements[len(replacements)-1] = p.getStringReplacement(dataString, 1)
}
}
return replacements
@ -908,10 +943,7 @@ func updateUserQuotaAfterFileWrite(conn *BaseConnection, virtualPath string, num
dataprovider.UpdateUserQuota(&conn.User, numFiles, fileSize, false) //nolint:errcheck
return
}
dataprovider.UpdateVirtualFolderQuota(&vfolder.BaseVirtualFolder, numFiles, fileSize, false) //nolint:errcheck
if vfolder.IsIncludedInUserQuota() {
dataprovider.UpdateUserQuota(&conn.User, numFiles, fileSize, false) //nolint:errcheck
}
dataprovider.UpdateUserFolderQuota(&vfolder, &conn.User, numFiles, fileSize, false)
}
func checkWriterPermsAndQuota(conn *BaseConnection, virtualPath string, numFiles int, expectedSize, truncatedSize int64) error {
@ -988,7 +1020,7 @@ func getFileWriter(conn *BaseConnection, virtualPath string, expectedSize int64)
return w, numFiles, truncatedSize, cancelFn, nil
}
func addZipEntry(wr *zipWriterWrapper, conn *BaseConnection, entryPath, baseDir string, recursion int) error {
func addZipEntry(wr *zipWriterWrapper, conn *BaseConnection, entryPath, baseDir string, info os.FileInfo, recursion int) error { //nolint:gocyclo
if entryPath == wr.Name {
// skip the archive itself
return nil
@ -998,10 +1030,13 @@ func addZipEntry(wr *zipWriterWrapper, conn *BaseConnection, entryPath, baseDir
return util.ErrRecursionTooDeep
}
recursion++
info, err := conn.DoStat(entryPath, 1, false)
if err != nil {
eventManagerLog(logger.LevelError, "unable to add zip entry %q, stat error: %v", entryPath, err)
return err
var err error
if info == nil {
info, err = conn.DoStat(entryPath, 1, false)
if err != nil {
eventManagerLog(logger.LevelError, "unable to add zip entry %q, stat error: %v", entryPath, err)
return err
}
}
entryName, err := getZipEntryName(entryPath, baseDir)
if err != nil {
@ -1039,7 +1074,7 @@ func addZipEntry(wr *zipWriterWrapper, conn *BaseConnection, entryPath, baseDir
}
for _, info := range contents {
fullPath := util.CleanPath(path.Join(entryPath, info.Name()))
if err := addZipEntry(wr, conn, fullPath, baseDir, recursion); err != nil {
if err := addZipEntry(wr, conn, fullPath, baseDir, info, recursion); err != nil {
eventManagerLog(logger.LevelError, "unable to add zip entry: %v", err)
return err
}
@ -1163,7 +1198,7 @@ func getMailAttachments(conn *BaseConnection, attachments []string, replacer *st
}
func replaceWithReplacer(input string, replacer *strings.Replacer) string {
if !strings.Contains(input, "{{") {
if !strings.Contains(input, "{{.") {
return input
}
return replacer.Replace(input)
@ -1250,7 +1285,7 @@ func getHTTPRuleActionEndpoint(c *dataprovider.EventActionHTTPConfig, replacer *
if err != nil {
return "", fmt.Errorf("invalid endpoint: %w", err)
}
if strings.Contains(u.Path, "{{") {
if strings.Contains(u.Path, "{{.") {
pathComponents := strings.Split(u.Path, "/")
for idx := range pathComponents {
part := replaceWithReplacer(pathComponents[idx], replacer)
@ -1284,7 +1319,7 @@ func writeHTTPPart(m *multipart.Writer, part dataprovider.HTTPPart, h textproto.
if part.Body != "" {
cType := h.Get("Content-Type")
if strings.Contains(strings.ToLower(cType), "application/json") {
replacements := params.getStringReplacements(addObjectData, true)
replacements := params.getStringReplacements(addObjectData, 1)
jsonReplacer := strings.NewReplacer(replacements...)
_, err = partWriter.Write(util.StringToBytes(replaceWithReplacer(part.Body, jsonReplacer)))
} else {
@ -1316,7 +1351,7 @@ func writeHTTPPart(m *multipart.Writer, part dataprovider.HTTPPart, h textproto.
return nil
}
func getHTTPRuleActionBody(c *dataprovider.EventActionHTTPConfig, replacer *strings.Replacer,
func getHTTPRuleActionBody(c *dataprovider.EventActionHTTPConfig, replacer *strings.Replacer, //nolint:gocyclo
cancel context.CancelFunc, user dataprovider.User, params *EventParams, addObjectData bool,
) (io.Reader, string, error) {
var body io.Reader
@ -1332,7 +1367,7 @@ func getHTTPRuleActionBody(c *dataprovider.EventActionHTTPConfig, replacer *stri
return bytes.NewBuffer(data), "", nil
}
if c.HasJSONBody() {
replacements := params.getStringReplacements(addObjectData, true)
replacements := params.getStringReplacements(addObjectData, 1)
jsonReplacer := strings.NewReplacer(replacements...)
return bytes.NewBufferString(replaceWithReplacer(c.Body, jsonReplacer)), "", nil
}
@ -1345,8 +1380,7 @@ func getHTTPRuleActionBody(c *dataprovider.EventActionHTTPConfig, replacer *stri
var conn *BaseConnection
if user.Username != "" {
var err error
user, err = getUserForEventAction(user)
if err != nil {
if err := getUserForEventAction(&user); err != nil {
return body, "", err
}
connectionID := fmt.Sprintf("%s_%s", protocolEventAction, xid.New().String())
@ -1362,6 +1396,9 @@ func getHTTPRuleActionBody(c *dataprovider.EventActionHTTPConfig, replacer *stri
go func() {
defer w.Close()
defer user.CloseFs() //nolint:errcheck
if conn != nil {
defer conn.CloseFS() //nolint:errcheck
}
for _, part := range c.Parts {
h := make(textproto.MIMEHeader)
@ -1417,7 +1454,7 @@ func executeHTTPRuleAction(c dataprovider.EventActionHTTPConfig, params *EventPa
addObjectData = c.HasObjectData()
}
replacements := params.getStringReplacements(addObjectData, false)
replacements := params.getStringReplacements(addObjectData, 0)
replacer := strings.NewReplacer(replacements...)
endpoint, err := getHTTPRuleActionEndpoint(&c, replacer)
if err != nil {
@ -1467,7 +1504,7 @@ func executeHTTPRuleAction(c dataprovider.EventActionHTTPConfig, params *EventPa
if resp.StatusCode < http.StatusOK || resp.StatusCode > http.StatusNoContent {
if rb, err := io.ReadAll(io.LimitReader(resp.Body, 2048)); err == nil {
eventManagerLog(logger.LevelDebug, "error notification response from endpoint %q: %s",
endpoint, util.BytesToString(rb))
endpoint, rb)
}
return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
@ -1476,6 +1513,9 @@ func executeHTTPRuleAction(c dataprovider.EventActionHTTPConfig, params *EventPa
}
func executeCommandRuleAction(c dataprovider.EventActionCommandConfig, params *EventParams) error {
if !dataprovider.IsActionCommandAllowed(c.Cmd) {
return fmt.Errorf("command %q is not allowed", c.Cmd)
}
addObjectData := false
if params.Object != nil {
for _, k := range c.EnvVars {
@ -1485,7 +1525,7 @@ func executeCommandRuleAction(c dataprovider.EventActionCommandConfig, params *E
}
}
}
replacements := params.getStringReplacements(addObjectData, false)
replacements := params.getStringReplacements(addObjectData, 0)
replacer := strings.NewReplacer(replacements...)
args := make([]string, 0, len(c.Args))
@ -1499,7 +1539,7 @@ func executeCommandRuleAction(c dataprovider.EventActionCommandConfig, params *E
cmd := exec.CommandContext(ctx, c.Cmd, args...)
cmd.Env = []string{}
for _, keyVal := range c.EnvVars {
if keyVal.Value == "$" {
if keyVal.Value == "$" && !strings.HasPrefix(strings.ToUpper(keyVal.Key), "SFTPGO_") {
val := os.Getenv(keyVal.Key)
if val == "" {
eventManagerLog(logger.LevelDebug, "empty value for environment variable %q", keyVal.Key)
@ -1540,9 +1580,16 @@ func executeEmailRuleAction(c dataprovider.EventActionEmailConfig, params *Event
addObjectData = true
}
}
replacements := params.getStringReplacements(addObjectData, false)
replacements := params.getStringReplacements(addObjectData, 0)
replacer := strings.NewReplacer(replacements...)
body := replaceWithReplacer(c.Body, replacer)
var body string
if c.ContentType == 1 {
replacements := params.getStringReplacements(addObjectData, 2)
bodyReplacer := strings.NewReplacer(replacements...)
body = replaceWithReplacer(c.Body, bodyReplacer)
} else {
body = replaceWithReplacer(c.Body, replacer)
}
subject := replaceWithReplacer(c.Subject, replacer)
recipients := getEmailAddressesWithReplacer(c.Recipients, replacer)
bcc := getEmailAddressesWithReplacer(c.Bcc, replacer)
@ -1565,8 +1612,7 @@ func executeEmailRuleAction(c dataprovider.EventActionEmailConfig, params *Event
if err != nil {
return err
}
user, err = getUserForEventAction(user)
if err != nil {
if err := getUserForEventAction(&user); err != nil {
return err
}
connectionID := fmt.Sprintf("%s_%s", protocolEventAction, xid.New().String())
@ -1576,6 +1622,8 @@ func executeEmailRuleAction(c dataprovider.EventActionEmailConfig, params *Event
return fmt.Errorf("error getting email attachments, unable to check root fs for user %q: %w", user.Username, err)
}
conn := NewBaseConnection(connectionID, protocolEventAction, "", "", user)
defer conn.CloseFS() //nolint:errcheck
res, err := getMailAttachments(conn, fileAttachments, replacer)
if err != nil {
return err
@ -1591,11 +1639,11 @@ func executeEmailRuleAction(c dataprovider.EventActionEmailConfig, params *Event
return nil
}
func getUserForEventAction(user dataprovider.User) (dataprovider.User, error) {
func getUserForEventAction(user *dataprovider.User) error {
err := user.LoadAndApplyGroupSettings()
if err != nil {
eventManagerLog(logger.LevelError, "unable to get group for user %q: %+v", user.Username, err)
return dataprovider.User{}, fmt.Errorf("unable to get groups for user %q", user.Username)
return fmt.Errorf("unable to get groups for user %q", user.Username)
}
user.UploadDataTransfer = 0
user.UploadBandwidth = 0
@ -1606,7 +1654,7 @@ func getUserForEventAction(user dataprovider.User) (dataprovider.User, error) {
for k := range user.Permissions {
user.Permissions[k] = []string{dataprovider.PermAny}
}
return user, nil
return nil
}
func replacePathsPlaceholders(paths []string, replacer *strings.Replacer) []string {
@ -1626,17 +1674,18 @@ func executeDeleteFileFsAction(conn *BaseConnection, item string, info os.FileIn
}
func executeDeleteFsActionForUser(deletes []string, replacer *strings.Replacer, user dataprovider.User) error {
user, err := getUserForEventAction(user)
if err != nil {
if err := getUserForEventAction(&user); err != nil {
return err
}
connectionID := fmt.Sprintf("%s_%s", protocolEventAction, xid.New().String())
err = user.CheckFsRoot(connectionID)
err := user.CheckFsRoot(connectionID)
defer user.CloseFs() //nolint:errcheck
if err != nil {
return fmt.Errorf("delete error, unable to check root fs for user %q: %w", user.Username, err)
}
conn := NewBaseConnection(connectionID, protocolEventAction, "", "", user)
defer conn.CloseFS() //nolint:errcheck
for _, item := range replacePathsPlaceholders(deletes, replacer) {
info, err := conn.DoStat(item, 0, false)
if err != nil {
@ -1694,17 +1743,18 @@ func executeDeleteFsRuleAction(deletes []string, replacer *strings.Replacer,
}
func executeMkDirsFsActionForUser(dirs []string, replacer *strings.Replacer, user dataprovider.User) error {
user, err := getUserForEventAction(user)
if err != nil {
if err := getUserForEventAction(&user); err != nil {
return err
}
connectionID := fmt.Sprintf("%s_%s", protocolEventAction, xid.New().String())
err = user.CheckFsRoot(connectionID)
err := user.CheckFsRoot(connectionID)
defer user.CloseFs() //nolint:errcheck
if err != nil {
return fmt.Errorf("mkdir error, unable to check root fs for user %q: %w", user.Username, err)
}
conn := NewBaseConnection(connectionID, protocolEventAction, "", "", user)
defer conn.CloseFS() //nolint:errcheck
for _, item := range replacePathsPlaceholders(dirs, replacer) {
if err = conn.CheckParentDirs(path.Dir(item)); err != nil {
return fmt.Errorf("unable to check parent dirs for %q, user %q: %w", item, user.Username, err)
@ -1750,24 +1800,29 @@ func executeMkdirFsRuleAction(dirs []string, replacer *strings.Replacer,
return nil
}
func executeRenameFsActionForUser(renames []dataprovider.KeyValue, replacer *strings.Replacer,
func executeRenameFsActionForUser(renames []dataprovider.RenameConfig, replacer *strings.Replacer,
user dataprovider.User,
) error {
user, err := getUserForEventAction(user)
if err != nil {
if err := getUserForEventAction(&user); err != nil {
return err
}
connectionID := fmt.Sprintf("%s_%s", protocolEventAction, xid.New().String())
err = user.CheckFsRoot(connectionID)
err := user.CheckFsRoot(connectionID)
defer user.CloseFs() //nolint:errcheck
if err != nil {
return fmt.Errorf("rename error, unable to check root fs for user %q: %w", user.Username, err)
}
conn := NewBaseConnection(connectionID, protocolEventAction, "", "", user)
defer conn.CloseFS() //nolint:errcheck
for _, item := range renames {
source := util.CleanPath(replaceWithReplacer(item.Key, replacer))
target := util.CleanPath(replaceWithReplacer(item.Value, replacer))
if err = conn.renameInternal(source, target, true); err != nil {
checks := 0
if item.UpdateModTime {
checks += vfs.CheckUpdateModTime
}
if err = conn.renameInternal(source, target, true, checks); err != nil {
return fmt.Errorf("unable to rename %q->%q, user %q: %w", source, target, user.Username, err)
}
eventManagerLog(logger.LevelDebug, "rename %q->%q ok, user %q", source, target, user.Username)
@ -1775,21 +1830,22 @@ func executeRenameFsActionForUser(renames []dataprovider.KeyValue, replacer *str
return nil
}
func executeCopyFsActionForUser(copy []dataprovider.KeyValue, replacer *strings.Replacer,
func executeCopyFsActionForUser(keyVals []dataprovider.KeyValue, replacer *strings.Replacer,
user dataprovider.User,
) error {
user, err := getUserForEventAction(user)
if err != nil {
if err := getUserForEventAction(&user); err != nil {
return err
}
connectionID := fmt.Sprintf("%s_%s", protocolEventAction, xid.New().String())
err = user.CheckFsRoot(connectionID)
err := user.CheckFsRoot(connectionID)
defer user.CloseFs() //nolint:errcheck
if err != nil {
return fmt.Errorf("copy error, unable to check root fs for user %q: %w", user.Username, err)
}
conn := NewBaseConnection(connectionID, protocolEventAction, "", "", user)
for _, item := range copy {
defer conn.CloseFS() //nolint:errcheck
for _, item := range keyVals {
source := util.CleanPath(replaceWithReplacer(item.Key, replacer))
target := util.CleanPath(replaceWithReplacer(item.Value, replacer))
if strings.HasSuffix(item.Key, "/") {
@ -1809,17 +1865,18 @@ func executeCopyFsActionForUser(copy []dataprovider.KeyValue, replacer *strings.
func executeExistFsActionForUser(exist []string, replacer *strings.Replacer,
user dataprovider.User,
) error {
user, err := getUserForEventAction(user)
if err != nil {
if err := getUserForEventAction(&user); err != nil {
return err
}
connectionID := fmt.Sprintf("%s_%s", protocolEventAction, xid.New().String())
err = user.CheckFsRoot(connectionID)
err := user.CheckFsRoot(connectionID)
defer user.CloseFs() //nolint:errcheck
if err != nil {
return fmt.Errorf("existence check error, unable to check root fs for user %q: %w", user.Username, err)
}
conn := NewBaseConnection(connectionID, protocolEventAction, "", "", user)
defer conn.CloseFS() //nolint:errcheck
for _, item := range replacePathsPlaceholders(exist, replacer) {
if _, err = conn.DoStat(item, 0, false); err != nil {
return fmt.Errorf("error checking existence for path %q, user %q: %w", item, user.Username, err)
@ -1829,7 +1886,7 @@ func executeExistFsActionForUser(exist []string, replacer *strings.Replacer,
return nil
}
func executeRenameFsRuleAction(renames []dataprovider.KeyValue, replacer *strings.Replacer,
func executeRenameFsRuleAction(renames []dataprovider.RenameConfig, replacer *strings.Replacer,
conditions dataprovider.ConditionOptions, params *EventParams,
) error {
users, err := params.getUsers()
@ -1863,7 +1920,7 @@ func executeRenameFsRuleAction(renames []dataprovider.KeyValue, replacer *string
return nil
}
func executeCopyFsRuleAction(copy []dataprovider.KeyValue, replacer *strings.Replacer,
func executeCopyFsRuleAction(keyVals []dataprovider.KeyValue, replacer *strings.Replacer,
conditions dataprovider.ConditionOptions, params *EventParams,
) error {
users, err := params.getUsers()
@ -1882,7 +1939,7 @@ func executeCopyFsRuleAction(copy []dataprovider.KeyValue, replacer *strings.Rep
}
}
executed++
if err = executeCopyFsActionForUser(copy, replacer, user); err != nil {
if err = executeCopyFsActionForUser(keyVals, replacer, user); err != nil {
failures = append(failures, user.Username)
params.AddError(err)
}
@ -1967,17 +2024,18 @@ func estimateZipSize(conn *BaseConnection, zipPath string, paths []string) (int6
func executeCompressFsActionForUser(c dataprovider.EventActionFsCompress, replacer *strings.Replacer,
user dataprovider.User,
) error {
user, err := getUserForEventAction(user)
if err != nil {
if err := getUserForEventAction(&user); err != nil {
return err
}
connectionID := fmt.Sprintf("%s_%s", protocolEventAction, xid.New().String())
err = user.CheckFsRoot(connectionID)
err := user.CheckFsRoot(connectionID)
defer user.CloseFs() //nolint:errcheck
if err != nil {
return fmt.Errorf("compress error, unable to check root fs for user %q: %w", user.Username, err)
}
conn := NewBaseConnection(connectionID, protocolEventAction, "", "", user)
defer conn.CloseFS() //nolint:errcheck
name := util.CleanPath(replaceWithReplacer(c.Name, replacer))
conn.CheckParentDirs(path.Dir(name)) //nolint:errcheck
paths := make([]string, 0, len(c.Paths))
@ -2011,7 +2069,7 @@ func executeCompressFsActionForUser(c dataprovider.EventActionFsCompress, replac
}
startTime := time.Now()
for _, item := range paths {
if err := addZipEntry(zipWriter, conn, item, baseDir, 0); err != nil {
if err := addZipEntry(zipWriter, conn, item, baseDir, nil, 0); err != nil {
closeWriterAndUpdateQuota(writer, conn, name, "", numFiles, truncatedSize, err, operationUpload, startTime) //nolint:errcheck
return err
}
@ -2096,7 +2154,7 @@ func executeFsRuleAction(c dataprovider.EventActionFilesystemConfig, conditions
params *EventParams,
) error {
addObjectData := false
replacements := params.getStringReplacements(addObjectData, false)
replacements := params.getStringReplacements(addObjectData, 0)
replacer := strings.NewReplacer(replacements...)
switch c.Type {
case dataprovider.FilesystemActionRename:
@ -2448,7 +2506,7 @@ func executePwdExpirationCheckForUser(user *dataprovider.User, config dataprovid
}
subject := "SFTPGo password expiration notification"
startTime := time.Now()
if err := smtp.SendEmail([]string{user.Email}, nil, subject, body.String(), smtp.EmailContentTypeTextHTML); err != nil {
if err := smtp.SendEmail(user.GetEmailAddresses(), nil, subject, body.String(), smtp.EmailContentTypeTextHTML); err != nil {
eventManagerLog(logger.LevelError, "unable to notify password expiration for user %s: %v, elapsed: %s",
user.Username, err, time.Since(startTime))
return err
@ -2496,7 +2554,7 @@ func executeAdminCheckAction(c *dataprovider.EventActionIDPAccountCheck, params
return nil, err
}
replacements := params.getStringReplacements(false, true)
replacements := params.getStringReplacements(false, 1)
replacer := strings.NewReplacer(replacements...)
data := replaceWithReplacer(c.TemplateAdmin, replacer)
@ -2505,19 +2563,57 @@ func executeAdminCheckAction(c *dataprovider.EventActionIDPAccountCheck, params
if err != nil {
return nil, err
}
if newAdmin.Password == "" {
newAdmin.Password = util.GenerateUniqueID()
}
if exists {
eventManagerLog(logger.LevelDebug, "updating admin %q after IDP login", params.Name)
// Not sure if this makes sense, but it shouldn't hurt.
if newAdmin.Password == "" {
newAdmin.Password = admin.Password
}
newAdmin.Filters.TOTPConfig = admin.Filters.TOTPConfig
newAdmin.Filters.RecoveryCodes = admin.Filters.RecoveryCodes
err = dataprovider.UpdateAdmin(&newAdmin, dataprovider.ActionExecutorSystem, "", "")
} else {
eventManagerLog(logger.LevelDebug, "creating admin %q after IDP login", params.Name)
if newAdmin.Password == "" {
newAdmin.Password = util.GenerateUniqueID()
}
err = dataprovider.AddAdmin(&newAdmin, dataprovider.ActionExecutorSystem, "", "")
}
return &newAdmin, err
}
func preserveUserProfile(user, newUser *dataprovider.User) {
if newUser.CanChangePassword() && user.Password != "" {
newUser.Password = user.Password
}
if newUser.CanManagePublicKeys() && len(user.PublicKeys) > 0 {
newUser.PublicKeys = user.PublicKeys
}
if newUser.CanManageTLSCerts() {
if len(user.Filters.TLSCerts) > 0 {
newUser.Filters.TLSCerts = user.Filters.TLSCerts
}
}
if newUser.CanChangeInfo() {
if user.Description != "" {
newUser.Description = user.Description
}
if user.Email != "" {
newUser.Email = user.Email
}
if len(user.Filters.AdditionalEmails) > 0 {
newUser.Filters.AdditionalEmails = user.Filters.AdditionalEmails
}
}
if newUser.CanChangeAPIKeyAuth() {
newUser.Filters.AllowAPIKeyAuth = user.Filters.AllowAPIKeyAuth
}
newUser.Filters.RecoveryCodes = user.Filters.RecoveryCodes
newUser.Filters.TOTPConfig = user.Filters.TOTPConfig
newUser.LastPasswordChange = user.LastPasswordChange
newUser.SetEmptySecretsIfNil()
}
func executeUserCheckAction(c *dataprovider.EventActionIDPAccountCheck, params *EventParams) (*dataprovider.User, error) {
user, err := dataprovider.UserExists(params.Name, "")
exists := err == nil
@ -2528,7 +2624,7 @@ func executeUserCheckAction(c *dataprovider.EventActionIDPAccountCheck, params *
if err != nil && !errors.Is(err, util.ErrNotFound) {
return nil, err
}
replacements := params.getStringReplacements(false, true)
replacements := params.getStringReplacements(false, 1)
replacer := strings.NewReplacer(replacements...)
data := replaceWithReplacer(c.TemplateUser, replacer)
@ -2539,6 +2635,7 @@ func executeUserCheckAction(c *dataprovider.EventActionIDPAccountCheck, params *
}
if exists {
eventManagerLog(logger.LevelDebug, "updating user %q after IDP login", params.Name)
preserveUserProfile(&user, &newUser)
err = dataprovider.UpdateUser(&newUser, dataprovider.ActionExecutorSystem, "", "")
} else {
eventManagerLog(logger.LevelDebug, "creating user %q after IDP login", params.Name)
@ -2551,9 +2648,14 @@ func executeUserCheckAction(c *dataprovider.EventActionIDPAccountCheck, params *
return &u, err
}
func executeRuleAction(action dataprovider.BaseEventAction, params *EventParams,
func executeRuleAction(action dataprovider.BaseEventAction, params *EventParams, //nolint:gocyclo
conditions dataprovider.ConditionOptions,
) error {
if len(conditions.EventStatuses) > 0 && !slices.Contains(conditions.EventStatuses, params.Status) {
eventManagerLog(logger.LevelDebug, "skipping action %s, event status %d does not match: %v",
action.Name, params.Status, conditions.EventStatuses)
return nil
}
var err error
switch action.Type {
@ -2585,6 +2687,8 @@ func executeRuleAction(action dataprovider.BaseEventAction, params *EventParams,
err = executeUserExpirationCheckRuleAction(conditions, params)
case dataprovider.ActionTypeUserInactivityCheck:
err = executeUserInactivityCheckRuleAction(action.Options.UserInactivityConfig, conditions, params, time.Now())
case dataprovider.ActionTypeRotateLogs:
err = logger.RotateLogFile()
default:
err = fmt.Errorf("unsupported action type: %d", action.Type)
}
@ -2742,6 +2846,16 @@ func (j *eventCronJob) getTask(rule *dataprovider.EventRule) (dataprovider.Task,
return dataprovider.Task{}, nil
}
func (j *eventCronJob) getEventParams() EventParams {
return EventParams{
Event: "Schedule",
Name: j.ruleName,
Status: 1,
Timestamp: time.Now(),
updateStatusFromError: true,
}
}
func (j *eventCronJob) Run() {
eventManagerLog(logger.LevelDebug, "executing scheduled rule %q", j.ruleName)
rule, err := dataprovider.EventRuleExists(j.ruleName)
@ -2792,9 +2906,9 @@ func (j *eventCronJob) Run() {
}
}(task.Name)
executeAsyncRulesActions([]dataprovider.EventRule{rule}, EventParams{Status: 1, updateStatusFromError: true})
executeAsyncRulesActions([]dataprovider.EventRule{rule}, j.getEventParams())
} else {
executeAsyncRulesActions([]dataprovider.EventRule{rule}, EventParams{Status: 1, updateStatusFromError: true})
executeAsyncRulesActions([]dataprovider.EventRule{rule}, j.getEventParams())
}
eventManagerLog(logger.LevelDebug, "execution for scheduled rule %q finished", j.ruleName)
}

View file

@ -800,6 +800,32 @@ func TestEventManagerErrors(t *testing.T) {
stopEventScheduler()
}
func TestDateTimePlaceholder(t *testing.T) {
oldTZ := Config.TZ
Config.TZ = ""
dateTime := time.Now()
params := EventParams{
Timestamp: dateTime,
}
replacements := params.getStringReplacements(false, 0)
r := strings.NewReplacer(replacements...)
res := r.Replace("{{.DateTime}}")
assert.Equal(t, dateTime.UTC().Format(dateTimeMillisFormat), res)
res = r.Replace("{{.Year}}-{{.Month}}-{{.Day}}T{{.Hour}}:{{.Minute}}")
assert.Equal(t, dateTime.UTC().Format(dateTimeMillisFormat)[:16], res)
Config.TZ = "local"
replacements = params.getStringReplacements(false, 0)
r = strings.NewReplacer(replacements...)
res = r.Replace("{{.DateTime}}")
assert.Equal(t, dateTime.Local().Format(dateTimeMillisFormat), res)
res = r.Replace("{{.Year}}-{{.Month}}-{{.Day}}T{{.Hour}}:{{.Minute}}")
assert.Equal(t, dateTime.Local().Format(dateTimeMillisFormat)[:16], res)
Config.TZ = oldTZ
}
func TestEventRuleActions(t *testing.T) {
actionName := "test rule action"
action := dataprovider.BaseEventAction{
@ -819,7 +845,7 @@ func TestEventRuleActions(t *testing.T) {
HTTPConfig: dataprovider.EventActionHTTPConfig{
Endpoint: "http://foo\x7f.com/", // invalid URL
SkipTLSVerify: true,
Body: `"data": "{{ObjectDataString}}"`,
Body: `"data": "{{.ObjectDataString}}"`,
Method: http.MethodPost,
QueryParameters: []dataprovider.KeyValue{
{
@ -887,7 +913,7 @@ func TestEventRuleActions(t *testing.T) {
assert.Contains(t, getErrorString(err), "error getting user")
action.Options.HTTPConfig.Parts = nil
action.Options.HTTPConfig.Body = "{{ObjectData}}"
action.Options.HTTPConfig.Body = "{{.ObjectData}}"
// test disk and transfer quota reset
username1 := "user1"
username2 := "user2"
@ -1166,10 +1192,12 @@ func TestEventRuleActions(t *testing.T) {
action.Options = dataprovider.BaseEventActionOptions{
FsConfig: dataprovider.EventActionFilesystemConfig{
Type: dataprovider.FilesystemActionRename,
Renames: []dataprovider.KeyValue{
Renames: []dataprovider.RenameConfig{
{
Key: "/source",
Value: "/target",
KeyValue: dataprovider.KeyValue{
Key: "/source",
Value: "/target",
},
},
},
},
@ -1221,7 +1249,7 @@ func TestEventRuleActions(t *testing.T) {
Type: dataprovider.FilesystemActionCompress,
Compress: dataprovider.EventActionFsCompress{
Name: "test.zip",
Paths: []string{"/{{VirtualPath}}"},
Paths: []string{"/{{.VirtualPath}}"},
},
},
}
@ -1386,6 +1414,31 @@ func TestIDPAccountCheckRule(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, username, user.Username)
assert.Equal(t, 1, user.Status)
assert.Empty(t, user.Password)
assert.Len(t, user.PublicKeys, 0)
assert.Len(t, user.Filters.TLSCerts, 0)
assert.Empty(t, user.Email)
assert.Empty(t, user.Description)
// Update the profile attribute and make sure they are preserved
user.Password = "secret"
user.Email = "example@example.com"
user.Filters.AdditionalEmails = []string{"alias@example.com"}
user.Description = "some desc"
user.Filters.TLSCerts = []string{serverCert}
user.PublicKeys = []string{"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC03jj0D+djk7pxIf/0OhrxrchJTRZklofJ1NoIu4752Sq02mdXmarMVsqJ1cAjV5LBVy3D1F5U6XW4rppkXeVtd04Pxb09ehtH0pRRPaoHHlALiJt8CoMpbKYMA8b3KXPPriGxgGomvtU2T2RMURSwOZbMtpsugfjYSWenyYX+VORYhylWnSXL961LTyC21ehd6d6QnW9G7E5hYMITMY9TuQZz3bROYzXiTsgN0+g6Hn7exFQp50p45StUMfV/SftCMdCxlxuyGny2CrN/vfjO7xxOo2uv7q1qm10Q46KPWJQv+pgZ/OfL+EDjy07n5QVSKHlbx+2nT4Q0EgOSQaCTYwn3YjtABfIxWwgAFdyj6YlPulCL22qU4MYhDcA6PSBwDdf8hvxBfvsiHdM+JcSHvv8/VeJhk6CmnZxGY0fxBupov27z3yEO8nAg8k+6PaUiW1MSUfuGMF/ktB8LOstXsEPXSszuyXiOv4DaryOXUiSn7bmRqKcEFlJusO6aZP0= nicola@p1"}
err = dataprovider.UpdateUser(user, "", "", "")
assert.NoError(t, err)
user, err = executeUserCheckAction(c, params)
assert.NoError(t, err)
assert.Equal(t, username, user.Username)
assert.Equal(t, 1, user.Status)
assert.NotEmpty(t, user.Password)
assert.Len(t, user.PublicKeys, 1)
assert.Len(t, user.Filters.TLSCerts, 1)
assert.NotEmpty(t, user.Email)
assert.Len(t, user.Filters.AdditionalEmails, 1)
assert.NotEmpty(t, user.Description)
err = dataprovider.DeleteUser(username, "", "", "")
assert.NoError(t, err)
@ -1686,10 +1739,12 @@ func TestFilesystemActionErrors(t *testing.T) {
assert.NoError(t, err)
err = dataprovider.AddUser(&user, "", "", "")
assert.NoError(t, err)
err = executeRenameFsActionForUser([]dataprovider.KeyValue{
err = executeRenameFsActionForUser([]dataprovider.RenameConfig{
{
Key: "/p1",
Value: "/p1",
KeyValue: dataprovider.KeyValue{
Key: "/p1",
Value: "/p1",
},
},
}, testReplacer, user)
if assert.Error(t, err) {
@ -1700,10 +1755,12 @@ func TestFilesystemActionErrors(t *testing.T) {
Options: dataprovider.BaseEventActionOptions{
FsConfig: dataprovider.EventActionFilesystemConfig{
Type: dataprovider.FilesystemActionRename,
Renames: []dataprovider.KeyValue{
Renames: []dataprovider.RenameConfig{
{
Key: "/p2",
Value: "/p2",
KeyValue: dataprovider.KeyValue{
Key: "/p2",
Value: "/p2",
},
},
},
},
@ -1789,7 +1846,7 @@ func TestFilesystemActionErrors(t *testing.T) {
Writer: zip.NewWriter(bytes.NewBuffer(nil)),
Entries: map[string]bool{},
}
err = addZipEntry(wr, conn, "/adir/sub/f.dat", "/adir/sub/sub", 0)
err = addZipEntry(wr, conn, "/adir/sub/f.dat", "/adir/sub/sub", nil, 0)
assert.Error(t, err)
assert.Contains(t, getErrorString(err), "is outside base dir")
}
@ -1799,7 +1856,7 @@ func TestFilesystemActionErrors(t *testing.T) {
Writer: zip.NewWriter(bytes.NewBuffer(nil)),
Entries: map[string]bool{},
}
err = addZipEntry(wr, conn, "/p1", "/", 2000)
err = addZipEntry(wr, conn, "/p1", "/", nil, 2000)
assert.ErrorIs(t, err, util.ErrRecursionTooDeep)
err = dataprovider.DeleteUser(username, "", "", "")
@ -1897,12 +1954,27 @@ func TestScheduledActions(t *testing.T) {
backupsPath := filepath.Join(os.TempDir(), "backups")
err := os.RemoveAll(backupsPath)
assert.NoError(t, err)
now := time.Now().UTC().Format(dateTimeMillisFormat)
// The backup action sets the home directory to the backup path.
expectedDirPath := filepath.Join(backupsPath, fmt.Sprintf("%s_%s_%s", now[0:4], now[5:7], now[8:10]))
action := &dataprovider.BaseEventAction{
Name: "action",
action1 := &dataprovider.BaseEventAction{
Name: "action1",
Type: dataprovider.ActionTypeBackup,
}
err = dataprovider.AddEventAction(action, "", "", "")
err = dataprovider.AddEventAction(action1, "", "", "")
assert.NoError(t, err)
action2 := &dataprovider.BaseEventAction{
Name: "action2",
Type: dataprovider.ActionTypeFilesystem,
Options: dataprovider.BaseEventActionOptions{
FsConfig: dataprovider.EventActionFilesystemConfig{
Type: dataprovider.FilesystemActionMkdirs,
MkDirs: []string{"{{.Year}}_{{.Month}}_{{.Day}}"},
},
},
}
err = dataprovider.AddEventAction(action2, "", "", "")
assert.NoError(t, err)
rule := &dataprovider.EventRule{
Name: "rule",
@ -1921,10 +1993,16 @@ func TestScheduledActions(t *testing.T) {
Actions: []dataprovider.EventAction{
{
BaseEventAction: dataprovider.BaseEventAction{
Name: action.Name,
Name: action1.Name,
},
Order: 1,
},
{
BaseEventAction: dataprovider.BaseEventAction{
Name: action2.Name,
},
Order: 2,
},
},
}
@ -1939,9 +2017,10 @@ func TestScheduledActions(t *testing.T) {
job.Run()
assert.DirExists(t, backupsPath)
assert.DirExists(t, expectedDirPath)
action.Type = dataprovider.ActionTypeEmail
action.Options = dataprovider.BaseEventActionOptions{
action1.Type = dataprovider.ActionTypeEmail
action1.Options = dataprovider.BaseEventActionOptions{
EmailConfig: dataprovider.EventActionEmailConfig{
Recipients: []string{"example@example.com"},
Subject: "test with attachments",
@ -1949,16 +2028,19 @@ func TestScheduledActions(t *testing.T) {
Attachments: []string{"/file1.txt"},
},
}
err = dataprovider.UpdateEventAction(action, "", "", "")
err = dataprovider.UpdateEventAction(action1, "", "", "")
assert.NoError(t, err)
job.Run() // action is not compatible with a scheduled rule
err = dataprovider.DeleteEventRule(rule.Name, "", "", "")
assert.NoError(t, err)
err = dataprovider.DeleteEventAction(action.Name, "", "", "")
err = dataprovider.DeleteEventAction(action1.Name, "", "", "")
assert.NoError(t, err)
err = dataprovider.DeleteEventAction(action2.Name, "", "", "")
assert.NoError(t, err)
err = os.RemoveAll(backupsPath)
assert.NoError(t, err)
stopEventScheduler()
}
@ -2077,11 +2159,11 @@ func TestWriteHTTPPartsError(t *testing.T) {
}
func TestReplacePathsPlaceholders(t *testing.T) {
replacer := strings.NewReplacer("{{VirtualPath}}", "/path1")
paths := []string{"{{VirtualPath}}", "/path1"}
replacer := strings.NewReplacer("{{.VirtualPath}}", "/path1")
paths := []string{"{{.VirtualPath}}", "/path1"}
paths = replacePathsPlaceholders(paths, replacer)
assert.Equal(t, []string{"/path1"}, paths)
paths = []string{"{{VirtualPath}}", "/path2"}
paths = []string{"{{.VirtualPath}}", "/path2"}
paths = replacePathsPlaceholders(paths, replacer)
assert.Equal(t, []string{"/path1", "/path2"}, paths)
}
@ -2174,7 +2256,7 @@ func TestOnDemandRule(t *testing.T) {
Recipients: []string{"example@example.org"},
Subject: "subject",
Body: "body",
Attachments: []string{"/{{VirtualPath}}"},
Attachments: []string{"/{{.VirtualPath}}"},
},
},
}
@ -2215,21 +2297,21 @@ func getErrorString(err error) string {
func TestHTTPEndpointWithPlaceholders(t *testing.T) {
c := dataprovider.EventActionHTTPConfig{
Endpoint: "http://127.0.0.1:8080/base/url/{{Name}}/{{VirtualPath}}/upload",
Endpoint: "http://127.0.0.1:8080/base/url/{{.Name}}/{{.VirtualPath}}/upload",
QueryParameters: []dataprovider.KeyValue{
{
Key: "u",
Value: "{{Name}}",
Value: "{{.Name}}",
},
{
Key: "p",
Value: "{{VirtualPath}}",
Value: "{{.VirtualPath}}",
},
},
}
name := "uname"
vPath := "/a dir/@ file.txt"
replacer := strings.NewReplacer("{{Name}}", name, "{{VirtualPath}}", vPath)
replacer := strings.NewReplacer("{{.Name}}", name, "{{.VirtualPath}}", vPath)
u, err := getHTTPRuleActionEndpoint(&c, replacer)
assert.NoError(t, err)
expected := "http://127.0.0.1:8080/base/url/" + url.PathEscape(name) + "/" + url.PathEscape(vPath) +
@ -2249,9 +2331,9 @@ func TestMetadataReplacement(t *testing.T) {
"key": "value",
},
}
replacements := params.getStringReplacements(false, false)
replacements := params.getStringReplacements(false, 0)
replacer := strings.NewReplacer(replacements...)
reader, _, err := getHTTPRuleActionBody(&dataprovider.EventActionHTTPConfig{Body: "{{Metadata}} {{MetadataString}}"}, replacer, nil, dataprovider.User{}, params, false)
reader, _, err := getHTTPRuleActionBody(&dataprovider.EventActionHTTPConfig{Body: "{{.Metadata}} {{.MetadataString}}"}, replacer, nil, dataprovider.User{}, params, false)
require.NoError(t, err)
data, err := io.ReadAll(reader)
require.NoError(t, err)

View file

@ -19,6 +19,8 @@ import (
"github.com/robfig/cron/v3"
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
"github.com/drakkan/sftpgo/v2/internal/logger"
"github.com/drakkan/sftpgo/v2/internal/util"
)
@ -36,7 +38,15 @@ func stopEventScheduler() {
func startEventScheduler() {
stopEventScheduler()
eventScheduler = cron.New(cron.WithLocation(time.UTC), cron.WithLogger(cron.DiscardLogger))
options := []cron.Option{
cron.WithLogger(cron.DiscardLogger),
}
if !dataprovider.UseLocalTime() {
eventManagerLog(logger.LevelDebug, "use UTC time for the scheduler")
options = append(options, cron.WithLocation(time.UTC))
}
eventScheduler = cron.New(options...)
eventManager.loadRules()
_, err := eventScheduler.AddFunc("@every 10m", eventManager.loadRules)
util.PanicOnError(err)

File diff suppressed because it is too large Load diff

View file

@ -17,6 +17,7 @@ package common
import (
"errors"
"fmt"
"slices"
"sort"
"sync"
"sync/atomic"
@ -94,7 +95,7 @@ func (r *RateLimiterConfig) validate() error {
}
r.Protocols = util.RemoveDuplicates(r.Protocols, true)
for _, protocol := range r.Protocols {
if !util.Contains(rateLimiterProtocolValues, protocol) {
if !slices.Contains(rateLimiterProtocolValues, protocol) {
return fmt.Errorf("invalid protocol %q", protocol)
}
}

View file

@ -25,6 +25,7 @@ import (
"math/rand"
"os"
"path/filepath"
"slices"
"sync"
"github.com/drakkan/sftpgo/v2/internal/logger"
@ -96,7 +97,7 @@ func (m *CertManager) loadCertificates() error {
}
logger.Debug(m.logSender, "", "TLS certificate %q successfully loaded, id %v", keyPair.Cert, keyPair.ID)
certs[keyPair.ID] = &newCert
if !util.Contains(m.monitorList, keyPair.Cert) {
if !slices.Contains(m.monitorList, keyPair.Cert) {
m.monitorList = append(m.monitorList, keyPair.Cert)
}
}
@ -190,7 +191,7 @@ func (m *CertManager) LoadCRLs() error {
logger.Debug(m.logSender, "", "CRL %q successfully loaded", revocationList)
crls = append(crls, crl)
if !util.Contains(m.monitorList, revocationList) {
if !slices.Contains(m.monitorList, revocationList) {
m.monitorList = append(m.monitorList, revocationList)
}
}

View file

@ -35,7 +35,7 @@ var (
)
// BaseTransfer contains protocols common transfer details for an upload or a download.
type BaseTransfer struct { //nolint:maligned
type BaseTransfer struct {
ID int64
BytesSent atomic.Int64
BytesReceived atomic.Int64
@ -329,6 +329,21 @@ func (t *BaseTransfer) getUploadFileSize() (int64, int, error) {
var fileSize int64
var deletedFiles int
switch dataprovider.GetQuotaTracking() {
case 0:
return fileSize, deletedFiles, errors.New("quota tracking disabled")
case 2:
if !t.Connection.User.HasQuotaRestrictions() {
vfolder, err := t.Connection.User.GetVirtualFolderForPath(path.Dir(t.requestPath))
if err != nil {
return fileSize, deletedFiles, errors.New("quota tracking disabled for this user")
}
if vfolder.IsIncludedInUserQuota() {
return fileSize, deletedFiles, errors.New("quota tracking disabled for this user and folder included in user quota")
}
}
}
info, err := t.Fs.Stat(t.fsPath)
if err == nil {
fileSize = info.Size()
@ -352,6 +367,9 @@ func (t *BaseTransfer) checkUploadOutsideHomeDir(err error) int {
if err == nil {
return 0
}
if t.ErrTransfer == nil {
t.ErrTransfer = err
}
if Config.TempPath == "" {
return 0
}
@ -391,7 +409,7 @@ func (t *BaseTransfer) Close() error {
t.effectiveFsPath, err)
} else if t.isAtomicUpload() {
if t.ErrTransfer == nil || Config.UploadMode&UploadModeAtomicWithResume != 0 {
_, _, err = t.Fs.Rename(t.effectiveFsPath, t.fsPath)
_, _, err = t.Fs.Rename(t.effectiveFsPath, t.fsPath, 0)
t.Connection.Log(logger.LevelDebug, "atomic upload completed, rename: %q -> %q, error: %v",
t.effectiveFsPath, t.fsPath, err)
// the file must be removed if it is uploaded to a path outside the home dir and cannot be renamed
@ -410,7 +428,8 @@ func (t *BaseTransfer) Close() error {
var uploadFileSize int64
if t.transferType == TransferDownload {
logger.TransferLog(downloadLogSender, t.fsPath, elapsed, t.BytesSent.Load(), t.Connection.User.Username,
t.Connection.ID, t.Connection.protocol, t.Connection.localAddr, t.Connection.remoteAddr, t.ftpMode)
t.Connection.ID, t.Connection.protocol, t.Connection.localAddr, t.Connection.remoteAddr, t.ftpMode,
t.ErrTransfer)
ExecuteActionNotification(t.Connection, operationDownload, t.fsPath, t.requestPath, "", "", "", //nolint:errcheck
t.BytesSent.Load(), t.ErrTransfer, elapsed, t.metadata)
} else {
@ -431,7 +450,8 @@ func (t *BaseTransfer) Close() error {
t.updateQuota(numFiles, uploadFileSize)
t.updateTimes()
logger.TransferLog(uploadLogSender, t.fsPath, elapsed, t.BytesReceived.Load(), t.Connection.User.Username,
t.Connection.ID, t.Connection.protocol, t.Connection.localAddr, t.Connection.remoteAddr, t.ftpMode)
t.Connection.ID, t.Connection.protocol, t.Connection.localAddr, t.Connection.remoteAddr, t.ftpMode,
t.ErrTransfer)
}
if t.ErrTransfer != nil {
t.Connection.Log(logger.LevelError, "transfer error: %v, path: %q", t.ErrTransfer, t.fsPath)
@ -516,11 +536,8 @@ func (t *BaseTransfer) updateQuota(numFiles int, fileSize int64) bool {
if t.transferType == TransferUpload && (numFiles != 0 || sizeDiff != 0) {
vfolder, err := t.Connection.User.GetVirtualFolderForPath(path.Dir(t.requestPath))
if err == nil {
dataprovider.UpdateVirtualFolderQuota(&vfolder.BaseVirtualFolder, numFiles, //nolint:errcheck
dataprovider.UpdateUserFolderQuota(&vfolder, &t.Connection.User, numFiles,
sizeDiff, false)
if vfolder.IsIncludedInUserQuota() {
dataprovider.UpdateUserQuota(&t.Connection.User, numFiles, sizeDiff, false) //nolint:errcheck
}
} else {
dataprovider.UpdateUserQuota(&t.Connection.User, numFiles, sizeDiff, false) //nolint:errcheck
}

View file

@ -306,8 +306,9 @@ func TestRemovePartialCryptoFile(t *testing.T) {
require.NoError(t, err)
u := dataprovider.User{
BaseUser: sdk.BaseUser{
Username: "test",
HomeDir: os.TempDir(),
Username: "test",
HomeDir: os.TempDir(),
QuotaFiles: 1000000,
},
}
conn := NewBaseConnection(fs.ConnectionID(), ProtocolSFTP, "", "", u)
@ -323,6 +324,9 @@ func TestRemovePartialCryptoFile(t *testing.T) {
assert.Equal(t, int64(0), size)
assert.Equal(t, 1, deletedFiles)
assert.NoFileExists(t, testFile)
err = transfer.Close()
assert.Error(t, err)
assert.Len(t, conn.GetTransfers(), 0)
}
func TestFTPMode(t *testing.T) {
@ -434,6 +438,11 @@ func TestTransferQuota(t *testing.T) {
}
err = transfer.CheckWrite()
assert.True(t, conn.IsQuotaExceededError(err))
err = transfer.Close()
assert.NoError(t, err)
assert.Len(t, conn.GetTransfers(), 0)
assert.Equal(t, int32(0), Connections.GetTotalTransfers())
}
func TestUploadOutsideHomeRenameError(t *testing.T) {

View file

@ -250,6 +250,7 @@ func TestTransfersCheckerDiskQuota(t *testing.T) {
Connections.Remove(fakeConn5.GetID())
stats := Connections.GetStats("")
assert.Len(t, stats, 0)
assert.Equal(t, int32(0), Connections.GetTotalTransfers())
err = dataprovider.DeleteUser(user.Username, "", "", "")
assert.NoError(t, err)
@ -368,11 +369,16 @@ func TestTransferCheckerTransferQuota(t *testing.T) {
if assert.Error(t, transfer4.errAbort) {
assert.Contains(t, transfer4.errAbort.Error(), ErrReadQuotaExceeded.Error())
}
err = transfer3.Close()
assert.NoError(t, err)
err = transfer4.Close()
assert.NoError(t, err)
Connections.Remove(fakeConn3.GetID())
Connections.Remove(fakeConn4.GetID())
stats := Connections.GetStats("")
assert.Len(t, stats, 0)
assert.Equal(t, int32(0), Connections.GetTotalTransfers())
err = dataprovider.DeleteUser(user.Username, "", "", "")
assert.NoError(t, err)

View file

@ -20,9 +20,11 @@ import (
"fmt"
"os"
"path/filepath"
"slices"
"strconv"
"strings"
kmsplugin "github.com/sftpgo/sdk/plugin/kms"
"github.com/spf13/viper"
"github.com/subosito/gotenv"
@ -91,30 +93,35 @@ var (
TLSCipherSuites: nil,
Protocols: nil,
Prefix: "",
ProxyMode: 0,
ProxyAllowed: nil,
ClientIPProxyHeader: "",
ClientIPHeaderDepth: 0,
DisableWWWAuthHeader: false,
}
defaultHTTPDBinding = httpd.Binding{
Address: "",
Port: 8080,
EnableWebAdmin: true,
EnableWebClient: true,
EnableRESTAPI: true,
EnabledLoginMethods: 0,
EnableHTTPS: false,
CertificateFile: "",
CertificateKeyFile: "",
MinTLSVersion: 12,
ClientAuthType: 0,
TLSCipherSuites: nil,
Protocols: nil,
ProxyAllowed: nil,
ClientIPProxyHeader: "",
ClientIPHeaderDepth: 0,
HideLoginURL: 0,
RenderOpenAPI: true,
Address: "",
Port: 8080,
EnableWebAdmin: true,
EnableWebClient: true,
EnableRESTAPI: true,
EnabledLoginMethods: 0,
DisabledLoginMethods: 0,
EnableHTTPS: false,
CertificateFile: "",
CertificateKeyFile: "",
MinTLSVersion: 12,
ClientAuthType: 0,
TLSCipherSuites: nil,
Protocols: nil,
ProxyMode: 0,
ProxyAllowed: nil,
ClientIPProxyHeader: "",
ClientIPHeaderDepth: 0,
HideLoginURL: 0,
RenderOpenAPI: true,
BaseURL: "",
Languages: []string{"en"},
OIDC: httpd.OIDC{
ClientID: "",
ClientSecret: "",
@ -130,20 +137,23 @@ var (
Debug: false,
},
Security: httpd.SecurityConf{
Enabled: false,
AllowedHosts: nil,
AllowedHostsAreRegex: false,
HostsProxyHeaders: nil,
HTTPSRedirect: false,
HTTPSHost: "",
HTTPSProxyHeaders: nil,
STSSeconds: 0,
STSIncludeSubdomains: false,
STSPreload: false,
ContentTypeNosniff: false,
ContentSecurityPolicy: "",
PermissionsPolicy: "",
CrossOriginOpenerPolicy: "",
Enabled: false,
AllowedHosts: nil,
AllowedHostsAreRegex: false,
HostsProxyHeaders: nil,
HTTPSRedirect: false,
HTTPSHost: "",
HTTPSProxyHeaders: nil,
STSSeconds: 0,
STSIncludeSubdomains: false,
STSPreload: false,
ContentTypeNosniff: false,
ContentSecurityPolicy: "",
PermissionsPolicy: "",
CrossOriginOpenerPolicy: "",
CrossOriginResourcePolicy: "",
CrossOriginEmbedderPolicy: "",
CacheControl: "",
},
Branding: httpd.Branding{},
}
@ -208,7 +218,6 @@ func Init() {
ProxySkipped: []string{},
PostConnectHook: "",
PostDisconnectHook: "",
DataRetentionHook: "",
MaxTotalConnections: 0,
MaxPerHostConnections: 20,
AllowListStatus: 0,
@ -226,13 +235,21 @@ func Init() {
ObservationTime: 30,
EntriesSoftLimit: 100,
EntriesHardLimit: 150,
LoginDelay: common.LoginDelay{
Success: 0,
PasswordFailed: 1000,
},
},
RateLimitersConfig: []common.RateLimiterConfig{defaultRateLimiter},
Umask: "",
ServerVersion: "",
TZ: "",
Metadata: common.MetadataConfig{
Read: 0,
},
EventManager: common.EventManagerConfig{
EnabledCommands: []string{},
},
},
ACME: acme.Configuration{
Email: "",
@ -262,6 +279,8 @@ func Init() {
PublicKeyAlgorithms: []string{},
TrustedUserCAKeys: []string{},
RevokedUserCertsFile: "",
OPKSSHPath: "",
OPKSSHChecksum: "",
LoginBannerFile: "",
EnabledSSHCommands: []string{},
KeyboardInteractiveAuthentication: true,
@ -390,6 +409,9 @@ func Init() {
SigningPassphrase: "",
SigningPassphraseFile: "",
TokenValidation: 0,
CookieLifetime: 20,
ShareCookieLifetime: 120,
JWTLifetime: 20,
MaxUploadFileSize: 0,
Cors: httpd.CorsConfig{
Enabled: false,
@ -568,6 +590,16 @@ func SetPluginsConfig(config []plugin.Config) {
globalConf.PluginsConfig = config
}
// HasKMSPlugin returns true if at least one KMS plugin is configured.
func HasKMSPlugin() bool {
for _, c := range globalConf.PluginsConfig {
if c.Type == kmsplugin.PluginName {
return true
}
}
return false
}
// GetMFAConfig returns multi-factor authentication config
func GetMFAConfig() mfa.Config {
return globalConf.MFAConfig
@ -614,7 +646,6 @@ func getRedactedGlobalConf() globalConfig {
conf.Common.StartupHook = util.GetRedactedURL(conf.Common.StartupHook)
conf.Common.PostConnectHook = util.GetRedactedURL(conf.Common.PostConnectHook)
conf.Common.PostDisconnectHook = util.GetRedactedURL(conf.Common.PostDisconnectHook)
conf.Common.DataRetentionHook = util.GetRedactedURL(conf.Common.DataRetentionHook)
conf.SFTPD.KeyboardInteractiveHook = util.GetRedactedURL(conf.SFTPD.KeyboardInteractiveHook)
conf.HTTPDConfig.SigningPassphrase = getRedactedPassword(conf.HTTPDConfig.SigningPassphrase)
conf.HTTPDConfig.Setup.InstallationCode = getRedactedPassword(conf.HTTPDConfig.Setup.InstallationCode)
@ -631,6 +662,16 @@ func getRedactedGlobalConf() globalConfig {
binding.OIDC.ClientSecret = getRedactedPassword(binding.OIDC.ClientSecret)
conf.HTTPDConfig.Bindings = append(conf.HTTPDConfig.Bindings, binding)
}
conf.KMSConfig.Secrets.MasterKeyString = getRedactedPassword(conf.KMSConfig.Secrets.MasterKeyString)
conf.PluginsConfig = nil
for _, plugin := range globalConf.PluginsConfig {
var args []string
for _, arg := range plugin.Args {
args = append(args, getRedactedPassword(arg))
}
plugin.Args = args
conf.PluginsConfig = append(conf.PluginsConfig, plugin)
}
return conf
}
@ -701,7 +742,7 @@ func checkOverrideDefaultSettings() {
}
}
if util.Contains(viper.AllKeys(), "mfa.totp") {
if slices.Contains(viper.AllKeys(), "mfa.totp") {
globalConf.MFAConfig.TOTP = nil
}
}
@ -787,6 +828,12 @@ func resetInvalidConfigs() {
logger.WarnToConsole("Non-fatal configuration error: %v", warn)
}
}
if globalConf.Common.RenameMode < 0 || globalConf.Common.RenameMode > 1 {
warn := fmt.Sprintf("invalid rename mode %d, reset to 0", globalConf.Common.RenameMode)
globalConf.Common.RenameMode = 0
logger.Warn(logSender, "", "Non-fatal configuration error: %v", warn)
logger.WarnToConsole("Non-fatal configuration error: %v", warn)
}
}
func loadBindingsFromEnv() {
@ -859,13 +906,13 @@ func getRateLimitersFromEnv(idx int) {
isSet = true
}
burst, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_COMMON__RATE_LIMITERS__%v__BURST", idx), 0)
burst, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_COMMON__RATE_LIMITERS__%v__BURST", idx), 32)
if ok {
rtlConfig.Burst = int(burst)
isSet = true
}
rtlType, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_COMMON__RATE_LIMITERS__%v__TYPE", idx), 0)
rtlType, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_COMMON__RATE_LIMITERS__%v__TYPE", idx), 32)
if ok {
rtlConfig.Type = int(rtlType)
isSet = true
@ -883,13 +930,13 @@ func getRateLimitersFromEnv(idx int) {
isSet = true
}
softLimit, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_COMMON__RATE_LIMITERS__%v__ENTRIES_SOFT_LIMIT", idx), 0)
softLimit, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_COMMON__RATE_LIMITERS__%v__ENTRIES_SOFT_LIMIT", idx), 32)
if ok {
rtlConfig.EntriesSoftLimit = int(softLimit)
isSet = true
}
hardLimit, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_COMMON__RATE_LIMITERS__%v__ENTRIES_HARD_LIMIT", idx), 0)
hardLimit, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_COMMON__RATE_LIMITERS__%v__ENTRIES_HARD_LIMIT", idx), 32)
if ok {
rtlConfig.EntriesHardLimit = int(hardLimit)
isSet = true
@ -925,7 +972,7 @@ func getKMSPluginFromEnv(idx int, pluginConfig *plugin.Config) bool {
func getAuthPluginFromEnv(idx int, pluginConfig *plugin.Config) bool {
isSet := false
authScope, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_PLUGINS__%v__AUTH_OPTIONS__SCOPE", idx), 0)
authScope, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_PLUGINS__%v__AUTH_OPTIONS__SCOPE", idx), 32)
if ok {
pluginConfig.AuthOptions.Scope = int(authScope)
isSet = true
@ -970,13 +1017,13 @@ func getNotifierPluginFromEnv(idx int, pluginConfig *plugin.Config) bool {
}
}
notifierRetryMaxTime, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_PLUGINS__%v__NOTIFIER_OPTIONS__RETRY_MAX_TIME", idx), 0)
notifierRetryMaxTime, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_PLUGINS__%v__NOTIFIER_OPTIONS__RETRY_MAX_TIME", idx), 32)
if ok {
pluginConfig.NotifierOptions.RetryMaxTime = int(notifierRetryMaxTime)
isSet = true
}
notifierRetryQueueMaxSize, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_PLUGINS__%v__NOTIFIER_OPTIONS__RETRY_QUEUE_MAX_SIZE", idx), 0)
notifierRetryQueueMaxSize, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_PLUGINS__%v__NOTIFIER_OPTIONS__RETRY_QUEUE_MAX_SIZE", idx), 32)
if ok {
pluginConfig.NotifierOptions.RetryQueueMaxSize = int(notifierRetryQueueMaxSize)
isSet = true
@ -1064,7 +1111,7 @@ func getSFTPDBindindFromEnv(idx int) {
isSet := false
port, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_SFTPD__BINDINGS__%v__PORT", idx), 0)
port, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_SFTPD__BINDINGS__%v__PORT", idx), 32)
if ok {
binding.Port = int(port)
isSet = true
@ -1151,19 +1198,13 @@ func getFTPDBindingSecurityFromEnv(idx int, binding *ftpd.Binding) bool {
isSet = true
}
tlsMode, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__TLS_MODE", idx), 0)
tlsMode, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__TLS_MODE", idx), 32)
if ok {
binding.TLSMode = int(tlsMode)
isSet = true
}
tlsSessionReuse, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__TLS_SESSION_REUSE", idx), 0)
if ok {
binding.TLSSessionReuse = int(tlsSessionReuse)
isSet = true
}
tlsVer, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__MIN_TLS_VERSION", idx), 0)
tlsVer, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__MIN_TLS_VERSION", idx), 32)
if ok {
binding.MinTLSVersion = int(tlsVer)
isSet = true
@ -1175,30 +1216,24 @@ func getFTPDBindingSecurityFromEnv(idx int, binding *ftpd.Binding) bool {
isSet = true
}
clientAuthType, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__CLIENT_AUTH_TYPE", idx), 0)
clientAuthType, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__CLIENT_AUTH_TYPE", idx), 32)
if ok {
binding.ClientAuthType = int(clientAuthType)
isSet = true
}
pasvSecurity, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__PASSIVE_CONNECTIONS_SECURITY", idx), 0)
pasvSecurity, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__PASSIVE_CONNECTIONS_SECURITY", idx), 32)
if ok {
binding.PassiveConnectionsSecurity = int(pasvSecurity)
isSet = true
}
activeSecurity, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__ACTIVE_CONNECTIONS_SECURITY", idx), 0)
activeSecurity, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__ACTIVE_CONNECTIONS_SECURITY", idx), 32)
if ok {
binding.ActiveConnectionsSecurity = int(activeSecurity)
isSet = true
}
ignoreASCIITransferType, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%d__IGNORE_ASCII_TRANSFER_TYPE", idx), 0)
if ok {
binding.IgnoreASCIITransferType = int(ignoreASCIITransferType)
isSet = true
}
return isSet
}
@ -1206,7 +1241,7 @@ func getFTPDBindingFromEnv(idx int) {
binding := getDefaultFTPDBinding(idx)
isSet := false
port, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__PORT", idx), 0)
port, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_FTPD__BINDINGS__%v__PORT", idx), 32)
if ok {
binding.Port = int(port)
isSet = true
@ -1286,13 +1321,13 @@ func getWebDAVBindingHTTPSConfigsFromEnv(idx int, binding *webdavd.Binding) bool
isSet = true
}
tlsVer, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_WEBDAVD__BINDINGS__%v__MIN_TLS_VERSION", idx), 0)
tlsVer, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_WEBDAVD__BINDINGS__%v__MIN_TLS_VERSION", idx), 32)
if ok {
binding.MinTLSVersion = int(tlsVer)
isSet = true
}
clientAuthType, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_WEBDAVD__BINDINGS__%v__CLIENT_AUTH_TYPE", idx), 0)
clientAuthType, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_WEBDAVD__BINDINGS__%v__CLIENT_AUTH_TYPE", idx), 32)
if ok {
binding.ClientAuthType = int(clientAuthType)
isSet = true
@ -1316,6 +1351,12 @@ func getWebDAVBindingHTTPSConfigsFromEnv(idx int, binding *webdavd.Binding) bool
func getWebDAVDBindingProxyConfigsFromEnv(idx int, binding *webdavd.Binding) bool {
isSet := false
proxyMode, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_WEBDAVD__BINDINGS__%v__PROXY_MODE", idx), 32)
if ok {
binding.ProxyMode = int(proxyMode)
isSet = true
}
proxyAllowed, ok := lookupStringListFromEnv(fmt.Sprintf("SFTPGO_WEBDAVD__BINDINGS__%v__PROXY_ALLOWED", idx))
if ok {
binding.ProxyAllowed = proxyAllowed
@ -1328,7 +1369,7 @@ func getWebDAVDBindingProxyConfigsFromEnv(idx int, binding *webdavd.Binding) boo
isSet = true
}
clientIPHeaderDepth, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_WEBDAVD__BINDINGS__%v__CLIENT_IP_HEADER_DEPTH", idx), 0)
clientIPHeaderDepth, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_WEBDAVD__BINDINGS__%v__CLIENT_IP_HEADER_DEPTH", idx), 32)
if ok {
binding.ClientIPHeaderDepth = int(clientIPHeaderDepth)
isSet = true
@ -1366,7 +1407,7 @@ func getWebDAVDBindingFromEnv(idx int) {
isSet := false
port, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_WEBDAVD__BINDINGS__%v__PORT", idx), 0)
port, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_WEBDAVD__BINDINGS__%v__PORT", idx), 32)
if ok {
binding.Port = int(port)
isSet = true
@ -1527,9 +1568,33 @@ func getHTTPDSecurityConfFromEnv(idx int) (httpd.SecurityConf, bool) { //nolint:
isSet = true
}
crossOriginOpenedPolicy, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__SECURITY__CROSS_ORIGIN_OPENER_POLICY", idx))
crossOriginOpenerPolicy, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__SECURITY__CROSS_ORIGIN_OPENER_POLICY", idx))
if ok {
result.CrossOriginOpenerPolicy = crossOriginOpenedPolicy
result.CrossOriginOpenerPolicy = crossOriginOpenerPolicy
isSet = true
}
crossOriginResourcePolicy, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__SECURITY__CROSS_ORIGIN_RESOURCE_POLICY", idx))
if ok {
result.CrossOriginResourcePolicy = crossOriginResourcePolicy
isSet = true
}
crossOriginEmbedderPolicy, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__SECURITY__CROSS_ORIGIN_EMBEDDER_POLICY", idx))
if ok {
result.CrossOriginEmbedderPolicy = crossOriginEmbedderPolicy
isSet = true
}
referredPolicy, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__SECURITY__REFERRER_POLICY", idx))
if ok {
result.ReferrerPolicy = referredPolicy
isSet = true
}
cacheControl, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__SECURITY__CACHE_CONTROL", idx))
if ok {
result.CacheControl = cacheControl
isSet = true
}
@ -1645,12 +1710,6 @@ func getHTTPDUIBrandingFromEnv(prefix string, branding httpd.UIBranding) (httpd.
isSet = true
}
loginImagePath, ok := os.LookupEnv(fmt.Sprintf("%s__LOGIN_IMAGE_PATH", prefix))
if ok {
branding.LoginImagePath = loginImagePath
isSet = true
}
disclaimerName, ok := os.LookupEnv(fmt.Sprintf("%s__DISCLAIMER_NAME", prefix))
if ok {
branding.DisclaimerName = disclaimerName
@ -1737,6 +1796,12 @@ func getHTTPDNestedObjectsFromEnv(idx int, binding *httpd.Binding) bool {
func getHTTPDBindingProxyConfigsFromEnv(idx int, binding *httpd.Binding) bool {
isSet := false
proxyMode, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__PROXY_MODE", idx), 32)
if ok {
binding.ProxyMode = int(proxyMode)
isSet = true
}
proxyAllowed, ok := lookupStringListFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__PROXY_ALLOWED", idx))
if ok {
binding.ProxyAllowed = proxyAllowed
@ -1749,7 +1814,7 @@ func getHTTPDBindingProxyConfigsFromEnv(idx int, binding *httpd.Binding) bool {
isSet = true
}
clientIPHeaderDepth, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__CLIENT_IP_HEADER_DEPTH", idx), 0)
clientIPHeaderDepth, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__CLIENT_IP_HEADER_DEPTH", idx), 32)
if ok {
binding.ClientIPHeaderDepth = int(clientIPHeaderDepth)
isSet = true
@ -1762,7 +1827,7 @@ func getHTTPDBindingFromEnv(idx int) { //nolint:gocyclo
binding := getDefaultHTTPBinding(idx)
isSet := false
port, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__PORT", idx), 0)
port, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__PORT", idx), 32)
if ok {
binding.Port = int(port)
isSet = true
@ -1804,31 +1869,49 @@ func getHTTPDBindingFromEnv(idx int) { //nolint:gocyclo
isSet = true
}
enabledLoginMethods, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__ENABLED_LOGIN_METHODS", idx), 0)
enabledLoginMethods, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__ENABLED_LOGIN_METHODS", idx), 32)
if ok {
binding.EnabledLoginMethods = int(enabledLoginMethods)
isSet = true
}
disabledLoginMethods, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__DISABLED_LOGIN_METHODS", idx), 32)
if ok {
binding.DisabledLoginMethods = int(disabledLoginMethods)
isSet = true
}
renderOpenAPI, ok := lookupBoolFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__RENDER_OPENAPI", idx))
if ok {
binding.RenderOpenAPI = renderOpenAPI
isSet = true
}
baseURL, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%d__BASE_URL", idx))
if ok {
binding.BaseURL = baseURL
isSet = true
}
languages, ok := lookupStringListFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%d__LANGUAGES", idx))
if ok {
binding.Languages = languages
isSet = true
}
enableHTTPS, ok := lookupBoolFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__ENABLE_HTTPS", idx))
if ok {
binding.EnableHTTPS = enableHTTPS
isSet = true
}
tlsVer, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__MIN_TLS_VERSION", idx), 0)
tlsVer, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__MIN_TLS_VERSION", idx), 32)
if ok {
binding.MinTLSVersion = int(tlsVer)
isSet = true
}
clientAuthType, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__CLIENT_AUTH_TYPE", idx), 0)
clientAuthType, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__CLIENT_AUTH_TYPE", idx), 32)
if ok {
binding.ClientAuthType = int(clientAuthType)
isSet = true
@ -1850,7 +1933,7 @@ func getHTTPDBindingFromEnv(idx int) { //nolint:gocyclo
isSet = true
}
hideLoginURL, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__HIDE_LOGIN_URL", idx), 0)
hideLoginURL, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__HIDE_LOGIN_URL", idx), 32)
if ok {
binding.HideLoginURL = int(hideLoginURL)
isSet = true
@ -1939,7 +2022,7 @@ func getCommandConfigsFromEnv(idx int) {
cfg.Path = path
}
timeout, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_COMMAND__COMMANDS__%v__TIMEOUT", idx), 0)
timeout, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_COMMAND__COMMANDS__%v__TIMEOUT", idx), 32)
if ok {
cfg.Timeout = int(timeout)
}
@ -1978,7 +2061,6 @@ func setViperDefaults() {
viper.SetDefault("common.proxy_skipped", globalConf.Common.ProxySkipped)
viper.SetDefault("common.post_connect_hook", globalConf.Common.PostConnectHook)
viper.SetDefault("common.post_disconnect_hook", globalConf.Common.PostDisconnectHook)
viper.SetDefault("common.data_retention_hook", globalConf.Common.DataRetentionHook)
viper.SetDefault("common.max_total_connections", globalConf.Common.MaxTotalConnections)
viper.SetDefault("common.max_per_host_connections", globalConf.Common.MaxPerHostConnections)
viper.SetDefault("common.allowlist_status", globalConf.Common.AllowListStatus)
@ -1995,9 +2077,13 @@ func setViperDefaults() {
viper.SetDefault("common.defender.observation_time", globalConf.Common.DefenderConfig.ObservationTime)
viper.SetDefault("common.defender.entries_soft_limit", globalConf.Common.DefenderConfig.EntriesSoftLimit)
viper.SetDefault("common.defender.entries_hard_limit", globalConf.Common.DefenderConfig.EntriesHardLimit)
viper.SetDefault("common.defender.login_delay.success", globalConf.Common.DefenderConfig.LoginDelay.Success)
viper.SetDefault("common.defender.login_delay.password_failed", globalConf.Common.DefenderConfig.LoginDelay.PasswordFailed)
viper.SetDefault("common.umask", globalConf.Common.Umask)
viper.SetDefault("common.server_version", globalConf.Common.ServerVersion)
viper.SetDefault("common.tz", globalConf.Common.TZ)
viper.SetDefault("common.metadata.read", globalConf.Common.Metadata.Read)
viper.SetDefault("common.event_manager.enabled_commands", globalConf.Common.EventManager.EnabledCommands)
viper.SetDefault("acme.email", globalConf.ACME.Email)
viper.SetDefault("acme.key_type", globalConf.ACME.KeyType)
viper.SetDefault("acme.certs_path", globalConf.ACME.CertsPath)
@ -2018,6 +2104,8 @@ func setViperDefaults() {
viper.SetDefault("sftpd.public_key_algorithms", globalConf.SFTPD.PublicKeyAlgorithms)
viper.SetDefault("sftpd.trusted_user_ca_keys", globalConf.SFTPD.TrustedUserCAKeys)
viper.SetDefault("sftpd.revoked_user_certs_file", globalConf.SFTPD.RevokedUserCertsFile)
viper.SetDefault("sftpd.opkssh_path", globalConf.SFTPD.OPKSSHPath)
viper.SetDefault("sftpd.opkssh_checksum", globalConf.SFTPD.OPKSSHChecksum)
viper.SetDefault("sftpd.login_banner_file", globalConf.SFTPD.LoginBannerFile)
viper.SetDefault("sftpd.enabled_ssh_commands", sftpd.GetDefaultSSHCommands())
viper.SetDefault("sftpd.keyboard_interactive_authentication", globalConf.SFTPD.KeyboardInteractiveAuthentication)
@ -2109,6 +2197,9 @@ func setViperDefaults() {
viper.SetDefault("httpd.signing_passphrase", globalConf.HTTPDConfig.SigningPassphrase)
viper.SetDefault("httpd.signing_passphrase_file", globalConf.HTTPDConfig.SigningPassphraseFile)
viper.SetDefault("httpd.token_validation", globalConf.HTTPDConfig.TokenValidation)
viper.SetDefault("httpd.cookie_lifetime", globalConf.HTTPDConfig.CookieLifetime)
viper.SetDefault("httpd.share_cookie_lifetime", globalConf.HTTPDConfig.ShareCookieLifetime)
viper.SetDefault("httpd.jwt_lifetime", globalConf.HTTPDConfig.JWTLifetime)
viper.SetDefault("httpd.max_upload_file_size", globalConf.HTTPDConfig.MaxUploadFileSize)
viper.SetDefault("httpd.cors.enabled", globalConf.HTTPDConfig.Cors.Enabled)
viper.SetDefault("httpd.cors.allowed_origins", globalConf.HTTPDConfig.Cors.AllowedOrigins)
@ -2182,7 +2273,7 @@ func lookupStringListFromEnv(envName string) ([]string, bool) {
value, ok := os.LookupEnv(envName)
if ok {
var result []string
for _, v := range strings.Split(value, ",") {
for v := range strings.SplitSeq(value, ",") {
val := strings.TrimSpace(v)
if val != "" {
result = append(result, val)

View file

@ -13,7 +13,6 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//go:build darwin
// +build darwin
package config

View file

@ -13,7 +13,6 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//go:build !linux && !darwin
// +build !linux,!darwin
package config

View file

@ -13,7 +13,6 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//go:build linux
// +build linux
package config

View file

@ -19,6 +19,7 @@ import (
"encoding/json"
"os"
"path/filepath"
"slices"
"testing"
"github.com/sftpgo/sdk/kms"
@ -36,7 +37,6 @@ import (
"github.com/drakkan/sftpgo/v2/internal/plugin"
"github.com/drakkan/sftpgo/v2/internal/sftpd"
"github.com/drakkan/sftpgo/v2/internal/smtp"
"github.com/drakkan/sftpgo/v2/internal/util"
"github.com/drakkan/sftpgo/v2/internal/webdavd"
)
@ -243,6 +243,26 @@ func TestInvalidInstallationHint(t *testing.T) {
assert.NoError(t, err)
}
func TestInvalidRenameMode(t *testing.T) {
reset()
confName := tempConfigName + ".json"
configFilePath := filepath.Join(configDir, confName)
commonConfig := config.GetCommonConfig()
commonConfig.RenameMode = 10
c := make(map[string]any)
c["common"] = commonConfig
jsonConf, err := json.Marshal(c)
assert.NoError(t, err)
err = os.WriteFile(configFilePath, jsonConf, os.ModePerm)
assert.NoError(t, err)
err = config.LoadConfig(configDir, confName)
assert.NoError(t, err)
assert.Equal(t, 0, config.GetCommonConfig().RenameMode)
err = os.Remove(configFilePath)
assert.NoError(t, err)
}
func TestDefenderProviderDriver(t *testing.T) {
if config.GetProviderConf().Driver != dataprovider.SQLiteDataProviderName {
t.Skip("this test is not supported with the current database provider")
@ -323,6 +343,18 @@ func TestSetGetConfig(t *testing.T) {
if assert.Len(t, config.GetPluginsConfig(), 1) {
assert.Equal(t, pluginConf[0].Type, config.GetPluginsConfig()[0].Type)
}
assert.False(t, config.HasKMSPlugin())
pluginConf = []plugin.Config{
{
Type: "notifier",
},
{
Type: "kms",
},
}
config.SetPluginsConfig(pluginConf)
assert.Len(t, config.GetPluginsConfig(), 2)
assert.True(t, config.HasKMSPlugin())
}
func TestServiceToStart(t *testing.T) {
@ -508,7 +540,7 @@ func TestOverrideSliceValues(t *testing.T) {
c = make(map[string]any)
c["httpd"] = httpd.Conf{
Bindings: []httpd.Binding{},
Bindings: nil,
}
jsonConf, err = json.Marshal(c)
assert.NoError(t, err)
@ -679,8 +711,8 @@ func TestPluginsFromEnv(t *testing.T) {
pluginConf := pluginsConf[0]
require.Equal(t, "notifier", pluginConf.Type)
require.Len(t, pluginConf.NotifierOptions.FsEvents, 2)
require.True(t, util.Contains(pluginConf.NotifierOptions.FsEvents, "upload"))
require.True(t, util.Contains(pluginConf.NotifierOptions.FsEvents, "download"))
require.True(t, slices.Contains(pluginConf.NotifierOptions.FsEvents, "upload"))
require.True(t, slices.Contains(pluginConf.NotifierOptions.FsEvents, "download"))
require.Len(t, pluginConf.NotifierOptions.ProviderEvents, 2)
require.Equal(t, "add", pluginConf.NotifierOptions.ProviderEvents[0])
require.Equal(t, "update", pluginConf.NotifierOptions.ProviderEvents[1])
@ -729,8 +761,8 @@ func TestPluginsFromEnv(t *testing.T) {
pluginConf = pluginsConf[0]
require.Equal(t, "notifier", pluginConf.Type)
require.Len(t, pluginConf.NotifierOptions.FsEvents, 2)
require.True(t, util.Contains(pluginConf.NotifierOptions.FsEvents, "upload"))
require.True(t, util.Contains(pluginConf.NotifierOptions.FsEvents, "download"))
require.True(t, slices.Contains(pluginConf.NotifierOptions.FsEvents, "upload"))
require.True(t, slices.Contains(pluginConf.NotifierOptions.FsEvents, "download"))
require.Len(t, pluginConf.NotifierOptions.ProviderEvents, 2)
require.Equal(t, "add", pluginConf.NotifierOptions.ProviderEvents[0])
require.Equal(t, "update", pluginConf.NotifierOptions.ProviderEvents[1])
@ -787,8 +819,8 @@ func TestRateLimitersFromEnv(t *testing.T) {
require.Equal(t, 2, limiters[0].Type)
protocols := limiters[0].Protocols
require.Len(t, protocols, 2)
require.True(t, util.Contains(protocols, common.ProtocolFTP))
require.True(t, util.Contains(protocols, common.ProtocolSSH))
require.True(t, slices.Contains(protocols, common.ProtocolFTP))
require.True(t, slices.Contains(protocols, common.ProtocolSSH))
require.True(t, limiters[0].GenerateDefenderEvents)
require.Equal(t, 50, limiters[0].EntriesSoftLimit)
require.Equal(t, 100, limiters[0].EntriesHardLimit)
@ -799,10 +831,10 @@ func TestRateLimitersFromEnv(t *testing.T) {
require.Equal(t, 2, limiters[1].Type)
protocols = limiters[1].Protocols
require.Len(t, protocols, 4)
require.True(t, util.Contains(protocols, common.ProtocolFTP))
require.True(t, util.Contains(protocols, common.ProtocolSSH))
require.True(t, util.Contains(protocols, common.ProtocolWebDAV))
require.True(t, util.Contains(protocols, common.ProtocolHTTP))
require.True(t, slices.Contains(protocols, common.ProtocolFTP))
require.True(t, slices.Contains(protocols, common.ProtocolSSH))
require.True(t, slices.Contains(protocols, common.ProtocolWebDAV))
require.True(t, slices.Contains(protocols, common.ProtocolHTTP))
require.False(t, limiters[1].GenerateDefenderEvents)
require.Equal(t, 100, limiters[1].EntriesSoftLimit)
require.Equal(t, 150, limiters[1].EntriesHardLimit)
@ -910,7 +942,6 @@ func TestFTPDBindingsFromEnv(t *testing.T) {
os.Setenv("SFTPGO_FTPD__BINDINGS__0__PORT", "2200")
os.Setenv("SFTPGO_FTPD__BINDINGS__0__APPLY_PROXY_CONFIG", "f")
os.Setenv("SFTPGO_FTPD__BINDINGS__0__TLS_MODE", "2")
os.Setenv("SFTPGO_FTPD__BINDINGS__0__TLS_SESSION_REUSE", "1")
os.Setenv("SFTPGO_FTPD__BINDINGS__0__FORCE_PASSIVE_IP", "127.0.1.2")
os.Setenv("SFTPGO_FTPD__BINDINGS__0__PASSIVE_IP_OVERRIDES__0__IP", "172.16.1.1")
os.Setenv("SFTPGO_FTPD__BINDINGS__0__PASSIVE_HOST", "127.0.1.3")
@ -935,7 +966,6 @@ func TestFTPDBindingsFromEnv(t *testing.T) {
os.Unsetenv("SFTPGO_FTPD__BINDINGS__0__PORT")
os.Unsetenv("SFTPGO_FTPD__BINDINGS__0__APPLY_PROXY_CONFIG")
os.Unsetenv("SFTPGO_FTPD__BINDINGS__0__TLS_MODE")
os.Unsetenv("SFTPGO_FTPD__BINDINGS__0__TLS_SESSION_REUSE")
os.Unsetenv("SFTPGO_FTPD__BINDINGS__0__FORCE_PASSIVE_IP")
os.Unsetenv("SFTPGO_FTPD__BINDINGS__0__PASSIVE_IP_OVERRIDES__0__IP")
os.Unsetenv("SFTPGO_FTPD__BINDINGS__0__PASSIVE_HOST")
@ -964,7 +994,6 @@ func TestFTPDBindingsFromEnv(t *testing.T) {
require.Equal(t, "127.0.0.1", bindings[0].Address)
require.False(t, bindings[0].ApplyProxyConfig)
require.Equal(t, 2, bindings[0].TLSMode)
require.Equal(t, 1, bindings[0].TLSSessionReuse)
require.Equal(t, 12, bindings[0].MinTLSVersion)
require.Equal(t, "127.0.1.2", bindings[0].ForcePassiveIP)
require.Len(t, bindings[0].PassiveIPOverrides, 0)
@ -976,12 +1005,10 @@ func TestFTPDBindingsFromEnv(t *testing.T) {
require.False(t, bindings[0].Debug)
require.Equal(t, 1, bindings[0].PassiveConnectionsSecurity)
require.Equal(t, 0, bindings[0].ActiveConnectionsSecurity)
require.Equal(t, 0, bindings[0].IgnoreASCIITransferType)
require.Equal(t, 2203, bindings[1].Port)
require.Equal(t, "127.0.1.1", bindings[1].Address)
require.True(t, bindings[1].ApplyProxyConfig) // default value
require.Equal(t, 1, bindings[1].TLSMode)
require.Equal(t, 0, bindings[1].TLSSessionReuse)
require.Equal(t, 13, bindings[1].MinTLSVersion)
require.Equal(t, "127.0.1.1", bindings[1].ForcePassiveIP)
require.Empty(t, bindings[1].PassiveHost)
@ -994,7 +1021,6 @@ func TestFTPDBindingsFromEnv(t *testing.T) {
require.Nil(t, bindings[1].TLSCipherSuites)
require.Equal(t, 0, bindings[1].PassiveConnectionsSecurity)
require.Equal(t, 1, bindings[1].ActiveConnectionsSecurity)
require.Equal(t, 1, bindings[1].IgnoreASCIITransferType)
require.True(t, bindings[1].Debug)
require.Equal(t, "cert.crt", bindings[1].CertificateFile)
require.Equal(t, "cert.key", bindings[1].CertificateKeyFile)
@ -1074,6 +1100,7 @@ func TestWebDAVBindingsFromEnv(t *testing.T) {
os.Setenv("SFTPGO_WEBDAVD__BINDINGS__1__ENABLE_HTTPS", "0")
os.Setenv("SFTPGO_WEBDAVD__BINDINGS__1__TLS_CIPHER_SUITES", "TLS_RSA_WITH_AES_128_CBC_SHA ")
os.Setenv("SFTPGO_WEBDAVD__BINDINGS__1__TLS_PROTOCOLS", "http/1.1 ")
os.Setenv("SFTPGO_WEBDAVD__BINDINGS__1__PROXY_MODE", "1")
os.Setenv("SFTPGO_WEBDAVD__BINDINGS__1__PROXY_ALLOWED", "192.168.10.1")
os.Setenv("SFTPGO_WEBDAVD__BINDINGS__1__CLIENT_IP_PROXY_HEADER", "X-Forwarded-For")
os.Setenv("SFTPGO_WEBDAVD__BINDINGS__1__CLIENT_IP_HEADER_DEPTH", "2")
@ -1093,6 +1120,7 @@ func TestWebDAVBindingsFromEnv(t *testing.T) {
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__1__ENABLE_HTTPS")
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__1__TLS_CIPHER_SUITES")
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__1__TLS_PROTOCOLS")
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__1__PROXY_MODE")
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__1__PROXY_ALLOWED")
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__1__CLIENT_IP_PROXY_HEADER")
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__1__CLIENT_IP_HEADER_DEPTH")
@ -1117,6 +1145,7 @@ func TestWebDAVBindingsFromEnv(t *testing.T) {
require.Equal(t, 12, bindings[0].MinTLSVersion)
require.Len(t, bindings[0].TLSCipherSuites, 0)
require.Len(t, bindings[0].Protocols, 0)
require.Equal(t, 0, bindings[0].ProxyMode)
require.Empty(t, bindings[0].Prefix)
require.Equal(t, 0, bindings[0].ClientIPHeaderDepth)
require.False(t, bindings[0].DisableWWWAuthHeader)
@ -1129,6 +1158,7 @@ func TestWebDAVBindingsFromEnv(t *testing.T) {
require.Equal(t, "TLS_RSA_WITH_AES_128_CBC_SHA", bindings[1].TLSCipherSuites[0])
require.Len(t, bindings[1].Protocols, 1)
assert.Equal(t, "http/1.1", bindings[1].Protocols[0])
require.Equal(t, 1, bindings[1].ProxyMode)
require.Equal(t, "192.168.10.1", bindings[1].ProxyAllowed[0])
require.Equal(t, "X-Forwarded-For", bindings[1].ClientIPProxyHeader)
require.Equal(t, 2, bindings[1].ClientIPHeaderDepth)
@ -1139,6 +1169,7 @@ func TestWebDAVBindingsFromEnv(t *testing.T) {
require.True(t, bindings[2].EnableHTTPS)
require.Equal(t, 13, bindings[2].MinTLSVersion)
require.Equal(t, 1, bindings[2].ClientAuthType)
require.Equal(t, 0, bindings[2].ProxyMode)
require.Nil(t, bindings[2].TLSCipherSuites)
require.Equal(t, "/dav2", bindings[2].Prefix)
require.Equal(t, "webdav.crt", bindings[2].CertificateFile)
@ -1167,12 +1198,16 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__ENABLE_WEB_CLIENT", "0")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__ENABLE_REST_API", "0")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__ENABLED_LOGIN_METHODS", "3")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__DISABLED_LOGIN_METHODS", "12")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__RENDER_OPENAPI", "0")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__BASE_URL", "https://example.com")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__LANGUAGES", "en,es")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__ENABLE_HTTPS", "1 ")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__MIN_TLS_VERSION", "13")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__CLIENT_AUTH_TYPE", "1")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__TLS_CIPHER_SUITES", " TLS_AES_256_GCM_SHA384 , TLS_CHACHA20_POLY1305_SHA256")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__TLS_PROTOCOLS", "h2, http/1.1")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__PROXY_MODE", "1")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__PROXY_ALLOWED", " 192.168.9.1 , 172.16.25.0/24")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__CLIENT_IP_PROXY_HEADER", "X-Real-IP")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__CLIENT_IP_HEADER_DEPTH", "2")
@ -1203,11 +1238,14 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__CONTENT_SECURITY_POLICY", "script-src $NONCE")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__PERMISSIONS_POLICY", "fullscreen=(), geolocation=()")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__CROSS_ORIGIN_OPENER_POLICY", "same-origin")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__CROSS_ORIGIN_RESOURCE_POLICY", "same-site")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__CROSS_ORIGIN_EMBEDDER_POLICY", "require-corp")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__CACHE_CONTROL", "private")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__REFERRER_POLICY", "no-referrer")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__EXTRA_CSS__0__PATH", "path1")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__EXTRA_CSS__1__PATH", "path2")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_ADMIN__FAVICON_PATH", "favicon.ico")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_CLIENT__LOGO_PATH", "logo.png")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_ADMIN__LOGIN_IMAGE_PATH", "login_image.png")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_CLIENT__DISCLAIMER_NAME", "disclaimer")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_ADMIN__DISCLAIMER_PATH", "disclaimer.html")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_CLIENT__DEFAULT_CSS", "default.css")
@ -1234,10 +1272,14 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__ENABLE_WEB_CLIENT")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__ENABLE_REST_API")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__ENABLED_LOGIN_METHODS")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__DISABLED_LOGIN_METHODS")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__RENDER_OPENAPI")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__BASE_URL")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__LANGUAGES")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__CLIENT_AUTH_TYPE")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__TLS_CIPHER_SUITES")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__TLS_PROTOCOLS")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__PROXY_MODE")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__PROXY_ALLOWED")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__CLIENT_IP_PROXY_HEADER")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__CLIENT_IP_HEADER_DEPTH")
@ -1268,11 +1310,14 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__CONTENT_SECURITY_POLICY")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__PERMISSIONS_POLICY")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__CROSS_ORIGIN_OPENER_POLICY")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__CROSS_ORIGIN_RESOURCE_POLICY")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__CROSS_ORIGIN_EMBEDDER_POLICY")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__CACHE_CONTROL")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__REFERRER_POLICY")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__EXTRA_CSS__0__PATH")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__EXTRA_CSS__1__PATH")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_ADMIN__FAVICON_PATH")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_CLIENT__LOGO_PATH")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_ADMIN__LOGIN_IMAGE_PATH")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_CLIENT__DISCLAIMER_NAME")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_ADMIN__DISCLAIMER_PATH")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_CLIENT__DEFAULT_CSS")
@ -1294,8 +1339,13 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
require.True(t, bindings[0].EnableWebClient)
require.True(t, bindings[0].EnableRESTAPI)
require.Equal(t, 0, bindings[0].EnabledLoginMethods)
require.Equal(t, 0, bindings[0].DisabledLoginMethods)
require.True(t, bindings[0].RenderOpenAPI)
require.Empty(t, bindings[0].BaseURL)
require.Len(t, bindings[0].Languages, 1)
assert.Contains(t, bindings[0].Languages, "en")
require.Len(t, bindings[0].TLSCipherSuites, 1)
require.Equal(t, 0, bindings[0].ProxyMode)
require.Empty(t, bindings[0].OIDC.ConfigURL)
require.Equal(t, "TLS_AES_128_GCM_SHA256", bindings[0].TLSCipherSuites[0])
require.Equal(t, 0, bindings[0].HideLoginURL)
@ -1304,6 +1354,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
require.Len(t, bindings[0].OIDC.Scopes, 3)
require.False(t, bindings[0].OIDC.InsecureSkipSignatureCheck)
require.False(t, bindings[0].OIDC.Debug)
require.Empty(t, bindings[0].Security.ReferrerPolicy)
require.Equal(t, 8000, bindings[1].Port)
require.Equal(t, "127.0.0.1", bindings[1].Address)
require.False(t, bindings[1].EnableHTTPS)
@ -1312,7 +1363,11 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
require.True(t, bindings[1].EnableWebClient)
require.True(t, bindings[1].EnableRESTAPI)
require.Equal(t, 0, bindings[1].EnabledLoginMethods)
require.Equal(t, 0, bindings[1].DisabledLoginMethods)
require.True(t, bindings[1].RenderOpenAPI)
require.Empty(t, bindings[1].BaseURL)
require.Len(t, bindings[1].Languages, 1)
assert.Contains(t, bindings[1].Languages, "en")
require.Nil(t, bindings[1].TLSCipherSuites)
require.Equal(t, 1, bindings[1].HideLoginURL)
require.Empty(t, bindings[1].OIDC.ClientID)
@ -1322,6 +1377,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
require.False(t, bindings[1].Security.Enabled)
require.Equal(t, "Web Admin", bindings[1].Branding.WebAdmin.Name)
require.Equal(t, "WebClient", bindings[1].Branding.WebClient.ShortName)
require.Equal(t, 0, bindings[1].ProxyMode)
require.Equal(t, 0, bindings[1].ClientIPHeaderDepth)
require.Equal(t, 9000, bindings[2].Port)
require.Equal(t, "127.0.1.1", bindings[2].Address)
@ -1331,7 +1387,12 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
require.False(t, bindings[2].EnableWebClient)
require.False(t, bindings[2].EnableRESTAPI)
require.Equal(t, 3, bindings[2].EnabledLoginMethods)
require.Equal(t, 12, bindings[2].DisabledLoginMethods)
require.False(t, bindings[2].RenderOpenAPI)
require.Equal(t, "https://example.com", bindings[2].BaseURL)
require.Len(t, bindings[2].Languages, 2)
assert.Contains(t, bindings[2].Languages, "en")
assert.Contains(t, bindings[2].Languages, "es")
require.Equal(t, 1, bindings[2].ClientAuthType)
require.Len(t, bindings[2].TLSCipherSuites, 2)
require.Equal(t, "TLS_AES_256_GCM_SHA384", bindings[2].TLSCipherSuites[0])
@ -1339,6 +1400,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
require.Len(t, bindings[2].Protocols, 2)
require.Equal(t, "h2", bindings[2].Protocols[0])
require.Equal(t, "http/1.1", bindings[2].Protocols[1])
require.Equal(t, 1, bindings[2].ProxyMode)
require.Len(t, bindings[2].ProxyAllowed, 2)
require.Equal(t, "192.168.9.1", bindings[2].ProxyAllowed[0])
require.Equal(t, "172.16.25.0/24", bindings[2].ProxyAllowed[1])
@ -1378,9 +1440,12 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
require.Equal(t, "script-src $NONCE", bindings[2].Security.ContentSecurityPolicy)
require.Equal(t, "fullscreen=(), geolocation=()", bindings[2].Security.PermissionsPolicy)
require.Equal(t, "same-origin", bindings[2].Security.CrossOriginOpenerPolicy)
require.Equal(t, "same-site", bindings[2].Security.CrossOriginResourcePolicy)
require.Equal(t, "require-corp", bindings[2].Security.CrossOriginEmbedderPolicy)
require.Equal(t, "private", bindings[2].Security.CacheControl)
require.Equal(t, "no-referrer", bindings[2].Security.ReferrerPolicy)
require.Equal(t, "favicon.ico", bindings[2].Branding.WebAdmin.FaviconPath)
require.Equal(t, "logo.png", bindings[2].Branding.WebClient.LogoPath)
require.Equal(t, "login_image.png", bindings[2].Branding.WebAdmin.LoginImagePath)
require.Equal(t, "disclaimer", bindings[2].Branding.WebClient.DisclaimerName)
require.Equal(t, "disclaimer.html", bindings[2].Branding.WebAdmin.DisclaimerPath)
require.Equal(t, []string{"default.css"}, bindings[2].Branding.WebClient.DefaultCSS)

View file

@ -21,6 +21,7 @@ import (
"net/url"
"os/exec"
"path/filepath"
"slices"
"strings"
"time"
@ -78,8 +79,8 @@ func executeAction(operation, executor, ip, objectType, objectName, role string,
if config.Actions.Hook == "" {
return
}
if !util.Contains(config.Actions.ExecuteOn, operation) ||
!util.Contains(config.Actions.ExecuteFor, objectType) {
if !slices.Contains(config.Actions.ExecuteOn, operation) ||
!slices.Contains(config.Actions.ExecuteFor, objectType) {
return
}
@ -141,14 +142,14 @@ func executeNotificationCommand(operation, executor, ip, objectType, objectName,
cmd := exec.CommandContext(ctx, config.Actions.Hook, args...)
cmd.Env = append(env,
fmt.Sprintf("SFTPGO_PROVIDER_ACTION=%vs", operation),
fmt.Sprintf("SFTPGO_PROVIDER_ACTION=%s", operation),
fmt.Sprintf("SFTPGO_PROVIDER_OBJECT_TYPE=%s", objectType),
fmt.Sprintf("SFTPGO_PROVIDER_OBJECT_NAME=%s", objectName),
fmt.Sprintf("SFTPGO_PROVIDER_USERNAME=%s", executor),
fmt.Sprintf("SFTPGO_PROVIDER_IP=%s", ip),
fmt.Sprintf("SFTPGO_PROVIDER_ROLE=%s", role),
fmt.Sprintf("SFTPGO_PROVIDER_TIMESTAMP=%d", util.GetTimeAsMsSinceEpoch(time.Now())),
fmt.Sprintf("SFTPGO_PROVIDER_OBJECT=%s", util.BytesToString(objectAsJSON)))
fmt.Sprintf("SFTPGO_PROVIDER_OBJECT=%s", objectAsJSON))
startTime := time.Now()
err := cmd.Run()

View file

@ -20,6 +20,7 @@ import (
"fmt"
"net"
"os"
"slices"
"strconv"
"strings"
@ -44,19 +45,12 @@ const (
PermAdminViewConnections = "view_conns"
PermAdminCloseConnections = "close_conns"
PermAdminViewServerStatus = "view_status"
PermAdminManageAdmins = "manage_admins"
PermAdminManageGroups = "manage_groups"
PermAdminManageFolders = "manage_folders"
PermAdminManageAPIKeys = "manage_apikeys"
PermAdminQuotaScans = "quota_scans"
PermAdminManageSystem = "manage_system"
PermAdminManageDefender = "manage_defender"
PermAdminViewDefender = "view_defender"
PermAdminRetentionChecks = "retention_checks"
PermAdminViewEvents = "view_events"
PermAdminManageEventRules = "manage_event_rules"
PermAdminManageRoles = "manage_roles"
PermAdminManageIPLists = "manage_ip_lists"
PermAdminDisableMFA = "disable_mfa"
)
@ -72,12 +66,9 @@ const (
var (
validAdminPerms = []string{PermAdminAny, PermAdminAddUsers, PermAdminChangeUsers, PermAdminDeleteUsers,
PermAdminViewUsers, PermAdminManageFolders, PermAdminManageGroups, PermAdminViewConnections,
PermAdminCloseConnections, PermAdminViewServerStatus, PermAdminManageAdmins, PermAdminManageRoles,
PermAdminManageEventRules, PermAdminManageAPIKeys, PermAdminQuotaScans, PermAdminManageSystem,
PermAdminManageDefender, PermAdminViewDefender, PermAdminManageIPLists, PermAdminRetentionChecks,
PermAdminViewEvents, PermAdminDisableMFA}
forbiddenPermsForRoleAdmins = []string{PermAdminAny, PermAdminManageAdmins, PermAdminManageSystem,
PermAdminManageEventRules, PermAdminManageIPLists, PermAdminManageRoles}
PermAdminCloseConnections, PermAdminViewServerStatus, PermAdminQuotaScans,
PermAdminManageDefender, PermAdminViewDefender, PermAdminViewEvents, PermAdminDisableMFA}
forbiddenPermsForRoleAdmins = []string{PermAdminAny}
)
// AdminTOTPConfig defines the time-based one time password configuration
@ -96,7 +87,7 @@ func (c *AdminTOTPConfig) validate(username string) error {
if c.ConfigName == "" {
return util.NewValidationError("totp: config name is mandatory")
}
if !util.Contains(mfa.GetAvailableTOTPConfigNames(), c.ConfigName) {
if !slices.Contains(mfa.GetAvailableTOTPConfigNames(), c.ConfigName) {
return util.NewValidationError(fmt.Sprintf("totp: config name %q not found", c.ConfigName))
}
if c.Secret.IsEmpty() {
@ -265,12 +256,7 @@ type Admin struct {
// Last login as unix timestamp in milliseconds
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 admins cannot be super administrators
Role string `json:"role,omitempty"`
}
@ -337,22 +323,18 @@ func (a *Admin) validatePermissions() error {
util.I18nErrorPermissionsRequired,
)
}
if util.Contains(a.Permissions, PermAdminAny) {
if slices.Contains(a.Permissions, PermAdminAny) {
a.Permissions = []string{PermAdminAny}
}
for _, perm := range a.Permissions {
if !util.Contains(validAdminPerms, perm) {
if !slices.Contains(validAdminPerms, perm) {
return util.NewValidationError(fmt.Sprintf("invalid permission: %q", perm))
}
if a.Role != "" {
if util.Contains(forbiddenPermsForRoleAdmins, perm) {
deniedPerms := strings.Join(forbiddenPermsForRoleAdmins, ",")
if slices.Contains(forbiddenPermsForRoleAdmins, perm) {
return util.NewI18nError(
util.NewValidationError(fmt.Sprintf("a role admin cannot have the following permissions: %q", deniedPerms)),
util.NewValidationError("a role admin cannot be a super admin"),
util.I18nErrorRoleAdminPerms,
util.I18nErrorArgs(map[string]any{
"val": deniedPerms,
}),
)
}
}
@ -382,11 +364,24 @@ func (a *Admin) validateGroups() error {
return nil
}
func (a *Admin) validate() error {
func (a *Admin) applyNamingRules() {
a.Username = config.convertName(a.Username)
a.Role = config.convertName(a.Role)
for idx := range a.Groups {
a.Groups[idx].Name = config.convertName(a.Groups[idx].Name)
}
}
func (a *Admin) validate() error { //nolint:gocyclo
a.SetEmptySecretsIfNil()
a.applyNamingRules()
a.Password = strings.TrimSpace(a.Password)
if a.Username == "" {
return util.NewI18nError(util.NewValidationError("username is mandatory"), util.I18nErrorUsernameRequired)
}
if !util.IsNameValid(a.Username) {
return util.NewI18nError(errInvalidInput, util.I18nErrorInvalidInput)
}
if err := checkReservedUsernames(a.Username); err != nil {
return util.NewI18nError(err, util.I18nErrorReservedUsername)
}
@ -499,7 +494,7 @@ func (a *Admin) checkUserAndPass(password, ip string) error {
if err := a.CanLogin(ip); err != nil {
return err
}
if a.Password == "" || password == "" {
if a.Password == "" || strings.TrimSpace(password) == "" {
return errors.New("credentials cannot be null or empty")
}
match, err := a.CheckPassword(password)
@ -559,10 +554,20 @@ func (a *Admin) SetNilSecretsIfEmpty() {
// HasPermission returns true if the admin has the specified permission
func (a *Admin) HasPermission(perm string) bool {
if util.Contains(a.Permissions, PermAdminAny) {
if slices.Contains(a.Permissions, PermAdminAny) {
return true
}
return util.Contains(a.Permissions, perm)
return slices.Contains(a.Permissions, perm)
}
// HasPermissions returns true if the admin has all the specified permissions
func (a *Admin) HasPermissions(perms ...string) bool {
for _, perm := range perms {
if !a.HasPermission(perm) {
return false
}
}
return len(perms) > 0
}
// GetAllowedIPAsString returns the allowed IP as comma separated string

View file

@ -148,6 +148,9 @@ func (k *APIKey) validate() error {
if k.Name == "" {
return util.NewValidationError("name is mandatory")
}
if !util.IsNameValid(k.Name) {
return util.NewI18nError(errInvalidInput, util.I18nErrorInvalidInput)
}
if k.Scope != APIKeyScopeAdmin && k.Scope != APIKeyScopeUser {
return util.NewValidationError(fmt.Sprintf("invalid scope: %v", k.Scope))
}

View file

@ -13,7 +13,6 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//go:build !nobolt
// +build !nobolt
package dataprovider
@ -25,10 +24,13 @@ import (
"fmt"
"net/netip"
"path/filepath"
"slices"
"sort"
"strconv"
"time"
bolt "go.etcd.io/bbolt"
bolterrors "go.etcd.io/bbolt/errors"
"github.com/drakkan/sftpgo/v2/internal/logger"
"github.com/drakkan/sftpgo/v2/internal/util"
@ -37,7 +39,7 @@ import (
)
const (
boltDatabaseVersion = 29
boltDatabaseVersion = 34
)
var (
@ -181,6 +183,50 @@ func (p *BoltProvider) updateAPIKeyLastUse(keyID string) error {
})
}
func (p *BoltProvider) getAdminSignature(username string) (string, error) {
var updatedAt int64
err := p.dbHandle.View(func(tx *bolt.Tx) error {
bucket, err := p.getAdminsBucket(tx)
if err != nil {
return err
}
u := bucket.Get([]byte(username))
var admin Admin
err = json.Unmarshal(u, &admin)
if err != nil {
return err
}
updatedAt = admin.UpdatedAt
return nil
})
if err != nil {
return "", err
}
return strconv.FormatInt(updatedAt, 10), nil
}
func (p *BoltProvider) getUserSignature(username string) (string, error) {
var updatedAt int64
err := p.dbHandle.View(func(tx *bolt.Tx) error {
bucket, err := p.getUsersBucket(tx)
if err != nil {
return err
}
u := bucket.Get([]byte(username))
var user User
err = json.Unmarshal(u, &user)
if err != nil {
return err
}
updatedAt = user.UpdatedAt
return nil
})
if err != nil {
return "", err
}
return strconv.FormatInt(updatedAt, 10), nil
}
func (p *BoltProvider) setUpdatedAt(username string) {
p.dbHandle.Update(func(tx *bolt.Tx) error { //nolint:errcheck
bucket, err := p.getUsersBucket(tx)
@ -400,9 +446,6 @@ func (p *BoltProvider) addAdmin(admin *Admin) error {
admin.LastLogin = 0
admin.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
admin.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
sort.Slice(admin.Groups, func(i, j int) bool {
return admin.Groups[i].Name < admin.Groups[j].Name
})
for idx := range admin.Groups {
err = p.addAdminToGroupMapping(admin.Username, admin.Groups[idx].Name, groupBucket)
if err != nil {
@ -461,9 +504,6 @@ func (p *BoltProvider) updateAdmin(admin *Admin) error {
if err = p.addAdminToRole(admin.Username, admin.Role, rolesBucket); err != nil {
return err
}
sort.Slice(admin.Groups, func(i, j int) bool {
return admin.Groups[i].Name < admin.Groups[j].Name
})
for idx := range admin.Groups {
err = p.addAdminToGroupMapping(admin.Username, admin.Groups[idx].Name, groupBucket)
if err != nil {
@ -675,18 +715,12 @@ func (p *BoltProvider) addUser(user *User) error {
if err := p.addUserToRole(user.Username, user.Role, rolesBucket); err != nil {
return err
}
sort.Slice(user.VirtualFolders, func(i, j int) bool {
return user.VirtualFolders[i].Name < user.VirtualFolders[j].Name
})
for idx := range user.VirtualFolders {
err = p.addRelationToFolderMapping(user.VirtualFolders[idx].Name, user, nil, foldersBucket)
if err != nil {
return err
}
}
sort.Slice(user.Groups, func(i, j int) bool {
return user.Groups[i].Name < user.Groups[j].Name
})
for idx := range user.Groups {
err = p.addUserToGroupMapping(user.Username, user.Groups[idx].Name, groupBucket)
if err != nil {
@ -1458,9 +1492,6 @@ func (p *BoltProvider) addGroup(group *Group) error {
group.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
group.Users = nil
group.Admins = nil
sort.Slice(group.VirtualFolders, func(i, j int) bool {
return group.VirtualFolders[i].Name < group.VirtualFolders[j].Name
})
for idx := range group.VirtualFolders {
err = p.addRelationToFolderMapping(group.VirtualFolders[idx].Name, nil, group, foldersBucket)
if err != nil {
@ -1503,9 +1534,6 @@ func (p *BoltProvider) updateGroup(group *Group) error {
return err
}
}
sort.Slice(group.VirtualFolders, func(i, j int) bool {
return group.VirtualFolders[i].Name < group.VirtualFolders[j].Name
})
for idx := range group.VirtualFolders {
err = p.addRelationToFolderMapping(group.VirtualFolders[idx].Name, nil, group, foldersBucket)
if err != nil {
@ -2088,11 +2116,11 @@ func (p *BoltProvider) addSharedSession(_ Session) error {
return ErrNotImplemented
}
func (p *BoltProvider) deleteSharedSession(_ string) error {
func (p *BoltProvider) deleteSharedSession(_ string, _ SessionType) error {
return ErrNotImplemented
}
func (p *BoltProvider) getSharedSession(_ string) (Session, error) {
func (p *BoltProvider) getSharedSession(_ string, _ SessionType) (Session, error) {
return Session{}, ErrNotImplemented
}
@ -3134,15 +3162,16 @@ func (p *BoltProvider) migrateDatabase() error {
case version == boltDatabaseVersion:
providerLog(logger.LevelDebug, "bolt database is up to date, current version: %d", version)
return ErrNoInitRequired
case version < 28:
err = fmt.Errorf("database schema version %d is too old, please see the upgrading docs", version)
case version < 33:
err = errSchemaVersionTooOld(version)
providerLog(logger.LevelError, "%v", err)
logger.ErrorToConsole("%v", err)
return err
case version == 28:
logger.InfoToConsole("updating database schema version: %d -> 29", version)
providerLog(logger.LevelInfo, "updating database schema version: %d -> 29", version)
return updateBoltDatabaseVersion(p.dbHandle, 29)
case version == 33:
logger.InfoToConsole("updating database schema version: %d -> 34", version)
providerLog(logger.LevelInfo, "updating database schema version: %d -> 34", version)
return updateBoltDatabaseVersion(p.dbHandle, 34)
default:
if version > boltDatabaseVersion {
providerLog(logger.LevelError, "database schema version %d is newer than the supported one: %d", version,
@ -3155,7 +3184,7 @@ func (p *BoltProvider) migrateDatabase() error {
}
}
func (p *BoltProvider) revertDatabase(targetVersion int) error { //nolint:gocyclo
func (p *BoltProvider) revertDatabase(targetVersion int) error {
dbVersion, err := getBoltDatabaseVersion(p.dbHandle)
if err != nil {
return err
@ -3164,10 +3193,11 @@ func (p *BoltProvider) revertDatabase(targetVersion int) error { //nolint:gocycl
return errors.New("current version match target version, nothing to do")
}
switch dbVersion.Version {
case 29:
logger.InfoToConsole("downgrading database schema version: %d -> 28", dbVersion.Version)
providerLog(logger.LevelInfo, "downgrading database schema version: %d -> 28", dbVersion.Version)
return updateBoltDatabaseVersion(p.dbHandle, 28)
case 34:
logger.InfoToConsole("downgrading database schema version: %d -> 33", dbVersion.Version)
providerLog(logger.LevelInfo, "downgrading database schema version: %d -> 33", dbVersion.Version)
return updateBoltDatabaseVersion(p.dbHandle, 33)
default:
return fmt.Errorf("database schema version not handled: %v", dbVersion.Version)
}
@ -3177,7 +3207,7 @@ func (p *BoltProvider) resetDatabase() error {
return p.dbHandle.Update(func(tx *bolt.Tx) error {
for _, bucketName := range boltBuckets {
err := tx.DeleteBucket(bucketName)
if err != nil && !errors.Is(err, bolt.ErrBucketNotFound) {
if err != nil && !errors.Is(err, bolterrors.ErrBucketNotFound) {
return fmt.Errorf("unable to remove bucket %v: %w", bucketName, err)
}
}
@ -3328,7 +3358,7 @@ func (p *BoltProvider) addAdminToRole(username, roleName string, bucket *bolt.Bu
if err != nil {
return err
}
if !util.Contains(role.Admins, username) {
if !slices.Contains(role.Admins, username) {
role.Admins = append(role.Admins, username)
buf, err := json.Marshal(role)
if err != nil {
@ -3353,7 +3383,7 @@ func (p *BoltProvider) removeAdminFromRole(username, roleName string, bucket *bo
if err != nil {
return err
}
if util.Contains(role.Admins, username) {
if slices.Contains(role.Admins, username) {
var admins []string
for _, admin := range role.Admins {
if admin != username {
@ -3383,7 +3413,7 @@ func (p *BoltProvider) addUserToRole(username, roleName string, bucket *bolt.Buc
if err != nil {
return err
}
if !util.Contains(role.Users, username) {
if !slices.Contains(role.Users, username) {
role.Users = append(role.Users, username)
buf, err := json.Marshal(role)
if err != nil {
@ -3408,7 +3438,7 @@ func (p *BoltProvider) removeUserFromRole(username, roleName string, bucket *bol
if err != nil {
return err
}
if util.Contains(role.Users, username) {
if slices.Contains(role.Users, username) {
var users []string
for _, user := range role.Users {
if user != username {
@ -3436,7 +3466,7 @@ func (p *BoltProvider) addRuleToActionMapping(ruleName, actionName string, bucke
if err != nil {
return err
}
if !util.Contains(action.Rules, ruleName) {
if !slices.Contains(action.Rules, ruleName) {
action.Rules = append(action.Rules, ruleName)
buf, err := json.Marshal(action)
if err != nil {
@ -3458,7 +3488,7 @@ func (p *BoltProvider) removeRuleFromActionMapping(ruleName, actionName string,
if err != nil {
return err
}
if util.Contains(action.Rules, ruleName) {
if slices.Contains(action.Rules, ruleName) {
var rules []string
for _, r := range action.Rules {
if r != ruleName {
@ -3485,7 +3515,7 @@ func (p *BoltProvider) addUserToGroupMapping(username, groupname string, bucket
if err != nil {
return err
}
if !util.Contains(group.Users, username) {
if !slices.Contains(group.Users, username) {
group.Users = append(group.Users, username)
buf, err := json.Marshal(group)
if err != nil {
@ -3530,7 +3560,7 @@ func (p *BoltProvider) addAdminToGroupMapping(username, groupname string, bucket
if err != nil {
return err
}
if !util.Contains(group.Admins, username) {
if !slices.Contains(group.Admins, username) {
group.Admins = append(group.Admins, username)
buf, err := json.Marshal(group)
if err != nil {
@ -3601,11 +3631,11 @@ func (p *BoltProvider) addRelationToFolderMapping(folderName string, user *User,
return err
}
updated := false
if user != nil && !util.Contains(folder.Users, user.Username) {
if user != nil && !slices.Contains(folder.Users, user.Username) {
folder.Users = append(folder.Users, user.Username)
updated = true
}
if group != nil && !util.Contains(folder.Groups, group.Name) {
if group != nil && !slices.Contains(folder.Groups, group.Name) {
folder.Groups = append(folder.Groups, group.Name)
updated = true
}
@ -3691,18 +3721,12 @@ func (p *BoltProvider) updateUserRelations(tx *bolt.Tx, user *User, oldUser User
if err = p.removeUserFromRole(oldUser.Username, oldUser.Role, rolesBucket); err != nil {
return err
}
sort.Slice(user.VirtualFolders, func(i, j int) bool {
return user.VirtualFolders[i].Name < user.VirtualFolders[j].Name
})
for idx := range user.VirtualFolders {
err = p.addRelationToFolderMapping(user.VirtualFolders[idx].Name, user, nil, foldersBucket)
if err != nil {
return err
}
}
sort.Slice(user.Groups, func(i, j int) bool {
return user.Groups[i].Name < user.Groups[j].Name
})
for idx := range user.Groups {
err = p.addUserToGroupMapping(user.Username, user.Groups[idx].Name, groupsBucket)
if err != nil {
@ -3899,7 +3923,7 @@ func getBoltDatabaseVersion(dbHandle *bolt.DB) (schemaVersion, error) {
v := bucket.Get(dbVersionKey)
if v == nil {
dbVersion = schemaVersion{
Version: 28,
Version: 33,
}
return nil
}

View file

@ -13,7 +13,6 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//go:build nobolt
// +build nobolt
package dataprovider

View file

@ -15,8 +15,12 @@
package dataprovider
import (
"bytes"
"encoding/json"
"fmt"
"image/png"
"net/url"
"slices"
"golang.org/x/crypto/ssh"
@ -28,18 +32,18 @@ import (
// Supported values for host keys, KEXs, ciphers, MACs
var (
supportedHostKeyAlgos = []string{ssh.KeyAlgoRSA}
supportedPublicKeyAlgos = []string{ssh.KeyAlgoRSA, ssh.InsecureKeyAlgoDSA}
supportedPublicKeyAlgos = []string{ssh.KeyAlgoRSA, ssh.InsecureKeyAlgoDSA} //nolint:staticcheck
supportedKexAlgos = []string{
ssh.KeyExchangeDH16SHA512, ssh.InsecureKeyExchangeDH14SHA1, ssh.InsecureKeyExchangeDH1SHA1,
ssh.InsecureKeyExchangeDHGEXSHA1,
}
supportedCiphers = []string{
ssh.InsecureCipherAES128CBC, ssh.InsecureCipherAES192CBC, ssh.InsecureCipherAES256CBC,
ssh.InsecureCipherAES128CBC,
ssh.InsecureCipherTripleDESCBC,
}
supportedMACs = []string{
ssh.HMACSHA512ETM, ssh.HMACSHA512,
ssh.InsecureHMACSHA1, ssh.InsecureHMACSHA196,
ssh.HMACSHA1, ssh.InsecureHMACSHA196,
}
)
@ -102,7 +106,7 @@ func (c *SFTPDConfigs) validate() error {
if algo == ssh.CertAlgoRSAv01 {
continue
}
if !util.Contains(supportedHostKeyAlgos, algo) {
if !slices.Contains(supportedHostKeyAlgos, algo) {
return util.NewValidationError(fmt.Sprintf("unsupported host key algorithm %q", algo))
}
hostKeyAlgos = append(hostKeyAlgos, algo)
@ -113,24 +117,27 @@ func (c *SFTPDConfigs) validate() error {
if algo == "diffie-hellman-group18-sha512" || algo == ssh.KeyExchangeDHGEXSHA256 {
continue
}
if !util.Contains(supportedKexAlgos, algo) {
if !slices.Contains(supportedKexAlgos, algo) {
return util.NewValidationError(fmt.Sprintf("unsupported KEX algorithm %q", algo))
}
kexAlgos = append(kexAlgos, algo)
}
c.KexAlgorithms = kexAlgos
for _, cipher := range c.Ciphers {
if !util.Contains(supportedCiphers, cipher) {
if slices.Contains([]string{"aes192-cbc", "aes256-cbc"}, cipher) {
continue
}
if !slices.Contains(supportedCiphers, cipher) {
return util.NewValidationError(fmt.Sprintf("unsupported cipher %q", cipher))
}
}
for _, mac := range c.MACs {
if !util.Contains(supportedMACs, mac) {
if !slices.Contains(supportedMACs, mac) {
return util.NewValidationError(fmt.Sprintf("unsupported MAC algorithm %q", mac))
}
}
for _, algo := range c.PublicKeyAlgos {
if !util.Contains(supportedPublicKeyAlgos, algo) {
if !slices.Contains(supportedPublicKeyAlgos, algo) {
return util.NewValidationError(fmt.Sprintf("unsupported public key algorithm %q", algo))
}
}
@ -193,19 +200,19 @@ func (c *SMTPOAuth2) validate() error {
if c.ClientID == "" {
return util.NewI18nError(
util.NewValidationError("smtp oauth2: client id is required"),
util.I18nErrorSMTPClientIDRequired,
util.I18nErrorClientIDRequired,
)
}
if c.ClientSecret == nil {
if c.ClientSecret == nil || c.ClientSecret.IsEmpty() {
return util.NewI18nError(
util.NewValidationError("smtp oauth2: client secret is required"),
util.I18nErrorSMTPClientSecretRequired,
util.I18nErrorClientSecretRequired,
)
}
if c.RefreshToken == nil {
if c.RefreshToken == nil || c.RefreshToken.IsEmpty() {
return util.NewI18nError(
util.NewValidationError("smtp oauth2: refresh token is required"),
util.I18nErrorSMTPRefreshTokenRequired,
util.I18nErrorRefreshTokenRequired,
)
}
if err := validateSMTPSecret(c.ClientSecret, "oauth2 client secret"); err != nil {
@ -305,6 +312,27 @@ func (c *SMTPConfigs) TryDecrypt() error {
return nil
}
func (c *SMTPConfigs) prepareForRendering() {
if c.Password != nil {
c.Password.Hide()
if c.Password.IsEmpty() {
c.Password = nil
}
}
if c.OAuth2.ClientSecret != nil {
c.OAuth2.ClientSecret.Hide()
if c.OAuth2.ClientSecret.IsEmpty() {
c.OAuth2.ClientSecret = nil
}
}
if c.OAuth2.RefreshToken != nil {
c.OAuth2.RefreshToken.Hide()
if c.OAuth2.RefreshToken.IsEmpty() {
c.OAuth2.RefreshToken = nil
}
}
}
func (c *SMTPConfigs) getACopy() *SMTPConfigs {
var password *kms.Secret
if c.Password != nil {
@ -387,13 +415,137 @@ func (c *ACMEConfigs) getACopy() *ACMEConfigs {
}
}
// BrandingConfig defines the branding configuration
type BrandingConfig struct {
Name string `json:"name"`
ShortName string `json:"short_name"`
Logo []byte `json:"logo"`
Favicon []byte `json:"favicon"`
DisclaimerName string `json:"disclaimer_name"`
DisclaimerURL string `json:"disclaimer_url"`
}
func (c *BrandingConfig) isEmpty() bool {
if c.Name != "" {
return false
}
if c.ShortName != "" {
return false
}
if len(c.Logo) > 0 {
return false
}
if len(c.Favicon) > 0 {
return false
}
if c.DisclaimerName != "" && c.DisclaimerURL != "" {
return false
}
return true
}
func (*BrandingConfig) validatePNG(b []byte, maxWidth, maxHeight int) error {
if len(b) == 0 {
return nil
}
// DecodeConfig is more efficient, but I'm not sure if this would lead to
// accepting invalid images in some edge cases and performance does not
// matter here.
img, err := png.Decode(bytes.NewBuffer(b))
if err != nil {
return util.NewI18nError(
util.NewValidationError("invalid PNG image"),
util.I18nErrorInvalidPNG,
)
}
bounds := img.Bounds()
if bounds.Dx() > maxWidth || bounds.Dy() > maxHeight {
return util.NewI18nError(
util.NewValidationError("invalid PNG image size"),
util.I18nErrorInvalidPNGSize,
)
}
return nil
}
func (c *BrandingConfig) validateDisclaimerURL() error {
if c.DisclaimerURL == "" {
return nil
}
u, err := url.Parse(c.DisclaimerURL)
if err != nil {
return util.NewI18nError(
util.NewValidationError("invalid disclaimer URL"),
util.I18nErrorInvalidDisclaimerURL,
)
}
if u.Scheme != "http" && u.Scheme != "https" {
return util.NewI18nError(
util.NewValidationError("invalid disclaimer URL scheme"),
util.I18nErrorInvalidDisclaimerURL,
)
}
return nil
}
func (c *BrandingConfig) validate() error {
if err := c.validateDisclaimerURL(); err != nil {
return err
}
if err := c.validatePNG(c.Logo, 512, 512); err != nil {
return err
}
return c.validatePNG(c.Favicon, 256, 256)
}
func (c *BrandingConfig) getACopy() BrandingConfig {
logo := make([]byte, len(c.Logo))
copy(logo, c.Logo)
favicon := make([]byte, len(c.Favicon))
copy(favicon, c.Favicon)
return BrandingConfig{
Name: c.Name,
ShortName: c.ShortName,
Logo: logo,
Favicon: favicon,
DisclaimerName: c.DisclaimerName,
DisclaimerURL: c.DisclaimerURL,
}
}
// BrandingConfigs defines the branding configuration for WebAdmin and WebClient UI
type BrandingConfigs struct {
WebAdmin BrandingConfig
WebClient BrandingConfig
}
func (c *BrandingConfigs) isEmpty() bool {
return c.WebAdmin.isEmpty() && c.WebClient.isEmpty()
}
func (c *BrandingConfigs) validate() error {
if err := c.WebAdmin.validate(); err != nil {
return err
}
return c.WebClient.validate()
}
func (c *BrandingConfigs) getACopy() *BrandingConfigs {
return &BrandingConfigs{
WebAdmin: c.WebAdmin.getACopy(),
WebClient: c.WebClient.getACopy(),
}
}
// Configs allows to set configuration keys disabled by default without
// modifying the config file or setting env vars
type Configs struct {
SFTPD *SFTPDConfigs `json:"sftpd,omitempty"`
SMTP *SMTPConfigs `json:"smtp,omitempty"`
ACME *ACMEConfigs `json:"acme,omitempty"`
UpdatedAt int64 `json:"updated_at,omitempty"`
SFTPD *SFTPDConfigs `json:"sftpd,omitempty"`
SMTP *SMTPConfigs `json:"smtp,omitempty"`
ACME *ACMEConfigs `json:"acme,omitempty"`
Branding *BrandingConfigs `json:"branding,omitempty"`
UpdatedAt int64 `json:"updated_at,omitempty"`
}
func (c *Configs) validate() error {
@ -412,6 +564,11 @@ func (c *Configs) validate() error {
return err
}
}
if c.Branding != nil {
if err := c.Branding.validate(); err != nil {
return err
}
}
return nil
}
@ -428,25 +585,11 @@ func (c *Configs) PrepareForRendering() {
if c.ACME != nil && c.ACME.isEmpty() {
c.ACME = nil
}
if c.Branding != nil && c.Branding.isEmpty() {
c.Branding = nil
}
if c.SMTP != nil {
if c.SMTP.Password != nil {
c.SMTP.Password.Hide()
if c.SMTP.Password.IsEmpty() {
c.SMTP.Password = nil
}
}
if c.SMTP.OAuth2.ClientSecret != nil {
c.SMTP.OAuth2.ClientSecret.Hide()
if c.SMTP.OAuth2.ClientSecret.IsEmpty() {
c.SMTP.OAuth2.ClientSecret = nil
}
}
if c.SMTP.OAuth2.RefreshToken != nil {
c.SMTP.OAuth2.RefreshToken.Hide()
if c.SMTP.OAuth2.RefreshToken.IsEmpty() {
c.SMTP.OAuth2.RefreshToken = nil
}
}
c.SMTP.prepareForRendering()
}
}
@ -470,6 +613,9 @@ func (c *Configs) SetNilsToEmpty() {
if c.ACME == nil {
c.ACME = &ACMEConfigs{}
}
if c.Branding == nil {
c.Branding = &BrandingConfigs{}
}
}
// RenderAsJSON implements the renderer interface used within plugins
@ -498,6 +644,9 @@ func (c *Configs) getACopy() Configs {
if c.ACME != nil {
result.ACME = c.ACME.getACopy()
}
if c.Branding != nil {
result.Branding = c.Branding.getACopy()
}
result.UpdatedAt = c.UpdatedAt
return result
}

View file

@ -44,6 +44,7 @@ import (
"path/filepath"
"regexp"
"runtime"
"slices"
"strconv"
"strings"
"sync"
@ -89,7 +90,7 @@ const (
CockroachDataProviderName = "cockroachdb"
// DumpVersion defines the version for the dump.
// For restore/load we support the current version and the previous one
DumpVersion = 16
DumpVersion = 17
argonPwdPrefix = "$argon2id$"
bcryptPwdPrefix = "$2a$"
@ -111,7 +112,6 @@ const (
operationDelete = "delete"
sqlPrefixValidChars = "abcdefghijklmnopqrstuvwxyz_0123456789"
maxHookResponseSize = 1048576 // 1MB
iso8601UTCFormat = "2006-01-02T15:04:05Z"
)
// Supported algorithms for hashing passwords.
@ -187,6 +187,8 @@ var (
ErrDuplicatedKey = errors.New("duplicated key not allowed")
// ErrForeignKeyViolated occurs when there is a foreign key constraint violation
ErrForeignKeyViolated = errors.New("violates foreign key constraint")
errInvalidInput = util.NewValidationError("Invalid input. Slashes (/ ), colons (:), control characters, and reserved system names are not allowed")
tz = ""
isAdminCreated atomic.Bool
validTLSUsernames = []string{string(sdk.TLSUsernameNone), string(sdk.TLSUsernameCN)}
config Config
@ -209,6 +211,7 @@ var (
sqlTableAdmins string
sqlTableAPIKeys string
sqlTableShares string
sqlTableSharesGroupsMapping string
sqlTableDefenderHosts string
sqlTableDefenderEvents string
sqlTableActiveTransfers string
@ -243,6 +246,7 @@ func initSQLTables() {
sqlTableAdmins = "admins"
sqlTableAPIKeys = "api_keys"
sqlTableShares = "shares"
sqlTableSharesGroupsMapping = "shares_groups_mapping"
sqlTableDefenderHosts = "defender_hosts"
sqlTableDefenderEvents = "defender_events"
sqlTableActiveTransfers = "active_transfers"
@ -518,7 +522,7 @@ type Config struct {
// GetShared returns the provider share mode.
// This method is called before the provider is initialized
func (c *Config) GetShared() int {
if !util.Contains(sharedProviders, c.Driver) {
if !slices.Contains(sharedProviders, c.Driver) {
return 0
}
return c.IsShared
@ -590,6 +594,16 @@ func (c *Config) doBackup() (string, error) {
return outputFile, nil
}
// SetTZ sets the configured timezone.
func SetTZ(val string) {
tz = val
}
// UseLocalTime returns true if local time should be used instead of UTC.
func UseLocalTime() bool {
return tz == "local"
}
// ExecuteBackup executes a backup
func ExecuteBackup() (string, error) {
return config.doBackup()
@ -759,6 +773,8 @@ type Provider interface {
updateLastLogin(username string) error
updateAdminLastLogin(username string) error
setUpdatedAt(username string)
getAdminSignature(username string) (string, error)
getUserSignature(username string) (string, error)
getFolders(limit, offset int, order string, minimal bool) ([]vfs.BaseVirtualFolder, error)
getFolderByName(name string) (vfs.BaseVirtualFolder, error)
addFolder(folder *vfs.BaseVirtualFolder) error
@ -810,8 +826,8 @@ type Provider interface {
cleanupActiveTransfers(before time.Time) error
getActiveTransfers(from time.Time) ([]ActiveTransfer, error)
addSharedSession(session Session) error
deleteSharedSession(key string) error
getSharedSession(key string) (Session, error)
deleteSharedSession(key string, sessionType SessionType) error
getSharedSession(key string, sessionType SessionType) (Session, error)
cleanupSharedSessions(sessionType SessionType, before int64) error
getEventActions(limit, offset int, order string, minimal bool) ([]BaseEventAction, error)
dumpEventActions() ([]BaseEventAction, error)
@ -874,7 +890,7 @@ func SetTempPath(fsPath string) {
}
func checkSharedMode() {
if !util.Contains(sharedProviders, config.Driver) {
if !slices.Contains(sharedProviders, config.Driver) {
config.IsShared = 0
}
}
@ -929,12 +945,13 @@ func checkDatabase(checkAdmins bool) error {
if config.UpdateMode == 0 {
err := provider.initializeDatabase()
if err != nil && err != ErrNoInitRequired {
logger.WarnToConsole("Unable to initialize data provider: %v", err)
providerLog(logger.LevelError, "Unable to initialize data provider: %v", err)
logger.WarnToConsole("unable to initialize data provider: %v", err)
providerLog(logger.LevelError, "unable to initialize data provider: %v", err)
return err
}
if err == nil {
logger.DebugToConsole("Data provider successfully initialized")
logger.DebugToConsole("data provider successfully initialized")
providerLog(logger.LevelInfo, "data provider successfully initialized")
}
err = provider.migrateDatabase()
if err != nil && err != ErrNoInitRequired {
@ -1040,6 +1057,7 @@ func validateSQLTablesPrefix() error {
sqlTableAdmins = config.SQLTablesPrefix + sqlTableAdmins
sqlTableAPIKeys = config.SQLTablesPrefix + sqlTableAPIKeys
sqlTableShares = config.SQLTablesPrefix + sqlTableShares
sqlTableSharesGroupsMapping = config.SQLTablesPrefix + sqlTableSharesGroupsMapping
sqlTableDefenderEvents = config.SQLTablesPrefix + sqlTableDefenderEvents
sqlTableDefenderHosts = config.SQLTablesPrefix + sqlTableDefenderHosts
sqlTableActiveTransfers = config.SQLTablesPrefix + sqlTableActiveTransfers
@ -1061,12 +1079,12 @@ func validateSQLTablesPrefix() error {
"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 "+
"schema version %q events actions %q events rules %q rules actions mapping %q tasks %q nodes %q roles %q"+
"ip lists %q configs %q",
"ip lists %q share groups mapping %q configs %q",
sqlTableUsers, sqlTableFolders, sqlTableUsersFoldersMapping, sqlTableAdmins, sqlTableAPIKeys,
sqlTableShares, sqlTableDefenderHosts, sqlTableDefenderEvents, sqlTableActiveTransfers, sqlTableGroups,
sqlTableUsersGroupsMapping, sqlTableAdminsGroupsMapping, sqlTableGroupsFoldersMapping, sqlTableSharedSessions,
sqlTableSchemaVersion, sqlTableEventsActions, sqlTableEventsRules, sqlTableRulesActionsMapping,
sqlTableTasks, sqlTableNodes, sqlTableRoles, sqlTableIPLists, sqlTableConfigs)
sqlTableTasks, sqlTableNodes, sqlTableRoles, sqlTableIPLists, sqlTableSharesGroupsMapping, sqlTableConfigs)
}
return nil
}
@ -1503,6 +1521,15 @@ func UpdateUserQuota(user *User, filesAdd int, sizeAdd int64, reset bool) error
return nil
}
// UpdateUserFolderQuota updates the quota for the given user and virtual folder.
func UpdateUserFolderQuota(folder *vfs.VirtualFolder, user *User, filesAdd int, sizeAdd int64, reset bool) {
if folder.IsIncludedInUserQuota() {
UpdateUserQuota(user, filesAdd, sizeAdd, reset) //nolint:errcheck
return
}
UpdateVirtualFolderQuota(&folder.BaseVirtualFolder, filesAdd, sizeAdd, reset) //nolint:errcheck
}
// UpdateVirtualFolderQuota updates the quota for the given virtual folder adding filesAdd and sizeAdd.
// If reset is true filesAdd and sizeAdd indicates the total files and the total size instead of the difference.
func UpdateVirtualFolderQuota(vfolder *vfs.BaseVirtualFolder, filesAdd int, sizeAdd int64, reset bool) error {
@ -1693,7 +1720,7 @@ func IPListEntryExists(ipOrNet string, listType IPListType) (IPListEntry, error)
// GetIPListEntries returns the IP list entries applying the specified criteria and search limit
func GetIPListEntries(listType IPListType, filter, from, order string, limit int) ([]IPListEntry, error) {
if !util.Contains(supportedIPListType, listType) {
if !slices.Contains(supportedIPListType, listType) {
return nil, util.NewValidationError(fmt.Sprintf("invalid list type %d", listType))
}
return provider.getIPListEntries(listType, filter, from, order, limit)
@ -2064,6 +2091,20 @@ func UserExists(username, role string) (User, error) {
return provider.userExists(username, role)
}
// GetAdminSignature returns the signature for the admin with the specified
// username.
func GetAdminSignature(username string) (string, error) {
username = config.convertName(username)
return provider.getAdminSignature(username)
}
// GetUserSignature returns the signature for the user with the specified
// username.
func GetUserSignature(username string) (string, error) {
username = config.convertName(username)
return provider.getUserSignature(username)
}
// GetUserWithGroupSettings tries to return the user with the specified username
// loading also the group settings
func GetUserWithGroupSettings(username, role string) (User, error) {
@ -2106,9 +2147,6 @@ func UpdateUserPassword(username, plainPwd, executor, ipAddress, role string) er
return err
}
userCopy := user.getACopy()
if err := userCopy.LoadAndApplyGroupSettings(); err != nil {
return err
}
userCopy.Password = plainPwd
if err := createUserPasswordHash(&userCopy); err != nil {
return err
@ -2206,8 +2244,8 @@ func AddSharedSession(session Session) error {
}
// DeleteSharedSession deletes the session with the specified key
func DeleteSharedSession(key string) error {
err := provider.deleteSharedSession(key)
func DeleteSharedSession(key string, sessionType SessionType) error {
err := provider.deleteSharedSession(key, sessionType)
if err != nil {
providerLog(logger.LevelError, "unable to add shared session, key %q, err: %v", key, err)
}
@ -2215,8 +2253,8 @@ func DeleteSharedSession(key string) error {
}
// GetSharedSession retrieves the session with the specified key
func GetSharedSession(key string) (Session, error) {
return provider.getSharedSession(key)
func GetSharedSession(key string, sessionType SessionType) (Session, error) {
return provider.getSharedSession(key, sessionType)
}
// CleanupSharedSessions removes the shared session with the specified type and
@ -2352,7 +2390,7 @@ func GetFolders(limit, offset int, order string, minimal bool) ([]vfs.BaseVirtua
}
func dumpUsers(data *BackupData, scopes []string) error {
if len(scopes) == 0 || util.Contains(scopes, DumpScopeUsers) {
if len(scopes) == 0 || slices.Contains(scopes, DumpScopeUsers) {
users, err := provider.dumpUsers()
if err != nil {
return err
@ -2363,7 +2401,7 @@ func dumpUsers(data *BackupData, scopes []string) error {
}
func dumpFolders(data *BackupData, scopes []string) error {
if len(scopes) == 0 || util.Contains(scopes, DumpScopeFolders) {
if len(scopes) == 0 || slices.Contains(scopes, DumpScopeFolders) {
folders, err := provider.dumpFolders()
if err != nil {
return err
@ -2374,7 +2412,7 @@ func dumpFolders(data *BackupData, scopes []string) error {
}
func dumpGroups(data *BackupData, scopes []string) error {
if len(scopes) == 0 || util.Contains(scopes, DumpScopeGroups) {
if len(scopes) == 0 || slices.Contains(scopes, DumpScopeGroups) {
groups, err := provider.dumpGroups()
if err != nil {
return err
@ -2385,7 +2423,7 @@ func dumpGroups(data *BackupData, scopes []string) error {
}
func dumpAdmins(data *BackupData, scopes []string) error {
if len(scopes) == 0 || util.Contains(scopes, DumpScopeAdmins) {
if len(scopes) == 0 || slices.Contains(scopes, DumpScopeAdmins) {
admins, err := provider.dumpAdmins()
if err != nil {
return err
@ -2396,7 +2434,7 @@ func dumpAdmins(data *BackupData, scopes []string) error {
}
func dumpAPIKeys(data *BackupData, scopes []string) error {
if len(scopes) == 0 || util.Contains(scopes, DumpScopeAPIKeys) {
if len(scopes) == 0 || slices.Contains(scopes, DumpScopeAPIKeys) {
apiKeys, err := provider.dumpAPIKeys()
if err != nil {
return err
@ -2407,7 +2445,7 @@ func dumpAPIKeys(data *BackupData, scopes []string) error {
}
func dumpShares(data *BackupData, scopes []string) error {
if len(scopes) == 0 || util.Contains(scopes, DumpScopeShares) {
if len(scopes) == 0 || slices.Contains(scopes, DumpScopeShares) {
shares, err := provider.dumpShares()
if err != nil {
return err
@ -2418,7 +2456,7 @@ func dumpShares(data *BackupData, scopes []string) error {
}
func dumpActions(data *BackupData, scopes []string) error {
if len(scopes) == 0 || util.Contains(scopes, DumpScopeActions) {
if len(scopes) == 0 || slices.Contains(scopes, DumpScopeActions) {
actions, err := provider.dumpEventActions()
if err != nil {
return err
@ -2429,7 +2467,7 @@ func dumpActions(data *BackupData, scopes []string) error {
}
func dumpRules(data *BackupData, scopes []string) error {
if len(scopes) == 0 || util.Contains(scopes, DumpScopeRules) {
if len(scopes) == 0 || slices.Contains(scopes, DumpScopeRules) {
rules, err := provider.dumpEventRules()
if err != nil {
return err
@ -2440,7 +2478,7 @@ func dumpRules(data *BackupData, scopes []string) error {
}
func dumpRoles(data *BackupData, scopes []string) error {
if len(scopes) == 0 || util.Contains(scopes, DumpScopeRoles) {
if len(scopes) == 0 || slices.Contains(scopes, DumpScopeRoles) {
roles, err := provider.dumpRoles()
if err != nil {
return err
@ -2451,7 +2489,7 @@ func dumpRoles(data *BackupData, scopes []string) error {
}
func dumpIPLists(data *BackupData, scopes []string) error {
if len(scopes) == 0 || util.Contains(scopes, DumpScopeIPLists) {
if len(scopes) == 0 || slices.Contains(scopes, DumpScopeIPLists) {
ipLists, err := provider.dumpIPListEntries()
if err != nil {
return err
@ -2462,7 +2500,7 @@ func dumpIPLists(data *BackupData, scopes []string) error {
}
func dumpConfigs(data *BackupData, scopes []string) error {
if len(scopes) == 0 || util.Contains(scopes, DumpScopeConfigs) {
if len(scopes) == 0 || slices.Contains(scopes, DumpScopeConfigs) {
configs, err := provider.getConfigs()
if err != nil {
return err
@ -2519,6 +2557,17 @@ func DumpData(scopes []string) (BackupData, error) {
func ParseDumpData(data []byte) (BackupData, error) {
var dump BackupData
err := json.Unmarshal(data, &dump)
if err != nil {
return dump, err
}
if dump.Version < 17 {
providerLog(logger.LevelInfo, "updating placeholders for actions restored from dump version %d", dump.Version)
eventActions, err := updateEventActionPlaceholders(dump.EventActions)
if err != nil {
return dump, fmt.Errorf("unable to update event action placeholders for dump version %d: %w", dump.Version, err)
}
dump.EventActions = eventActions
}
return dump, err
}
@ -2568,9 +2617,8 @@ func createProvider(basePath string) error {
return initializeBoltProvider(basePath)
case MemoryDataProviderName:
if err := initializeMemoryProvider(basePath); err != nil {
msg := fmt.Sprintf("provider initialized but data loading failed: %v", err)
logger.Warn(logSender, "", msg)
logger.WarnToConsole(msg)
logger.Warn(logSender, "", "provider initialized but data loading failed: %v", err)
logger.WarnToConsole("provider initialized but data loading failed: %v", err)
}
return nil
default:
@ -2684,7 +2732,7 @@ func validateUserGroups(user *User) error {
groupNames := make(map[string]bool)
for _, g := range user.Groups {
if g.Type < sdk.GroupTypePrimary && g.Type > sdk.GroupTypeMembership {
if g.Type < sdk.GroupTypePrimary || g.Type > sdk.GroupTypeMembership {
return util.NewValidationError(fmt.Sprintf("invalid group type: %v", g.Type))
}
if g.Type == sdk.GroupTypePrimary {
@ -2715,6 +2763,7 @@ func validateAssociatedVirtualFolders(vfolders []vfs.VirtualFolder) ([]vfs.Virtu
folderNames := make(map[string]bool)
for _, v := range vfolders {
v.Name = config.convertName(v.Name)
if v.VirtualPath == "" {
return nil, util.NewI18nError(
util.NewValidationError("mount/virtual path is mandatory"),
@ -2766,7 +2815,7 @@ func validateUserTOTPConfig(c *UserTOTPConfig, username string) error {
if c.ConfigName == "" {
return util.NewValidationError("totp: config name is mandatory")
}
if !util.Contains(mfa.GetAvailableTOTPConfigNames(), c.ConfigName) {
if !slices.Contains(mfa.GetAvailableTOTPConfigNames(), c.ConfigName) {
return util.NewValidationError(fmt.Sprintf("totp: config name %q not found", c.ConfigName))
}
if c.Secret.IsEmpty() {
@ -2782,7 +2831,7 @@ func validateUserTOTPConfig(c *UserTOTPConfig, username string) error {
return util.NewValidationError("totp: specify at least one protocol")
}
for _, protocol := range c.Protocols {
if !util.Contains(MFAProtocols, protocol) {
if !slices.Contains(MFAProtocols, protocol) {
return util.NewValidationError(fmt.Sprintf("totp: invalid protocol %q", protocol))
}
}
@ -2815,7 +2864,7 @@ func validateUserPermissions(permsToCheck map[string][]string) (map[string][]str
return permissions, util.NewValidationError("invalid permissions")
}
for _, p := range perms {
if !util.Contains(ValidPerms, p) {
if !slices.Contains(ValidPerms, p) {
return permissions, util.NewValidationError(fmt.Sprintf("invalid permission: %q", p))
}
}
@ -2829,7 +2878,7 @@ func validateUserPermissions(permsToCheck map[string][]string) (map[string][]str
if dir != cleanedDir && cleanedDir == "/" {
return permissions, util.NewValidationError(fmt.Sprintf("cannot set permissions for invalid subdirectory: %q is an alias for \"/\"", dir))
}
if util.Contains(perms, PermAny) {
if slices.Contains(perms, PermAny) {
permissions[cleanedDir] = []string{PermAny}
} else {
permissions[cleanedDir] = util.RemoveDuplicates(perms, false)
@ -2870,11 +2919,18 @@ func validatePublicKeys(user *User) error {
util.I18nErrorPubKeyInvalid,
)
}
if out.Type() == ssh.InsecureKeyAlgoDSA { //nolint:staticcheck
providerLog(logger.LevelError, "dsa public key not accepted, position: %d", idx)
return util.NewI18nError(
util.NewValidationError(fmt.Sprintf("DSA key format is insecure and it is not allowed for key at position %d", idx)),
util.I18nErrorKeyInsecure,
)
}
if k, ok := out.(ssh.CryptoPublicKey); ok {
cryptoKey := k.CryptoPublicKey()
if rsaKey, ok := cryptoKey.(*rsa.PublicKey); ok {
if size := rsaKey.N.BitLen(); size < 2048 {
providerLog(logger.LevelError, "rsa key with size %d not accepted, minimum 2048", size)
providerLog(logger.LevelError, "rsa key with size %d at position %d not accepted, minimum 2048", size, idx)
return util.NewI18nError(
util.NewValidationError(fmt.Sprintf("invalid size %d for rsa key at position %d, minimum 2048",
size, idx)),
@ -2905,7 +2961,7 @@ func validateFiltersPatternExtensions(baseFilters *sdk.BaseUserFilters) error {
util.I18nErrorFilePatternPathInvalid,
)
}
if util.Contains(filteredPaths, cleanedPath) {
if slices.Contains(filteredPaths, cleanedPath) {
return util.NewI18nError(
util.NewValidationError(fmt.Sprintf("duplicate file patterns filter for path %q", f.Path)),
util.I18nErrorFilePatternDuplicated,
@ -3024,13 +3080,13 @@ func validateFilterProtocols(filters *sdk.BaseUserFilters) error {
return util.NewValidationError("invalid denied_protocols")
}
for _, p := range filters.DeniedProtocols {
if !util.Contains(ValidProtocols, p) {
if !slices.Contains(ValidProtocols, p) {
return util.NewValidationError(fmt.Sprintf("invalid denied protocol %q", p))
}
}
for _, p := range filters.TwoFactorAuthProtocols {
if !util.Contains(MFAProtocols, p) {
if !slices.Contains(MFAProtocols, p) {
return util.NewValidationError(fmt.Sprintf("invalid two factor protocol %q", p))
}
}
@ -3086,7 +3142,7 @@ func validateBaseFilters(filters *sdk.BaseUserFilters) error {
return util.NewValidationError("invalid denied_login_methods")
}
for _, loginMethod := range filters.DeniedLoginMethods {
if !util.Contains(ValidLoginMethods, loginMethod) {
if !slices.Contains(ValidLoginMethods, loginMethod) {
return util.NewValidationError(fmt.Sprintf("invalid login method: %q", loginMethod))
}
}
@ -3094,7 +3150,7 @@ func validateBaseFilters(filters *sdk.BaseUserFilters) error {
return err
}
if filters.TLSUsername != "" {
if !util.Contains(validTLSUsernames, string(filters.TLSUsername)) {
if !slices.Contains(validTLSUsernames, string(filters.TLSUsername)) {
return util.NewValidationError(fmt.Sprintf("invalid TLS username: %q", filters.TLSUsername))
}
}
@ -3104,7 +3160,7 @@ func validateBaseFilters(filters *sdk.BaseUserFilters) error {
}
filters.TLSCerts = certs
for _, opts := range filters.WebClient {
if !util.Contains(sdk.WebClientOptions, opts) {
if !slices.Contains(sdk.WebClientOptions, opts) {
return util.NewValidationError(fmt.Sprintf("invalid web client options %q", opts))
}
}
@ -3172,19 +3228,19 @@ func validateAccessTimeFilters(filters *sdk.BaseUserFilters) error {
}
func validateCombinedUserFilters(user *User) error {
if user.Filters.TOTPConfig.Enabled && util.Contains(user.Filters.WebClient, sdk.WebClientMFADisabled) {
if user.Filters.TOTPConfig.Enabled && slices.Contains(user.Filters.WebClient, sdk.WebClientMFADisabled) {
return util.NewI18nError(
util.NewValidationError("two-factor authentication cannot be disabled for a user with an active configuration"),
util.I18nErrorDisableActive2FA,
)
}
if user.Filters.RequirePasswordChange && util.Contains(user.Filters.WebClient, sdk.WebClientPasswordChangeDisabled) {
if user.Filters.RequirePasswordChange && slices.Contains(user.Filters.WebClient, sdk.WebClientPasswordChangeDisabled) {
return util.NewI18nError(
util.NewValidationError("you cannot require password change and at the same time disallow it"),
util.I18nErrorPwdChangeConflict,
)
}
if len(user.Filters.TwoFactorAuthProtocols) > 0 && util.Contains(user.Filters.WebClient, sdk.WebClientMFADisabled) {
if len(user.Filters.TwoFactorAuthProtocols) > 0 && slices.Contains(user.Filters.WebClient, sdk.WebClientMFADisabled) {
return util.NewI18nError(
util.NewValidationError("you cannot require two-factor authentication and at the same time disallow it"),
util.I18nError2FAConflict,
@ -3193,19 +3249,37 @@ func validateCombinedUserFilters(user *User) error {
return nil
}
func validateBaseParams(user *User) error {
if user.Username == "" {
return util.NewI18nError(util.NewValidationError("username is mandatory"), util.I18nErrorUsernameRequired)
}
if err := checkReservedUsernames(user.Username); err != nil {
return util.NewI18nError(err, util.I18nErrorReservedUsername)
}
func validateEmails(user *User) error {
if user.Email != "" && !util.IsEmailValid(user.Email) {
return util.NewI18nError(
util.NewValidationError(fmt.Sprintf("email %q is not valid", user.Email)),
util.I18nErrorInvalidEmail,
)
}
for _, email := range user.Filters.AdditionalEmails {
if !util.IsEmailValid(email) {
return util.NewI18nError(
util.NewValidationError(fmt.Sprintf("email %q is not valid", email)),
util.I18nErrorInvalidEmail,
)
}
}
return nil
}
func validateBaseParams(user *User) error {
if user.Username == "" {
return util.NewI18nError(util.NewValidationError("username is mandatory"), util.I18nErrorUsernameRequired)
}
if !util.IsNameValid(user.Username) {
return util.NewI18nError(errInvalidInput, util.I18nErrorInvalidInput)
}
if err := checkReservedUsernames(user.Username); err != nil {
return util.NewI18nError(err, util.I18nErrorReservedUsername)
}
if err := validateEmails(user); err != nil {
return err
}
if config.NamingRules&1 == 0 && !usernameRegex.MatchString(user.Username) {
return util.NewI18nError(
util.NewValidationError(fmt.Sprintf("username %q is not valid, the following characters are allowed: a-zA-Z0-9-_.~", user.Username)),
@ -3266,6 +3340,19 @@ func hashPlainPassword(plainPwd string) (string, error) {
func createUserPasswordHash(user *User) error {
if user.Password != "" && !user.IsPasswordHashed() {
for _, g := range user.Groups {
if g.Type == sdk.GroupTypePrimary {
group, err := GroupExists(g.Name)
if err != nil {
return errors.New("unable to load group password policies")
}
if minEntropy := group.UserSettings.Filters.PasswordStrength; minEntropy > 0 {
if err := passwordvalidator.Validate(user.Password, float64(minEntropy)); err != nil {
return util.NewI18nError(util.NewValidationError(err.Error()), util.I18nErrorPasswordComplexity)
}
}
}
}
if minEntropy := user.getMinPasswordEntropy(); minEntropy > 0 {
if err := passwordvalidator.Validate(user.Password, minEntropy); err != nil {
return util.NewI18nError(util.NewValidationError(err.Error()), util.I18nErrorPasswordComplexity)
@ -3288,6 +3375,9 @@ func ValidateFolder(folder *vfs.BaseVirtualFolder) error {
if folder.Name == "" {
return util.NewI18nError(util.NewValidationError("folder name is mandatory"), util.I18nErrorNameRequired)
}
if !util.IsNameValid(folder.Name) {
return util.NewI18nError(errInvalidInput, util.I18nErrorInvalidInput)
}
if config.NamingRules&1 == 0 && !usernameRegex.MatchString(folder.Name) {
return util.NewI18nError(
util.NewValidationError(fmt.Sprintf("folder name %q is not valid, the following characters are allowed: a-zA-Z0-9-_.~", folder.Name)),
@ -3317,6 +3407,7 @@ func ValidateUser(user *User) error {
user.OIDCCustomFields = nil
user.HasPassword = false
user.SetEmptySecretsIfNil()
user.applyNamingRules()
buildUserHomeDir(user)
if err := validateBaseParams(user); err != nil {
return err
@ -3466,7 +3557,7 @@ func checkUserAndPass(user *User, password, ip, protocol string) (User, error) {
if err != nil {
return *user, ErrInvalidCredentials
}
if user.Password == "" || password == "" {
if user.Password == "" || strings.TrimSpace(password) == "" {
return *user, errors.New("credentials cannot be null or empty")
}
if !user.Filters.Hooks.CheckPasswordDisabled {
@ -3505,7 +3596,7 @@ func checkUserPasscode(user *User, password, protocol string) (string, error) {
if user.Filters.TOTPConfig.Enabled {
switch protocol {
case protocolFTP:
if util.Contains(user.Filters.TOTPConfig.Protocols, protocol) {
if slices.Contains(user.Filters.TOTPConfig.Protocols, protocol) {
// the TOTP passcode has six digits
pwdLen := len(password)
if pwdLen < 7 {
@ -3641,7 +3732,8 @@ func comparePbkdf2PasswordAndHash(password, hashedPassword string) (bool, error)
}
func getSSLMode() string {
if config.Driver == PGSQLDataProviderName || config.Driver == CockroachDataProviderName {
switch config.Driver {
case PGSQLDataProviderName, CockroachDataProviderName:
switch config.SSLMode {
case 0:
return "disable"
@ -3656,7 +3748,7 @@ func getSSLMode() string {
case 5:
return "allow"
}
} else if config.Driver == MySQLDataProviderName {
case MySQLDataProviderName:
if config.requireCustomTLSForMySQL() {
return "custom"
}
@ -3711,7 +3803,7 @@ func doBuiltinKeyboardInteractiveAuth(user *User, client ssh.KeyboardInteractive
if err := user.LoadAndApplyGroupSettings(); err != nil {
return 0, err
}
hasSecondFactor := user.Filters.TOTPConfig.Enabled && util.Contains(user.Filters.TOTPConfig.Protocols, protocolSSH)
hasSecondFactor := user.Filters.TOTPConfig.Enabled && slices.Contains(user.Filters.TOTPConfig.Protocols, protocolSSH)
if !isPartialAuth || !hasSecondFactor {
answers, err := client("", "", []string{"Password: "}, []bool{false})
if err != nil {
@ -3729,7 +3821,7 @@ func doBuiltinKeyboardInteractiveAuth(user *User, client ssh.KeyboardInteractive
}
func checkKeyboardInteractiveSecondFactor(user *User, client ssh.KeyboardInteractiveChallenge, protocol string) (int, error) {
if !user.Filters.TOTPConfig.Enabled || !util.Contains(user.Filters.TOTPConfig.Protocols, protocolSSH) {
if !user.Filters.TOTPConfig.Enabled || !slices.Contains(user.Filters.TOTPConfig.Protocols, protocolSSH) {
return 1, nil
}
err := user.Filters.TOTPConfig.Secret.TryDecrypt()
@ -3853,7 +3945,7 @@ func getKeyboardInteractiveAnswers(client ssh.KeyboardInteractiveChallenge, resp
}
if len(answers) == 1 && response.CheckPwd > 0 {
if response.CheckPwd == 2 {
if !user.Filters.TOTPConfig.Enabled || !util.Contains(user.Filters.TOTPConfig.Protocols, protocolSSH) {
if !user.Filters.TOTPConfig.Enabled || !slices.Contains(user.Filters.TOTPConfig.Protocols, protocolSSH) {
providerLog(logger.LevelInfo, "keyboard interactive auth error: unable to check TOTP passcode, TOTP is not enabled for user %q",
user.Username)
return answers, errors.New("TOTP not enabled for SSH protocol")
@ -4059,7 +4151,7 @@ func getPasswordHookResponse(username, password, ip, protocol string) ([]byte, e
fmt.Sprintf("SFTPGO_AUTHD_IP=%s", ip),
fmt.Sprintf("SFTPGO_AUTHD_PROTOCOL=%s", protocol),
)
return cmd.Output()
return getCmdOutput(cmd, "check_password_hook")
}
func executeCheckPasswordHook(username, password, ip, protocol string) (checkPasswordResponse, error) {
@ -4115,15 +4207,17 @@ func getPreLoginHookResponse(loginMethod, ip, protocol string, userAsJSON []byte
cmd := exec.CommandContext(ctx, config.PreLoginHook, args...)
cmd.Env = append(env,
fmt.Sprintf("SFTPGO_LOGIND_USER=%s", util.BytesToString(userAsJSON)),
fmt.Sprintf("SFTPGO_LOGIND_USER=%s", userAsJSON),
fmt.Sprintf("SFTPGO_LOGIND_METHOD=%s", loginMethod),
fmt.Sprintf("SFTPGO_LOGIND_IP=%s", ip),
fmt.Sprintf("SFTPGO_LOGIND_PROTOCOL=%s", protocol),
)
return cmd.Output()
return getCmdOutput(cmd, "pre_login_hook")
}
func executePreLoginHook(username, loginMethod, ip, protocol string, oidcTokenFields *map[string]any) (User, error) {
var user User
u, mergedUser, userAsJSON, err := getUserAndJSONForHook(username, oidcTokenFields)
if err != nil {
return u, err
@ -4146,55 +4240,41 @@ func executePreLoginHook(username, loginMethod, ip, protocol string, oidcTokenFi
}
return u, nil
}
userID := u.ID
userUsedQuotaSize := u.UsedQuotaSize
userUsedQuotaFiles := u.UsedQuotaFiles
userUsedDownloadTransfer := u.UsedDownloadDataTransfer
userUsedUploadTransfer := u.UsedUploadDataTransfer
userLastQuotaUpdate := u.LastQuotaUpdate
userLastLogin := u.LastLogin
userFirstDownload := u.FirstDownload
userFirstUpload := u.FirstUpload
userLastPwdChange := u.LastPasswordChange
userCreatedAt := u.CreatedAt
totpConfig := u.Filters.TOTPConfig
recoveryCodes := u.Filters.RecoveryCodes
err = json.Unmarshal(out, &u)
err = json.Unmarshal(out, &user)
if err != nil {
return u, fmt.Errorf("invalid pre-login hook response %q, error: %v", util.BytesToString(out), err)
return u, fmt.Errorf("invalid pre-login hook response %q, error: %v", out, err)
}
u.ID = userID
u.UsedQuotaSize = userUsedQuotaSize
u.UsedQuotaFiles = userUsedQuotaFiles
u.UsedUploadDataTransfer = userUsedUploadTransfer
u.UsedDownloadDataTransfer = userUsedDownloadTransfer
u.LastQuotaUpdate = userLastQuotaUpdate
u.LastLogin = userLastLogin
u.LastPasswordChange = userLastPwdChange
u.FirstDownload = userFirstDownload
u.FirstUpload = userFirstUpload
u.CreatedAt = userCreatedAt
if userID == 0 {
err = provider.addUser(&u)
} else {
u.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
if u.ID > 0 {
user.ID = u.ID
user.UsedQuotaSize = u.UsedQuotaSize
user.UsedQuotaFiles = u.UsedQuotaFiles
user.UsedUploadDataTransfer = u.UsedUploadDataTransfer
user.UsedDownloadDataTransfer = u.UsedDownloadDataTransfer
user.LastQuotaUpdate = u.LastQuotaUpdate
user.LastLogin = u.LastLogin
user.LastPasswordChange = u.LastPasswordChange
user.FirstDownload = u.FirstDownload
user.FirstUpload = u.FirstUpload
// preserve TOTP config and recovery codes
u.Filters.TOTPConfig = totpConfig
u.Filters.RecoveryCodes = recoveryCodes
err = provider.updateUser(&u)
if err == nil {
webDAVUsersCache.swap(&u, "")
user.Filters.TOTPConfig = u.Filters.TOTPConfig
user.Filters.RecoveryCodes = u.Filters.RecoveryCodes
if err := provider.updateUser(&user); err != nil {
return u, err
}
} else {
if err := provider.addUser(&user); err != nil {
return u, err
}
}
user, err = provider.userExists(user.Username, "")
if err != nil {
return u, err
}
providerLog(logger.LevelDebug, "user %q added/updated from pre-login hook response, id: %d", username, userID)
if userID == 0 {
return provider.userExists(username, "")
providerLog(logger.LevelDebug, "user %q added/updated from pre-login hook response, id: %d", username, u.ID)
if u.ID > 0 {
webDAVUsersCache.swap(&user, "")
}
return u, nil
return user, nil
}
// ExecutePostLoginHook executes the post login hook if defined
@ -4257,7 +4337,7 @@ func ExecutePostLoginHook(user *User, loginMethod, ip, protocol string, err erro
cmd := exec.CommandContext(ctx, config.PostLoginHook, args...)
cmd.Env = append(env,
fmt.Sprintf("SFTPGO_LOGIND_USER=%s", util.BytesToString(userAsJSON)),
fmt.Sprintf("SFTPGO_LOGIND_USER=%s", userAsJSON),
fmt.Sprintf("SFTPGO_LOGIND_IP=%s", ip),
fmt.Sprintf("SFTPGO_LOGIND_METHOD=%s", loginMethod),
fmt.Sprintf("SFTPGO_LOGIND_STATUS=%s", status),
@ -4326,7 +4406,7 @@ func getExternalAuthResponse(username, password, pkey, keyboardInteractive, ip,
cmd := exec.CommandContext(ctx, config.ExternalAuthHook, args...)
cmd.Env = append(env,
fmt.Sprintf("SFTPGO_AUTHD_USERNAME=%s", username),
fmt.Sprintf("SFTPGO_AUTHD_USER=%s", util.BytesToString(userAsJSON)),
fmt.Sprintf("SFTPGO_AUTHD_USER=%s", userAsJSON),
fmt.Sprintf("SFTPGO_AUTHD_IP=%s", ip),
fmt.Sprintf("SFTPGO_AUTHD_PASSWORD=%s", password),
fmt.Sprintf("SFTPGO_AUTHD_PUBLIC_KEY=%s", pkey),
@ -4334,7 +4414,7 @@ func getExternalAuthResponse(username, password, pkey, keyboardInteractive, ip,
fmt.Sprintf("SFTPGO_AUTHD_TLS_CERT=%s", strings.ReplaceAll(tlsCert, "\n", "\\n")),
fmt.Sprintf("SFTPGO_AUTHD_KEYBOARD_INTERACTIVE=%v", keyboardInteractive))
return cmd.Output()
return getCmdOutput(cmd, "external_auth_hook")
}
func updateUserFromExtAuthResponse(user *User, password, pkey string) {
@ -4442,7 +4522,7 @@ func doExternalAuth(username, password string, pubKey []byte, keyboardInteractiv
// preserve TOTP config and recovery codes
user.Filters.TOTPConfig = u.Filters.TOTPConfig
user.Filters.RecoveryCodes = u.Filters.RecoveryCodes
err = provider.updateUser(&user)
user, err = updateUserAfterExternalAuth(&user)
if err == nil {
if protocol != protocolWebDAV {
webDAVUsersCache.swap(&user, password)
@ -4514,7 +4594,7 @@ func doPluginAuth(username, password string, pubKey []byte, ip, protocol string,
// preserve TOTP config and recovery codes
user.Filters.TOTPConfig = u.Filters.TOTPConfig
user.Filters.RecoveryCodes = u.Filters.RecoveryCodes
err = provider.updateUser(&user)
user, err = updateUserAfterExternalAuth(&user)
if err == nil {
if protocol != protocolWebDAV {
webDAVUsersCache.swap(&user, password)
@ -4530,6 +4610,13 @@ func doPluginAuth(username, password string, pubKey []byte, ip, protocol string,
return provider.userExists(user.Username, "")
}
func updateUserAfterExternalAuth(user *User) (User, error) {
if err := provider.updateUser(user); err != nil {
return *user, err
}
return provider.userExists(user.Username, "")
}
func getUserForHook(username string, oidcTokenFields *map[string]any) (User, User, error) {
u, err := provider.userExists(username, "")
if err != nil {
@ -4601,6 +4688,59 @@ func isExternalAuthConfigured(loginMethod string) bool {
}
}
func replaceTemplateVars(input string) string {
var result strings.Builder
i := 0
for i < len(input) {
if i+2 <= len(input) && input[i:i+2] == "{{" {
if i+2 < len(input) {
nextChar := input[i+2]
if nextChar == ' ' || nextChar == '.' || nextChar == '-' {
// Don't replace if followed by space, dot or minus.
result.WriteString("{{")
i += 2
continue
}
}
// Find the closing "}}"
closing := strings.Index(input[i:], "}}")
if closing != -1 {
// Replace with {{. only if it's a proper template variable.
result.WriteString("{{.")
result.WriteString(input[i+2 : i+closing])
result.WriteString("}}")
i += closing + 2
continue
}
}
result.WriteByte(input[i])
i++
}
return result.String()
}
func updateEventActionPlaceholders(actions []BaseEventAction) ([]BaseEventAction, error) {
var result []BaseEventAction
for _, action := range actions {
options, err := json.Marshal(action.Options)
if err != nil {
return nil, err
}
convertedOptions := replaceTemplateVars(string(options))
var opts BaseEventActionOptions
err = json.Unmarshal([]byte(convertedOptions), &opts)
if err != nil {
return nil, err
}
action.Options = opts
result = append(result, action)
}
return result, nil
}
func getConfigPath(name, configDir string) string {
if !util.IsFileInputValid(name) {
return ""
@ -4612,12 +4752,47 @@ func getConfigPath(name, configDir string) string {
}
func checkReservedUsernames(username string) error {
if util.Contains(reservedUsers, username) {
if slices.Contains(reservedUsers, username) {
return util.NewValidationError("this username is reserved")
}
return nil
}
func errSchemaVersionTooOld(version int) error {
return fmt.Errorf("database schema version %d is too old, please see the upgrading docs: https://docs.sftpgo.com/latest/data-provider/#upgrading", version)
}
func getCmdOutput(cmd *exec.Cmd, sender string) ([]byte, error) {
var stdout bytes.Buffer
cmd.Stdout = &stdout
stderr, err := cmd.StderrPipe()
if err != nil {
return nil, err
}
err = cmd.Start()
if err != nil {
return nil, err
}
scanner := bufio.NewScanner(stderr)
go func() {
for scanner.Scan() {
if out := scanner.Text(); out != "" {
logger.Log(logger.LevelWarn, sender, "", "%s", out)
}
}
if err := scanner.Err(); err != nil {
logger.Log(logger.LevelError, sender, "", "error reading stderr: %v", err)
}
}()
err = cmd.Wait()
return stdout.Bytes(), err
}
func providerLog(level logger.LogLevel, format string, v ...any) {
logger.Log(level, logSender, "", format, v...)
}

View file

@ -23,6 +23,7 @@ import (
"net/http"
"path"
"path/filepath"
"slices"
"strings"
"time"
@ -49,17 +50,21 @@ const (
ActionTypeUserExpirationCheck
ActionTypeIDPAccountCheck
ActionTypeUserInactivityCheck
ActionTypeRotateLogs
)
var (
supportedEventActions = []int{ActionTypeHTTP, ActionTypeCommand, ActionTypeEmail, ActionTypeFilesystem,
ActionTypeBackup, ActionTypeUserQuotaReset, ActionTypeFolderQuotaReset, ActionTypeTransferQuotaReset,
ActionTypeDataRetentionCheck, ActionTypePasswordExpirationCheck,
ActionTypeUserExpirationCheck, ActionTypeUserInactivityCheck, ActionTypeIDPAccountCheck}
ActionTypeDataRetentionCheck, ActionTypePasswordExpirationCheck, ActionTypeUserExpirationCheck,
ActionTypeUserInactivityCheck, ActionTypeIDPAccountCheck, ActionTypeRotateLogs}
// EnabledActionCommands defines the system commands that can be executed via EventManager,
// an empty list means that no command is allowed to be executed.
EnabledActionCommands []string
)
func isActionTypeValid(action int) bool {
return util.Contains(supportedEventActions, action)
return slices.Contains(supportedEventActions, action)
}
func getActionTypeAsString(action int) string {
@ -88,6 +93,8 @@ func getActionTypeAsString(action int) string {
return util.I18nActionTypeUserInactivityCheck
case ActionTypeIDPAccountCheck:
return util.I18nActionTypeIDPCheck
case ActionTypeRotateLogs:
return util.I18nActionTypeRotateLogs
default:
return util.I18nActionTypeCommand
}
@ -112,7 +119,7 @@ var (
)
func isEventTriggerValid(trigger int) bool {
return util.Contains(supportedEventTriggers, trigger)
return slices.Contains(supportedEventTriggers, trigger)
}
func getTriggerTypeAsString(trigger int) string {
@ -166,7 +173,7 @@ var (
)
func isFilesystemActionValid(value int) bool {
return util.Contains(supportedFsActions, value)
return slices.Contains(supportedFsActions, value)
}
func getFsActionTypeAsString(value int) string {
@ -335,7 +342,7 @@ func (c *EventActionHTTPConfig) validateMultiparts() error {
)
}
for _, k := range c.Headers {
if strings.ToLower(k.Key) == "content-type" {
if strings.EqualFold(k.Key, "content-type") {
return util.NewI18nError(
util.NewValidationError("content type is automatically set for multipart requests"),
util.I18nErrorMultipartCType,
@ -377,7 +384,7 @@ func (c *EventActionHTTPConfig) validate(additionalData string) error {
return util.NewValidationError(fmt.Sprintf("could not encrypt HTTP password: %v", err))
}
}
if !util.Contains(SupportedHTTPActionMethods, c.Method) {
if !slices.Contains(SupportedHTTPActionMethods, c.Method) {
return util.NewValidationError(fmt.Sprintf("unsupported HTTP method: %s", c.Method))
}
for _, kv := range c.QueryParameters {
@ -398,11 +405,11 @@ func (c *EventActionHTTPConfig) GetContext() (context.Context, context.CancelFun
// HasObjectData returns true if the {{ObjectData}} placeholder is defined
func (c *EventActionHTTPConfig) HasObjectData() bool {
if strings.Contains(c.Body, "{{ObjectData}}") {
if strings.Contains(c.Body, "{{ObjectData}}") || strings.Contains(c.Body, "{{ObjectDataString}}") {
return true
}
for _, part := range c.Parts {
if strings.Contains(part.Body, "{{ObjectData}}") {
if strings.Contains(part.Body, "{{ObjectData}}") || strings.Contains(part.Body, "{{ObjectDataString}}") {
return true
}
}
@ -446,6 +453,11 @@ func (c *EventActionHTTPConfig) GetHTTPClient() *http.Client {
return client
}
// IsActionCommandAllowed returns true if the specified command is allowed
func IsActionCommandAllowed(cmd string) bool {
return slices.Contains(EnabledActionCommands, cmd)
}
// EventActionCommandConfig defines the configuration for a command event target
type EventActionCommandConfig struct {
Cmd string `json:"cmd,omitempty"`
@ -458,6 +470,9 @@ func (c *EventActionCommandConfig) validate() error {
if c.Cmd == "" {
return util.NewI18nError(util.NewValidationError("command is required"), util.I18nErrorCommandRequired)
}
if !IsActionCommandAllowed(c.Cmd) {
return util.NewValidationError(fmt.Sprintf("command %q is not allowed", c.Cmd))
}
if !filepath.IsAbs(c.Cmd) {
return util.NewI18nError(
util.NewValidationError("invalid command, it must be an absolute path"),
@ -656,12 +671,21 @@ func (c *EventActionFsCompress) validate() error {
return nil
}
// RenameConfig defines the configuration for a filesystem rename
type RenameConfig struct {
// key is the source and target the value
KeyValue
// This setting only applies to storage providers that support
// changing modification times.
UpdateModTime bool `json:"update_modtime,omitempty"`
}
// EventActionFilesystemConfig defines the configuration for filesystem actions
type EventActionFilesystemConfig struct {
// Filesystem actions, see the above enum
Type int `json:"type,omitempty"`
// files/dirs to rename, key is the source and target the value
Renames []KeyValue `json:"renames,omitempty"`
// files/dirs to rename
Renames []RenameConfig `json:"renames,omitempty"`
// directories to create
MkDirs []string `json:"mkdirs,omitempty"`
// files/dirs to delete
@ -702,9 +726,9 @@ func (c *EventActionFilesystemConfig) validateRenames() error {
if len(c.Renames) == 0 {
return util.NewI18nError(util.NewValidationError("no path to rename specified"), util.I18nErrorPathRequired)
}
for idx, kv := range c.Renames {
key := strings.TrimSpace(kv.Key)
value := strings.TrimSpace(kv.Value)
for idx, cfg := range c.Renames {
key := strings.TrimSpace(cfg.Key)
value := strings.TrimSpace(cfg.Value)
if key == "" || value == "" {
return util.NewValidationError("invalid paths to rename")
}
@ -722,9 +746,12 @@ func (c *EventActionFilesystemConfig) validateRenames() error {
util.I18nErrorRootNotAllowed,
)
}
c.Renames[idx] = KeyValue{
Key: key,
Value: value,
c.Renames[idx] = RenameConfig{
KeyValue: KeyValue{
Key: key,
Value: value,
},
UpdateModTime: cfg.UpdateModTime,
}
}
return nil
@ -888,7 +915,7 @@ func (c *EventActionFilesystemConfig) getACopy() EventActionFilesystemConfig {
return EventActionFilesystemConfig{
Type: c.Type,
Renames: cloneKeyValues(c.Renames),
Renames: cloneRenameConfigs(c.Renames),
MkDirs: mkdirs,
Deletes: deletes,
Exist: exist,
@ -1226,6 +1253,15 @@ func (a *BaseEventAction) validate() error {
if a.Name == "" {
return util.NewI18nError(util.NewValidationError("name is mandatory"), util.I18nErrorNameRequired)
}
if !util.IsNameValid(a.Name) {
return util.NewI18nError(errInvalidInput, util.I18nErrorInvalidInput)
}
if config.NamingRules&1 == 0 && !usernameRegex.MatchString(a.Name) {
return util.NewI18nError(
util.NewValidationError(fmt.Sprintf("name %q is not valid, the following characters are allowed: a-zA-Z0-9-_.~", a.Name)),
util.I18nErrorInvalidUser,
)
}
if !isActionTypeValid(a.Type) {
return util.NewValidationError(fmt.Sprintf("invalid action type: %d", a.Type))
}
@ -1277,7 +1313,7 @@ func (a *EventAction) validateAssociation(trigger int, fsEvents []string) error
}
if trigger == EventTriggerFsEvent {
for _, ev := range fsEvents {
if !util.Contains(allowedSyncFsEvents, ev) {
if !slices.Contains(allowedSyncFsEvents, ev) {
return util.NewI18nError(
util.NewValidationError("sync execution is only supported for upload and pre-* events"),
util.I18nErrorEvSyncUnsupportedFs,
@ -1320,6 +1356,7 @@ type ConditionOptions struct {
ProviderObjects []string `json:"provider_objects,omitempty"`
MinFileSize int64 `json:"min_size,omitempty"`
MaxFileSize int64 `json:"max_size,omitempty"`
EventStatuses []int `json:"event_statuses,omitempty"`
// allow to execute scheduled tasks concurrently from multiple instances
ConcurrentExecution bool `json:"concurrent_execution,omitempty"`
}
@ -1329,6 +1366,8 @@ func (f *ConditionOptions) getACopy() ConditionOptions {
copy(protocols, f.Protocols)
providerObjects := make([]string, len(f.ProviderObjects))
copy(providerObjects, f.ProviderObjects)
statuses := make([]int, len(f.EventStatuses))
copy(statuses, f.EventStatuses)
return ConditionOptions{
Names: cloneConditionPatterns(f.Names),
@ -1339,10 +1378,20 @@ func (f *ConditionOptions) getACopy() ConditionOptions {
ProviderObjects: providerObjects,
MinFileSize: f.MinFileSize,
MaxFileSize: f.MaxFileSize,
EventStatuses: statuses,
ConcurrentExecution: f.ConcurrentExecution,
}
}
func (f *ConditionOptions) validateStatuses() error {
for _, status := range f.EventStatuses {
if status < 0 || status > 3 {
return util.NewValidationError(fmt.Sprintf("invalid event_status %d", status))
}
}
return nil
}
func (f *ConditionOptions) validate() error {
if err := validateConditionPatterns(f.Names); err != nil {
return err
@ -1358,12 +1407,12 @@ func (f *ConditionOptions) validate() error {
}
for _, p := range f.Protocols {
if !util.Contains(SupportedRuleConditionProtocols, p) {
if !slices.Contains(SupportedRuleConditionProtocols, p) {
return util.NewValidationError(fmt.Sprintf("unsupported rule condition protocol: %q", p))
}
}
for _, p := range f.ProviderObjects {
if !util.Contains(SupporteRuleConditionProviderObjects, p) {
if !slices.Contains(SupporteRuleConditionProviderObjects, p) {
return util.NewValidationError(fmt.Sprintf("unsupported provider object: %q", p))
}
}
@ -1373,6 +1422,9 @@ func (f *ConditionOptions) validate() error {
util.ByteCountSI(f.MaxFileSize), util.ByteCountSI(f.MinFileSize)))
}
}
if err := f.validateStatuses(); err != nil {
return err
}
if config.IsShared == 0 {
f.ConcurrentExecution = false
}
@ -1465,16 +1517,16 @@ func (c *EventConditions) validate(trigger int) error {
)
}
for _, ev := range c.FsEvents {
if !util.Contains(SupportedFsEvents, ev) {
if !slices.Contains(SupportedFsEvents, ev) {
return util.NewValidationError(fmt.Sprintf("unsupported fs event: %q", ev))
}
}
case EventTriggerProviderEvent:
c.FsEvents = nil
c.Schedules = nil
c.Options.GroupNames = nil
c.Options.FsPaths = nil
c.Options.Protocols = nil
c.Options.EventStatuses = nil
c.Options.MinFileSize = 0
c.Options.MaxFileSize = 0
c.IDPLoginEvent = 0
@ -1485,7 +1537,7 @@ func (c *EventConditions) validate(trigger int) error {
)
}
for _, ev := range c.ProviderEvents {
if !util.Contains(SupportedProviderEvents, ev) {
if !slices.Contains(SupportedProviderEvents, ev) {
return util.NewValidationError(fmt.Sprintf("unsupported provider event: %q", ev))
}
}
@ -1494,6 +1546,7 @@ func (c *EventConditions) validate(trigger int) error {
c.ProviderEvents = nil
c.Options.FsPaths = nil
c.Options.Protocols = nil
c.Options.EventStatuses = nil
c.Options.MinFileSize = 0
c.Options.MaxFileSize = 0
c.Options.ProviderObjects = nil
@ -1509,6 +1562,7 @@ func (c *EventConditions) validate(trigger int) error {
c.Options.RoleNames = nil
c.Options.FsPaths = nil
c.Options.Protocols = nil
c.Options.EventStatuses = nil
c.Options.MinFileSize = 0
c.Options.MaxFileSize = 0
c.Schedules = nil
@ -1518,6 +1572,7 @@ func (c *EventConditions) validate(trigger int) error {
c.ProviderEvents = nil
c.Options.FsPaths = nil
c.Options.Protocols = nil
c.Options.EventStatuses = nil
c.Options.MinFileSize = 0
c.Options.MaxFileSize = 0
c.Options.ProviderObjects = nil
@ -1531,10 +1586,11 @@ func (c *EventConditions) validate(trigger int) error {
c.Options.RoleNames = nil
c.Options.FsPaths = nil
c.Options.Protocols = nil
c.Options.EventStatuses = nil
c.Options.MinFileSize = 0
c.Options.MaxFileSize = 0
c.Schedules = nil
if !util.Contains(supportedIDPLoginEvents, c.IDPLoginEvent) {
if !slices.Contains(supportedIDPLoginEvents, c.IDPLoginEvent) {
return util.NewValidationError(fmt.Sprintf("invalid Identity Provider login event %d", c.IDPLoginEvent))
}
default:
@ -1544,6 +1600,7 @@ func (c *EventConditions) validate(trigger int) error {
c.Options.RoleNames = nil
c.Options.FsPaths = nil
c.Options.Protocols = nil
c.Options.EventStatuses = nil
c.Options.MinFileSize = 0
c.Options.MaxFileSize = 0
c.Schedules = nil
@ -1624,10 +1681,19 @@ func (r *EventRule) isStatusValid() bool {
return r.Status >= 0 && r.Status <= 1
}
func (r *EventRule) validate() error {
func (r *EventRule) validate() error { //nolint:gocyclo
if r.Name == "" {
return util.NewI18nError(util.NewValidationError("name is mandatory"), util.I18nErrorNameRequired)
}
if !util.IsNameValid(r.Name) {
return util.NewI18nError(errInvalidInput, util.I18nErrorInvalidInput)
}
if config.NamingRules&1 == 0 && !usernameRegex.MatchString(r.Name) {
return util.NewI18nError(
util.NewValidationError(fmt.Sprintf("name %q is not valid, the following characters are allowed: a-zA-Z0-9-_.~", r.Name)),
util.I18nErrorInvalidUser,
)
}
if !r.isStatusValid() {
return util.NewValidationError(fmt.Sprintf("invalid event rule status: %d", r.Status))
}
@ -1687,7 +1753,7 @@ func (r *EventRule) validateMandatorySyncActions() error {
return nil
}
for _, ev := range r.Conditions.FsEvents {
if util.Contains(mandatorySyncFsEvents, ev) {
if slices.Contains(mandatorySyncFsEvents, ev) {
return util.NewI18nError(
util.NewValidationError(fmt.Sprintf("event %q requires at least a sync action", ev)),
util.I18nErrorRuleSyncActionRequired,
@ -1705,7 +1771,7 @@ func (r *EventRule) checkIPBlockedAndCertificateActions() error {
ActionTypeDataRetentionCheck, ActionTypeFilesystem, ActionTypePasswordExpirationCheck,
ActionTypeUserExpirationCheck}
for _, action := range r.Actions {
if util.Contains(unavailableActions, action.Type) {
if slices.Contains(unavailableActions, action.Type) {
return fmt.Errorf("action %q, type %q is not supported for event trigger %q",
action.Name, getActionTypeAsString(action.Type), getTriggerTypeAsString(r.Trigger))
}
@ -1721,7 +1787,7 @@ func (r *EventRule) checkProviderEventActions(providerObjectType string) error {
ActionTypeDataRetentionCheck, ActionTypeFilesystem,
ActionTypePasswordExpirationCheck, ActionTypeUserExpirationCheck}
for _, action := range r.Actions {
if util.Contains(userSpecificActions, action.Type) && providerObjectType != actionObjectUser {
if slices.Contains(userSpecificActions, action.Type) && providerObjectType != actionObjectUser {
return fmt.Errorf("action %q, type %q is only supported for provider user events",
action.Name, getActionTypeAsString(action.Type))
}
@ -1829,6 +1895,20 @@ func (r *EventRule) RenderAsJSON(reload bool) ([]byte, error) {
return json.Marshal(r)
}
func cloneRenameConfigs(renames []RenameConfig) []RenameConfig {
res := make([]RenameConfig, 0, len(renames))
for _, c := range renames {
res = append(res, RenameConfig{
KeyValue: KeyValue{
Key: c.Key,
Value: c.Value,
},
UpdateModTime: c.UpdateModTime,
})
}
return res
}
func cloneKeyValues(keyVals []KeyValue) []KeyValue {
res := make([]KeyValue, 0, len(keyVals))
for _, kv := range keyVals {

View file

@ -132,11 +132,22 @@ func (g *Group) hasRedactedSecret() bool {
return g.UserSettings.FsConfig.HasRedactedSecret()
}
func (g *Group) applyNamingRules() {
g.Name = config.convertName(g.Name)
for idx := range g.VirtualFolders {
g.VirtualFolders[idx].Name = config.convertName(g.VirtualFolders[idx].Name)
}
}
func (g *Group) validate() error {
g.SetEmptySecretsIfNil()
g.applyNamingRules()
if g.Name == "" {
return util.NewI18nError(util.NewValidationError("name is mandatory"), util.I18nErrorNameRequired)
}
if !util.IsNameValid(g.Name) {
return util.NewI18nError(errInvalidInput, util.I18nErrorInvalidInput)
}
if config.NamingRules&1 == 0 && !usernameRegex.MatchString(g.Name) {
return util.NewI18nError(
util.NewValidationError(fmt.Sprintf("name %q is not valid, the following characters are allowed: a-zA-Z0-9-_.~", g.Name)),

View file

@ -19,6 +19,7 @@ import (
"fmt"
"net"
"net/netip"
"slices"
"strings"
"sync"
"sync/atomic"
@ -85,7 +86,7 @@ var (
// CheckIPListType returns an error if the provided IP list type is not valid
func CheckIPListType(t IPListType) error {
if !util.Contains(supportedIPListType, t) {
if !slices.Contains(supportedIPListType, t) {
return util.NewValidationError(fmt.Sprintf("invalid list type %d", t))
}
return nil
@ -417,6 +418,10 @@ func (l *IPList) IsListed(ip, protocol string) (bool, int, error) {
l.mu.RLock()
defer l.mu.RUnlock()
if l.Ranges.Len() == 0 {
return false, 0, nil
}
parsedIP := net.ParseIP(ip)
if parsedIP == nil {
return false, 0, fmt.Errorf("invalid IP %s", ip)

View file

@ -22,7 +22,9 @@ import (
"net/netip"
"os"
"path/filepath"
"slices"
"sort"
"strconv"
"sync"
"time"
@ -206,6 +208,32 @@ func (p *MemoryProvider) updateAPIKeyLastUse(keyID string) error {
return nil
}
func (p *MemoryProvider) getAdminSignature(username string) (string, error) {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
return "", errMemoryProviderClosed
}
admin, err := p.adminExistsInternal(username)
if err != nil {
return "", err
}
return strconv.FormatInt(admin.UpdatedAt, 10), nil
}
func (p *MemoryProvider) getUserSignature(username string) (string, error) {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
if p.dbHandle.isClosed {
return "", errMemoryProviderClosed
}
user, err := p.userExistsInternal(username)
if err != nil {
return "", err
}
return strconv.FormatInt(user.UpdatedAt, 10), nil
}
func (p *MemoryProvider) setUpdatedAt(username string) {
p.dbHandle.Lock()
defer p.dbHandle.Unlock()
@ -348,9 +376,6 @@ func (p *MemoryProvider) addUser(user *User) error {
if err := p.addUserToRole(user.Username, user.Role); err != nil {
return err
}
sort.Slice(user.Groups, func(i, j int) bool {
return user.Groups[i].Name < user.Groups[j].Name
})
var mappedGroups []string
for idx := range user.Groups {
if err = p.addUserToGroupMapping(user.Username, user.Groups[idx].Name); err != nil {
@ -362,9 +387,6 @@ func (p *MemoryProvider) addUser(user *User) error {
}
mappedGroups = append(mappedGroups, user.Groups[idx].Name)
}
sort.Slice(user.VirtualFolders, func(i, j int) bool {
return user.VirtualFolders[i].Name < user.VirtualFolders[j].Name
})
var mappedFolders []string
for idx := range user.VirtualFolders {
if err = p.addUserToFolderMapping(user.Username, user.VirtualFolders[idx].Name); err != nil {
@ -410,9 +432,6 @@ func (p *MemoryProvider) updateUser(user *User) error { //nolint:gocyclo
for idx := range u.Groups {
p.removeUserFromGroupMapping(u.Username, u.Groups[idx].Name)
}
sort.Slice(user.Groups, func(i, j int) bool {
return user.Groups[i].Name < user.Groups[j].Name
})
for idx := range user.Groups {
if err = p.addUserToGroupMapping(user.Username, user.Groups[idx].Name); err != nil {
// try to add old mapping
@ -428,9 +447,6 @@ func (p *MemoryProvider) updateUser(user *User) error { //nolint:gocyclo
for _, oldFolder := range u.VirtualFolders {
p.removeRelationFromFolderMapping(oldFolder.Name, u.Username, "")
}
sort.Slice(user.VirtualFolders, func(i, j int) bool {
return user.VirtualFolders[i].Name < user.VirtualFolders[j].Name
})
for idx := range user.VirtualFolders {
if err = p.addUserToFolderMapping(user.Username, user.VirtualFolders[idx].Name); err != nil {
// try to add old mapping
@ -743,9 +759,6 @@ func (p *MemoryProvider) addAdmin(admin *Admin) error {
return err
}
var mappedAdmins []string
sort.Slice(admin.Groups, func(i, j int) bool {
return admin.Groups[i].Name < admin.Groups[j].Name
})
for idx := range admin.Groups {
if err = p.addAdminToGroupMapping(admin.Username, admin.Groups[idx].Name); err != nil {
// try to remove group mapping
@ -788,9 +801,6 @@ func (p *MemoryProvider) updateAdmin(admin *Admin) error {
for idx := range a.Groups {
p.removeAdminFromGroupMapping(a.Username, a.Groups[idx].Name)
}
sort.Slice(admin.Groups, func(i, j int) bool {
return admin.Groups[i].Name < admin.Groups[j].Name
})
for idx := range admin.Groups {
if err = p.addAdminToGroupMapping(admin.Username, admin.Groups[idx].Name); err != nil {
// try to add old mapping
@ -1054,9 +1064,6 @@ func (p *MemoryProvider) addGroup(group *Group) error {
group.UpdatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
group.Users = nil
group.Admins = nil
sort.Slice(group.VirtualFolders, func(i, j int) bool {
return group.VirtualFolders[i].Name < group.VirtualFolders[j].Name
})
var mappedFolders []string
for idx := range group.VirtualFolders {
if err = p.addGroupToFolderMapping(group.Name, group.VirtualFolders[idx].Name); err != nil {
@ -1090,9 +1097,6 @@ func (p *MemoryProvider) updateGroup(group *Group) error {
for _, oldFolder := range g.VirtualFolders {
p.removeRelationFromFolderMapping(oldFolder.Name, "", g.Name)
}
sort.Slice(group.VirtualFolders, func(i, j int) bool {
return group.VirtualFolders[i].Name < group.VirtualFolders[j].Name
})
for idx := range group.VirtualFolders {
if err = p.addGroupToFolderMapping(group.Name, group.VirtualFolders[idx].Name); err != nil {
// try to add old mapping
@ -1210,7 +1214,7 @@ func (p *MemoryProvider) addRuleToActionMapping(ruleName, actionName string) err
if err != nil {
return util.NewGenericError(fmt.Sprintf("action %q does not exist", actionName))
}
if !util.Contains(a.Rules, ruleName) {
if !slices.Contains(a.Rules, ruleName) {
a.Rules = append(a.Rules, ruleName)
p.dbHandle.actions[actionName] = a
}
@ -1223,7 +1227,7 @@ func (p *MemoryProvider) removeRuleFromActionMapping(ruleName, actionName string
providerLog(logger.LevelWarn, "action %q does not exist, cannot remove from mapping", actionName)
return
}
if util.Contains(a.Rules, ruleName) {
if slices.Contains(a.Rules, ruleName) {
var rules []string
for _, r := range a.Rules {
if r != ruleName {
@ -1240,7 +1244,7 @@ func (p *MemoryProvider) addAdminToGroupMapping(username, groupname string) erro
if err != nil {
return err
}
if !util.Contains(g.Admins, username) {
if !slices.Contains(g.Admins, username) {
g.Admins = append(g.Admins, username)
p.dbHandle.groups[groupname] = g
}
@ -1283,7 +1287,7 @@ func (p *MemoryProvider) addUserToGroupMapping(username, groupname string) error
if err != nil {
return err
}
if !util.Contains(g.Users, username) {
if !slices.Contains(g.Users, username) {
g.Users = append(g.Users, username)
p.dbHandle.groups[groupname] = g
}
@ -1313,7 +1317,7 @@ func (p *MemoryProvider) addAdminToRole(username, role string) error {
if err != nil {
return fmt.Errorf("%w: role %q does not exist", ErrForeignKeyViolated, role)
}
if !util.Contains(r.Admins, username) {
if !slices.Contains(r.Admins, username) {
r.Admins = append(r.Admins, username)
p.dbHandle.roles[role] = r
}
@ -1347,7 +1351,7 @@ func (p *MemoryProvider) addUserToRole(username, role string) error {
if err != nil {
return fmt.Errorf("%w: role %q does not exist", ErrForeignKeyViolated, role)
}
if !util.Contains(r.Users, username) {
if !slices.Contains(r.Users, username) {
r.Users = append(r.Users, username)
p.dbHandle.roles[role] = r
}
@ -1378,7 +1382,7 @@ func (p *MemoryProvider) addUserToFolderMapping(username, foldername string) err
if err != nil {
return util.NewGenericError(fmt.Sprintf("unable to get folder %q: %v", foldername, err))
}
if !util.Contains(f.Users, username) {
if !slices.Contains(f.Users, username) {
f.Users = append(f.Users, username)
p.dbHandle.vfolders[foldername] = f
}
@ -1390,7 +1394,7 @@ func (p *MemoryProvider) addGroupToFolderMapping(name, foldername string) error
if err != nil {
return util.NewGenericError(fmt.Sprintf("unable to get folder %q: %v", foldername, err))
}
if !util.Contains(f.Groups, name) {
if !slices.Contains(f.Groups, name) {
f.Groups = append(f.Groups, name)
p.dbHandle.vfolders[foldername] = f
}
@ -2089,11 +2093,11 @@ func (p *MemoryProvider) addSharedSession(_ Session) error {
return ErrNotImplemented
}
func (p *MemoryProvider) deleteSharedSession(_ string) error {
func (p *MemoryProvider) deleteSharedSession(_ string, _ SessionType) error {
return ErrNotImplemented
}
func (p *MemoryProvider) getSharedSession(_ string) (Session, error) {
func (p *MemoryProvider) getSharedSession(_ string, _ SessionType) (Session, error) {
return Session{}, ErrNotImplemented
}

View file

@ -13,7 +13,6 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//go:build !nomysql
// +build !nomysql
package dataprovider
@ -39,11 +38,11 @@ import (
const (
mysqlResetSQL = "DROP TABLE IF EXISTS `{{api_keys}}` CASCADE;" +
"DROP TABLE IF EXISTS `{{folders_mapping}}` CASCADE;" +
"DROP TABLE IF EXISTS `{{users_folders_mapping}}` CASCADE;" +
"DROP TABLE IF EXISTS `{{users_groups_mapping}}` CASCADE;" +
"DROP TABLE IF EXISTS `{{admins_groups_mapping}}` CASCADE;" +
"DROP TABLE IF EXISTS `{{groups_folders_mapping}}` CASCADE;" +
"DROP TABLE IF EXISTS `{{shares_groups_mapping}}` CASCADE;" +
"DROP TABLE IF EXISTS `{{admins}}` CASCADE;" +
"DROP TABLE IF EXISTS `{{folders}}` CASCADE;" +
"DROP TABLE IF EXISTS `{{shares}}` CASCADE;" +
@ -84,8 +83,8 @@ const (
"CREATE TABLE `{{groups}}` (`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, `user_settings` longtext NULL);" +
"CREATE TABLE `{{shared_sessions}}` (`key` varchar(128) NOT NULL PRIMARY KEY, " +
"`data` longtext NOT NULL, `type` integer NOT NULL, `timestamp` bigint NOT NULL);" +
"CREATE TABLE `{{shared_sessions}}` (`key` varchar(128) NOT NULL, `type` integer NOT NULL, `data` longtext NOT NULL, " +
"`timestamp` bigint NOT NULL, PRIMARY KEY (`key`, `type`));" +
"CREATE TABLE `{{users}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `username` varchar(255) NOT NULL UNIQUE, " +
"`status` integer NOT NULL, `expiration_date` bigint NOT NULL, `description` varchar(512) NULL, `password` longtext NULL, " +
"`public_keys` longtext NULL, `home_dir` longtext NOT NULL, `uid` bigint NOT NULL, `gid` bigint NOT NULL, " +
@ -95,38 +94,41 @@ const (
"`last_login` bigint NOT NULL, `filters` longtext NULL, `filesystem` longtext NULL, `additional_info` longtext NULL, " +
"`created_at` bigint NOT NULL, `updated_at` bigint NOT NULL, `email` varchar(255) NULL, " +
"`upload_data_transfer` integer NOT NULL, `download_data_transfer` integer NOT NULL, " +
"`total_data_transfer` integer NOT NULL, `used_upload_data_transfer` integer NOT NULL, " +
"`used_download_data_transfer` integer NOT NULL, `deleted_at` bigint NOT NULL, `first_download` bigint NOT NULL, " +
"`total_data_transfer` integer NOT NULL, `used_upload_data_transfer` bigint NOT NULL, " +
"`used_download_data_transfer` bigint NOT NULL, `deleted_at` bigint NOT NULL, `first_download` bigint NOT NULL, " +
"`first_upload` bigint NOT NULL, `last_password_change` bigint NOT NULL, `role_id` integer NULL);" +
"CREATE TABLE `{{groups_folders_mapping}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, " +
"`group_id` integer NOT NULL, `folder_id` integer NOT NULL, " +
"`virtual_path` longtext NOT NULL, `quota_size` bigint NOT NULL, `quota_files` integer NOT NULL);" +
"`virtual_path` longtext NOT NULL, `quota_size` bigint NOT NULL, `quota_files` integer NOT NULL, `sort_order` integer NOT NULL);" +
"CREATE TABLE `{{users_groups_mapping}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, " +
"`user_id` integer NOT NULL, `group_id` integer NOT NULL, `group_type` integer NOT NULL);" +
"`user_id` integer NOT NULL, `group_id` integer NOT NULL, `group_type` integer NOT NULL, `sort_order` integer NOT NULL);" +
"CREATE TABLE `{{users_folders_mapping}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `virtual_path` longtext NOT NULL, " +
"`quota_size` bigint NOT NULL, `quota_files` integer NOT NULL, `folder_id` integer NOT NULL, `user_id` integer NOT NULL);" +
"`quota_size` bigint NOT NULL, `quota_files` integer NOT NULL, `folder_id` integer NOT NULL, `user_id` integer NOT NULL, `sort_order` integer NOT NULL);" +
"ALTER TABLE `{{users_folders_mapping}}` ADD CONSTRAINT `{{prefix}}unique_user_folder_mapping` " +
"UNIQUE (`user_id`, `folder_id`);" +
"ALTER TABLE `{{users_folders_mapping}}` ADD CONSTRAINT `{{prefix}}users_folders_mapping_user_id_fk_users_id` " +
"FOREIGN KEY (`user_id`) REFERENCES `{{users}}` (`id`) ON DELETE CASCADE;" +
"ALTER TABLE `{{users_folders_mapping}}` ADD CONSTRAINT `{{prefix}}users_folders_mapping_folder_id_fk_folders_id` " +
"FOREIGN KEY (`folder_id`) REFERENCES `{{folders}}` (`id`) ON DELETE CASCADE;" +
"CREATE INDEX `{{prefix}}users_folders_mapping_sort_order_idx` ON `{{users_folders_mapping}}` (`sort_order`);" +
"ALTER TABLE `{{users_groups_mapping}}` ADD CONSTRAINT `{{prefix}}unique_user_group_mapping` UNIQUE (`user_id`, `group_id`);" +
"ALTER TABLE `{{groups_folders_mapping}}` ADD CONSTRAINT `{{prefix}}unique_group_folder_mapping` UNIQUE (`group_id`, `folder_id`);" +
"ALTER TABLE `{{users_groups_mapping}}` ADD CONSTRAINT `{{prefix}}users_groups_mapping_group_id_fk_groups_id` " +
"FOREIGN KEY (`group_id`) REFERENCES `{{groups}}` (`id`) ON DELETE NO ACTION;" +
"ALTER TABLE `{{users_groups_mapping}}` ADD CONSTRAINT `{{prefix}}users_groups_mapping_user_id_fk_users_id` " +
"FOREIGN KEY (`user_id`) REFERENCES `{{users}}` (`id`) ON DELETE CASCADE; " +
"CREATE INDEX `{{prefix}}users_groups_mapping_sort_order_idx` ON `{{users_groups_mapping}}` (`sort_order`);" +
"ALTER TABLE `{{groups_folders_mapping}}` ADD CONSTRAINT `{{prefix}}groups_folders_mapping_folder_id_fk_folders_id` " +
"FOREIGN KEY (`folder_id`) REFERENCES `{{folders}}` (`id`) ON DELETE CASCADE;" +
"ALTER TABLE `{{groups_folders_mapping}}` ADD CONSTRAINT `{{prefix}}groups_folders_mapping_group_id_fk_groups_id` " +
"FOREIGN KEY (`group_id`) REFERENCES `{{groups}}` (`id`) ON DELETE CASCADE;" +
"CREATE INDEX `{{prefix}}groups_folders_mapping_sort_order_idx` ON `{{groups_folders_mapping}}` (`sort_order`); " +
"CREATE TABLE `{{shares}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, " +
"`share_id` varchar(60) NOT NULL UNIQUE, `name` varchar(255) NOT NULL, `description` varchar(512) NULL, " +
"`scope` integer NOT NULL, `paths` longtext NOT NULL, `created_at` bigint NOT NULL, " +
"`updated_at` bigint NOT NULL, `last_use_at` bigint NOT NULL, `expires_at` bigint NOT NULL, " +
"`password` longtext NULL, `max_tokens` integer NOT NULL, `used_tokens` integer NOT NULL, " +
"`allow_from` longtext NULL, `user_id` integer NOT NULL);" +
"`allow_from` longtext NULL, `options` longtext NULL, `user_id` integer NOT NULL);" +
"ALTER TABLE `{{shares}}` ADD CONSTRAINT `{{prefix}}shares_user_id_fk_users_id` " +
"FOREIGN KEY (`user_id`) REFERENCES `{{users}}` (`id`) ON DELETE CASCADE;" +
"CREATE TABLE `{{api_keys}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `name` varchar(255) NOT NULL, `key_id` varchar(50) NOT NULL UNIQUE," +
@ -150,13 +152,14 @@ const (
"ALTER TABLE `{{rules_actions_mapping}}` ADD CONSTRAINT `{{prefix}}rules_actions_mapping_action_id_fk_events_targets_id` " +
"FOREIGN KEY (`action_id`) REFERENCES `{{events_actions}}` (`id`) ON DELETE NO ACTION;" +
"CREATE TABLE `{{admins_groups_mapping}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, " +
" `admin_id` integer NOT NULL, `group_id` integer NOT NULL, `options` longtext NOT NULL);" +
" `admin_id` integer NOT NULL, `group_id` integer NOT NULL, `options` longtext NOT NULL, `sort_order` integer NOT NULL);" +
"ALTER TABLE `{{admins_groups_mapping}}` ADD CONSTRAINT `{{prefix}}unique_admin_group_mapping` " +
"UNIQUE (`admin_id`, `group_id`);" +
"ALTER TABLE `{{admins_groups_mapping}}` ADD CONSTRAINT `{{prefix}}admins_groups_mapping_admin_id_fk_admins_id` " +
"FOREIGN KEY (`admin_id`) REFERENCES `{{admins}}` (`id`) ON DELETE CASCADE;" +
"ALTER TABLE `{{admins_groups_mapping}}` ADD CONSTRAINT `{{prefix}}admins_groups_mapping_group_id_fk_groups_id` " +
"FOREIGN KEY (`group_id`) REFERENCES `{{groups}}` (`id`) ON DELETE CASCADE;" +
"CREATE INDEX `{{prefix}}admins_groups_mapping_sort_order_idx` ON `{{admins_groups_mapping}}` (`sort_order`); " +
"CREATE TABLE `{{nodes}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, " +
"`name` varchar(255) NOT NULL UNIQUE, `data` longtext NOT NULL, `created_at` bigint NOT NULL, " +
"`updated_at` bigint NOT NULL);" +
@ -193,11 +196,17 @@ const (
"CREATE INDEX `{{prefix}}ip_lists_updated_at_idx` ON `{{ip_lists}}` (`updated_at`);" +
"CREATE INDEX `{{prefix}}ip_lists_deleted_at_idx` ON `{{ip_lists}}` (`deleted_at`);" +
"CREATE INDEX `{{prefix}}ip_lists_first_last_idx` ON `{{ip_lists}}` (`first`, `last`);" +
"INSERT INTO {{schema_version}} (version) VALUES (28);"
mysqlV29SQL = "ALTER TABLE `{{users}}` MODIFY `used_download_data_transfer` bigint NOT NULL;" +
"ALTER TABLE `{{users}}` MODIFY `used_upload_data_transfer` bigint NOT NULL;"
mysqlV29DownSQL = "ALTER TABLE `{{users}}` MODIFY `used_upload_data_transfer` integer NOT NULL;" +
"ALTER TABLE `{{users}}` MODIFY `used_download_data_transfer` integer NOT NULL;"
"INSERT INTO {{schema_version}} (version) VALUES (33);"
mysqlV34SQL = "CREATE TABLE `{{shares_groups_mapping}}` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY," +
"`share_id` integer NOT NULL, `group_id` integer NOT NULL, `permissions` integer NOT NULL," +
"`sort_order` integer NOT NULL," +
"CONSTRAINT `{{prefix}}unique_share_group_mapping` UNIQUE (`share_id`, `group_id`)," +
"CONSTRAINT `{{prefix}}shares_groups_mapping_share_id_fk` FOREIGN KEY (`share_id`) REFERENCES `{{shares}}` (`id`) ON DELETE CASCADE," +
"CONSTRAINT `{{prefix}}shares_groups_mapping_group_id_fk` FOREIGN KEY (`group_id`) REFERENCES `{{groups}}` (`id`) ON DELETE CASCADE); " +
"CREATE INDEX `{{prefix}}shares_groups_mapping_sort_order_idx` ON `{{shares_groups_mapping}}` (`sort_order`); " +
"CREATE INDEX `{{prefix}}shares_groups_mapping_share_id_idx` ON `{{shares_groups_mapping}}` (`share_id`); " +
"CREATE INDEX `{{prefix}}shares_groups_mapping_group_id_idx` ON `{{shares_groups_mapping}}` (`group_id`);"
mysqlV34DownSQL = "DROP TABLE IF EXISTS `{{shares_groups_mapping}}`;"
)
// MySQLProvider defines the auth provider for MySQL/MariaDB database
@ -329,6 +338,14 @@ func (p *MySQLProvider) getUsedQuota(username string) (int, int64, int64, int64,
return sqlCommonGetUsedQuota(username, p.dbHandle)
}
func (p *MySQLProvider) getAdminSignature(username string) (string, error) {
return sqlCommonGetAdminSignature(username, p.dbHandle)
}
func (p *MySQLProvider) getUserSignature(username string) (string, error) {
return sqlCommonGetUserSignature(username, p.dbHandle)
}
func (p *MySQLProvider) setUpdatedAt(username string) {
sqlCommonSetUpdatedAt(username, p.dbHandle)
}
@ -583,12 +600,12 @@ func (p *MySQLProvider) addSharedSession(session Session) error {
return sqlCommonAddSession(session, p.dbHandle)
}
func (p *MySQLProvider) deleteSharedSession(key string) error {
return sqlCommonDeleteSession(key, p.dbHandle)
func (p *MySQLProvider) deleteSharedSession(key string, sessionType SessionType) error {
return sqlCommonDeleteSession(key, sessionType, p.dbHandle)
}
func (p *MySQLProvider) getSharedSession(key string) (Session, error) {
return sqlCommonGetSession(key, p.dbHandle)
func (p *MySQLProvider) getSharedSession(key string, sessionType SessionType) (Session, error) {
return sqlCommonGetSession(key, sessionType, p.dbHandle)
}
func (p *MySQLProvider) cleanupSharedSessions(sessionType SessionType, before int64) error {
@ -776,11 +793,11 @@ func (p *MySQLProvider) initializeDatabase() error {
if errors.Is(err, sql.ErrNoRows) {
return errSchemaVersionEmpty
}
logger.InfoToConsole("creating initial database schema, version 28")
providerLog(logger.LevelInfo, "creating initial database schema, version 28")
logger.InfoToConsole("creating initial database schema, version 33")
providerLog(logger.LevelInfo, "creating initial database schema, version 33")
initialSQL := sqlReplaceAll(mysqlInitialSQL)
return sqlCommonExecSQLAndUpdateDBVersion(p.dbHandle, strings.Split(initialSQL, ";"), 28, true)
return sqlCommonExecSQLAndUpdateDBVersion(p.dbHandle, strings.Split(initialSQL, ";"), 33, true)
}
func (p *MySQLProvider) migrateDatabase() error {
@ -793,13 +810,13 @@ func (p *MySQLProvider) migrateDatabase() error {
case version == sqlDatabaseVersion:
providerLog(logger.LevelDebug, "sql database is up to date, current version: %d", version)
return ErrNoInitRequired
case version < 28:
err = fmt.Errorf("database schema version %d is too old, please see the upgrading docs", version)
case version < 33:
err = errSchemaVersionTooOld(version)
providerLog(logger.LevelError, "%v", err)
logger.ErrorToConsole("%v", err)
return err
case version == 28:
return updateMySQLDatabaseFrom28To29(p.dbHandle)
case version == 33:
return updateMySQLDatabaseFromV33(p.dbHandle)
default:
if version > sqlDatabaseVersion {
providerLog(logger.LevelError, "database schema version %d is newer than the supported one: %d", version,
@ -822,8 +839,8 @@ func (p *MySQLProvider) revertDatabase(targetVersion int) error {
}
switch dbVersion.Version {
case 29:
return downgradeMySQLDatabaseFrom29To28(p.dbHandle)
case 34:
return downgradeMySQLDatabaseFromV34(p.dbHandle)
default:
return fmt.Errorf("database schema version not handled: %d", dbVersion.Version)
}
@ -862,18 +879,29 @@ func (p *MySQLProvider) normalizeError(err error, fieldType int) error {
return err
}
func updateMySQLDatabaseFrom28To29(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database schema version: 28 -> 29")
providerLog(logger.LevelInfo, "updating database schema version: 28 -> 29")
sql := strings.ReplaceAll(mysqlV29SQL, "{{users}}", sqlTableUsers)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, strings.Split(sql, ";"), 29, true)
func updateMySQLDatabaseFromV33(dbHandle *sql.DB) error {
return updateMySQLDatabaseFrom33To34(dbHandle)
}
func downgradeMySQLDatabaseFrom29To28(dbHandle *sql.DB) error {
logger.InfoToConsole("downgrading database schema version: 29 -> 28")
providerLog(logger.LevelInfo, "downgrading database schema version: 29 -> 28")
sql := strings.ReplaceAll(mysqlV29DownSQL, "{{users}}", sqlTableUsers)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, strings.Split(sql, ";"), 28, false)
func downgradeMySQLDatabaseFromV34(dbHandle *sql.DB) error {
return downgradeMySQLDatabaseFrom34To33(dbHandle)
}
func updateMySQLDatabaseFrom33To34(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database schema version: 33 -> 34")
providerLog(logger.LevelInfo, "updating database schema version: 33 -> 34")
sql := strings.ReplaceAll(mysqlV34SQL, "{{prefix}}", config.SQLTablesPrefix)
sql = strings.ReplaceAll(sql, "{{shares}}", sqlTableShares)
sql = strings.ReplaceAll(sql, "{{shares_groups_mapping}}", sqlTableSharesGroupsMapping)
sql = strings.ReplaceAll(sql, "{{groups}}", sqlTableGroups)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, strings.Split(sql, ";"), 34, true)
}
func downgradeMySQLDatabaseFrom34To33(dbHandle *sql.DB) error {
logger.InfoToConsole("downgrading database schema version: 34 -> 33")
providerLog(logger.LevelInfo, "downgrading database schema version: 34 -> 33")
sql := strings.ReplaceAll(mysqlV34DownSQL, "{{shares_groups_mapping}}", sqlTableSharesGroupsMapping)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, strings.Split(sql, ";"), 33, false)
}

View file

@ -13,7 +13,6 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//go:build nomysql
// +build nomysql
package dataprovider

View file

@ -17,21 +17,21 @@ package dataprovider
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"hash/fnv"
"io"
"net/http"
"strconv"
"strings"
"time"
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jwt"
"github.com/rs/xid"
"github.com/go-jose/go-jose/v4"
"github.com/drakkan/sftpgo/v2/internal/httpclient"
"github.com/drakkan/sftpgo/v2/internal/jwt"
"github.com/drakkan/sftpgo/v2/internal/kms"
"github.com/drakkan/sftpgo/v2/internal/logger"
"github.com/drakkan/sftpgo/v2/internal/util"
@ -45,7 +45,8 @@ const (
const (
// NodeTokenHeader defines the header to use for the node auth token
NodeTokenHeader = "X-SFTPGO-Node"
NodeTokenHeader = "X-SFTPGO-Node"
nodeTokenAudience = "node"
)
var (
@ -99,7 +100,7 @@ func (n *NodeData) validate() error {
if n.Proto != NodeProtoHTTP && n.Proto != NodeProtoHTTPS {
return util.NewValidationError(fmt.Sprintf("invalid node proto: %s", n.Proto))
}
n.Key = kms.NewPlainSecret(util.BytesToString(util.GenerateRandomBytes(32)))
n.Key = kms.NewPlainSecret(util.GenerateOpaqueString())
n.Key.SetAdditionalData(n.Host)
if err := n.Key.Encrypt(); err != nil {
return fmt.Errorf("unable to encrypt node key: %w", err)
@ -108,12 +109,12 @@ func (n *NodeData) validate() error {
}
func (n *NodeData) getNodeName() string {
h := fnv.New64a()
h := sha256.New()
var b bytes.Buffer
b.WriteString(fmt.Sprintf("%s:%d", n.Host, n.Port))
fmt.Fprintf(&b, "%s:%d", n.Host, n.Port)
h.Write(b.Bytes())
return strconv.FormatUint(h.Sum64(), 10)
return hex.EncodeToString(h.Sum(nil))
}
// Node defines a cluster node
@ -131,33 +132,26 @@ func (n *Node) validate() error {
return n.Data.validate()
}
func (n *Node) authenticate(token string) (string, string, error) {
func (n *Node) authenticate(token string) (*jwt.Claims, error) {
if err := n.Data.Key.TryDecrypt(); err != nil {
providerLog(logger.LevelError, "unable to decrypt node key: %v", err)
return "", "", err
return nil, err
}
if token == "" {
return "", "", ErrInvalidCredentials
return nil, ErrInvalidCredentials
}
t, err := jwt.Parse([]byte(token), jwt.WithKey(jwa.HS256, []byte(n.Data.Key.GetPayload())), jwt.WithValidate(true))
claims, err := jwt.VerifyTokenWithKey(token, []jose.SignatureAlgorithm{jose.HS256}, []byte(n.Data.Key.GetPayload()))
if err != nil {
return "", "", fmt.Errorf("unable to parse and validate token: %v", err)
return nil, fmt.Errorf("unable to parse and validate token: %v", err)
}
var adminUsername, role string
if admin, ok := t.Get("admin"); ok {
if val, ok := admin.(string); ok && val != "" {
adminUsername = val
}
if claims.Username == "" {
return nil, errors.New("no admin username associated with node token")
}
if adminUsername == "" {
return "", "", errors.New("no admin username associated with node token")
if !claims.Audience.Contains(nodeTokenAudience) {
return nil, errors.New("invalid node token audience")
}
if r, ok := t.Get("role"); ok {
if val, ok := r.(string); ok && val != "" {
role = val
}
}
return adminUsername, role, nil
return claims, nil
}
// getBaseURL returns the base URL for this node
@ -174,35 +168,37 @@ func (n *Node) getBaseURL() string {
}
// generateAuthToken generates a new auth token
func (n *Node) generateAuthToken(username, role string) (string, error) {
func (n *Node) generateAuthToken(username, role string, permissions []string) (string, error) {
if err := n.Data.Key.TryDecrypt(); err != nil {
return "", fmt.Errorf("unable to decrypt node key: %w", err)
}
now := time.Now().UTC()
t := jwt.New()
t.Set("admin", username) //nolint:errcheck
t.Set("role", role) //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.ExpirationKey, now.Add(1*time.Minute)) //nolint:errcheck
payload, err := jwt.Sign(t, jwt.WithKey(jwa.HS256, []byte(n.Data.Key.GetPayload())))
signer, err := jwt.NewSigner(jose.HS256, []byte(n.Data.Key.GetPayload()))
if err != nil {
return "", fmt.Errorf("unable to create signer: %w", err)
}
claims := &jwt.Claims{
Username: username,
Role: role,
Permissions: permissions,
}
claims.Audience = []string{nodeTokenAudience}
claims.SetExpiry(time.Now().Add(1 * time.Minute))
payload, err := signer.Sign(claims)
if err != nil {
return "", fmt.Errorf("unable to sign authentication token: %w", err)
}
return util.BytesToString(payload), nil
return payload, nil
}
func (n *Node) prepareRequest(ctx context.Context, username, role, relativeURL, method string,
body io.Reader,
permissions []string, body io.Reader,
) (*http.Request, error) {
url := fmt.Sprintf("%s%s", n.getBaseURL(), relativeURL)
req, err := http.NewRequestWithContext(ctx, method, url, body)
if err != nil {
return nil, err
}
token, err := n.generateAuthToken(username, role)
token, err := n.generateAuthToken(username, role, permissions)
if err != nil {
return nil, err
}
@ -212,11 +208,11 @@ func (n *Node) prepareRequest(ctx context.Context, username, role, relativeURL,
// SendGetRequest sends an HTTP GET request to this node.
// The responseHolder must be a pointer
func (n *Node) SendGetRequest(username, role, relativeURL string, responseHolder any) error {
func (n *Node) SendGetRequest(username, role, relativeURL string, permissions []string, responseHolder any) error {
ctx, cancel := context.WithTimeout(context.Background(), nodeReqTimeout)
defer cancel()
req, err := n.prepareRequest(ctx, username, role, relativeURL, http.MethodGet, nil)
req, err := n.prepareRequest(ctx, username, role, relativeURL, http.MethodGet, permissions, nil)
if err != nil {
return err
}
@ -244,11 +240,11 @@ func (n *Node) SendGetRequest(username, role, relativeURL string, responseHolder
}
// SendDeleteRequest sends an HTTP DELETE request to this node
func (n *Node) SendDeleteRequest(username, role, relativeURL string) error {
func (n *Node) SendDeleteRequest(username, role, relativeURL string, permissions []string) error {
ctx, cancel := context.WithTimeout(context.Background(), nodeReqTimeout)
defer cancel()
req, err := n.prepareRequest(ctx, username, role, relativeURL, http.MethodDelete, nil)
req, err := n.prepareRequest(ctx, username, role, relativeURL, http.MethodDelete, permissions, nil)
if err != nil {
return err
}
@ -268,9 +264,9 @@ func (n *Node) SendDeleteRequest(username, role, relativeURL string) error {
}
// AuthenticateNodeToken check the validity of the provided token
func AuthenticateNodeToken(token string) (string, string, error) {
func AuthenticateNodeToken(token string) (*jwt.Claims, error) {
if currentNode == nil {
return "", "", errNoClusterNodes
return nil, errNoClusterNodes
}
return currentNode.authenticate(token)
}

View file

@ -13,7 +13,6 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//go:build !nopgsql
// +build !nopgsql
package dataprovider
@ -24,6 +23,7 @@ import (
"errors"
"fmt"
"net"
"slices"
"strconv"
"strings"
"time"
@ -40,11 +40,11 @@ import (
const (
pgsqlResetSQL = `DROP TABLE IF EXISTS "{{api_keys}}" CASCADE;
DROP TABLE IF EXISTS "{{folders_mapping}}" CASCADE;
DROP TABLE IF EXISTS "{{users_folders_mapping}}" CASCADE;
DROP TABLE IF EXISTS "{{users_groups_mapping}}" CASCADE;
DROP TABLE IF EXISTS "{{admins_groups_mapping}}" CASCADE;
DROP TABLE IF EXISTS "{{groups_folders_mapping}}" CASCADE;
DROP TABLE IF EXISTS "{{shares_groups_mapping}}" CASCADE;
DROP TABLE IF EXISTS "{{admins}}" CASCADE;
DROP TABLE IF EXISTS "{{folders}}" CASCADE;
DROP TABLE IF EXISTS "{{shares}}" CASCADE;
@ -85,8 +85,8 @@ CREATE TABLE "{{folders}}" ("id" integer NOT NULL PRIMARY KEY GENERATED ALWAYS A
"filesystem" text NULL);
CREATE TABLE "{{groups}}" ("id" integer NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY, "name" varchar(255) NOT NULL UNIQUE,
"description" varchar(512) NULL, "created_at" bigint NOT NULL, "updated_at" bigint NOT NULL, "user_settings" text NULL);
CREATE TABLE "{{shared_sessions}}" ("key" varchar(128) NOT NULL PRIMARY KEY,
"data" text NOT NULL, "type" integer NOT NULL, "timestamp" bigint NOT NULL);
CREATE TABLE "{{shared_sessions}}" ("key" varchar(128) NOT NULL, "type" integer NOT NULL,
"data" text NOT NULL, "timestamp" bigint NOT NULL, PRIMARY KEY ("key", "type"));
CREATE TABLE "{{users}}" ("id" integer NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY, "username" varchar(255) NOT NULL UNIQUE, "status" integer NOT NULL,
"expiration_date" bigint NOT NULL, "description" varchar(512) NULL, "password" text NULL, "public_keys" text NULL,
"home_dir" text NOT NULL, "uid" bigint NOT NULL, "gid" bigint NOT NULL, "max_sessions" integer NOT NULL,
@ -95,14 +95,14 @@ CREATE TABLE "{{users}}" ("id" integer NOT NULL PRIMARY KEY GENERATED ALWAYS AS
"download_bandwidth" integer NOT NULL, "last_login" bigint NOT NULL, "filters" text NULL, "filesystem" text NULL,
"additional_info" text NULL, "created_at" bigint NOT NULL, "updated_at" bigint NOT NULL, "email" varchar(255) NULL,
"upload_data_transfer" integer NOT NULL, "download_data_transfer" integer NOT NULL, "total_data_transfer" integer NOT NULL,
"used_upload_data_transfer" integer NOT NULL, "used_download_data_transfer" integer NOT NULL, "deleted_at" bigint NOT NULL,
"used_upload_data_transfer" bigint NOT NULL, "used_download_data_transfer" bigint NOT NULL, "deleted_at" bigint NOT NULL,
"first_download" bigint NOT NULL, "first_upload" bigint NOT NULL, "last_password_change" bigint NOT NULL, "role_id" integer NULL);
CREATE TABLE "{{groups_folders_mapping}}" ("id" integer NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY, "group_id" integer NOT NULL,
"folder_id" integer NOT NULL, "virtual_path" text NOT NULL, "quota_size" bigint NOT NULL, "quota_files" integer NOT NULL);
"folder_id" integer NOT NULL, "virtual_path" text NOT NULL, "quota_size" bigint NOT NULL, "quota_files" integer NOT NULL, "sort_order" integer NOT NULL);
CREATE TABLE "{{users_groups_mapping}}" ("id" integer NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY, "user_id" integer NOT NULL,
"group_id" integer NOT NULL, "group_type" integer NOT NULL);
"group_id" integer NOT NULL, "group_type" integer NOT NULL, "sort_order" integer NOT NULL);
CREATE TABLE "{{users_folders_mapping}}" ("id" integer NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY, "virtual_path" text NOT NULL,
"quota_size" bigint NOT NULL, "quota_files" integer NOT NULL, "folder_id" integer NOT NULL, "user_id" integer NOT NULL);
"quota_size" bigint NOT NULL, "quota_files" integer NOT NULL, "sort_order" integer NOT NULL, "folder_id" integer NOT NULL, "user_id" integer NOT NULL);
ALTER TABLE "{{users_folders_mapping}}" ADD CONSTRAINT "{{prefix}}unique_user_folder_mapping" UNIQUE ("user_id", "folder_id");
ALTER TABLE "{{users_folders_mapping}}" ADD CONSTRAINT "{{prefix}}users_folders_mapping_folder_id_fk_folders_id"
FOREIGN KEY ("folder_id") REFERENCES "{{folders}}" ("id") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE;
@ -112,7 +112,7 @@ CREATE TABLE "{{shares}}" ("id" integer NOT NULL PRIMARY KEY GENERATED ALWAYS AS
"share_id" varchar(60) NOT NULL UNIQUE, "name" varchar(255) NOT NULL, "description" varchar(512) NULL,
"scope" integer NOT NULL, "paths" text NOT NULL, "created_at" bigint NOT NULL, "updated_at" bigint NOT NULL,
"last_use_at" bigint NOT NULL, "expires_at" bigint NOT NULL, "password" text NULL,
"max_tokens" integer NOT NULL, "used_tokens" integer NOT NULL, "allow_from" text NULL,
"max_tokens" integer NOT NULL, "used_tokens" integer NOT NULL, "allow_from" text NULL, "options" text NULL,
"user_id" integer NOT NULL);
ALTER TABLE "{{shares}}" ADD CONSTRAINT "{{prefix}}shares_user_id_fk_users_id" FOREIGN KEY ("user_id")
REFERENCES "{{users}}" ("id") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE;
@ -132,12 +132,14 @@ FOREIGN KEY ("group_id") REFERENCES "{{groups}}" ("id") MATCH SIMPLE ON UPDATE N
CREATE INDEX "{{prefix}}users_groups_mapping_user_id_idx" ON "{{users_groups_mapping}}" ("user_id");
ALTER TABLE "{{users_groups_mapping}}" ADD CONSTRAINT "{{prefix}}users_groups_mapping_user_id_fk_users_id"
FOREIGN KEY ("user_id") REFERENCES "{{users}}" ("id") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE;
CREATE INDEX "{{prefix}}users_groups_mapping_sort_order_idx" ON "{{users_groups_mapping}}" ("sort_order");
CREATE INDEX "{{prefix}}groups_folders_mapping_folder_id_idx" ON "{{groups_folders_mapping}}" ("folder_id");
ALTER TABLE "{{groups_folders_mapping}}" ADD CONSTRAINT "{{prefix}}groups_folders_mapping_folder_id_fk_folders_id"
FOREIGN KEY ("folder_id") REFERENCES "{{folders}}" ("id") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE;
CREATE INDEX "{{prefix}}groups_folders_mapping_group_id_idx" ON "{{groups_folders_mapping}}" ("group_id");
ALTER TABLE "{{groups_folders_mapping}}" ADD CONSTRAINT "{{prefix}}groups_folders_mapping_group_id_fk_groups_id"
FOREIGN KEY ("group_id") REFERENCES "{{groups}}" ("id") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE;
CREATE INDEX "{{prefix}}groups_folders_mapping_sort_order_idx" ON "{{groups_folders_mapping}}" ("sort_order");
CREATE TABLE "{{events_rules}}" ("id" integer NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY, "name" varchar(255) NOT NULL UNIQUE,
"status" integer NOT NULL, "description" varchar(512) NULL, "created_at" bigint NOT NULL, "updated_at" bigint NOT NULL,
"trigger" integer NOT NULL, "conditions" text NOT NULL, "deleted_at" bigint NOT NULL);
@ -153,7 +155,7 @@ FOREIGN KEY ("rule_id") REFERENCES "{{events_rules}}" ("id") MATCH SIMPLE ON UPD
ALTER TABLE "{{rules_actions_mapping}}" ADD CONSTRAINT "{{prefix}}rules_actions_mapping_action_id_fk_events_targets_id"
FOREIGN KEY ("action_id") REFERENCES "{{events_actions}}" ("id") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION;
CREATE TABLE "{{admins_groups_mapping}}" ("id" integer NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
"admin_id" integer NOT NULL, "group_id" integer NOT NULL, "options" text NOT NULL);
"admin_id" integer NOT NULL, "group_id" integer NOT NULL, "options" text NOT NULL, "sort_order" integer NOT NULL);
ALTER TABLE "{{admins_groups_mapping}}" ADD CONSTRAINT "{{prefix}}unique_admin_group_mapping" UNIQUE ("admin_id", "group_id");
ALTER TABLE "{{admins_groups_mapping}}" ADD CONSTRAINT "{{prefix}}admins_groups_mapping_admin_id_fk_admins_id"
FOREIGN KEY ("admin_id") REFERENCES "{{admins}}" ("id") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE;
@ -176,6 +178,7 @@ CREATE TABLE "{{configs}}" ("id" integer NOT NULL PRIMARY KEY GENERATED ALWAYS A
INSERT INTO {{configs}} (configs) VALUES ('{}');
CREATE INDEX "{{prefix}}users_folders_mapping_folder_id_idx" ON "{{users_folders_mapping}}" ("folder_id");
CREATE INDEX "{{prefix}}users_folders_mapping_user_id_idx" ON "{{users_folders_mapping}}" ("user_id");
CREATE INDEX "{{prefix}}users_folders_mapping_sort_order_idx" ON "{{users_folders_mapping}}" ("sort_order");
CREATE INDEX "{{prefix}}api_keys_admin_id_idx" ON "{{api_keys}}" ("admin_id");
CREATE INDEX "{{prefix}}api_keys_user_id_idx" ON "{{api_keys}}" ("user_id");
CREATE INDEX "{{prefix}}users_updated_at_idx" ON "{{users}}" ("updated_at");
@ -198,6 +201,7 @@ CREATE INDEX "{{prefix}}rules_actions_mapping_action_id_idx" ON "{{rules_actions
CREATE INDEX "{{prefix}}rules_actions_mapping_order_idx" ON "{{rules_actions_mapping}}" ("order");
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_sort_order_idx" ON "{{admins_groups_mapping}}" ("sort_order");
CREATE INDEX "{{prefix}}admins_role_id_idx" ON "{{admins}}" ("role_id");
CREATE INDEX "{{prefix}}users_role_id_idx" ON "{{users}}" ("role_id");
CREATE INDEX "{{prefix}}ip_lists_type_idx" ON "{{ip_lists}}" ("type");
@ -205,16 +209,24 @@ CREATE INDEX "{{prefix}}ip_lists_ipornet_idx" ON "{{ip_lists}}" ("ipornet");
CREATE INDEX "{{prefix}}ip_lists_updated_at_idx" ON "{{ip_lists}}" ("updated_at");
CREATE INDEX "{{prefix}}ip_lists_deleted_at_idx" ON "{{ip_lists}}" ("deleted_at");
CREATE INDEX "{{prefix}}ip_lists_first_last_idx" ON "{{ip_lists}}" ("first", "last");
INSERT INTO {{schema_version}} (version) VALUES (28);
INSERT INTO {{schema_version}} (version) VALUES (33);
`
// not supported in CockroachDB
ipListsLikeIndex = `CREATE INDEX "{{prefix}}ip_lists_ipornet_like_idx" ON "{{ip_lists}}" ("ipornet" varchar_pattern_ops);`
pgsqlV29SQL = `ALTER TABLE "{{users}}" ALTER COLUMN "used_download_data_transfer" TYPE bigint;
ALTER TABLE "{{users}}" ALTER COLUMN "used_upload_data_transfer" TYPE bigint;
`
pgsqlV29DownSQL = `ALTER TABLE "{{users}}" ALTER COLUMN "used_upload_data_transfer" TYPE integer;
ALTER TABLE "{{users}}" ALTER COLUMN "used_download_data_transfer" TYPE integer;
pgsqlV34SQL = `CREATE TABLE "{{shares_groups_mapping}}" (
"id" integer NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
"share_id" integer NOT NULL,
"group_id" integer NOT NULL,
"permissions" integer NOT NULL,
"sort_order" integer NOT NULL,
CONSTRAINT "{{prefix}}unique_share_group_mapping" UNIQUE ("share_id", "group_id"),
CONSTRAINT "{{prefix}}shares_groups_mapping_share_id_fk" FOREIGN KEY ("share_id") REFERENCES "{{shares}}"("id") ON DELETE CASCADE,
CONSTRAINT "{{prefix}}shares_groups_mapping_group_id_fk" FOREIGN KEY ("group_id") REFERENCES "{{groups}}"("id") ON DELETE CASCADE);
CREATE INDEX "{{prefix}}shares_groups_mapping_sort_order_idx" ON "{{shares_groups_mapping}}" ("sort_order");
CREATE INDEX "{{prefix}}shares_groups_mapping_share_id_idx" ON "{{shares_groups_mapping}}" ("share_id");
CREATE INDEX "{{prefix}}shares_groups_mapping_group_id_idx" ON "{{shares_groups_mapping}}" ("group_id");
`
pgsqlV34DownSQL = `DROP TABLE IF EXISTS "{{shares_groups_mapping}}";`
)
var (
@ -274,7 +286,7 @@ func getPGSQLHostsAndPorts(configHost string, configPort int) (string, string) {
defaultPort = "5432"
}
for _, hostport := range strings.Split(configHost, ",") {
for hostport := range strings.SplitSeq(configHost, ",") {
hostport = strings.TrimSpace(hostport)
if hostport == "" {
continue
@ -311,7 +323,7 @@ func getPGSQLConnectionString(redactedPwd bool) string {
if config.DisableSNI {
connectionString += " sslsni=0"
}
if util.Contains(pgSQLTargetSessionAttrs, config.TargetSessionAttrs) {
if slices.Contains(pgSQLTargetSessionAttrs, config.TargetSessionAttrs) {
connectionString += fmt.Sprintf(" target_session_attrs='%s'", config.TargetSessionAttrs)
}
} else {
@ -348,6 +360,14 @@ func (p *PGSQLProvider) getUsedQuota(username string) (int, int64, int64, int64,
return sqlCommonGetUsedQuota(username, p.dbHandle)
}
func (p *PGSQLProvider) getAdminSignature(username string) (string, error) {
return sqlCommonGetAdminSignature(username, p.dbHandle)
}
func (p *PGSQLProvider) getUserSignature(username string) (string, error) {
return sqlCommonGetUserSignature(username, p.dbHandle)
}
func (p *PGSQLProvider) setUpdatedAt(username string) {
sqlCommonSetUpdatedAt(username, p.dbHandle)
}
@ -602,12 +622,12 @@ func (p *PGSQLProvider) addSharedSession(session Session) error {
return sqlCommonAddSession(session, p.dbHandle)
}
func (p *PGSQLProvider) deleteSharedSession(key string) error {
return sqlCommonDeleteSession(key, p.dbHandle)
func (p *PGSQLProvider) deleteSharedSession(key string, sessionType SessionType) error {
return sqlCommonDeleteSession(key, sessionType, p.dbHandle)
}
func (p *PGSQLProvider) getSharedSession(key string) (Session, error) {
return sqlCommonGetSession(key, p.dbHandle)
func (p *PGSQLProvider) getSharedSession(key string, sessionType SessionType) (Session, error) {
return sqlCommonGetSession(key, sessionType, p.dbHandle)
}
func (p *PGSQLProvider) cleanupSharedSessions(sessionType SessionType, before int64) error {
@ -795,8 +815,8 @@ func (p *PGSQLProvider) initializeDatabase() error {
if errors.Is(err, sql.ErrNoRows) {
return errSchemaVersionEmpty
}
logger.InfoToConsole("creating initial database schema, version 28")
providerLog(logger.LevelInfo, "creating initial database schema, version 28")
logger.InfoToConsole("creating initial database schema, version 33")
providerLog(logger.LevelInfo, "creating initial database schema, version 33")
var initialSQL string
if config.Driver == CockroachDataProviderName {
initialSQL = sqlReplaceAll(pgsqlInitial)
@ -805,10 +825,10 @@ func (p *PGSQLProvider) initializeDatabase() error {
initialSQL = sqlReplaceAll(pgsqlInitial + ipListsLikeIndex)
}
return sqlCommonExecSQLAndUpdateDBVersion(p.dbHandle, []string{initialSQL}, 28, true)
return sqlCommonExecSQLAndUpdateDBVersion(p.dbHandle, []string{initialSQL}, 33, true)
}
func (p *PGSQLProvider) migrateDatabase() error { //nolint:dupl
func (p *PGSQLProvider) migrateDatabase() error {
dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, true)
if err != nil {
return err
@ -818,13 +838,13 @@ func (p *PGSQLProvider) migrateDatabase() error { //nolint:dupl
case version == sqlDatabaseVersion:
providerLog(logger.LevelDebug, "sql database is up to date, current version: %d", version)
return ErrNoInitRequired
case version < 28:
err = fmt.Errorf("database schema version %d is too old, please see the upgrading docs", version)
case version < 33:
err = errSchemaVersionTooOld(version)
providerLog(logger.LevelError, "%v", err)
logger.ErrorToConsole("%v", err)
return err
case version == 28:
return updatePGSQLDatabaseFrom28To29(p.dbHandle)
case version == 33:
return updatePGSQLDatabaseFromV33(p.dbHandle)
default:
if version > sqlDatabaseVersion {
providerLog(logger.LevelError, "database schema version %d is newer than the supported one: %d", version,
@ -847,8 +867,8 @@ func (p *PGSQLProvider) revertDatabase(targetVersion int) error {
}
switch dbVersion.Version {
case 29:
return downgradePGSQLDatabaseFrom29To28(p.dbHandle)
case 34:
return downgradePGSQLDatabaseFromV34(p.dbHandle)
default:
return fmt.Errorf("database schema version not handled: %d", dbVersion.Version)
}
@ -887,18 +907,29 @@ func (p *PGSQLProvider) normalizeError(err error, fieldType int) error {
return err
}
func updatePGSQLDatabaseFrom28To29(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database schema version: 28 -> 29")
providerLog(logger.LevelInfo, "updating database schema version: 28 -> 29")
sql := strings.ReplaceAll(pgsqlV29SQL, "{{users}}", sqlTableUsers)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 29, true)
func updatePGSQLDatabaseFromV33(dbHandle *sql.DB) error {
return updatePGSQLDatabaseFrom33To34(dbHandle)
}
func downgradePGSQLDatabaseFrom29To28(dbHandle *sql.DB) error {
logger.InfoToConsole("downgrading database schema version: 29 -> 28")
providerLog(logger.LevelInfo, "downgrading database schema version: 29 -> 28")
sql := strings.ReplaceAll(pgsqlV29DownSQL, "{{users}}", sqlTableUsers)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 28, false)
func downgradePGSQLDatabaseFromV34(dbHandle *sql.DB) error {
return downgradePGSQLDatabaseFrom34To33(dbHandle)
}
func updatePGSQLDatabaseFrom33To34(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database schema version: 33 -> 34")
providerLog(logger.LevelInfo, "updating database schema version: 33 -> 34")
sql := strings.ReplaceAll(pgsqlV34SQL, "{{prefix}}", config.SQLTablesPrefix)
sql = strings.ReplaceAll(sql, "{{shares}}", sqlTableShares)
sql = strings.ReplaceAll(sql, "{{shares_groups_mapping}}", sqlTableSharesGroupsMapping)
sql = strings.ReplaceAll(sql, "{{groups}}", sqlTableGroups)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 34, true)
}
func downgradePGSQLDatabaseFrom34To33(dbHandle *sql.DB) error {
logger.InfoToConsole("downgrading database schema version: 34 -> 33")
providerLog(logger.LevelInfo, "downgrading database schema version: 34 -> 33")
sql := strings.ReplaceAll(pgsqlV34DownSQL, "{{shares_groups_mapping}}", sqlTableSharesGroupsMapping)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 33, false)
}

View file

@ -13,7 +13,6 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//go:build nopgsql
// +build nopgsql
package dataprovider

View file

@ -57,6 +57,9 @@ func (r *Role) validate() error {
if r.Name == "" {
return util.NewI18nError(util.NewValidationError("name is mandatory"), util.I18nErrorNameRequired)
}
if !util.IsNameValid(r.Name) {
return util.NewI18nError(errInvalidInput, util.I18nErrorInvalidInput)
}
if len(r.Name) > 255 {
return util.NewValidationError("name is too long, 255 is the maximum length allowed")
}

View file

@ -84,11 +84,14 @@ func addScheduledCacheUpdates() error {
func checkDataprovider() {
if currentNode != nil {
if err := provider.updateNodeTimestamp(); err != nil {
err := provider.updateNodeTimestamp()
if err != nil {
providerLog(logger.LevelError, "unable to update node timestamp: %v", err)
} else {
providerLog(logger.LevelDebug, "node timestamp updated")
}
metric.UpdateDataProviderAvailability(err)
return
}
err := provider.checkAvailability()
if err != nil {

View file

@ -89,6 +89,11 @@ func (s *Share) GetAllowedFromAsString() string {
return strings.Join(s.AllowFrom, ",")
}
// IsPasswordHashed returns true if the password is hashed
func (s *Share) IsPasswordHashed() bool {
return util.IsStringPrefixInSlice(s.Password, hashPwdPrefixes)
}
func (s *Share) getACopy() Share {
allowFrom := make([]string, len(s.AllowFrom))
copy(allowFrom, s.AllowFrom)
@ -141,7 +146,7 @@ func (s *Share) HasRedactedPassword() bool {
func (s *Share) hashPassword() error {
if s.Password != "" && !util.IsStringPrefixInSlice(s.Password, internalHashPwdPrefixes) {
user, err := UserExists(s.Username, "")
user, err := GetUserWithGroupSettings(s.Username, "")
if err != nil {
return util.NewGenericError(fmt.Sprintf("unable to validate user: %v", err))
}
@ -201,13 +206,16 @@ func (s *Share) validatePaths() error {
return nil
}
func (s *Share) validate() error {
func (s *Share) validate() error { //nolint:gocyclo
if s.ShareID == "" {
return util.NewValidationError("share_id is mandatory")
}
if s.Name == "" {
return util.NewI18nError(util.NewValidationError("name is mandatory"), util.I18nErrorNameRequired)
}
if !util.IsNameValid(s.Name) {
return util.NewI18nError(errInvalidInput, util.I18nErrorInvalidInput)
}
if s.Scope < ShareScopeRead || s.Scope > ShareScopeReadWrite {
return util.NewI18nError(util.NewValidationError(fmt.Sprintf("invalid scope: %v", s.Scope)), util.I18nErrorShareScope)
}

View file

@ -23,6 +23,7 @@ import (
"fmt"
"net/netip"
"runtime/debug"
"strconv"
"strings"
"time"
@ -35,7 +36,7 @@ import (
)
const (
sqlDatabaseVersion = 29
sqlDatabaseVersion = 34
defaultSQLQueryTimeout = 10 * time.Second
longSQLQueryTimeout = 60 * time.Second
)
@ -70,6 +71,7 @@ func sqlReplaceAll(sql string) string {
sql = strings.ReplaceAll(sql, "{{groups_folders_mapping}}", sqlTableGroupsFoldersMapping)
sql = strings.ReplaceAll(sql, "{{api_keys}}", sqlTableAPIKeys)
sql = strings.ReplaceAll(sql, "{{shares}}", sqlTableShares)
sql = strings.ReplaceAll(sql, "{{shares_groups_mapping}}", sqlTableSharesGroupsMapping)
sql = strings.ReplaceAll(sql, "{{defender_events}}", sqlTableDefenderEvents)
sql = strings.ReplaceAll(sql, "{{defender_hosts}}", sqlTableDefenderHosts)
sql = strings.ReplaceAll(sql, "{{active_transfers}}", sqlTableActiveTransfers)
@ -1248,6 +1250,32 @@ func sqlCommonUpdateQuota(username string, filesAdd int, sizeAdd int64, reset bo
return err
}
func sqlCommonGetAdminSignature(username string, dbHandle *sql.DB) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
q := getAdminSignatureQuery()
var updatedAt int64
err := dbHandle.QueryRowContext(ctx, q, username).Scan(&updatedAt)
if err != nil {
return "", err
}
return strconv.FormatInt(updatedAt, 10), nil
}
func sqlCommonGetUserSignature(username string, dbHandle *sql.DB) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
q := getUserSignatureQuery()
var updatedAt int64
err := dbHandle.QueryRowContext(ctx, q, username).Scan(&updatedAt)
if err != nil {
return "", err
}
return strconv.FormatInt(updatedAt, 10), nil
}
func sqlCommonGetUsedQuota(username string, dbHandle *sql.DB) (int, int64, int64, int64, error) {
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
@ -2496,9 +2524,9 @@ func sqlCommonClearUserGroupMapping(ctx context.Context, user *User, dbHandle sq
return err
}
func sqlCommonAddUserFolderMapping(ctx context.Context, user *User, folder *vfs.VirtualFolder, dbHandle sqlQuerier) error {
func sqlCommonAddUserFolderMapping(ctx context.Context, user *User, folder *vfs.VirtualFolder, sortOrder int, dbHandle sqlQuerier) error {
q := getAddUserFolderMappingQuery()
_, err := dbHandle.ExecContext(ctx, q, folder.VirtualPath, folder.QuotaSize, folder.QuotaFiles, folder.Name, user.Username)
_, err := dbHandle.ExecContext(ctx, q, folder.VirtualPath, folder.QuotaSize, folder.QuotaFiles, folder.Name, user.Username, sortOrder)
return err
}
@ -2508,27 +2536,29 @@ func sqlCommonClearAdminGroupMapping(ctx context.Context, admin *Admin, dbHandle
return err
}
func sqlCommonAddGroupFolderMapping(ctx context.Context, group *Group, folder *vfs.VirtualFolder, dbHandle sqlQuerier) error {
func sqlCommonAddGroupFolderMapping(ctx context.Context, group *Group, folder *vfs.VirtualFolder, sortOrder int,
dbHandle sqlQuerier,
) error {
q := getAddGroupFolderMappingQuery()
_, err := dbHandle.ExecContext(ctx, q, folder.VirtualPath, folder.QuotaSize, folder.QuotaFiles, folder.Name, group.Name)
_, err := dbHandle.ExecContext(ctx, q, folder.VirtualPath, folder.QuotaSize, folder.QuotaFiles, folder.Name, group.Name, sortOrder)
return err
}
func sqlCommonAddUserGroupMapping(ctx context.Context, username, groupName string, groupType int, dbHandle sqlQuerier) error {
func sqlCommonAddUserGroupMapping(ctx context.Context, username, groupName string, groupType, sortOrder int, dbHandle sqlQuerier) error {
q := getAddUserGroupMappingQuery()
_, err := dbHandle.ExecContext(ctx, q, username, groupName, groupType)
_, err := dbHandle.ExecContext(ctx, q, username, groupName, groupType, sortOrder)
return err
}
func sqlCommonAddAdminGroupMapping(ctx context.Context, username, groupName string, mappingOptions AdminGroupMappingOptions,
dbHandle sqlQuerier,
sortOrder int, dbHandle sqlQuerier,
) error {
options, err := json.Marshal(mappingOptions)
if err != nil {
return err
}
q := getAddAdminGroupMappingQuery()
_, err = dbHandle.ExecContext(ctx, q, username, groupName, options)
_, err = dbHandle.ExecContext(ctx, q, username, groupName, options, sortOrder)
return err
}
@ -2539,7 +2569,7 @@ func generateGroupVirtualFoldersMapping(ctx context.Context, group *Group, dbHan
}
for idx := range group.VirtualFolders {
vfolder := &group.VirtualFolders[idx]
err = sqlCommonAddGroupFolderMapping(ctx, group, vfolder, dbHandle)
err = sqlCommonAddGroupFolderMapping(ctx, group, vfolder, idx, dbHandle)
if err != nil {
return err
}
@ -2554,7 +2584,7 @@ func generateUserVirtualFoldersMapping(ctx context.Context, user *User, dbHandle
}
for idx := range user.VirtualFolders {
vfolder := &user.VirtualFolders[idx]
err = sqlCommonAddUserFolderMapping(ctx, user, vfolder, dbHandle)
err = sqlCommonAddUserFolderMapping(ctx, user, vfolder, idx, dbHandle)
if err != nil {
return err
}
@ -2567,8 +2597,8 @@ func generateUserGroupMapping(ctx context.Context, user *User, dbHandle sqlQueri
if err != nil {
return err
}
for _, group := range user.Groups {
err = sqlCommonAddUserGroupMapping(ctx, user.Username, group.Name, group.Type, dbHandle)
for idx, group := range user.Groups {
err = sqlCommonAddUserGroupMapping(ctx, user.Username, group.Name, group.Type, idx, dbHandle)
if err != nil {
return err
}
@ -2581,8 +2611,8 @@ func generateAdminGroupMapping(ctx context.Context, admin *Admin, dbHandle sqlQu
if err != nil {
return err
}
for _, group := range admin.Groups {
err = sqlCommonAddAdminGroupMapping(ctx, admin.Username, group.Name, group.Options, dbHandle)
for idx, group := range admin.Groups {
err = sqlCommonAddAdminGroupMapping(ctx, admin.Username, group.Name, group.Options, idx, dbHandle)
if err != nil {
return err
}
@ -3238,14 +3268,14 @@ func sqlCommonAddSession(session Session, dbHandle *sql.DB) error {
return err
}
func sqlCommonGetSession(key string, dbHandle sqlQuerier) (Session, error) {
func sqlCommonGetSession(key string, sessionType SessionType, dbHandle sqlQuerier) (Session, error) {
var session Session
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
q := getSessionQuery()
var data []byte // type hint, some driver will use string instead of []byte if the type is any
err := dbHandle.QueryRowContext(ctx, q, key).Scan(&session.Key, &data, &session.Type, &session.Timestamp)
err := dbHandle.QueryRowContext(ctx, q, key, sessionType).Scan(&session.Key, &data, &session.Type, &session.Timestamp)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return session, util.NewRecordNotFoundError(err.Error())
@ -3256,12 +3286,12 @@ func sqlCommonGetSession(key string, dbHandle sqlQuerier) (Session, error) {
return session, nil
}
func sqlCommonDeleteSession(key string, dbHandle *sql.DB) error {
func sqlCommonDeleteSession(key string, sessionType SessionType, dbHandle *sql.DB) error {
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
q := getDeleteSessionQuery()
res, err := dbHandle.ExecContext(ctx, q, key)
res, err := dbHandle.ExecContext(ctx, q, key, sessionType)
if err != nil {
return err
}
@ -3936,16 +3966,22 @@ func sqlCommonUpdateDatabaseVersion(ctx context.Context, dbHandle sqlQuerier, ve
}
func sqlCommonExecSQLAndUpdateDBVersion(dbHandle *sql.DB, sqlQueries []string, newVersion int, isUp bool) error {
if err := sqlAcquireLock(dbHandle); err != nil {
return err
}
defer sqlReleaseLock(dbHandle)
ctx, cancel := context.WithTimeout(context.Background(), longSQLQueryTimeout)
defer cancel()
conn, err := dbHandle.Conn(ctx)
if err != nil {
return fmt.Errorf("unable to get connection from pool: %w", err)
}
defer conn.Close()
if err := sqlAcquireLock(conn); err != nil {
return err
}
defer sqlReleaseLock(conn)
if newVersion > 0 {
currentVersion, err := sqlCommonGetDatabaseVersion(dbHandle, false)
currentVersion, err := sqlCommonGetDatabaseVersion(conn, false)
if err == nil {
if (isUp && currentVersion.Version >= newVersion) || (!isUp && currentVersion.Version <= newVersion) {
providerLog(logger.LevelInfo, "current schema version: %v, requested: %v, did you execute simultaneous migrations?",
@ -3955,7 +3991,7 @@ func sqlCommonExecSQLAndUpdateDBVersion(dbHandle *sql.DB, sqlQueries []string, n
}
}
return sqlCommonExecuteTx(ctx, dbHandle, func(tx *sql.Tx) error {
return sqlCommonExecuteTxOnConn(ctx, conn, func(tx *sql.Tx) error {
for _, q := range sqlQueries {
if strings.TrimSpace(q) == "" {
continue
@ -3972,7 +4008,7 @@ func sqlCommonExecSQLAndUpdateDBVersion(dbHandle *sql.DB, sqlQueries []string, n
})
}
func sqlAcquireLock(dbHandle *sql.DB) error {
func sqlAcquireLock(dbHandle *sql.Conn) error {
ctx, cancel := context.WithTimeout(context.Background(), longSQLQueryTimeout)
defer cancel()
@ -4001,7 +4037,7 @@ func sqlAcquireLock(dbHandle *sql.DB) error {
return nil
}
func sqlReleaseLock(dbHandle *sql.DB) {
func sqlReleaseLock(dbHandle *sql.Conn) {
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
@ -4023,6 +4059,20 @@ func sqlReleaseLock(dbHandle *sql.DB) {
}
}
func sqlCommonExecuteTxOnConn(ctx context.Context, conn *sql.Conn, txFn func(*sql.Tx) error) error {
tx, err := conn.BeginTx(ctx, nil)
if err != nil {
return err
}
err = txFn(tx)
if err != nil {
tx.Rollback() //nolint:errcheck
return err
}
return tx.Commit()
}
func sqlCommonExecuteTx(ctx context.Context, dbHandle *sql.DB, txFn func(*sql.Tx) error) error {
if config.Driver == CockroachDataProviderName {
return crdb.ExecuteTx(ctx, dbHandle, nil, txFn)

View file

@ -12,8 +12,7 @@
// 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/>.
//go:build !nosqlite
// +build !nosqlite
//go:build !nosqlite && cgo
package dataprovider
@ -24,6 +23,7 @@ import (
"errors"
"fmt"
"path/filepath"
"strings"
"time"
"github.com/mattn/go-sqlite3"
@ -36,11 +36,11 @@ import (
const (
sqliteResetSQL = `DROP TABLE IF EXISTS "{{api_keys}}";
DROP TABLE IF EXISTS "{{folders_mapping}}";
DROP TABLE IF EXISTS "{{users_folders_mapping}}";
DROP TABLE IF EXISTS "{{users_groups_mapping}}";
DROP TABLE IF EXISTS "{{admins_groups_mapping}}";
DROP TABLE IF EXISTS "{{groups_folders_mapping}}";
DROP TABLE IF EXISTS "{{shares_groups_mapping}}";
DROP TABLE IF EXISTS "{{admins}}";
DROP TABLE IF EXISTS "{{folders}}";
DROP TABLE IF EXISTS "{{shares}}";
@ -82,8 +82,8 @@ CREATE TABLE "{{folders}}" ("id" integer NOT NULL PRIMARY KEY, "name" varchar(25
"last_quota_update" bigint NOT NULL, "filesystem" text NULL);
CREATE TABLE "{{groups}}" ("id" integer NOT NULL PRIMARY KEY, "name" varchar(255) NOT NULL UNIQUE,
"description" varchar(512) NULL, "created_at" bigint NOT NULL, "updated_at" bigint NOT NULL, "user_settings" text NULL);
CREATE TABLE "{{shared_sessions}}" ("key" varchar(128) NOT NULL PRIMARY KEY, "data" text NOT NULL,
"type" integer NOT NULL, "timestamp" bigint NOT NULL);
CREATE TABLE "{{shared_sessions}}" ("key" varchar(128) NOT NULL, "type" integer NOT NULL,
"data" text NOT NULL, "timestamp" bigint NOT NULL, PRIMARY KEY ("key", "type"));
CREATE TABLE "{{users}}" ("id" integer NOT NULL PRIMARY KEY, "username" varchar(255) NOT NULL UNIQUE,
"status" integer NOT NULL, "expiration_date" bigint NOT NULL, "description" varchar(512) NULL, "password" text NULL,
"public_keys" text NULL, "home_dir" text NOT NULL, "uid" bigint NOT NULL, "gid" bigint NOT NULL,
@ -98,21 +98,21 @@ CREATE TABLE "{{users}}" ("id" integer NOT NULL PRIMARY KEY, "username" varchar(
CREATE TABLE "{{groups_folders_mapping}}" ("id" integer NOT NULL PRIMARY KEY,
"folder_id" integer NOT NULL REFERENCES "{{folders}}" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
"group_id" integer NOT NULL REFERENCES "{{groups}}" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
"virtual_path" text NOT NULL, "quota_size" bigint NOT NULL, "quota_files" integer NOT NULL,
"virtual_path" text NOT NULL, "quota_size" bigint NOT NULL, "quota_files" integer NOT NULL, "sort_order" integer NOT NULL,
CONSTRAINT "{{prefix}}unique_group_folder_mapping" UNIQUE ("group_id", "folder_id"));
CREATE TABLE "{{users_groups_mapping}}" ("id" integer NOT NULL PRIMARY KEY,
"user_id" integer NOT NULL REFERENCES "{{users}}" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
"group_id" integer NOT NULL REFERENCES "{{groups}}" ("id") ON DELETE NO ACTION,
"group_type" integer NOT NULL, CONSTRAINT "{{prefix}}unique_user_group_mapping" UNIQUE ("user_id", "group_id"));
"group_type" integer NOT NULL, "sort_order" integer NOT NULL, CONSTRAINT "{{prefix}}unique_user_group_mapping" UNIQUE ("user_id", "group_id"));
CREATE TABLE "{{users_folders_mapping}}" ("id" integer NOT NULL PRIMARY KEY,
"user_id" integer NOT NULL REFERENCES "{{users}}" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
"folder_id" integer NOT NULL REFERENCES "{{folders}}" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
"virtual_path" text NOT NULL, "quota_size" bigint NOT NULL, "quota_files" integer NOT NULL,
"virtual_path" text NOT NULL, "quota_size" bigint NOT NULL, "quota_files" integer NOT NULL, "sort_order" integer NOT NULL,
CONSTRAINT "{{prefix}}unique_user_folder_mapping" UNIQUE ("user_id", "folder_id"));
CREATE TABLE "{{shares}}" ("id" integer NOT NULL PRIMARY KEY, "share_id" varchar(60) NOT NULL UNIQUE,
"name" varchar(255) NOT NULL, "description" varchar(512) NULL, "scope" integer NOT NULL, "paths" text NOT NULL,
"created_at" bigint NOT NULL, "updated_at" bigint NOT NULL, "last_use_at" bigint NOT NULL, "expires_at" bigint NOT NULL,
"password" text NULL, "max_tokens" integer NOT NULL, "used_tokens" integer NOT NULL, "allow_from" text NULL,
"password" text NULL, "max_tokens" integer NOT NULL, "used_tokens" integer NOT NULL, "allow_from" text NULL, "options" text NULL,
"user_id" integer NOT NULL REFERENCES "{{users}}" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED);
CREATE TABLE "{{api_keys}}" ("id" integer NOT NULL PRIMARY KEY, "name" varchar(255) NOT NULL,
"key_id" varchar(50) NOT NULL UNIQUE, "api_key" varchar(255) NOT NULL UNIQUE, "scope" integer NOT NULL,
@ -134,7 +134,7 @@ CREATE TABLE "{{tasks}}" ("id" integer NOT NULL PRIMARY KEY, "name" varchar(255)
CREATE TABLE "{{admins_groups_mapping}}" ("id" integer NOT NULL PRIMARY KEY,
"admin_id" integer NOT NULL REFERENCES "{{admins}}" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
"group_id" integer NOT NULL REFERENCES "{{groups}}" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
"options" text NOT NULL, CONSTRAINT "{{prefix}}unique_admin_group_mapping" UNIQUE ("admin_id", "group_id"));
"options" text NOT NULL, "sort_order" integer NOT NULL, CONSTRAINT "{{prefix}}unique_admin_group_mapping" UNIQUE ("admin_id", "group_id"));
CREATE TABLE "{{ip_lists}}" ("id" integer NOT NULL PRIMARY KEY,
"type" integer NOT NULL, "ipornet" varchar(50) NOT NULL, "mode" integer NOT NULL, "description" varchar(512) NULL,
"first" BLOB NOT NULL, "last" BLOB NOT NULL, "ip_type" integer NOT NULL, "protocols" integer NOT NULL,
@ -144,10 +144,13 @@ CREATE TABLE "{{configs}}" ("id" integer NOT NULL PRIMARY KEY, "configs" text NO
INSERT INTO {{configs}} (configs) VALUES ('{}');
CREATE INDEX "{{prefix}}users_folders_mapping_folder_id_idx" ON "{{users_folders_mapping}}" ("folder_id");
CREATE INDEX "{{prefix}}users_folders_mapping_user_id_idx" ON "{{users_folders_mapping}}" ("user_id");
CREATE INDEX "{{prefix}}users_folders_mapping_sort_order_idx" ON "{{users_folders_mapping}}" ("sort_order");
CREATE INDEX "{{prefix}}users_groups_mapping_group_id_idx" ON "{{users_groups_mapping}}" ("group_id");
CREATE INDEX "{{prefix}}users_groups_mapping_user_id_idx" ON "{{users_groups_mapping}}" ("user_id");
CREATE INDEX "{{prefix}}users_groups_mapping_sort_order_idx" ON "{{users_groups_mapping}}" ("sort_order");
CREATE INDEX "{{prefix}}groups_folders_mapping_folder_id_idx" ON "{{groups_folders_mapping}}" ("folder_id");
CREATE INDEX "{{prefix}}groups_folders_mapping_group_id_idx" ON "{{groups_folders_mapping}}" ("group_id");
CREATE INDEX "{{prefix}}groups_folders_mapping_sort_order_idx" ON "{{groups_folders_mapping}}" ("sort_order");
CREATE INDEX "{{prefix}}api_keys_admin_id_idx" ON "{{api_keys}}" ("admin_id");
CREATE INDEX "{{prefix}}api_keys_user_id_idx" ON "{{api_keys}}" ("user_id");
CREATE INDEX "{{prefix}}users_updated_at_idx" ON "{{users}}" ("updated_at");
@ -170,6 +173,7 @@ CREATE INDEX "{{prefix}}rules_actions_mapping_action_id_idx" ON "{{rules_actions
CREATE INDEX "{{prefix}}rules_actions_mapping_order_idx" ON "{{rules_actions_mapping}}" ("order");
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_sort_order_idx" ON "{{admins_groups_mapping}}" ("sort_order");
CREATE INDEX "{{prefix}}users_role_id_idx" ON "{{users}}" ("role_id");
CREATE INDEX "{{prefix}}admins_role_id_idx" ON "{{admins}}" ("role_id");
CREATE INDEX "{{prefix}}ip_lists_type_idx" ON "{{ip_lists}}" ("type");
@ -178,8 +182,22 @@ CREATE INDEX "{{prefix}}ip_lists_ip_type_idx" ON "{{ip_lists}}" ("ip_type");
CREATE INDEX "{{prefix}}ip_lists_ip_updated_at_idx" ON "{{ip_lists}}" ("updated_at");
CREATE INDEX "{{prefix}}ip_lists_ip_deleted_at_idx" ON "{{ip_lists}}" ("deleted_at");
CREATE INDEX "{{prefix}}ip_lists_first_last_idx" ON "{{ip_lists}}" ("first", "last");
INSERT INTO {{schema_version}} (version) VALUES (28);
INSERT INTO {{schema_version}} (version) VALUES (33);
`
sqliteV34SQL = `
CREATE TABLE "{{shares_groups_mapping}}" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"share_id" integer NOT NULL REFERENCES "{{shares}}" ("id") ON DELETE CASCADE,
"group_id" integer NOT NULL REFERENCES "{{groups}}" ("id") ON DELETE CASCADE,
"permissions" integer NOT NULL,
"sort_order" integer NOT NULL,
CONSTRAINT "{{prefix}}unique_share_group_mapping" UNIQUE ("share_id", "group_id")
);
CREATE INDEX "{{prefix}}shares_groups_mapping_sort_order_idx" ON "{{shares_groups_mapping}}" ("sort_order");
CREATE INDEX "{{prefix}}shares_groups_mapping_group_id_idx" ON "{{shares_groups_mapping}}" ("group_id");
CREATE INDEX "{{prefix}}shares_groups_mapping_share_id_idx" ON "{{shares_groups_mapping}}" ("share_id");
`
sqliteV34DownSQL = `DROP TABLE IF EXISTS "{{shares_groups_mapping}}";`
)
// SQLiteProvider defines the auth provider for SQLite database
@ -215,7 +233,7 @@ func initializeSQLiteProvider(basePath string) error {
providerLog(logger.LevelDebug, "sqlite database handle created, connection string: %q", connectionString)
dbHandle.SetMaxOpenConns(1)
provider = &SQLiteProvider{dbHandle: dbHandle}
return nil
return executePragmaOptimize(dbHandle)
}
func (p *SQLiteProvider) checkAvailability() error {
@ -246,6 +264,14 @@ func (p *SQLiteProvider) getUsedQuota(username string) (int, int64, int64, int64
return sqlCommonGetUsedQuota(username, p.dbHandle)
}
func (p *SQLiteProvider) getAdminSignature(username string) (string, error) {
return sqlCommonGetAdminSignature(username, p.dbHandle)
}
func (p *SQLiteProvider) getUserSignature(username string) (string, error) {
return sqlCommonGetUserSignature(username, p.dbHandle)
}
func (p *SQLiteProvider) setUpdatedAt(username string) {
sqlCommonSetUpdatedAt(username, p.dbHandle)
}
@ -500,12 +526,12 @@ func (p *SQLiteProvider) addSharedSession(session Session) error {
return sqlCommonAddSession(session, p.dbHandle)
}
func (p *SQLiteProvider) deleteSharedSession(key string) error {
return sqlCommonDeleteSession(key, p.dbHandle)
func (p *SQLiteProvider) deleteSharedSession(key string, sessionType SessionType) error {
return sqlCommonDeleteSession(key, sessionType, p.dbHandle)
}
func (p *SQLiteProvider) getSharedSession(key string) (Session, error) {
return sqlCommonGetSession(key, p.dbHandle)
func (p *SQLiteProvider) getSharedSession(key string, sessionType SessionType) (Session, error) {
return sqlCommonGetSession(key, sessionType, p.dbHandle)
}
func (p *SQLiteProvider) cleanupSharedSessions(sessionType SessionType, before int64) error {
@ -693,13 +719,13 @@ func (p *SQLiteProvider) initializeDatabase() error {
if errors.Is(err, sql.ErrNoRows) {
return errSchemaVersionEmpty
}
logger.InfoToConsole("creating initial database schema, version 28")
providerLog(logger.LevelInfo, "creating initial database schema, version 28")
logger.InfoToConsole("creating initial database schema, version 33")
providerLog(logger.LevelInfo, "creating initial database schema, version 33")
sql := sqlReplaceAll(sqliteInitialSQL)
return sqlCommonExecSQLAndUpdateDBVersion(p.dbHandle, []string{sql}, 28, true)
return sqlCommonExecSQLAndUpdateDBVersion(p.dbHandle, []string{sql}, 33, true)
}
func (p *SQLiteProvider) migrateDatabase() error { //nolint:dupl
func (p *SQLiteProvider) migrateDatabase() error {
dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, true)
if err != nil {
return err
@ -709,13 +735,13 @@ func (p *SQLiteProvider) migrateDatabase() error { //nolint:dupl
case version == sqlDatabaseVersion:
providerLog(logger.LevelDebug, "sql database is up to date, current version: %d", version)
return ErrNoInitRequired
case version < 28:
err = fmt.Errorf("database schema version %d is too old, please see the upgrading docs", version)
case version < 33:
err = errSchemaVersionTooOld(version)
providerLog(logger.LevelError, "%v", err)
logger.ErrorToConsole("%v", err)
return err
case version == 28:
return updateSQLiteDatabaseFrom28To29(p.dbHandle)
case version == 33:
return updateSQLiteDatabaseFromV33(p.dbHandle)
default:
if version > sqlDatabaseVersion {
providerLog(logger.LevelError, "database schema version %d is newer than the supported one: %d", version,
@ -738,8 +764,8 @@ func (p *SQLiteProvider) revertDatabase(targetVersion int) error {
}
switch dbVersion.Version {
case 29:
return downgradeSQLiteDatabaseFrom29To28(p.dbHandle)
case 34:
return downgradeSQLiteDatabaseFromV34(p.dbHandle)
default:
return fmt.Errorf("database schema version not handled: %d", dbVersion.Version)
}
@ -777,24 +803,39 @@ func (p *SQLiteProvider) normalizeError(err error, fieldType int) error {
return err
}
func updateSQLiteDatabaseFrom28To29(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database schema version: 28 -> 29")
providerLog(logger.LevelInfo, "updating database schema version: 28 -> 29")
func executePragmaOptimize(dbHandle *sql.DB) error {
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
return sqlCommonUpdateDatabaseVersion(ctx, dbHandle, 29)
_, err := dbHandle.ExecContext(ctx, "PRAGMA optimize;")
return err
}
func downgradeSQLiteDatabaseFrom29To28(dbHandle *sql.DB) error {
logger.InfoToConsole("downgrading database schema version: 29 -> 28")
providerLog(logger.LevelInfo, "downgrading database schema version: 29 -> 28")
func updateSQLiteDatabaseFromV33(dbHandle *sql.DB) error {
return updateSQLiteDatabaseFrom33To34(dbHandle)
}
ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
defer cancel()
func downgradeSQLiteDatabaseFromV34(dbHandle *sql.DB) error {
return downgradeSQLiteDatabaseFrom34To33(dbHandle)
}
return sqlCommonUpdateDatabaseVersion(ctx, dbHandle, 28)
func updateSQLiteDatabaseFrom33To34(dbHandle *sql.DB) error {
logger.InfoToConsole("updating database schema version: 33 -> 34")
providerLog(logger.LevelInfo, "updating database schema version: 33 -> 34")
sql := strings.ReplaceAll(sqliteV34SQL, "{{prefix}}", config.SQLTablesPrefix)
sql = strings.ReplaceAll(sql, "{{shares}}", sqlTableShares)
sql = strings.ReplaceAll(sql, "{{shares_groups_mapping}}", sqlTableSharesGroupsMapping)
sql = strings.ReplaceAll(sql, "{{groups}}", sqlTableGroups)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 34, true)
}
func downgradeSQLiteDatabaseFrom34To33(dbHandle *sql.DB) error {
logger.InfoToConsole("downgrading database schema version: 34 -> 33")
providerLog(logger.LevelInfo, "downgrading database schema version: 34 -> 33")
sql := strings.ReplaceAll(sqliteV34DownSQL, "{{shares_groups_mapping}}", sqlTableSharesGroupsMapping)
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 33, false)
}
/*func setPragmaFK(dbHandle *sql.DB, value string) error {

View file

@ -12,8 +12,7 @@
// 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/>.
//go:build nosqlite
// +build nosqlite
//go:build nosqlite || !cgo
package dataprovider

View file

@ -81,25 +81,27 @@ func getAddSessionQuery() string {
"ON DUPLICATE KEY UPDATE `data`=VALUES(`data`), `timestamp`=VALUES(`timestamp`)",
sqlTableSharedSessions, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3])
}
return fmt.Sprintf(`INSERT INTO %s (key,data,type,timestamp) VALUES (%s,%s,%s,%s) ON CONFLICT(key) DO UPDATE SET data=
return fmt.Sprintf(`INSERT INTO %s (key,data,type,timestamp) VALUES (%s,%s,%s,%s) ON CONFLICT(key,type) DO UPDATE SET data=
EXCLUDED.data, timestamp=EXCLUDED.timestamp`,
sqlTableSharedSessions, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3])
}
func getDeleteSessionQuery() string {
if config.Driver == MySQLDataProviderName {
return fmt.Sprintf("DELETE FROM %s WHERE `key` = %s", sqlTableSharedSessions, sqlPlaceholders[0])
return fmt.Sprintf("DELETE FROM %s WHERE `key` = %s AND `type` = %s",
sqlTableSharedSessions, sqlPlaceholders[0], sqlPlaceholders[1])
}
return fmt.Sprintf(`DELETE FROM %s WHERE key = %s`, sqlTableSharedSessions, sqlPlaceholders[0])
return fmt.Sprintf(`DELETE FROM %s WHERE key = %s AND type = %s`,
sqlTableSharedSessions, sqlPlaceholders[0], sqlPlaceholders[1])
}
func getSessionQuery() string {
if config.Driver == MySQLDataProviderName {
return fmt.Sprintf("SELECT `key`,`data`,`type`,`timestamp` FROM %s WHERE `key` = %s", sqlTableSharedSessions,
sqlPlaceholders[0])
return fmt.Sprintf("SELECT `key`,`data`,`type`,`timestamp` FROM %s WHERE `key` = %s AND `type` = %s",
sqlTableSharedSessions, sqlPlaceholders[0], sqlPlaceholders[1])
}
return fmt.Sprintf(`SELECT key,data,type,timestamp FROM %s WHERE key = %s`, sqlTableSharedSessions,
sqlPlaceholders[0])
return fmt.Sprintf(`SELECT key,data,type,timestamp FROM %s WHERE key = %s AND type = %s`,
sqlTableSharedSessions, sqlPlaceholders[0], sqlPlaceholders[1])
}
func getCleanupSessionsQuery() string {
@ -650,6 +652,14 @@ func getUpdateQuotaQuery(reset bool) string {
WHERE username = %s`, sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3])
}
func getAdminSignatureQuery() string {
return fmt.Sprintf(`SELECT updated_at FROM %s WHERE username = %s`, sqlTableAdmins, sqlPlaceholders[0])
}
func getUserSignatureQuery() string {
return fmt.Sprintf(`SELECT updated_at FROM %s WHERE username = %s`, sqlTableUsers, sqlPlaceholders[0])
}
func getSetUpdateAtQuery() string {
return fmt.Sprintf(`UPDATE %s SET updated_at = %s WHERE username = %s`, sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1])
}
@ -757,10 +767,10 @@ func getClearUserGroupMappingQuery() string {
}
func getAddUserGroupMappingQuery() string {
return fmt.Sprintf(`INSERT INTO %s (user_id,group_id,group_type) VALUES ((SELECT id FROM %s WHERE username = %s),
(SELECT id FROM %s WHERE name = %s),%s)`,
return fmt.Sprintf(`INSERT INTO %s (user_id,group_id,group_type,sort_order) VALUES ((SELECT id FROM %s WHERE username = %s),
(SELECT id FROM %s WHERE name = %s),%s,%s)`,
sqlTableUsersGroupsMapping, sqlTableUsers, sqlPlaceholders[0], getSQLQuotedName(sqlTableGroups),
sqlPlaceholders[1], sqlPlaceholders[2])
sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3])
}
func getClearAdminGroupMappingQuery() string {
@ -769,10 +779,10 @@ func getClearAdminGroupMappingQuery() string {
}
func getAddAdminGroupMappingQuery() string {
return fmt.Sprintf(`INSERT INTO %s (admin_id,group_id,options) VALUES ((SELECT id FROM %s WHERE username = %s),
(SELECT id FROM %s WHERE name = %s),%s)`,
return fmt.Sprintf(`INSERT INTO %s (admin_id,group_id,options,sort_order) VALUES ((SELECT id FROM %s WHERE username = %s),
(SELECT id FROM %s WHERE name = %s),%s,%s)`,
sqlTableAdminsGroupsMapping, sqlTableAdmins, sqlPlaceholders[0], getSQLQuotedName(sqlTableGroups),
sqlPlaceholders[1], sqlPlaceholders[2])
sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3])
}
func getClearGroupFolderMappingQuery() string {
@ -781,10 +791,10 @@ func getClearGroupFolderMappingQuery() string {
}
func getAddGroupFolderMappingQuery() string {
return fmt.Sprintf(`INSERT INTO %s (virtual_path,quota_size,quota_files,folder_id,group_id)
VALUES (%s,%s,%s,(SELECT id FROM %s WHERE name = %s),(SELECT id FROM %s WHERE name = %s))`,
return fmt.Sprintf(`INSERT INTO %s (virtual_path,quota_size,quota_files,folder_id,group_id,sort_order)
VALUES (%s,%s,%s,(SELECT id FROM %s WHERE name = %s),(SELECT id FROM %s WHERE name = %s),%s)`,
sqlTableGroupsFoldersMapping, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlTableFolders,
sqlPlaceholders[3], getSQLQuotedName(sqlTableGroups), sqlPlaceholders[4])
sqlPlaceholders[3], getSQLQuotedName(sqlTableGroups), sqlPlaceholders[4], sqlPlaceholders[5])
}
func getClearUserFolderMappingQuery() string {
@ -793,10 +803,10 @@ func getClearUserFolderMappingQuery() string {
}
func getAddUserFolderMappingQuery() string {
return fmt.Sprintf(`INSERT INTO %s (virtual_path,quota_size,quota_files,folder_id,user_id)
VALUES (%s,%s,%s,(SELECT id FROM %s WHERE name = %s),(SELECT id FROM %s WHERE username = %s))`,
return fmt.Sprintf(`INSERT INTO %s (virtual_path,quota_size,quota_files,folder_id,user_id,sort_order)
VALUES (%s,%s,%s,(SELECT id FROM %s WHERE name = %s),(SELECT id FROM %s WHERE username = %s),%s)`,
sqlTableUsersFoldersMapping, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlTableFolders,
sqlPlaceholders[3], sqlTableUsers, sqlPlaceholders[4])
sqlPlaceholders[3], sqlTableUsers, sqlPlaceholders[4], sqlPlaceholders[5])
}
func getFoldersQuery(order string, minimal bool) string {
@ -838,7 +848,7 @@ func getRelatedGroupsForUsersQuery(users []User) string {
sb.WriteString(")")
}
return fmt.Sprintf(`SELECT g.name,ug.group_type,ug.user_id FROM %s g INNER JOIN %s ug ON g.id = ug.group_id WHERE
ug.user_id IN %s ORDER BY g.name`, getSQLQuotedName(sqlTableGroups), sqlTableUsersGroupsMapping, sb.String())
ug.user_id IN %s ORDER BY ug.sort_order`, getSQLQuotedName(sqlTableGroups), sqlTableUsersGroupsMapping, sb.String())
}
func getRelatedGroupsForAdminsQuery(admins []Admin) string {
@ -855,7 +865,7 @@ func getRelatedGroupsForAdminsQuery(admins []Admin) string {
sb.WriteString(")")
}
return fmt.Sprintf(`SELECT g.name,ag.options,ag.admin_id FROM %s g INNER JOIN %s ag ON g.id = ag.group_id WHERE
ag.admin_id IN %s ORDER BY g.name`, getSQLQuotedName(sqlTableGroups), sqlTableAdminsGroupsMapping, sb.String())
ag.admin_id IN %s ORDER BY ag.sort_order`, getSQLQuotedName(sqlTableGroups), sqlTableAdminsGroupsMapping, sb.String())
}
func getRelatedFoldersForUsersQuery(users []User) string {
@ -873,7 +883,7 @@ func getRelatedFoldersForUsersQuery(users []User) string {
}
return fmt.Sprintf(`SELECT f.id,f.name,f.path,f.used_quota_size,f.used_quota_files,f.last_quota_update,fm.virtual_path,
fm.quota_size,fm.quota_files,fm.user_id,f.filesystem,f.description FROM %s f INNER JOIN %s fm ON f.id = fm.folder_id WHERE
fm.user_id IN %s ORDER BY f.name`, sqlTableFolders, sqlTableUsersFoldersMapping, sb.String())
fm.user_id IN %s ORDER BY fm.sort_order`, sqlTableFolders, sqlTableUsersFoldersMapping, sb.String())
}
func getRelatedUsersForFoldersQuery(folders []vfs.BaseVirtualFolder) string {
@ -960,7 +970,7 @@ func getRelatedFoldersForGroupsQuery(groups []Group) string {
}
return fmt.Sprintf(`SELECT f.id,f.name,f.path,f.used_quota_size,f.used_quota_files,f.last_quota_update,fm.virtual_path,
fm.quota_size,fm.quota_files,fm.group_id,f.filesystem,f.description FROM %s f INNER JOIN %s fm ON f.id = fm.folder_id WHERE
fm.group_id IN %s ORDER BY f.name`, sqlTableFolders, sqlTableGroupsFoldersMapping, sb.String())
fm.group_id IN %s ORDER BY fm.sort_order`, sqlTableFolders, sqlTableGroupsFoldersMapping, sb.String())
}
func getActiveTransfersQuery() string {

View file

@ -12,8 +12,7 @@
// 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/>.
//go:build unixcrypt
// +build unixcrypt
//go:build unixcrypt && cgo
package dataprovider

View file

@ -12,8 +12,7 @@
// 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/>.
//go:build !unixcrypt
// +build !unixcrypt
//go:build !unixcrypt || !cgo
package dataprovider

View file

@ -22,6 +22,8 @@ import (
"net"
"os"
"path"
"path/filepath"
"slices"
"strconv"
"strings"
"time"
@ -123,6 +125,8 @@ type UserFilters struct {
sdk.BaseUserFilters
// User must change password from WebClient/REST API at next login.
RequirePasswordChange bool `json:"require_password_change,omitempty"`
// AdditionalEmails defines additional email addresses
AdditionalEmails []string `json:"additional_emails,omitempty"`
// Time-based one time passwords configuration
TOTPConfig UserTOTPConfig `json:"totp_config,omitempty"`
// Recovery codes to use if the user loses access to their second factor auth device.
@ -342,7 +346,11 @@ func (u *User) isTimeBasedAccessAllowed(when time.Time) bool {
if when.IsZero() {
when = time.Now()
}
when = when.UTC()
if UseLocalTime() {
when = when.Local()
} else {
when = when.UTC()
}
weekDay := when.Weekday()
hhMM := when.Format("15:04")
for _, p := range u.Filters.AccessTime {
@ -399,6 +407,15 @@ func (u *User) CheckMaxShareExpiration(expiresAt time.Time) error {
return nil
}
// GetEmailAddresses returns all the email addresses.
func (u *User) GetEmailAddresses() []string {
var res []string
if u.Email != "" {
res = append(res, u.Email)
}
return slices.Concat(res, u.Filters.AdditionalEmails)
}
// GetSubDirPermissions returns permissions for sub directories
func (u *User) GetSubDirPermissions() []sdk.DirectoryPermissions {
var result []sdk.DirectoryPermissions
@ -840,20 +857,20 @@ func (u *User) HasPermissionsInside(virtualPath string) bool {
// HasPerm returns true if the user has the given permission or any permission
func (u *User) HasPerm(permission, path string) bool {
perms := u.GetPermissionsForPath(path)
if util.Contains(perms, PermAny) {
if slices.Contains(perms, PermAny) {
return true
}
return util.Contains(perms, permission)
return slices.Contains(perms, permission)
}
// HasAnyPerm returns true if the user has at least one of the given permissions
func (u *User) HasAnyPerm(permissions []string, path string) bool {
perms := u.GetPermissionsForPath(path)
if util.Contains(perms, PermAny) {
if slices.Contains(perms, PermAny) {
return true
}
for _, permission := range permissions {
if util.Contains(perms, permission) {
if slices.Contains(perms, permission) {
return true
}
}
@ -863,11 +880,11 @@ func (u *User) HasAnyPerm(permissions []string, path string) bool {
// HasPerms returns true if the user has all the given permissions
func (u *User) HasPerms(permissions []string, path string) bool {
perms := u.GetPermissionsForPath(path)
if util.Contains(perms, PermAny) {
if slices.Contains(perms, PermAny) {
return true
}
for _, permission := range permissions {
if !util.Contains(perms, permission) {
if !slices.Contains(perms, permission) {
return false
}
}
@ -927,11 +944,11 @@ func (u *User) IsLoginMethodAllowed(loginMethod, protocol string) bool {
if len(u.Filters.DeniedLoginMethods) == 0 {
return true
}
if util.Contains(u.Filters.DeniedLoginMethods, loginMethod) {
if slices.Contains(u.Filters.DeniedLoginMethods, loginMethod) {
return false
}
if protocol == protocolSSH && loginMethod == LoginMethodPassword {
if util.Contains(u.Filters.DeniedLoginMethods, SSHLoginMethodPassword) {
if slices.Contains(u.Filters.DeniedLoginMethods, SSHLoginMethodPassword) {
return false
}
}
@ -965,10 +982,10 @@ func (u *User) IsPartialAuth() bool {
method == SSHLoginMethodPassword {
continue
}
if method == LoginMethodPassword && util.Contains(u.Filters.DeniedLoginMethods, SSHLoginMethodPassword) {
if method == LoginMethodPassword && slices.Contains(u.Filters.DeniedLoginMethods, SSHLoginMethodPassword) {
continue
}
if !util.Contains(SSHMultiStepsLoginMethods, method) {
if !slices.Contains(SSHMultiStepsLoginMethods, method) {
return false
}
}
@ -982,7 +999,7 @@ func (u *User) GetAllowedLoginMethods() []string {
if method == SSHLoginMethodPassword {
continue
}
if !util.Contains(u.Filters.DeniedLoginMethods, method) {
if !slices.Contains(u.Filters.DeniedLoginMethods, method) {
allowedMethods = append(allowedMethods, method)
}
}
@ -1052,7 +1069,7 @@ func (u *User) IsFileAllowed(virtualPath string) (bool, int) {
// CanManageMFA returns true if the user can add a multi-factor authentication configuration
func (u *User) CanManageMFA() bool {
if util.Contains(u.Filters.WebClient, sdk.WebClientMFADisabled) {
if slices.Contains(u.Filters.WebClient, sdk.WebClientMFADisabled) {
return false
}
return len(mfa.GetAvailableTOTPConfigs()) > 0
@ -1073,39 +1090,39 @@ func (u *User) skipExternalAuth() bool {
// CanManageShares returns true if the user can add, update and list shares
func (u *User) CanManageShares() bool {
return !util.Contains(u.Filters.WebClient, sdk.WebClientSharesDisabled)
return !slices.Contains(u.Filters.WebClient, sdk.WebClientSharesDisabled)
}
// CanResetPassword returns true if this user is allowed to reset its password
func (u *User) CanResetPassword() bool {
return !util.Contains(u.Filters.WebClient, sdk.WebClientPasswordResetDisabled)
return !slices.Contains(u.Filters.WebClient, sdk.WebClientPasswordResetDisabled)
}
// CanChangePassword returns true if this user is allowed to change its password
func (u *User) CanChangePassword() bool {
return !util.Contains(u.Filters.WebClient, sdk.WebClientPasswordChangeDisabled)
return !slices.Contains(u.Filters.WebClient, sdk.WebClientPasswordChangeDisabled)
}
// CanChangeAPIKeyAuth returns true if this user is allowed to enable/disable API key authentication
func (u *User) CanChangeAPIKeyAuth() bool {
return !util.Contains(u.Filters.WebClient, sdk.WebClientAPIKeyAuthChangeDisabled)
return !slices.Contains(u.Filters.WebClient, sdk.WebClientAPIKeyAuthChangeDisabled)
}
// CanChangeInfo returns true if this user is allowed to change its info such as email and description
func (u *User) CanChangeInfo() bool {
return !util.Contains(u.Filters.WebClient, sdk.WebClientInfoChangeDisabled)
return !slices.Contains(u.Filters.WebClient, sdk.WebClientInfoChangeDisabled)
}
// CanManagePublicKeys returns true if this user is allowed to manage public keys
// from the WebClient. Used in WebClient UI
func (u *User) CanManagePublicKeys() bool {
return !util.Contains(u.Filters.WebClient, sdk.WebClientPubKeyChangeDisabled)
return !slices.Contains(u.Filters.WebClient, sdk.WebClientPubKeyChangeDisabled)
}
// CanManageTLSCerts returns true if this user is allowed to manage TLS certificates
// from the WebClient. Used in WebClient UI
func (u *User) CanManageTLSCerts() bool {
return !util.Contains(u.Filters.WebClient, sdk.WebClientTLSCertChangeDisabled)
return !slices.Contains(u.Filters.WebClient, sdk.WebClientTLSCertChangeDisabled)
}
// CanUpdateProfile returns true if the user is allowed to update the profile.
@ -1117,7 +1134,7 @@ func (u *User) CanUpdateProfile() bool {
// CanAddFilesFromWeb returns true if the client can add files from the web UI.
// The specified target is the directory where the files must be uploaded
func (u *User) CanAddFilesFromWeb(target string) bool {
if util.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
if slices.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
return false
}
return u.HasPerm(PermUpload, target) || u.HasPerm(PermOverwrite, target)
@ -1126,7 +1143,7 @@ func (u *User) CanAddFilesFromWeb(target string) bool {
// CanAddDirsFromWeb returns true if the client can add directories from the web UI.
// The specified target is the directory where the new directory must be created
func (u *User) CanAddDirsFromWeb(target string) bool {
if util.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
if slices.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
return false
}
return u.HasPerm(PermCreateDirs, target)
@ -1135,7 +1152,7 @@ func (u *User) CanAddDirsFromWeb(target string) bool {
// CanRenameFromWeb returns true if the client can rename objects from the web UI.
// The specified src and dest are the source and target directories for the rename.
func (u *User) CanRenameFromWeb(src, dest string) bool {
if util.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
if slices.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
return false
}
return u.HasAnyPerm(permsRenameAny, src) && u.HasAnyPerm(permsRenameAny, dest)
@ -1144,7 +1161,7 @@ func (u *User) CanRenameFromWeb(src, dest string) bool {
// CanDeleteFromWeb returns true if the client can delete objects from the web UI.
// The specified target is the parent directory for the object to delete
func (u *User) CanDeleteFromWeb(target string) bool {
if util.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
if slices.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
return false
}
return u.HasAnyPerm(permsDeleteAny, target)
@ -1153,7 +1170,7 @@ func (u *User) CanDeleteFromWeb(target string) bool {
// CanCopyFromWeb returns true if the client can copy objects from the web UI.
// The specified src and dest are the source and target directories for the copy.
func (u *User) CanCopyFromWeb(src, dest string) bool {
if util.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
if slices.Contains(u.Filters.WebClient, sdk.WebClientWriteDisabled) {
return false
}
if !u.HasPerm(PermListItems, src) {
@ -1213,7 +1230,7 @@ func (u *User) MustSetSecondFactor() bool {
return true
}
for _, p := range u.Filters.TwoFactorAuthProtocols {
if !util.Contains(u.Filters.TOTPConfig.Protocols, p) {
if !slices.Contains(u.Filters.TOTPConfig.Protocols, p) {
return true
}
}
@ -1224,11 +1241,11 @@ func (u *User) MustSetSecondFactor() bool {
// MustSetSecondFactorForProtocol returns true if the user must set a second factor authentication
// for the specified protocol
func (u *User) MustSetSecondFactorForProtocol(protocol string) bool {
if util.Contains(u.Filters.TwoFactorAuthProtocols, protocol) {
if slices.Contains(u.Filters.TwoFactorAuthProtocols, protocol) {
if !u.Filters.TOTPConfig.Enabled {
return true
}
if !util.Contains(u.Filters.TOTPConfig.Protocols, protocol) {
if !slices.Contains(u.Filters.TOTPConfig.Protocols, protocol) {
return true
}
}
@ -1571,7 +1588,7 @@ func (u *User) mergeCryptFsConfig(group *Group) {
func (u *User) mergeWithPrimaryGroup(group *Group, replacer *strings.Replacer) {
if group.UserSettings.HomeDir != "" {
u.HomeDir = u.replacePlaceholder(group.UserSettings.HomeDir, replacer)
u.HomeDir = filepath.Clean(u.replacePlaceholder(group.UserSettings.HomeDir, replacer))
}
if group.UserSettings.FsConfig.Provider != 0 {
u.FsConfig = u.replaceFsConfigPlaceholders(group.UserSettings.FsConfig, replacer)
@ -1748,6 +1765,17 @@ func (u *User) hasRole(role string) bool {
return role == u.Role
}
func (u *User) applyNamingRules() {
u.Username = config.convertName(u.Username)
u.Role = config.convertName(u.Role)
for idx := range u.Groups {
u.Groups[idx].Name = config.convertName(u.Groups[idx].Name)
}
for idx := range u.VirtualFolders {
u.VirtualFolders[idx].Name = config.convertName(u.VirtualFolders[idx].Name)
}
}
func (u *User) getACopy() User {
u.SetEmptySecretsIfNil()
pubKeys := make([]string, len(u.PublicKeys))
@ -1779,6 +1807,8 @@ func (u *User) getACopy() User {
filters.TOTPConfig.Secret = u.Filters.TOTPConfig.Secret.Clone()
filters.TOTPConfig.Protocols = make([]string, len(u.Filters.TOTPConfig.Protocols))
copy(filters.TOTPConfig.Protocols, u.Filters.TOTPConfig.Protocols)
filters.AdditionalEmails = make([]string, len(u.Filters.AdditionalEmails))
copy(filters.AdditionalEmails, u.Filters.AdditionalEmails)
filters.RecoveryCodes = make([]RecoveryCode, 0, len(u.Filters.RecoveryCodes))
for _, code := range u.Filters.RecoveryCodes {
if code.Secret == nil {

View file

@ -134,6 +134,7 @@ func TestBasicFTPHandlingCryptFs(t *testing.T) {
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,
50*time.Millisecond)
assert.Equal(t, int32(0), common.Connections.GetTotalTransfers())
}
func TestBufferedCryptFs(t *testing.T) {
@ -179,6 +180,7 @@ func TestBufferedCryptFs(t *testing.T) {
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,
50*time.Millisecond)
assert.Equal(t, int32(0), common.Connections.GetTotalTransfers())
}
func TestZeroBytesTransfersCryptFs(t *testing.T) {

Some files were not shown because too many files have changed in this diff Show more