From de089e51fdd5f3cd59f60263acb81770c70bbf2b Mon Sep 17 00:00:00 2001 From: Nicola Murino Date: Wed, 21 Feb 2024 20:45:10 +0100 Subject: [PATCH] Web: allow to require password change and two-factor for admins Signed-off-by: Nicola Murino --- go.mod | 50 +-- go.sum | 100 +++--- internal/dataprovider/admin.go | 6 + internal/httpd/api_admin.go | 3 + internal/httpd/api_mfa.go | 19 +- internal/httpd/api_utils.go | 1 + internal/httpd/httpd_test.go | 207 +++++++++++++ internal/httpd/internal_test.go | 8 + internal/httpd/middleware.go | 39 ++- internal/httpd/server.go | 476 +++++++++++++++-------------- internal/httpd/webadmin.go | 18 +- internal/httpdtest/httpdtest.go | 6 + internal/util/i18n.go | 1 + openapi/openapi.yaml | 4 + static/locales/en/translation.json | 5 +- static/locales/it/translation.json | 5 +- templates/webadmin/admin.html | 24 ++ templates/webadmin/mfa.html | 11 + 18 files changed, 650 insertions(+), 333 deletions(-) diff --git a/go.mod b/go.mod index 701ef19f..a8a1508d 100644 --- a/go.mod +++ b/go.mod @@ -9,15 +9,15 @@ require ( 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.25.0 - github.com/aws/aws-sdk-go-v2/config v1.27.0 - github.com/aws/aws-sdk-go-v2/credentials v1.17.0 - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.0 - github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.2 - github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.20.1 - github.com/aws/aws-sdk-go-v2/service/s3 v1.50.1 - github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.27.1 - github.com/aws/aws-sdk-go-v2/service/sts v1.27.0 + github.com/aws/aws-sdk-go-v2 v1.25.1 + github.com/aws/aws-sdk-go-v2/config v1.27.2 + github.com/aws/aws-sdk-go-v2/credentials v1.17.2 + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.1 + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.4 + github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.20.3 + github.com/aws/aws-sdk-go-v2/service/s3 v1.50.3 + github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.27.3 + github.com/aws/aws-sdk-go-v2/service/sts v1.27.2 github.com/bmatcuk/doublestar/v4 v4.6.1 github.com/cockroachdb/cockroach-go/v2 v2.3.6 github.com/coreos/go-oidc/v3 v3.9.0 @@ -38,7 +38,7 @@ require ( github.com/hashicorp/go-retryablehttp v0.7.5 github.com/jackc/pgx/v5 v5.5.3 github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126 - github.com/klauspost/compress v1.17.6 + github.com/klauspost/compress v1.17.7 github.com/lestrrat-go/jwx/v2 v2.0.20 github.com/lithammer/shortuuid/v3 v3.0.7 github.com/mattn/go-sqlite3 v1.14.22 @@ -74,7 +74,7 @@ require ( golang.org/x/sys v0.17.0 golang.org/x/term v0.17.0 golang.org/x/time v0.5.0 - google.golang.org/api v0.165.0 + google.golang.org/api v0.166.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 ) @@ -85,18 +85,18 @@ require ( cloud.google.com/go/iam v1.1.6 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2 // indirect github.com/ajg/form v1.5.1 // indirect - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.0 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.0 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.0 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.1 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.1 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.1 // 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.0 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.0 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.0 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.0 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.0 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.19.0 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.22.0 // indirect - github.com/aws/smithy-go v1.20.0 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.1 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.19.2 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.22.2 // indirect + github.com/aws/smithy-go v1.20.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/boombuler/barcode v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.2.1 // indirect @@ -173,9 +173,9 @@ require ( golang.org/x/tools v0.18.0 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240213162025-012b6fc9bca9 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240213162025-012b6fc9bca9 // indirect + google.golang.org/genproto v0.0.0-20240221002015-b0ce06bbee7c // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240221002015-b0ce06bbee7c // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c // indirect google.golang.org/grpc v1.61.1 // indirect google.golang.org/protobuf v1.32.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/go.sum b/go.sum index abef5690..c29435f9 100644 --- a/go.sum +++ b/go.sum @@ -33,48 +33,48 @@ github.com/alexedwards/argon2id v1.0.0 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHc 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.25.0 h1:sv7+1JVJxOu/dD/sz/csHX7jFqmP001TIY7aytBWDSQ= -github.com/aws/aws-sdk-go-v2 v1.25.0/go.mod h1:G104G1Aho5WqF+SR3mDIobTABQzpYV0WxMsKxlMggOA= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.0 h1:2UO6/nT1lCZq1LqM67Oa4tdgP1CvL1sLSxvuD+VrOeE= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.0/go.mod h1:5zGj2eA85ClyedTDK+Whsu+w9yimnVIZvhvBKrDquM8= -github.com/aws/aws-sdk-go-v2/config v1.27.0 h1:J5sdGCAHuWKIXLeXiqr8II/adSvetkx0qdZwdbXXpb0= -github.com/aws/aws-sdk-go-v2/config v1.27.0/go.mod h1:cfh8v69nuSUohNFMbIISP2fhmblGmYEOKs5V53HiHnk= -github.com/aws/aws-sdk-go-v2/credentials v1.17.0 h1:lMW2x6sKBsiAJrpi1doOXqWFyEPoE886DTb1X0wb7So= -github.com/aws/aws-sdk-go-v2/credentials v1.17.0/go.mod h1:uT41FIH8cCIxOdUYIL0PYyHlL1NoneDuDSCwg5VE/5o= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.0 h1:xWCwjjvVz2ojYTP4kBKUuUh9ZrXfcAXpflhOUUeXg1k= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.0/go.mod h1:j3fACuqXg4oMTQOR2yY7m0NmJY0yBK4L4sLsRXq1Ins= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.2 h1:VEekE/fJWqAWYozxFQ07B+h8NdvTPAYhV13xIBenuO0= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.2/go.mod h1:8vozqAHmDNmoD4YbuDKIfpnLbByzngczL4My1RELLVo= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.0 h1:NPs/EqVO+ajwOoq56EfcGKa3L3ruWuazkIw1BqxwOPw= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.0/go.mod h1:D+duLy2ylgatV+yTlQ8JTuLfDD0BnFvnQRc+o6tbZ4M= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.0 h1:ks7KGMVUMoDzcxNWUlEdI+/lokMFD136EL6DWmUOV80= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.0/go.mod h1:hL6BWM/d/qz113fVitZjbXR0E+RCTU1+x+1Idyn5NgE= +github.com/aws/aws-sdk-go-v2 v1.25.1 h1:P7hU6A5qEdmajGwvae/zDkOq+ULLC9tQBTwqqiwFGpI= +github.com/aws/aws-sdk-go-v2 v1.25.1/go.mod h1:Evoc5AsmtveRt1komDwIsjHFyrP5tDuF1D1U+6z6pNo= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.1 h1:gTK2uhtAPtFcdRRJilZPx8uJLL2J85xK11nKtWL0wfU= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.1/go.mod h1:sxpLb+nZk7tIfCWChfd+h4QwHNUR57d8hA1cleTkjJo= +github.com/aws/aws-sdk-go-v2/config v1.27.2 h1:XnMKB9JRjfnxg9ZkUic4MiapnWJISWRo8HVM+7nx9qQ= +github.com/aws/aws-sdk-go-v2/config v1.27.2/go.mod h1:z/XIktFoVIKNEqX/811vx4eHetrC3tAkgJKL1ZY/KM4= +github.com/aws/aws-sdk-go-v2/credentials v1.17.2 h1:tCZXWtH0HiIEZ50NJ7/QEaXmuzEd36L+2JUiZkp2nsc= +github.com/aws/aws-sdk-go-v2/credentials v1.17.2/go.mod h1:7Zo+D6q4auSIo3p4EItuTKTk7J+RqjASISZqLvmUgpc= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.1 h1:lk1ZZFbdb24qpOwVC1AwYNrswUjAxeyey6kFBVANudQ= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.1/go.mod h1:/xJ6x1NehNGCX4tvGzzj2bq5TBOT/Yxq+qbL9Jpx2Vk= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.4 h1:yuhSpqtahkrC8kRCU5v4gEaTDy/ccTIPIkufIRF7YTk= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.4/go.mod h1:q3SxgP2WD9YRLCybtyse8EgO3vKKWVmxlTmBNeRXPyk= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.1 h1:evvi7FbTAoFxdP/mixmP7LIYzQWAmzBcwNB/es9XPNc= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.1/go.mod h1:rH61DT6FDdikhPghymripNUCsf+uVF4Cnk4c4DBKH64= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.1 h1:RAnaIrbxPtlXNVI/OIlh1sidTQ3e1qM6LRjs7N0bE0I= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.1/go.mod h1:nbgAGkH5lk0RZRMh6A4K/oG6Xj11eC/1CyDow+DUAFI= 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.0 h1:TkbRExyKSVHELwG9gz2+gql37jjec2R5vus9faTomwE= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.0/go.mod h1:T3/9xMKudHhnj8it5EqIrhvv11tVZqWYkKcot+BFStc= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.0 h1:a33HuFlO0KsveiP90IUJh8Xr/cx9US2PqkSroaLc+o8= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.0/go.mod h1:SxIkWpByiGbhbHYTo9CMTUnx2G4p4ZQMrDPcRRy//1c= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.0 h1:UiSyK6ent6OKpkMJN3+k5HZ4sk4UfchEaaW5wv7SblQ= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.0/go.mod h1:l7kzl8n8DXoRyFz5cIMG70HnPauWa649TUhgw8Rq6lo= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.0 h1:SHN/umDLTmFTmYfI+gkanz6da3vK8Kvj/5wkqnTHbuA= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.0/go.mod h1:l8gPU5RYGOFHJqWEpPMoRTP0VoaWQSkJdKo+hwWnnDA= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.0 h1:l5puwOHr7IxECuPMIuZG7UKOzAnF24v6t4l+Z5Moay4= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.0/go.mod h1:Oov79flWa/n7Ni+lQC3z+VM7PoRM47omRqbJU9B5Y7E= -github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.20.1 h1:eHNChn4Sp+g1hdz4rkx96n1l/LpJEQLDuFB0V+fA/yg= -github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.20.1/go.mod h1:9ev55pJx9xNX3UAOKzZmbmaTbwwuLTCemOJPsd7rUz8= -github.com/aws/aws-sdk-go-v2/service/s3 v1.50.1 h1:bjpWJEXch7moIt3PX2r5XpGROsletl7enqG1Q3Te1Dc= -github.com/aws/aws-sdk-go-v2/service/s3 v1.50.1/go.mod h1:1o/W6JFUuREj2ExoQ21vHJgO7wakvjhol91M9eknFgs= -github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.27.1 h1:ss/HbHbONu0uscM549++4YanT6MnjNN0BGhE5pZRfG4= -github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.27.1/go.mod h1:JsJDZFHwLGZu6dxhV9EV1gJrMnCeE4GEXubSZA59xdA= -github.com/aws/aws-sdk-go-v2/service/sso v1.19.0 h1:u6OkVDxtBPnxPkZ9/63ynEe+8kHbtS5IfaC4PzVxzWM= -github.com/aws/aws-sdk-go-v2/service/sso v1.19.0/go.mod h1:YqbU3RS/pkDVu+v+Nwxvn0i1WB0HkNWEePWbmODEbbs= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.22.0 h1:6DL0qu5+315wbsAEEmzK+P9leRwNbkp+lGjPC+CEvb8= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.22.0/go.mod h1:olUAyg+FaoFaL/zFaeQQONjOZ9HXoxgvI/c7mQTYz7M= -github.com/aws/aws-sdk-go-v2/service/sts v1.27.0 h1:cjTRjh700H36MQ8M0LnDn33W3JmwC77mdxIIyPWCdpM= -github.com/aws/aws-sdk-go-v2/service/sts v1.27.0/go.mod h1:nXfOBMWPokIbOY+Gi7a1psWMSvskUCemZzI+SMB7Akc= -github.com/aws/smithy-go v1.20.0 h1:6+kZsCXZwKxZS9RfISnPc4EXlHoyAkm2hPuM8X2BrrQ= -github.com/aws/smithy-go v1.20.0/go.mod h1:uo5RKksAl4PzhqaAbjd4rLgFoq5koTsQKYuGe7dklGc= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.1 h1:rtYJd3w6IWCTVS8vmMaiXjW198noh2PBm5CiXyJea9o= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.1/go.mod h1:zvXu+CTlib30LUy4LTNFc6HTZ/K6zCae5YIHTdX9wIo= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1 h1:EyBZibRTVAs6ECHZOw5/wlylS9OcTzwyjeQMudmREjE= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1/go.mod h1:JKpmtYhhPs7D97NL/ltqz7yCkERFW5dOlHyVl66ZYF8= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.1 h1:5Wxh862HkXL9CbQ83BIkWKLIgQapGeuh5zG2G9OZtQk= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.1/go.mod h1:V7GLA01pNUxMCYSQsibdVrqUrNIYIT/9lCOyR8ExNvQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.1 h1:cVP8mng1RjDyI3JN/AXFCn5FHNlsBaBH0/MBtG1bg0o= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.1/go.mod h1:C8sQjoyAsdfjC7hpy4+S6B92hnFzx0d0UAyHicaOTIE= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.1 h1:OYmmIcyw19f7x0qLBLQ3XsrCZSSyLhxd9GXng5evsN4= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.1/go.mod h1:s5rqdn74Vdg10k61Pwf4ZHEApOSD6CKRe6qpeHDq32I= +github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.20.3 h1:uOHZ8HCjUHrRUi+sezA1yCeJVwa4Yy91tZDrWn1sT8w= +github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.20.3/go.mod h1:TzgisXFoXgCssgP11SzC6KrvcyCErz5c3w++m3xFOfo= +github.com/aws/aws-sdk-go-v2/service/s3 v1.50.3 h1:Cv/HH7sLzEdJMYQi4MCNHxZeyubQNOOIdVc0VU0lo3Q= +github.com/aws/aws-sdk-go-v2/service/s3 v1.50.3/go.mod h1:lTW7O4iMAnO2o7H3XJTvqaWFZCH6zIPs+eP7RdG/yp0= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.27.3 h1:LP38dk6XSNKyWAr3ZNEVECBPjEnoP+/SGvOfX0tRy+U= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.27.3/go.mod h1:RA3ERghFSivbTf0Sbsxv/grUuLMcyAjm0F/PylJMmEs= +github.com/aws/aws-sdk-go-v2/service/sso v1.19.2 h1:pnj8llQoBAHD4UmbM8UM5GdfycFJKMhgPSeaOyRaZ34= +github.com/aws/aws-sdk-go-v2/service/sso v1.19.2/go.mod h1:x6/tCd1o/AOKQR+iYnjrzhJxD+w0xRN34asGPaSV7ew= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.22.2 h1:L4yhKxW6HbTSQ08OsvPJuaspaLE40qMgprgXUNFUiMg= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.22.2/go.mod h1:lZB123q0SVQ3dfIbEOcGzhQHrwVBcHVReNS9tm20oU4= +github.com/aws/aws-sdk-go-v2/service/sts v1.27.2 h1:Dr+7r/p20XpN+1U5tVNZfA2bLq0kQ9IjVBM0iAyMMLg= +github.com/aws/aws-sdk-go-v2/service/sts v1.27.2/go.mod h1:ozhhG9/NB5c9jcmhGq6tX9dpp21LYdmRWRQVppASim4= +github.com/aws/smithy-go v1.20.1 h1:4SZlSlMr36UEqC7XOyRVb27XMeZubNcBNN+9IgEPIQw= +github.com/aws/smithy-go v1.20.1/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= 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= @@ -252,8 +252,8 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y 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.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI= -github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= +github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= 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= @@ -529,8 +529,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/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.165.0 h1:zd5d4JIIIaYYsfVy1HzoXYZ9rWCSBxxAglbczzo7Bgc= -google.golang.org/api v0.165.0/go.mod h1:2OatzO7ZDQsoS7IFf3rvsE17/TldiU3F/zxFHeqUB5o= +google.golang.org/api v0.166.0 h1:6m4NUwrZYhAaVIHZWxaKjw1L1vNAjtMwORmKRyEEo24= +google.golang.org/api v0.166.0/go.mod h1:4FcBc686KFi7QI/U51/2GKKevfZMpM17sCdibqe/bSA= 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/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= @@ -538,12 +538,12 @@ google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJ 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-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y= -google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:mqHbVIp48Muh7Ywss/AD6I5kNVKZMmAa/QEW58Gxp2s= -google.golang.org/genproto/googleapis/api v0.0.0-20240213162025-012b6fc9bca9 h1:4++qSzdWBUy9/2x8L5KZgwZw+mjJZ2yDSCGMVM0YzRs= -google.golang.org/genproto/googleapis/api v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:PVreiBMirk8ypES6aw9d4p6iiBNSIfZEBqr3UGoAi2E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240213162025-012b6fc9bca9 h1:hZB7eLIaYlW9qXRfCq/qDaPdbeY3757uARz5Vvfv+cY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:YUWgXUFRPfoYK1IHMuxH5K6nPEXSCzIMljnQ59lLRCk= +google.golang.org/genproto v0.0.0-20240221002015-b0ce06bbee7c h1:Zmyn5CV/jxzKnF+3d+xzbomACPwLQqVpLTpyXN5uTaQ= +google.golang.org/genproto v0.0.0-20240221002015-b0ce06bbee7c/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo= +google.golang.org/genproto/googleapis/api v0.0.0-20240221002015-b0ce06bbee7c h1:9g7erC9qu44ks7UK4gDNlnk4kOxZG707xKm4jVniy6o= +google.golang.org/genproto/googleapis/api v0.0.0-20240221002015-b0ce06bbee7c/go.mod h1:5iCWqnniDlqZHrd3neWVTOwvh/v6s3232omMecelax8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c h1:NUsgEN92SQQqzfA+YtqYNqYmB3DMMYLlIwUZAQFVFbo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY= 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= diff --git a/internal/dataprovider/admin.go b/internal/dataprovider/admin.go index 2d994987..dac80ea4 100644 --- a/internal/dataprovider/admin.go +++ b/internal/dataprovider/admin.go @@ -199,6 +199,10 @@ type AdminFilters struct { AllowList []string `json:"allow_list,omitempty"` // API key auth allows to impersonate this administrator with an API key AllowAPIKeyAuth bool `json:"allow_api_key_auth,omitempty"` + // A password change is required at the next login + RequirePasswordChange bool `json:"require_password_change,omitempty"` + // Require two factor authentication + RequireTwoFactor bool `json:"require_two_factor"` // Time-based one time passwords configuration TOTPConfig AdminTOTPConfig `json:"totp_config,omitempty"` // Recovery codes to use if the user loses access to their second factor auth device. @@ -615,6 +619,8 @@ func (a *Admin) getACopy() Admin { filters := AdminFilters{} filters.AllowList = make([]string, len(a.Filters.AllowList)) filters.AllowAPIKeyAuth = a.Filters.AllowAPIKeyAuth + filters.RequirePasswordChange = a.Filters.RequirePasswordChange + filters.RequireTwoFactor = a.Filters.RequireTwoFactor filters.TOTPConfig.Enabled = a.Filters.TOTPConfig.Enabled filters.TOTPConfig.ConfigName = a.Filters.TOTPConfig.ConfigName filters.TOTPConfig.Secret = a.Filters.TOTPConfig.Secret.Clone() diff --git a/internal/httpd/api_admin.go b/internal/httpd/api_admin.go index b26e2512..b958478a 100644 --- a/internal/httpd/api_admin.go +++ b/internal/httpd/api_admin.go @@ -150,6 +150,8 @@ func updateAdmin(w http.ResponseWriter, r *http.Request) { sendAPIResponse(w, r, errors.New("you cannot add/change your role"), "", http.StatusBadRequest) return } + updatedAdmin.Filters.RequirePasswordChange = admin.Filters.RequirePasswordChange + updatedAdmin.Filters.RequireTwoFactor = admin.Filters.RequireTwoFactor } updatedAdmin.ID = admin.ID updatedAdmin.Username = admin.Username @@ -317,6 +319,7 @@ func doChangeAdminPassword(r *http.Request, currentPassword, newPassword, confir } admin.Password = newPassword + admin.Filters.RequirePasswordChange = false return dataprovider.UpdateAdmin(&admin, dataprovider.ActionExecutorSelf, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role) } diff --git a/internal/httpd/api_mfa.go b/internal/httpd/api_mfa.go index fbd5a361..0b7e282e 100644 --- a/internal/httpd/api_mfa.go +++ b/internal/httpd/api_mfa.go @@ -122,23 +122,25 @@ func saveTOTPConfig(w http.ResponseWriter, r *http.Request) { code := getNewRecoveryCode() recoveryCodes = append(recoveryCodes, dataprovider.RecoveryCode{Secret: kms.NewPlainSecret(code)}) } + baseURL := webBaseClientPath if claims.hasUserAudience() { if err := saveUserTOTPConfig(claims.Username, r, recoveryCodes); err != nil { sendAPIResponse(w, r, err, "", getRespStatus(err)) return } - if claims.MustSetTwoFactorAuth { - // force logout - defer func() { - c := jwtTokenClaims{} - c.removeCookie(w, r, webBaseClientPath) - }() - } } else { if err := saveAdminTOTPConfig(claims.Username, r, recoveryCodes); err != nil { sendAPIResponse(w, r, err, "", getRespStatus(err)) return } + baseURL = webBasePath + } + if claims.MustSetTwoFactorAuth { + // force logout + defer func() { + c := jwtTokenClaims{} + c.removeCookie(w, r, baseURL) + }() } sendAPIResponse(w, r, nil, "TOTP configuration saved", http.StatusOK) @@ -303,6 +305,9 @@ func saveAdminTOTPConfig(username string, r *http.Request, recoveryCodes []datap if err != nil { return util.NewValidationError(fmt.Sprintf("unable to decode JSON body: %v", err)) } + if !admin.Filters.TOTPConfig.Enabled && admin.Filters.RequireTwoFactor { + return util.NewValidationError("two-factor authentication must be enabled") + } if admin.Filters.TOTPConfig.Enabled { if admin.CountUnusedRecoveryCodes() < 5 && admin.Filters.TOTPConfig.Enabled { admin.Filters.RecoveryCodes = recoveryCodes diff --git a/internal/httpd/api_utils.go b/internal/httpd/api_utils.go index d3e74f94..d198536b 100644 --- a/internal/httpd/api_utils.go +++ b/internal/httpd/api_utils.go @@ -830,6 +830,7 @@ func handleResetPassword(r *http.Request, code, newPassword, confirmPassword str return &admin, &user, util.NewValidationError("unable to associate the confirmation code with an existing admin") } admin.Password = newPassword + admin.Filters.RequirePasswordChange = false err = dataprovider.UpdateAdmin(&admin, dataprovider.ActionExecutorSelf, ipAddr, admin.Role) if err != nil { return &admin, &user, util.NewGenericError(fmt.Sprintf("unable to set the new password: %v", err)) diff --git a/internal/httpd/httpd_test.go b/internal/httpd/httpd_test.go index 894ef21a..500732bd 100644 --- a/internal/httpd/httpd_test.go +++ b/internal/httpd/httpd_test.go @@ -3501,6 +3501,194 @@ func TestTwoFactorRequirementsGroupLevel(t *testing.T) { assert.NoError(t, err) } +func TestAdminMustChangePasswordRequirement(t *testing.T) { + admin := getTestAdmin() + admin.Username = altAdminUsername + admin.Password = altAdminPassword + admin.Filters.RequirePasswordChange = true + admin, _, err := httpdtest.AddAdmin(admin, http.StatusCreated) + assert.NoError(t, err) + + token, _, err := httpdtest.GetToken(altAdminUsername, altAdminPassword) + assert.NoError(t, err) + httpdtest.SetJWTToken(token) + + _, _, err = httpdtest.GetUsers(0, 0, http.StatusForbidden) + assert.NoError(t, err) + _, _, err = httpdtest.GetStatus(http.StatusForbidden) + assert.NoError(t, err) + + _, err = httpdtest.ChangeAdminPassword(altAdminPassword, defaultTokenAuthPass, http.StatusOK) + assert.NoError(t, err) + + httpdtest.SetJWTToken("") + + admin, _, err = httpdtest.GetAdminByUsername(admin.Username, http.StatusOK) + assert.NoError(t, err) + assert.False(t, admin.Filters.RequirePasswordChange) + + // get a new token + token, _, err = httpdtest.GetToken(altAdminUsername, defaultTokenAuthPass) + assert.NoError(t, err) + httpdtest.SetJWTToken(token) + + _, _, err = httpdtest.GetUsers(0, 0, http.StatusOK) + assert.NoError(t, err) + + desc := xid.New().String() + admin.Filters.RequirePasswordChange = true + admin.Filters.RequireTwoFactor = true + admin.Description = desc + _, _, err = httpdtest.UpdateAdmin(admin, http.StatusOK) + if assert.Error(t, err) { + assert.ErrorContains(t, err, "require password change mismatch") + } + admin, _, err = httpdtest.GetAdminByUsername(admin.Username, http.StatusOK) + assert.NoError(t, err) + assert.False(t, admin.Filters.RequirePasswordChange) + assert.False(t, admin.Filters.RequireTwoFactor) + assert.Equal(t, desc, admin.Description) + + httpdtest.SetJWTToken("") + + admin.Filters.RequirePasswordChange = true + _, _, err = httpdtest.UpdateAdmin(admin, http.StatusOK) + assert.NoError(t, err) + // test the same for the WebAdmin + webToken, err := getJWTWebTokenFromTestServer(altAdminUsername, defaultTokenAuthPass) + assert.NoError(t, err) + req, err := http.NewRequest(http.MethodGet, webUsersPath, nil) + assert.NoError(t, err) + req.RequestURI = webUsersPath + setJWTCookieForReq(req, webToken) + rr := executeRequest(req) + checkResponseCode(t, http.StatusForbidden, rr) + + csrfToken, err := getCSRFToken(httpBaseURL + webLoginPath) + assert.NoError(t, err) + form := make(url.Values) + form.Set(csrfFormToken, csrfToken) + form.Set("current_password", defaultTokenAuthPass) + form.Set("new_password1", altAdminPassword) + form.Set("new_password2", altAdminPassword) + req, err = http.NewRequest(http.MethodPost, webChangeAdminPwdPath, bytes.NewBuffer([]byte(form.Encode()))) + assert.NoError(t, err) + req.RemoteAddr = defaultRemoteAddr + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + setJWTCookieForReq(req, webToken) + rr = executeRequest(req) + checkResponseCode(t, http.StatusFound, rr) + assert.Equal(t, webLoginPath, rr.Header().Get("Location")) + + admin, _, err = httpdtest.GetAdminByUsername(admin.Username, http.StatusOK) + assert.NoError(t, err) + assert.False(t, admin.Filters.RequirePasswordChange) + + webToken, err = getJWTWebTokenFromTestServer(altAdminUsername, altAdminPassword) + assert.NoError(t, err) + req, err = http.NewRequest(http.MethodGet, webUsersPath, nil) + assert.NoError(t, err) + req.RequestURI = webUsersPath + setJWTCookieForReq(req, webToken) + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr) + + _, err = httpdtest.RemoveAdmin(admin, http.StatusOK) + assert.NoError(t, err) +} + +func TestAdminTwoFactorRequirements(t *testing.T) { + admin := getTestAdmin() + admin.Username = altAdminUsername + admin.Password = altAdminPassword + admin.Filters.RequireTwoFactor = true + admin, _, err := httpdtest.AddAdmin(admin, http.StatusCreated) + assert.NoError(t, err) + + token, err := getJWTAPITokenFromTestServer(altAdminUsername, altAdminPassword) + assert.NoError(t, err) + req, err := http.NewRequest(http.MethodGet, serverStatusPath, nil) + assert.NoError(t, err) + setBearerForReq(req, token) + rr := executeRequest(req) + checkResponseCode(t, http.StatusForbidden, rr) + assert.Contains(t, rr.Body.String(), "Two-factor authentication requirements not met") + + webToken, err := getJWTWebTokenFromTestServer(altAdminUsername, altAdminPassword) + assert.NoError(t, err) + req, err = http.NewRequest(http.MethodGet, webFoldersPath, nil) + assert.NoError(t, err) + req.RequestURI = webFoldersPath + setJWTCookieForReq(req, webToken) + rr = executeRequest(req) + checkResponseCode(t, http.StatusForbidden, rr) + assert.Contains(t, rr.Body.String(), util.I18nError2FARequiredGeneric) + // add TOTP config + configName, key, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], altAdminUsername) + assert.NoError(t, err) + adminTOTPConfig := dataprovider.AdminTOTPConfig{ + Enabled: true, + ConfigName: configName, + Secret: kms.NewPlainSecret(key.Secret()), + } + asJSON, err := json.Marshal(adminTOTPConfig) + assert.NoError(t, err) + req, err = http.NewRequest(http.MethodPost, adminTOTPSavePath, bytes.NewBuffer(asJSON)) + assert.NoError(t, err) + setBearerForReq(req, token) + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr) + admin, _, err = httpdtest.GetAdminByUsername(altAdminUsername, http.StatusOK) + assert.NoError(t, err) + assert.True(t, admin.Filters.TOTPConfig.Enabled) + + passcode, err := generateTOTPPasscode(key.Secret()) + assert.NoError(t, err) + req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("%v%v", httpBaseURL, tokenPath), nil) + assert.NoError(t, err) + req.Header.Set("X-SFTPGO-OTP", passcode) + req.SetBasicAuth(altAdminUsername, altAdminPassword) + resp, err := httpclient.GetHTTPClient().Do(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + responseHolder := make(map[string]any) + err = render.DecodeJSON(resp.Body, &responseHolder) + assert.NoError(t, err) + token = responseHolder["access_token"].(string) + assert.NotEmpty(t, token) + err = resp.Body.Close() + assert.NoError(t, err) + + req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("%v%v", httpBaseURL, serverStatusPath), nil) + assert.NoError(t, err) + setBearerForReq(req, token) + resp, err = httpclient.GetHTTPClient().Do(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + err = resp.Body.Close() + assert.NoError(t, err) + // try to disable 2FA + disableReq := map[string]any{ + "enabled": false, + } + asJSON, err = json.Marshal(disableReq) + assert.NoError(t, err) + req, err = http.NewRequest(http.MethodPost, fmt.Sprintf("%v%v", httpBaseURL, adminTOTPSavePath), bytes.NewBuffer(asJSON)) + assert.NoError(t, err) + setBearerForReq(req, token) + resp, err = httpclient.GetHTTPClient().Do(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + bodyResp, err := io.ReadAll(resp.Body) + assert.NoError(t, err) + assert.Contains(t, string(bodyResp), "two-factor authentication must be enabled") + err = resp.Body.Close() + assert.NoError(t, err) + + _, err = httpdtest.RemoveAdmin(admin, http.StatusOK) + assert.NoError(t, err) +} + func TestLoginUserAPITOTP(t *testing.T) { user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated) assert.NoError(t, err) @@ -19835,6 +20023,20 @@ func TestAdminUpdateSelfMock(t *testing.T) { assert.Contains(t, rr.Body.String(), util.I18nErrorAdminSelfDisable) form.Set("status", "1") + form.Set("require_two_factor", "1") + form.Set("require_password_change", "1") + req, _ = http.NewRequest(http.MethodPost, path.Join(webAdminPath, defaultTokenAuthUser), bytes.NewBuffer([]byte(form.Encode()))) + req.RemoteAddr = defaultRemoteAddr + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + setJWTCookieForReq(req, token) + rr = executeRequest(req) + checkResponseCode(t, http.StatusSeeOther, rr) + + admin, _, err = httpdtest.GetAdminByUsername(defaultTokenAuthUser, http.StatusOK) + assert.NoError(t, err) + assert.False(t, admin.Filters.RequirePasswordChange) + assert.False(t, admin.Filters.RequireTwoFactor) + form.Set("role", "my role") req, _ = http.NewRequest(http.MethodPost, path.Join(webAdminPath, defaultTokenAuthUser), bytes.NewBuffer([]byte(form.Encode()))) req.RemoteAddr = defaultRemoteAddr @@ -24375,6 +24577,7 @@ func TestAdminForgotPassword(t *testing.T) { a := getTestAdmin() a.Username = altAdminUsername a.Password = altAdminPassword + a.Filters.RequirePasswordChange = true admin, _, err := httpdtest.AddAdmin(a, http.StatusCreated) assert.NoError(t, err) @@ -24514,6 +24717,10 @@ func TestAdminForgotPassword(t *testing.T) { rr = executeRequest(req) checkResponseCode(t, http.StatusNotFound, rr) + admin, _, err = httpdtest.GetAdminByUsername(admin.Username, http.StatusOK) + assert.NoError(t, err) + assert.False(t, admin.Filters.RequirePasswordChange) + _, err = httpdtest.RemoveAdmin(admin, http.StatusOK) assert.NoError(t, err) } diff --git a/internal/httpd/internal_test.go b/internal/httpd/internal_test.go index 5be25ca1..1dfb8975 100644 --- a/internal/httpd/internal_test.go +++ b/internal/httpd/internal_test.go @@ -1554,6 +1554,14 @@ func TestJWTTokenValidation(t *testing.T) { fn.ServeHTTP(rr, req.WithContext(ctx)) assert.Equal(t, http.StatusBadRequest, rr.Code) + fn = server.checkAuthRequirements(r) + rr = httptest.NewRecorder() + req, _ = http.NewRequest(http.MethodPost, webGroupsPath, nil) + req.RequestURI = webGroupsPath + ctx = jwtauth.NewContext(req.Context(), token, errTest) + fn.ServeHTTP(rr, req.WithContext(ctx)) + assert.Equal(t, http.StatusBadRequest, rr.Code) + rr = httptest.NewRecorder() req, _ = http.NewRequest(http.MethodPost, userSharesPath, nil) req.RequestURI = userSharesPath diff --git a/internal/httpd/middleware.go b/internal/httpd/middleware.go index 523b6c59..8c98f02b 100644 --- a/internal/httpd/middleware.go +++ b/internal/httpd/middleware.go @@ -223,7 +223,11 @@ func (s *httpdServer) checkAuthRequirements(next http.Handler) http.Handler { _, claims, err := jwtauth.FromContext(r.Context()) if err != nil { if isWebRequest(r) { - s.renderClientBadRequestPage(w, r, err) + if isWebClientRequest(r) { + s.renderClientBadRequestPage(w, r, err) + } else { + s.renderBadRequestPage(w, r, err) + } } else { sendAPIResponse(w, r, err, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) } @@ -234,16 +238,23 @@ func (s *httpdServer) checkAuthRequirements(next http.Handler) http.Handler { if tokenClaims.MustSetTwoFactorAuth || tokenClaims.MustChangePassword { var err error if tokenClaims.MustSetTwoFactorAuth { - protocols := strings.Join(tokenClaims.RequiredTwoFactorProtocols, ", ") - err = util.NewI18nError( - util.NewGenericError( - fmt.Sprintf("Two-factor authentication requirements not met, please configure two-factor authentication for the following protocols: %v", - protocols)), - util.I18nError2FARequired, - util.I18nErrorArgs(map[string]any{ - "val": protocols, - }), - ) + if len(tokenClaims.RequiredTwoFactorProtocols) > 0 { + protocols := strings.Join(tokenClaims.RequiredTwoFactorProtocols, ", ") + err = util.NewI18nError( + util.NewGenericError( + fmt.Sprintf("Two-factor authentication requirements not met, please configure two-factor authentication for the following protocols: %v", + protocols)), + util.I18nError2FARequired, + util.I18nErrorArgs(map[string]any{ + "val": protocols, + }), + ) + } else { + err = util.NewI18nError( + util.NewGenericError("Two-factor authentication requirements not met, please configure two-factor authentication"), + util.I18nError2FARequiredGeneric, + ) + } } else { err = util.NewI18nError( util.NewGenericError("Password change required. Please set a new password to continue to use your account"), @@ -251,7 +262,11 @@ func (s *httpdServer) checkAuthRequirements(next http.Handler) http.Handler { ) } if isWebRequest(r) { - s.renderClientForbiddenPage(w, r, err) + if isWebClientRequest(r) { + s.renderClientForbiddenPage(w, r, err) + } else { + s.renderForbiddenPage(w, r, err) + } } else { sendAPIResponse(w, r, err, "", http.StatusForbidden) } diff --git a/internal/httpd/server.go b/internal/httpd/server.go index 4a77b24b..4fddeb19 100644 --- a/internal/httpd/server.go +++ b/internal/httpd/server.go @@ -794,6 +794,8 @@ func (s *httpdServer) loginAdmin( Role: admin.Role, Signature: admin.GetSignature(), HideUserPageSections: admin.Filters.Preferences.HideUserPageSections, + MustSetTwoFactorAuth: admin.Filters.RequireTwoFactor && !admin.Filters.TOTPConfig.Enabled, + MustChangePassword: admin.Filters.RequirePasswordChange, } audience := tokenAudienceWebAdmin @@ -982,10 +984,12 @@ func (s *httpdServer) getToken(w http.ResponseWriter, r *http.Request) { func (s *httpdServer) generateAndSendToken(w http.ResponseWriter, r *http.Request, admin dataprovider.Admin, ip string) { c := jwtTokenClaims{ - Username: admin.Username, - Permissions: admin.Permissions, - Role: admin.Role, - Signature: admin.GetSignature(), + Username: admin.Username, + Permissions: admin.Permissions, + Role: admin.Role, + Signature: admin.GetSignature(), + MustSetTwoFactorAuth: admin.Filters.RequireTwoFactor && !admin.Filters.TOTPConfig.Enabled, + MustChangePassword: admin.Filters.RequirePasswordChange, } resp, err := c.createTokenResponse(s.tokenAuth, tokenAudienceAPI, ip) @@ -1318,7 +1322,7 @@ func (s *httpdServer) initializeRouter() { router.With(forbidAPIKeyAuthentication).Get(logoutPath, s.logout) router.With(forbidAPIKeyAuthentication).Get(adminProfilePath, getAdminProfile) - router.With(forbidAPIKeyAuthentication).Put(adminProfilePath, updateAdminProfile) + router.With(forbidAPIKeyAuthentication, s.checkAuthRequirements).Put(adminProfilePath, updateAdminProfile) router.With(forbidAPIKeyAuthentication).Put(adminPwdPath, changeAdminPassword) // admin TOTP APIs router.With(forbidAPIKeyAuthentication).Get(adminTOTPConfigsPath, getTOTPConfigs) @@ -1328,62 +1332,6 @@ func (s *httpdServer) initializeRouter() { router.With(forbidAPIKeyAuthentication).Get(admin2FARecoveryCodesPath, getRecoveryCodes) router.With(forbidAPIKeyAuthentication).Post(admin2FARecoveryCodesPath, generateRecoveryCodes) - router.With(s.checkPerm(dataprovider.PermAdminViewServerStatus)). - Get(serverStatusPath, func(w http.ResponseWriter, r *http.Request) { - r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) - render.JSON(w, r, getServicesStatus()) - }) - - router.With(s.checkPerm(dataprovider.PermAdminViewConnections)).Get(activeConnectionsPath, getActiveConnections) - router.With(s.checkPerm(dataprovider.PermAdminCloseConnections)). - Delete(activeConnectionsPath+"/{connectionID}", handleCloseConnection) - router.With(s.checkPerm(dataprovider.PermAdminQuotaScans)).Get(quotasBasePath+"/users/scans", getUsersQuotaScans) - router.With(s.checkPerm(dataprovider.PermAdminQuotaScans)).Post(quotasBasePath+"/users/{username}/scan", startUserQuotaScan) - router.With(s.checkPerm(dataprovider.PermAdminQuotaScans)).Get(quotasBasePath+"/folders/scans", getFoldersQuotaScans) - router.With(s.checkPerm(dataprovider.PermAdminQuotaScans)).Post(quotasBasePath+"/folders/{name}/scan", startFolderQuotaScan) - router.With(s.checkPerm(dataprovider.PermAdminViewUsers)).Get(userPath, getUsers) - router.With(s.checkPerm(dataprovider.PermAdminAddUsers)).Post(userPath, addUser) - router.With(s.checkPerm(dataprovider.PermAdminViewUsers)).Get(userPath+"/{username}", getUserByUsername) //nolint:goconst - router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Put(userPath+"/{username}", updateUser) - router.With(s.checkPerm(dataprovider.PermAdminDeleteUsers)).Delete(userPath+"/{username}", deleteUser) - router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Put(userPath+"/{username}/2fa/disable", disableUser2FA) - router.With(s.checkPerm(dataprovider.PermAdminManageFolders)).Get(folderPath, getFolders) - router.With(s.checkPerm(dataprovider.PermAdminManageFolders)).Get(folderPath+"/{name}", getFolderByName) //nolint:goconst - router.With(s.checkPerm(dataprovider.PermAdminManageFolders)).Post(folderPath, addFolder) - router.With(s.checkPerm(dataprovider.PermAdminManageFolders)).Put(folderPath+"/{name}", updateFolder) - router.With(s.checkPerm(dataprovider.PermAdminManageFolders)).Delete(folderPath+"/{name}", deleteFolder) - router.With(s.checkPerm(dataprovider.PermAdminManageGroups)).Get(groupPath, getGroups) - router.With(s.checkPerm(dataprovider.PermAdminManageGroups)).Get(groupPath+"/{name}", getGroupByName) - router.With(s.checkPerm(dataprovider.PermAdminManageGroups)).Post(groupPath, addGroup) - router.With(s.checkPerm(dataprovider.PermAdminManageGroups)).Put(groupPath+"/{name}", updateGroup) - router.With(s.checkPerm(dataprovider.PermAdminManageGroups)).Delete(groupPath+"/{name}", deleteGroup) - router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Get(dumpDataPath, dumpData) - router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Get(loadDataPath, loadData) - router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Post(loadDataPath, loadDataFromRequest) - router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Put(quotasBasePath+"/users/{username}/usage", - updateUserQuotaUsage) - router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Put(quotasBasePath+"/users/{username}/transfer-usage", - updateUserTransferQuotaUsage) - router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Put(quotasBasePath+"/folders/{name}/usage", - updateFolderQuotaUsage) - router.With(s.checkPerm(dataprovider.PermAdminViewDefender)).Get(defenderHosts, getDefenderHosts) - router.With(s.checkPerm(dataprovider.PermAdminViewDefender)).Get(defenderHosts+"/{id}", getDefenderHostByID) - router.With(s.checkPerm(dataprovider.PermAdminManageDefender)).Delete(defenderHosts+"/{id}", deleteDefenderHostByID) - router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Get(adminPath, getAdmins) - router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Post(adminPath, addAdmin) - router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Get(adminPath+"/{username}", getAdminByUsername) - router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Put(adminPath+"/{username}", updateAdmin) - router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Delete(adminPath+"/{username}", deleteAdmin) - router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Put(adminPath+"/{username}/2fa/disable", disableAdmin2FA) - router.With(s.checkPerm(dataprovider.PermAdminRetentionChecks)).Get(retentionChecksPath, getRetentionChecks) - router.With(s.checkPerm(dataprovider.PermAdminRetentionChecks)).Post(retentionBasePath+"/{username}/check", - startRetentionCheck) - router.With(s.checkPerm(dataprovider.PermAdminViewEvents), compressor.Handler). - Get(fsEventsPath, searchFsEvents) - router.With(s.checkPerm(dataprovider.PermAdminViewEvents), compressor.Handler). - Get(providerEventsPath, searchProviderEvents) - router.With(s.checkPerm(dataprovider.PermAdminViewEvents), compressor.Handler). - Get(logEventsPath, searchLogEvents) router.With(forbidAPIKeyAuthentication, s.checkPerm(dataprovider.PermAdminManageAPIKeys)). Get(apiKeysPath, getAPIKeys) router.With(forbidAPIKeyAuthentication, s.checkPerm(dataprovider.PermAdminManageAPIKeys)). @@ -1394,27 +1342,88 @@ func (s *httpdServer) initializeRouter() { Put(apiKeysPath+"/{id}", updateAPIKey) router.With(forbidAPIKeyAuthentication, s.checkPerm(dataprovider.PermAdminManageAPIKeys)). Delete(apiKeysPath+"/{id}", deleteAPIKey) - router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Get(eventActionsPath, getEventActions) - router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Get(eventActionsPath+"/{name}", getEventActionByName) - router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Post(eventActionsPath, addEventAction) - router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Put(eventActionsPath+"/{name}", updateEventAction) - router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Delete(eventActionsPath+"/{name}", deleteEventAction) - router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Get(eventRulesPath, getEventRules) - router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Get(eventRulesPath+"/{name}", getEventRuleByName) - router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Post(eventRulesPath, addEventRule) - router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Put(eventRulesPath+"/{name}", updateEventRule) - router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Delete(eventRulesPath+"/{name}", deleteEventRule) - router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Post(eventRulesPath+"/run/{name}", runOnDemandRule) - router.With(s.checkPerm(dataprovider.PermAdminManageRoles)).Get(rolesPath, getRoles) - router.With(s.checkPerm(dataprovider.PermAdminManageRoles)).Post(rolesPath, addRole) - router.With(s.checkPerm(dataprovider.PermAdminManageRoles)).Get(rolesPath+"/{name}", getRoleByName) - router.With(s.checkPerm(dataprovider.PermAdminManageRoles)).Put(rolesPath+"/{name}", updateRole) - router.With(s.checkPerm(dataprovider.PermAdminManageRoles)).Delete(rolesPath+"/{name}", deleteRole) - router.With(s.checkPerm(dataprovider.PermAdminManageIPLists), compressor.Handler).Get(ipListsPath+"/{type}", getIPListEntries) //nolint:goconst - router.With(s.checkPerm(dataprovider.PermAdminManageIPLists)).Post(ipListsPath+"/{type}", addIPListEntry) - router.With(s.checkPerm(dataprovider.PermAdminManageIPLists)).Get(ipListsPath+"/{type}/{ipornet}", getIPListEntry) //nolint:goconst - router.With(s.checkPerm(dataprovider.PermAdminManageIPLists)).Put(ipListsPath+"/{type}/{ipornet}", updateIPListEntry) - router.With(s.checkPerm(dataprovider.PermAdminManageIPLists)).Delete(ipListsPath+"/{type}/{ipornet}", deleteIPListEntry) + + router.Group(func(router chi.Router) { + router.Use(s.checkAuthRequirements) + + router.With(s.checkPerm(dataprovider.PermAdminViewServerStatus)). + Get(serverStatusPath, func(w http.ResponseWriter, r *http.Request) { + r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) + render.JSON(w, r, getServicesStatus()) + }) + + router.With(s.checkPerm(dataprovider.PermAdminViewConnections)).Get(activeConnectionsPath, getActiveConnections) + router.With(s.checkPerm(dataprovider.PermAdminCloseConnections)). + Delete(activeConnectionsPath+"/{connectionID}", handleCloseConnection) + router.With(s.checkPerm(dataprovider.PermAdminQuotaScans)).Get(quotasBasePath+"/users/scans", getUsersQuotaScans) + router.With(s.checkPerm(dataprovider.PermAdminQuotaScans)).Post(quotasBasePath+"/users/{username}/scan", startUserQuotaScan) + router.With(s.checkPerm(dataprovider.PermAdminQuotaScans)).Get(quotasBasePath+"/folders/scans", getFoldersQuotaScans) + router.With(s.checkPerm(dataprovider.PermAdminQuotaScans)).Post(quotasBasePath+"/folders/{name}/scan", startFolderQuotaScan) + router.With(s.checkPerm(dataprovider.PermAdminViewUsers)).Get(userPath, getUsers) + router.With(s.checkPerm(dataprovider.PermAdminAddUsers)).Post(userPath, addUser) + router.With(s.checkPerm(dataprovider.PermAdminViewUsers)).Get(userPath+"/{username}", getUserByUsername) //nolint:goconst + router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Put(userPath+"/{username}", updateUser) + router.With(s.checkPerm(dataprovider.PermAdminDeleteUsers)).Delete(userPath+"/{username}", deleteUser) + router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Put(userPath+"/{username}/2fa/disable", disableUser2FA) + router.With(s.checkPerm(dataprovider.PermAdminManageFolders)).Get(folderPath, getFolders) + router.With(s.checkPerm(dataprovider.PermAdminManageFolders)).Get(folderPath+"/{name}", getFolderByName) //nolint:goconst + router.With(s.checkPerm(dataprovider.PermAdminManageFolders)).Post(folderPath, addFolder) + router.With(s.checkPerm(dataprovider.PermAdminManageFolders)).Put(folderPath+"/{name}", updateFolder) + router.With(s.checkPerm(dataprovider.PermAdminManageFolders)).Delete(folderPath+"/{name}", deleteFolder) + router.With(s.checkPerm(dataprovider.PermAdminManageGroups)).Get(groupPath, getGroups) + router.With(s.checkPerm(dataprovider.PermAdminManageGroups)).Get(groupPath+"/{name}", getGroupByName) + router.With(s.checkPerm(dataprovider.PermAdminManageGroups)).Post(groupPath, addGroup) + router.With(s.checkPerm(dataprovider.PermAdminManageGroups)).Put(groupPath+"/{name}", updateGroup) + router.With(s.checkPerm(dataprovider.PermAdminManageGroups)).Delete(groupPath+"/{name}", deleteGroup) + router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Get(dumpDataPath, dumpData) + router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Get(loadDataPath, loadData) + router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Post(loadDataPath, loadDataFromRequest) + router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Put(quotasBasePath+"/users/{username}/usage", + updateUserQuotaUsage) + router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Put(quotasBasePath+"/users/{username}/transfer-usage", + updateUserTransferQuotaUsage) + router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Put(quotasBasePath+"/folders/{name}/usage", + updateFolderQuotaUsage) + router.With(s.checkPerm(dataprovider.PermAdminViewDefender)).Get(defenderHosts, getDefenderHosts) + router.With(s.checkPerm(dataprovider.PermAdminViewDefender)).Get(defenderHosts+"/{id}", getDefenderHostByID) + router.With(s.checkPerm(dataprovider.PermAdminManageDefender)).Delete(defenderHosts+"/{id}", deleteDefenderHostByID) + router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Get(adminPath, getAdmins) + router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Post(adminPath, addAdmin) + router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Get(adminPath+"/{username}", getAdminByUsername) + router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Put(adminPath+"/{username}", updateAdmin) + router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Delete(adminPath+"/{username}", deleteAdmin) + router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Put(adminPath+"/{username}/2fa/disable", disableAdmin2FA) + router.With(s.checkPerm(dataprovider.PermAdminRetentionChecks)).Get(retentionChecksPath, getRetentionChecks) + router.With(s.checkPerm(dataprovider.PermAdminRetentionChecks)).Post(retentionBasePath+"/{username}/check", + startRetentionCheck) + router.With(s.checkPerm(dataprovider.PermAdminViewEvents), compressor.Handler). + Get(fsEventsPath, searchFsEvents) + router.With(s.checkPerm(dataprovider.PermAdminViewEvents), compressor.Handler). + Get(providerEventsPath, searchProviderEvents) + router.With(s.checkPerm(dataprovider.PermAdminViewEvents), compressor.Handler). + Get(logEventsPath, searchLogEvents) + router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Get(eventActionsPath, getEventActions) + router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Get(eventActionsPath+"/{name}", getEventActionByName) + router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Post(eventActionsPath, addEventAction) + router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Put(eventActionsPath+"/{name}", updateEventAction) + router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Delete(eventActionsPath+"/{name}", deleteEventAction) + router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Get(eventRulesPath, getEventRules) + router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Get(eventRulesPath+"/{name}", getEventRuleByName) + router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Post(eventRulesPath, addEventRule) + router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Put(eventRulesPath+"/{name}", updateEventRule) + router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Delete(eventRulesPath+"/{name}", deleteEventRule) + router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Post(eventRulesPath+"/run/{name}", runOnDemandRule) + router.With(s.checkPerm(dataprovider.PermAdminManageRoles)).Get(rolesPath, getRoles) + router.With(s.checkPerm(dataprovider.PermAdminManageRoles)).Post(rolesPath, addRole) + router.With(s.checkPerm(dataprovider.PermAdminManageRoles)).Get(rolesPath+"/{name}", getRoleByName) + router.With(s.checkPerm(dataprovider.PermAdminManageRoles)).Put(rolesPath+"/{name}", updateRole) + router.With(s.checkPerm(dataprovider.PermAdminManageRoles)).Delete(rolesPath+"/{name}", deleteRole) + router.With(s.checkPerm(dataprovider.PermAdminManageIPLists), compressor.Handler).Get(ipListsPath+"/{type}", getIPListEntries) //nolint:goconst + router.With(s.checkPerm(dataprovider.PermAdminManageIPLists)).Post(ipListsPath+"/{type}", addIPListEntry) + router.With(s.checkPerm(dataprovider.PermAdminManageIPLists)).Get(ipListsPath+"/{type}/{ipornet}", getIPListEntry) //nolint:goconst + router.With(s.checkPerm(dataprovider.PermAdminManageIPLists)).Put(ipListsPath+"/{type}/{ipornet}", updateIPListEntry) + router.With(s.checkPerm(dataprovider.PermAdminManageIPLists)).Delete(ipListsPath+"/{type}/{ipornet}", deleteIPListEntry) + }) }) s.router.Get(userTokenPath, s.getUserToken) @@ -1675,8 +1684,9 @@ func (s *httpdServer) setupWebAdminRoutes() { router.Use(jwtAuthenticatorWebAdmin) router.Get(webLogoutPath, s.handleWebAdminLogout) - router.With(s.refreshCookie, s.requireBuiltinLogin).Get(webAdminProfilePath, s.handleWebAdminProfile) - router.With(s.requireBuiltinLogin).Post(webAdminProfilePath, s.handleWebAdminProfilePost) + router.With(s.refreshCookie, s.checkAuthRequirements, s.requireBuiltinLogin).Get( + webAdminProfilePath, s.handleWebAdminProfile) + router.With(s.checkAuthRequirements, s.requireBuiltinLogin).Post(webAdminProfilePath, s.handleWebAdminProfilePost) router.With(s.refreshCookie, s.requireBuiltinLogin).Get(webChangeAdminPwdPath, s.handleWebAdminChangePwd) router.With(s.requireBuiltinLogin).Post(webChangeAdminPwdPath, s.handleWebAdminChangePwdPost) @@ -1689,153 +1699,157 @@ func (s *httpdServer) setupWebAdminRoutes() { getRecoveryCodes) router.With(verifyCSRFHeader, s.requireBuiltinLogin).Post(webAdminRecoveryCodesPath, generateRecoveryCodes) - router.With(s.checkPerm(dataprovider.PermAdminViewUsers), s.refreshCookie). - Get(webUsersPath, s.handleGetWebUsers) - router.With(s.checkPerm(dataprovider.PermAdminViewUsers), compressor.Handler, s.refreshCookie). - Get(webUsersPath+jsonAPISuffix, getAllUsers) - router.With(s.checkPerm(dataprovider.PermAdminAddUsers), s.refreshCookie). - Get(webUserPath, s.handleWebAddUserGet) - router.With(s.checkPerm(dataprovider.PermAdminChangeUsers), s.refreshCookie). - Get(webUserPath+"/{username}", s.handleWebUpdateUserGet) - router.With(s.checkPerm(dataprovider.PermAdminAddUsers)).Post(webUserPath, s.handleWebAddUserPost) - router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Post(webUserPath+"/{username}", - s.handleWebUpdateUserPost) - router.With(s.checkPerm(dataprovider.PermAdminManageGroups), s.refreshCookie). - Get(webGroupsPath, s.handleWebGetGroups) - router.With(s.checkPerm(dataprovider.PermAdminManageGroups), compressor.Handler, s.refreshCookie). - Get(webGroupsPath+jsonAPISuffix, getAllGroups) - router.With(s.checkPerm(dataprovider.PermAdminManageGroups), s.refreshCookie). - Get(webGroupPath, s.handleWebAddGroupGet) - router.With(s.checkPerm(dataprovider.PermAdminManageGroups)).Post(webGroupPath, s.handleWebAddGroupPost) - router.With(s.checkPerm(dataprovider.PermAdminManageGroups), s.refreshCookie). - Get(webGroupPath+"/{name}", s.handleWebUpdateGroupGet) - router.With(s.checkPerm(dataprovider.PermAdminManageGroups)).Post(webGroupPath+"/{name}", - s.handleWebUpdateGroupPost) - router.With(s.checkPerm(dataprovider.PermAdminManageGroups), verifyCSRFHeader). - Delete(webGroupPath+"/{name}", deleteGroup) - router.With(s.checkPerm(dataprovider.PermAdminViewConnections), s.refreshCookie). - Get(webConnectionsPath, s.handleWebGetConnections) - router.With(s.checkPerm(dataprovider.PermAdminViewConnections), s.refreshCookie). - Get(webConnectionsPath+jsonAPISuffix, getActiveConnections) - router.With(s.checkPerm(dataprovider.PermAdminManageFolders), s.refreshCookie). - Get(webFoldersPath, s.handleWebGetFolders) - router.With(s.checkPerm(dataprovider.PermAdminManageFolders), compressor.Handler, s.refreshCookie). - Get(webFoldersPath+jsonAPISuffix, getAllFolders) - router.With(s.checkPerm(dataprovider.PermAdminManageFolders), s.refreshCookie). - Get(webFolderPath, s.handleWebAddFolderGet) - router.With(s.checkPerm(dataprovider.PermAdminManageFolders)).Post(webFolderPath, s.handleWebAddFolderPost) - router.With(s.checkPerm(dataprovider.PermAdminViewServerStatus), s.refreshCookie). - Get(webStatusPath, s.handleWebGetStatus) - router.With(s.checkPerm(dataprovider.PermAdminManageAdmins), s.refreshCookie). - Get(webAdminsPath, s.handleGetWebAdmins) - router.With(s.checkPerm(dataprovider.PermAdminManageAdmins), compressor.Handler, s.refreshCookie). - Get(webAdminsPath+jsonAPISuffix, getAllAdmins) - router.With(s.checkPerm(dataprovider.PermAdminManageAdmins), s.refreshCookie). - Get(webAdminPath, s.handleWebAddAdminGet) - router.With(s.checkPerm(dataprovider.PermAdminManageAdmins), s.refreshCookie). - Get(webAdminPath+"/{username}", s.handleWebUpdateAdminGet) - router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Post(webAdminPath, s.handleWebAddAdminPost) - router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Post(webAdminPath+"/{username}", - s.handleWebUpdateAdminPost) - router.With(s.checkPerm(dataprovider.PermAdminManageAdmins), verifyCSRFHeader). - Delete(webAdminPath+"/{username}", deleteAdmin) - router.With(s.checkPerm(dataprovider.PermAdminCloseConnections), verifyCSRFHeader). - Delete(webConnectionsPath+"/{connectionID}", handleCloseConnection) - router.With(s.checkPerm(dataprovider.PermAdminManageFolders), s.refreshCookie). - Get(webFolderPath+"/{name}", s.handleWebUpdateFolderGet) - router.With(s.checkPerm(dataprovider.PermAdminManageFolders)).Post(webFolderPath+"/{name}", - s.handleWebUpdateFolderPost) - router.With(s.checkPerm(dataprovider.PermAdminManageFolders), verifyCSRFHeader). - Delete(webFolderPath+"/{name}", deleteFolder) - router.With(s.checkPerm(dataprovider.PermAdminQuotaScans), verifyCSRFHeader). - Post(webScanVFolderPath+"/{name}", startFolderQuotaScan) - router.With(s.checkPerm(dataprovider.PermAdminDeleteUsers), verifyCSRFHeader). - Delete(webUserPath+"/{username}", deleteUser) - router.With(s.checkPerm(dataprovider.PermAdminQuotaScans), verifyCSRFHeader). - Post(webQuotaScanPath+"/{username}", startUserQuotaScan) - router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Get(webMaintenancePath, s.handleWebMaintenance) - router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Get(webBackupPath, dumpData) - router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Post(webRestorePath, s.handleWebRestore) - router.With(s.checkPerm(dataprovider.PermAdminManageSystem), s.refreshCookie). - Get(webTemplateUser, s.handleWebTemplateUserGet) - router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Post(webTemplateUser, s.handleWebTemplateUserPost) - router.With(s.checkPerm(dataprovider.PermAdminManageSystem), s.refreshCookie). - Get(webTemplateFolder, s.handleWebTemplateFolderGet) - router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Post(webTemplateFolder, s.handleWebTemplateFolderPost) - router.With(s.checkPerm(dataprovider.PermAdminViewDefender)).Get(webDefenderPath, s.handleWebDefenderPage) - router.With(s.checkPerm(dataprovider.PermAdminViewDefender)).Get(webDefenderHostsPath, getDefenderHosts) - router.With(s.checkPerm(dataprovider.PermAdminManageDefender)).Delete(webDefenderHostsPath+"/{id}", - deleteDefenderHostByID) - router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), compressor.Handler, s.refreshCookie). - Get(webAdminEventActionsPath+jsonAPISuffix, getAllActions) - router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), s.refreshCookie). - Get(webAdminEventActionsPath, s.handleWebGetEventActions) - router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), s.refreshCookie). - Get(webAdminEventActionPath, s.handleWebAddEventActionGet) - router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Post(webAdminEventActionPath, - s.handleWebAddEventActionPost) - router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), s.refreshCookie). - Get(webAdminEventActionPath+"/{name}", s.handleWebUpdateEventActionGet) - router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Post(webAdminEventActionPath+"/{name}", - s.handleWebUpdateEventActionPost) - router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), verifyCSRFHeader). - Delete(webAdminEventActionPath+"/{name}", deleteEventAction) - router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), compressor.Handler, s.refreshCookie). - Get(webAdminEventRulesPath+jsonAPISuffix, getAllRules) - router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), s.refreshCookie). - Get(webAdminEventRulesPath, s.handleWebGetEventRules) - router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), s.refreshCookie). - Get(webAdminEventRulePath, s.handleWebAddEventRuleGet) - router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Post(webAdminEventRulePath, - s.handleWebAddEventRulePost) - router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), s.refreshCookie). - Get(webAdminEventRulePath+"/{name}", s.handleWebUpdateEventRuleGet) - router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Post(webAdminEventRulePath+"/{name}", - s.handleWebUpdateEventRulePost) - router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), verifyCSRFHeader). - Delete(webAdminEventRulePath+"/{name}", deleteEventRule) - router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), verifyCSRFHeader). - Post(webAdminEventRulePath+"/run/{name}", runOnDemandRule) - router.With(s.checkPerm(dataprovider.PermAdminManageRoles), s.refreshCookie). - Get(webAdminRolesPath, s.handleWebGetRoles) - router.With(s.checkPerm(dataprovider.PermAdminManageRoles), compressor.Handler, s.refreshCookie). - Get(webAdminRolesPath+jsonAPISuffix, getAllRoles) - router.With(s.checkPerm(dataprovider.PermAdminManageRoles), s.refreshCookie). - Get(webAdminRolePath, s.handleWebAddRoleGet) - router.With(s.checkPerm(dataprovider.PermAdminManageRoles)).Post(webAdminRolePath, s.handleWebAddRolePost) - router.With(s.checkPerm(dataprovider.PermAdminManageRoles), s.refreshCookie). - Get(webAdminRolePath+"/{name}", s.handleWebUpdateRoleGet) - router.With(s.checkPerm(dataprovider.PermAdminManageRoles)).Post(webAdminRolePath+"/{name}", - s.handleWebUpdateRolePost) - router.With(s.checkPerm(dataprovider.PermAdminManageRoles), verifyCSRFHeader). - Delete(webAdminRolePath+"/{name}", deleteRole) - router.With(s.checkPerm(dataprovider.PermAdminViewEvents), s.refreshCookie).Get(webEventsPath, - s.handleWebGetEvents) - router.With(s.checkPerm(dataprovider.PermAdminViewEvents), compressor.Handler, s.refreshCookie). - Get(webEventsFsSearchPath, searchFsEvents) - router.With(s.checkPerm(dataprovider.PermAdminViewEvents), compressor.Handler, s.refreshCookie). - Get(webEventsProviderSearchPath, searchProviderEvents) - router.With(s.checkPerm(dataprovider.PermAdminViewEvents), compressor.Handler, s.refreshCookie). - Get(webEventsLogSearchPath, searchLogEvents) - router.With(s.checkPerm(dataprovider.PermAdminManageIPLists)).Get(webIPListsPath, s.handleWebIPListsPage) - router.With(s.checkPerm(dataprovider.PermAdminManageIPLists), compressor.Handler, s.refreshCookie). - Get(webIPListsPath+"/{type}", getIPListEntries) - router.With(s.checkPerm(dataprovider.PermAdminManageIPLists), s.refreshCookie).Get(webIPListPath+"/{type}", - s.handleWebAddIPListEntryGet) - router.With(s.checkPerm(dataprovider.PermAdminManageIPLists)).Post(webIPListPath+"/{type}", - s.handleWebAddIPListEntryPost) - router.With(s.checkPerm(dataprovider.PermAdminManageIPLists), s.refreshCookie).Get(webIPListPath+"/{type}/{ipornet}", - s.handleWebUpdateIPListEntryGet) - router.With(s.checkPerm(dataprovider.PermAdminManageIPLists)).Post(webIPListPath+"/{type}/{ipornet}", - s.handleWebUpdateIPListEntryPost) - router.With(s.checkPerm(dataprovider.PermAdminManageIPLists), verifyCSRFHeader). - Delete(webIPListPath+"/{type}/{ipornet}", deleteIPListEntry) - router.With(s.checkPerm(dataprovider.PermAdminManageSystem), s.refreshCookie).Get(webConfigsPath, s.handleWebConfigs) - router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Post(webConfigsPath, s.handleWebConfigsPost) - router.With(s.checkPerm(dataprovider.PermAdminManageSystem), verifyCSRFHeader, s.refreshCookie). - Post(webConfigsPath+"/smtp/test", testSMTPConfig) - router.With(s.checkPerm(dataprovider.PermAdminManageSystem), verifyCSRFHeader, s.refreshCookie). - Post(webOAuth2TokenPath, handleSMTPOAuth2TokenRequestPost) + router.Group(func(router chi.Router) { + router.Use(s.checkAuthRequirements) + + router.With(s.checkPerm(dataprovider.PermAdminViewUsers), s.refreshCookie). + Get(webUsersPath, s.handleGetWebUsers) + router.With(s.checkPerm(dataprovider.PermAdminViewUsers), compressor.Handler, s.refreshCookie). + Get(webUsersPath+jsonAPISuffix, getAllUsers) + router.With(s.checkPerm(dataprovider.PermAdminAddUsers), s.refreshCookie). + Get(webUserPath, s.handleWebAddUserGet) + router.With(s.checkPerm(dataprovider.PermAdminChangeUsers), s.refreshCookie). + Get(webUserPath+"/{username}", s.handleWebUpdateUserGet) + router.With(s.checkPerm(dataprovider.PermAdminAddUsers)).Post(webUserPath, s.handleWebAddUserPost) + router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Post(webUserPath+"/{username}", + s.handleWebUpdateUserPost) + router.With(s.checkPerm(dataprovider.PermAdminManageGroups), s.refreshCookie). + Get(webGroupsPath, s.handleWebGetGroups) + router.With(s.checkPerm(dataprovider.PermAdminManageGroups), compressor.Handler, s.refreshCookie). + Get(webGroupsPath+jsonAPISuffix, getAllGroups) + router.With(s.checkPerm(dataprovider.PermAdminManageGroups), s.refreshCookie). + Get(webGroupPath, s.handleWebAddGroupGet) + router.With(s.checkPerm(dataprovider.PermAdminManageGroups)).Post(webGroupPath, s.handleWebAddGroupPost) + router.With(s.checkPerm(dataprovider.PermAdminManageGroups), s.refreshCookie). + Get(webGroupPath+"/{name}", s.handleWebUpdateGroupGet) + router.With(s.checkPerm(dataprovider.PermAdminManageGroups)).Post(webGroupPath+"/{name}", + s.handleWebUpdateGroupPost) + router.With(s.checkPerm(dataprovider.PermAdminManageGroups), verifyCSRFHeader). + Delete(webGroupPath+"/{name}", deleteGroup) + router.With(s.checkPerm(dataprovider.PermAdminViewConnections), s.refreshCookie). + Get(webConnectionsPath, s.handleWebGetConnections) + router.With(s.checkPerm(dataprovider.PermAdminViewConnections), s.refreshCookie). + Get(webConnectionsPath+jsonAPISuffix, getActiveConnections) + router.With(s.checkPerm(dataprovider.PermAdminManageFolders), s.refreshCookie). + Get(webFoldersPath, s.handleWebGetFolders) + router.With(s.checkPerm(dataprovider.PermAdminManageFolders), compressor.Handler, s.refreshCookie). + Get(webFoldersPath+jsonAPISuffix, getAllFolders) + router.With(s.checkPerm(dataprovider.PermAdminManageFolders), s.refreshCookie). + Get(webFolderPath, s.handleWebAddFolderGet) + router.With(s.checkPerm(dataprovider.PermAdminManageFolders)).Post(webFolderPath, s.handleWebAddFolderPost) + router.With(s.checkPerm(dataprovider.PermAdminViewServerStatus), s.refreshCookie). + Get(webStatusPath, s.handleWebGetStatus) + router.With(s.checkPerm(dataprovider.PermAdminManageAdmins), s.refreshCookie). + Get(webAdminsPath, s.handleGetWebAdmins) + router.With(s.checkPerm(dataprovider.PermAdminManageAdmins), compressor.Handler, s.refreshCookie). + Get(webAdminsPath+jsonAPISuffix, getAllAdmins) + router.With(s.checkPerm(dataprovider.PermAdminManageAdmins), s.refreshCookie). + Get(webAdminPath, s.handleWebAddAdminGet) + router.With(s.checkPerm(dataprovider.PermAdminManageAdmins), s.refreshCookie). + Get(webAdminPath+"/{username}", s.handleWebUpdateAdminGet) + router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Post(webAdminPath, s.handleWebAddAdminPost) + router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Post(webAdminPath+"/{username}", + s.handleWebUpdateAdminPost) + router.With(s.checkPerm(dataprovider.PermAdminManageAdmins), verifyCSRFHeader). + Delete(webAdminPath+"/{username}", deleteAdmin) + router.With(s.checkPerm(dataprovider.PermAdminCloseConnections), verifyCSRFHeader). + Delete(webConnectionsPath+"/{connectionID}", handleCloseConnection) + router.With(s.checkPerm(dataprovider.PermAdminManageFolders), s.refreshCookie). + Get(webFolderPath+"/{name}", s.handleWebUpdateFolderGet) + router.With(s.checkPerm(dataprovider.PermAdminManageFolders)).Post(webFolderPath+"/{name}", + s.handleWebUpdateFolderPost) + router.With(s.checkPerm(dataprovider.PermAdminManageFolders), verifyCSRFHeader). + Delete(webFolderPath+"/{name}", deleteFolder) + router.With(s.checkPerm(dataprovider.PermAdminQuotaScans), verifyCSRFHeader). + Post(webScanVFolderPath+"/{name}", startFolderQuotaScan) + router.With(s.checkPerm(dataprovider.PermAdminDeleteUsers), verifyCSRFHeader). + Delete(webUserPath+"/{username}", deleteUser) + router.With(s.checkPerm(dataprovider.PermAdminQuotaScans), verifyCSRFHeader). + Post(webQuotaScanPath+"/{username}", startUserQuotaScan) + router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Get(webMaintenancePath, s.handleWebMaintenance) + router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Get(webBackupPath, dumpData) + router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Post(webRestorePath, s.handleWebRestore) + router.With(s.checkPerm(dataprovider.PermAdminManageSystem), s.refreshCookie). + Get(webTemplateUser, s.handleWebTemplateUserGet) + router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Post(webTemplateUser, s.handleWebTemplateUserPost) + router.With(s.checkPerm(dataprovider.PermAdminManageSystem), s.refreshCookie). + Get(webTemplateFolder, s.handleWebTemplateFolderGet) + router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Post(webTemplateFolder, s.handleWebTemplateFolderPost) + router.With(s.checkPerm(dataprovider.PermAdminViewDefender)).Get(webDefenderPath, s.handleWebDefenderPage) + router.With(s.checkPerm(dataprovider.PermAdminViewDefender)).Get(webDefenderHostsPath, getDefenderHosts) + router.With(s.checkPerm(dataprovider.PermAdminManageDefender)).Delete(webDefenderHostsPath+"/{id}", + deleteDefenderHostByID) + router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), compressor.Handler, s.refreshCookie). + Get(webAdminEventActionsPath+jsonAPISuffix, getAllActions) + router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), s.refreshCookie). + Get(webAdminEventActionsPath, s.handleWebGetEventActions) + router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), s.refreshCookie). + Get(webAdminEventActionPath, s.handleWebAddEventActionGet) + router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Post(webAdminEventActionPath, + s.handleWebAddEventActionPost) + router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), s.refreshCookie). + Get(webAdminEventActionPath+"/{name}", s.handleWebUpdateEventActionGet) + router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Post(webAdminEventActionPath+"/{name}", + s.handleWebUpdateEventActionPost) + router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), verifyCSRFHeader). + Delete(webAdminEventActionPath+"/{name}", deleteEventAction) + router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), compressor.Handler, s.refreshCookie). + Get(webAdminEventRulesPath+jsonAPISuffix, getAllRules) + router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), s.refreshCookie). + Get(webAdminEventRulesPath, s.handleWebGetEventRules) + router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), s.refreshCookie). + Get(webAdminEventRulePath, s.handleWebAddEventRuleGet) + router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Post(webAdminEventRulePath, + s.handleWebAddEventRulePost) + router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), s.refreshCookie). + Get(webAdminEventRulePath+"/{name}", s.handleWebUpdateEventRuleGet) + router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Post(webAdminEventRulePath+"/{name}", + s.handleWebUpdateEventRulePost) + router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), verifyCSRFHeader). + Delete(webAdminEventRulePath+"/{name}", deleteEventRule) + router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), verifyCSRFHeader). + Post(webAdminEventRulePath+"/run/{name}", runOnDemandRule) + router.With(s.checkPerm(dataprovider.PermAdminManageRoles), s.refreshCookie). + Get(webAdminRolesPath, s.handleWebGetRoles) + router.With(s.checkPerm(dataprovider.PermAdminManageRoles), compressor.Handler, s.refreshCookie). + Get(webAdminRolesPath+jsonAPISuffix, getAllRoles) + router.With(s.checkPerm(dataprovider.PermAdminManageRoles), s.refreshCookie). + Get(webAdminRolePath, s.handleWebAddRoleGet) + router.With(s.checkPerm(dataprovider.PermAdminManageRoles)).Post(webAdminRolePath, s.handleWebAddRolePost) + router.With(s.checkPerm(dataprovider.PermAdminManageRoles), s.refreshCookie). + Get(webAdminRolePath+"/{name}", s.handleWebUpdateRoleGet) + router.With(s.checkPerm(dataprovider.PermAdminManageRoles)).Post(webAdminRolePath+"/{name}", + s.handleWebUpdateRolePost) + router.With(s.checkPerm(dataprovider.PermAdminManageRoles), verifyCSRFHeader). + Delete(webAdminRolePath+"/{name}", deleteRole) + router.With(s.checkPerm(dataprovider.PermAdminViewEvents), s.refreshCookie).Get(webEventsPath, + s.handleWebGetEvents) + router.With(s.checkPerm(dataprovider.PermAdminViewEvents), compressor.Handler, s.refreshCookie). + Get(webEventsFsSearchPath, searchFsEvents) + router.With(s.checkPerm(dataprovider.PermAdminViewEvents), compressor.Handler, s.refreshCookie). + Get(webEventsProviderSearchPath, searchProviderEvents) + router.With(s.checkPerm(dataprovider.PermAdminViewEvents), compressor.Handler, s.refreshCookie). + Get(webEventsLogSearchPath, searchLogEvents) + router.With(s.checkPerm(dataprovider.PermAdminManageIPLists)).Get(webIPListsPath, s.handleWebIPListsPage) + router.With(s.checkPerm(dataprovider.PermAdminManageIPLists), compressor.Handler, s.refreshCookie). + Get(webIPListsPath+"/{type}", getIPListEntries) + router.With(s.checkPerm(dataprovider.PermAdminManageIPLists), s.refreshCookie).Get(webIPListPath+"/{type}", + s.handleWebAddIPListEntryGet) + router.With(s.checkPerm(dataprovider.PermAdminManageIPLists)).Post(webIPListPath+"/{type}", + s.handleWebAddIPListEntryPost) + router.With(s.checkPerm(dataprovider.PermAdminManageIPLists), s.refreshCookie).Get(webIPListPath+"/{type}/{ipornet}", + s.handleWebUpdateIPListEntryGet) + router.With(s.checkPerm(dataprovider.PermAdminManageIPLists)).Post(webIPListPath+"/{type}/{ipornet}", + s.handleWebUpdateIPListEntryPost) + router.With(s.checkPerm(dataprovider.PermAdminManageIPLists), verifyCSRFHeader). + Delete(webIPListPath+"/{type}/{ipornet}", deleteIPListEntry) + router.With(s.checkPerm(dataprovider.PermAdminManageSystem), s.refreshCookie).Get(webConfigsPath, s.handleWebConfigs) + router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Post(webConfigsPath, s.handleWebConfigsPost) + router.With(s.checkPerm(dataprovider.PermAdminManageSystem), verifyCSRFHeader, s.refreshCookie). + Post(webConfigsPath+"/smtp/test", testSMTPConfig) + router.With(s.checkPerm(dataprovider.PermAdminManageSystem), verifyCSRFHeader, s.refreshCookie). + Post(webOAuth2TokenPath, handleSMTPOAuth2TokenRequestPost) + }) }) } } diff --git a/internal/httpd/webadmin.go b/internal/httpd/webadmin.go index 639ca98c..f163d502 100644 --- a/internal/httpd/webadmin.go +++ b/internal/httpd/webadmin.go @@ -210,12 +210,13 @@ type changePasswordPage struct { type mfaPage struct { basePage - TOTPConfigs []string - TOTPConfig dataprovider.AdminTOTPConfig - GenerateTOTPURL string - ValidateTOTPURL string - SaveTOTPURL string - RecCodesURL string + TOTPConfigs []string + TOTPConfig dataprovider.AdminTOTPConfig + GenerateTOTPURL string + ValidateTOTPURL string + SaveTOTPURL string + RecCodesURL string + RequireTwoFactor bool } type maintenancePage struct { @@ -786,6 +787,7 @@ func (s *httpdServer) renderMFAPage(w http.ResponseWriter, r *http.Request) { return } data.TOTPConfig = admin.Filters.TOTPConfig + data.RequireTwoFactor = admin.Filters.RequireTwoFactor renderAdminTemplate(w, templateMFA, data) } @@ -1741,6 +1743,8 @@ func getAdminFromPostFields(r *http.Request) (dataprovider.Admin, error) { admin.Role = strings.TrimSpace(r.Form.Get("role")) admin.Filters.AllowList = getSliceFromDelimitedValues(r.Form.Get("allowed_ip"), ",") admin.Filters.AllowAPIKeyAuth = r.Form.Get("allow_api_key_auth") != "" + admin.Filters.RequireTwoFactor = r.Form.Get("require_two_factor") != "" + admin.Filters.RequirePasswordChange = r.Form.Get("require_password_change") != "" admin.AdditionalInfo = r.Form.Get("additional_info") admin.Description = r.Form.Get("description") admin.Filters.Preferences.HideUserPageSections = getAdminHiddenUserPageSections(r) @@ -3016,6 +3020,8 @@ func (s *httpdServer) handleWebUpdateAdminPost(w http.ResponseWriter, r *http.Re ), false) return } + updatedAdmin.Filters.RequirePasswordChange = admin.Filters.RequirePasswordChange + updatedAdmin.Filters.RequireTwoFactor = admin.Filters.RequireTwoFactor } err = dataprovider.UpdateAdmin(&updatedAdmin, claims.Username, ipAddr, claims.Role) if err != nil { diff --git a/internal/httpdtest/httpdtest.go b/internal/httpdtest/httpdtest.go index 953c6116..71ffa12b 100644 --- a/internal/httpdtest/httpdtest.go +++ b/internal/httpdtest/httpdtest.go @@ -1970,6 +1970,12 @@ func compareAdminFilters(expected, actual dataprovider.AdminFilters) error { if expected.Preferences.DefaultUsersExpiration != actual.Preferences.DefaultUsersExpiration { return errors.New("default users expiration mismatch") } + if expected.RequirePasswordChange != actual.RequirePasswordChange { + return errors.New("require password change mismatch") + } + if expected.RequireTwoFactor != actual.RequireTwoFactor { + return errors.New("require two factor mismatch") + } return nil } diff --git a/internal/util/i18n.go b/internal/util/i18n.go index 540b1a20..85ffda1c 100644 --- a/internal/util/i18n.go +++ b/internal/util/i18n.go @@ -159,6 +159,7 @@ const ( I18nErrorShareExpired = "share.expired" I18nErrorLoginFromIPDenied = "login.ip_not_allowed" I18nError2FARequired = "login.two_factor_required" + I18nError2FARequiredGeneric = "login.two_factor_required_generic" I18nErrorNoOIDCFeature = "general.no_oidc_feature" I18nErrorNoPermissions = "general.no_permissions" I18nErrorShareBrowsePaths = "share.browsable_multiple_paths" diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index ba904785..bce6e97a 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -6003,6 +6003,10 @@ components: allow_api_key_auth: type: boolean description: 'API key auth allows to impersonate this administrator with an API key' + require_two_factor: + type: boolean + require_password_change: + type: boolean totp_config: $ref: '#/components/schemas/AdminTOTPConfig' recovery_codes: diff --git a/static/locales/en/translation.json b/static/locales/en/translation.json index 57891c9a..75940d1e 100644 --- a/static/locales/en/translation.json +++ b/static/locales/en/translation.json @@ -99,6 +99,7 @@ "reset_ok_login_error": "The password reset completed successfully but an unexpected error occurred while signing in", "ip_not_allowed": "Login is not allowed from this IP address", "two_factor_required": "Set up two-factor authentication, it is required for the following protocols: {{val}}", + "two_factor_required_generic": "Set up two-factor authentication, it is mandatory for your account", "link": "Go to {{link}}" }, "theme": { @@ -386,6 +387,7 @@ "msg_enabled": "Two-factor authentication is enabled", "msg_disabled": "Secure Your Account", "msg_info": "Two-factor authentication adds an extra layer of security to your account. To log in you'll need to provide an additional authentication code.", + "require": "Two-factor authentication is required for this account", "require_for": "Require 2FA for", "generate": "Generate new secret key", "recovery_codes": "Recovery codes", @@ -727,7 +729,8 @@ "user_page_pref_help": "You can hide some sections from the user page. These are not security settings and are not enforced server side in any way. They are only intended to simplify the add/update user page", "hide_sections": "Hide sections", "default_users_expiration": "Default users expiration", - "default_users_expiration_help": "Default expiration for new users as number of days" + "default_users_expiration_help": "Default expiration for new users as number of days", + "require_pwd_change_help": "A password change is required at the next login" }, "connections": { "view_manage": "View and manage connections", diff --git a/static/locales/it/translation.json b/static/locales/it/translation.json index 26673f6f..97252b62 100644 --- a/static/locales/it/translation.json +++ b/static/locales/it/translation.json @@ -99,6 +99,7 @@ "reset_ok_login_error": "La reimpostazione della password è stata completata correttamente ma si è verificato un errore imprevisto durante l'accesso", "ip_not_allowed": "L'accesso non è consentito da questo indirizzo IP", "two_factor_required": "Configura l'autenticazione a due fattori, è obbligatoria per i seguenti protocolli: {{val}}", + "two_factor_required_generic": "Configura l'autenticazione a due fattori, è obbligatoria per il tuo account", "link": "Vai a {{link}}" }, "theme": { @@ -386,6 +387,7 @@ "msg_enabled": "L'autenticazione a due fattori è abilitata", "msg_disabled": "Metti il tuo account in sicurezza", "msg_info": "L'autenticazione a due fattori aggiunge un ulteriore livello di sicurezza al tuo account. Per accedere dovrai fornire un ulteriore codice di autenticazione.", + "require": "L'autenticazione a due fattori è obbligatoria per questo account", "require_for": "Richiedi 2FA per", "generate": "Genera una nuova chiave segreta", "recovery_codes": "Codici di ripristino", @@ -727,7 +729,8 @@ "user_page_pref_help": "Puoi nascondere alcune sezioni dalla pagina utente. Queste non sono impostazioni di sicurezza e non vengono verificate lato server. Hanno il solo scopo di semplificare la pagina di creazione/modifica utenti", "hide_sections": "Nascondi sezioni", "default_users_expiration": "Scadenza predefinita utenti", - "default_users_expiration_help": "Scadenza predefinita per i nuovi utenti espressa in numero di giorni" + "default_users_expiration_help": "Scadenza predefinita per i nuovi utenti espressa in numero di giorni", + "require_pwd_change_help": "Il cambio password è obbligatorio al prossimo accesso" }, "connections": { "view_manage": "Visualizza e gestisci connessioni attive", diff --git a/templates/webadmin/admin.html b/templates/webadmin/admin.html index cbdc83f5..baf5957d 100644 --- a/templates/webadmin/admin.html +++ b/templates/webadmin/admin.html @@ -42,6 +42,18 @@ explicit grant from the SFTPGo Team (support@sftpgo.com). +
+ +
+
+ + +
+
+
+
@@ -245,6 +257,18 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
+
+ +
+
+ + +
+
+
+
diff --git a/templates/webadmin/mfa.html b/templates/webadmin/mfa.html index f8bde85b..d90f9193 100644 --- a/templates/webadmin/mfa.html +++ b/templates/webadmin/mfa.html @@ -344,6 +344,16 @@ explicit grant from the SFTPGo Team (support@sftpgo.com). } function disableConfig() { + //{{- if .RequireTwoFactor}} + ModalAlert.fire({ + text: $.t('2fa.require'), + icon: "warning", + confirmButtonText: $.t('general.ok'), + customClass: { + confirmButton: "btn btn-primary" + } + }); + //{{- else}} ModalAlert.fire({ text: $.t('2fa.disable_question'), icon: "warning", @@ -358,6 +368,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com). doSaveConfig(document.querySelector('#disable_btn'), null, false, true); } }); + //{{- end}} } function validatePasscode() {