Web: allow to require password change and two-factor for admins

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2024-02-21 20:45:10 +01:00
parent 51ae2d7301
commit de089e51fd
No known key found for this signature in database
GPG key ID: 935D2952DEC4EECF
18 changed files with 650 additions and 333 deletions

50
go.mod
View file

@ -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

100
go.sum
View file

@ -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=

View file

@ -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()

View file

@ -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)
}

View file

@ -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

View file

@ -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))

View file

@ -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)
}

View file

@ -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

View file

@ -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) {
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,6 +238,7 @@ func (s *httpdServer) checkAuthRequirements(next http.Handler) http.Handler {
if tokenClaims.MustSetTwoFactorAuth || tokenClaims.MustChangePassword {
var err error
if tokenClaims.MustSetTwoFactorAuth {
if len(tokenClaims.RequiredTwoFactorProtocols) > 0 {
protocols := strings.Join(tokenClaims.RequiredTwoFactorProtocols, ", ")
err = util.NewI18nError(
util.NewGenericError(
@ -244,6 +249,12 @@ func (s *httpdServer) checkAuthRequirements(next http.Handler) http.Handler {
"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) {
if isWebClientRequest(r) {
s.renderClientForbiddenPage(w, r, err)
} else {
s.renderForbiddenPage(w, r, err)
}
} else {
sendAPIResponse(w, r, err, "", http.StatusForbidden)
}

View file

@ -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
@ -986,6 +988,8 @@ func (s *httpdServer) generateAndSendToken(w http.ResponseWriter, r *http.Reques
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,6 +1332,20 @@ func (s *httpdServer) initializeRouter() {
router.With(forbidAPIKeyAuthentication).Get(admin2FARecoveryCodesPath, getRecoveryCodes)
router.With(forbidAPIKeyAuthentication).Post(admin2FARecoveryCodesPath, generateRecoveryCodes)
router.With(forbidAPIKeyAuthentication, s.checkPerm(dataprovider.PermAdminManageAPIKeys)).
Get(apiKeysPath, getAPIKeys)
router.With(forbidAPIKeyAuthentication, s.checkPerm(dataprovider.PermAdminManageAPIKeys)).
Post(apiKeysPath, addAPIKey)
router.With(forbidAPIKeyAuthentication, s.checkPerm(dataprovider.PermAdminManageAPIKeys)).
Get(apiKeysPath+"/{id}", getAPIKeyByID)
router.With(forbidAPIKeyAuthentication, s.checkPerm(dataprovider.PermAdminManageAPIKeys)).
Put(apiKeysPath+"/{id}", updateAPIKey)
router.With(forbidAPIKeyAuthentication, s.checkPerm(dataprovider.PermAdminManageAPIKeys)).
Delete(apiKeysPath+"/{id}", deleteAPIKey)
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)
@ -1384,16 +1402,6 @@ func (s *httpdServer) initializeRouter() {
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)).
Post(apiKeysPath, addAPIKey)
router.With(forbidAPIKeyAuthentication, s.checkPerm(dataprovider.PermAdminManageAPIKeys)).
Get(apiKeysPath+"/{id}", getAPIKeyByID)
router.With(forbidAPIKeyAuthentication, s.checkPerm(dataprovider.PermAdminManageAPIKeys)).
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)
@ -1416,6 +1424,7 @@ func (s *httpdServer) initializeRouter() {
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,6 +1699,9 @@ func (s *httpdServer) setupWebAdminRoutes() {
getRecoveryCodes)
router.With(verifyCSRFHeader, s.requireBuiltinLogin).Post(webAdminRecoveryCodesPath, generateRecoveryCodes)
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).
@ -1837,5 +1850,6 @@ func (s *httpdServer) setupWebAdminRoutes() {
router.With(s.checkPerm(dataprovider.PermAdminManageSystem), verifyCSRFHeader, s.refreshCookie).
Post(webOAuth2TokenPath, handleSMTPOAuth2TokenRequestPost)
})
})
}
}

View file

@ -216,6 +216,7 @@ type mfaPage struct {
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 {

View file

@ -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
}

View file

@ -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"

View file

@ -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:

View file

@ -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",

View file

@ -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",

View file

@ -42,6 +42,18 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</div>
</div>
<div class="form-group row align-items-center mt-10 {{if eq .LoggedUser.Username .Admin.Username}}d-none{{end}}">
<label data-i18n="user.require_pwd_change" class="col-md-3 col-form-label" for="idRequirePasswordChange">Require password change</label>
<div class="col-md-9">
<div class="form-check form-switch form-check-custom form-check-solid">
<input class="form-check-input" type="checkbox" id="idRequirePasswordChange" name="require_password_change" {{if .Admin.Filters.RequirePasswordChange}}checked="checked"{{end}}/>
<label data-i18n="admin.require_pwd_change_help" class="form-check-label fw-semibold text-gray-800" for="idRequirePasswordChange">
A password change is required at the next login
</label>
</div>
</div>
</div>
<div class="form-group row mt-10">
<label for="idStatus" data-i18n="general.status" class="col-md-3 col-form-label">Status</label>
<div class="col-md-9">
@ -245,6 +257,18 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</div>
</div>
<div class="form-group row align-items-center mt-10 {{if eq .LoggedUser.Username .Admin.Username}}d-none{{end}}">
<label data-i18n="title.two_factor_auth_short" class="col-md-3 col-form-label" for="idRequire2FA">2FA</label>
<div class="col-md-9">
<div class="form-check form-switch form-check-custom form-check-solid">
<input class="form-check-input" type="checkbox" id="idRequire2FA" name="require_two_factor" {{if .Admin.Filters.RequireTwoFactor}}checked{{end}}/>
<label data-i18n="2fa.require" class="form-check-label fw-semibold text-gray-800" for="idRequire2FA">
Two-factor authentication is required
</label>
</div>
</div>
</div>
<div class="form-group row mt-10">
<label for="idAdditionalInfo" data-i18n="general.additional_info" class="col-md-3 col-form-label">Additional info</label>
<div class="col-md-9">

View file

@ -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() {