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:
parent
51ae2d7301
commit
de089e51fd
18 changed files with 650 additions and 333 deletions
50
go.mod
50
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
|
||||
|
|
100
go.sum
100
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=
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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() {
|
||||
|
|
Loading…
Reference in a new issue