WebAdmin: allow to disable 2FA

Before it was only possible using REST API

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2024-02-23 18:24:07 +01:00
parent 76ffa107dd
commit a577d8b3cd
No known key found for this signature in database
GPG key ID: 935D2952DEC4EECF
17 changed files with 312 additions and 129 deletions

30
go.mod
View file

@ -10,14 +10,14 @@ require (
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.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/config v1.27.3
github.com/aws/aws-sdk-go-v2/credentials v1.17.3
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/aws/aws-sdk-go-v2/feature/s3/manager v1.16.5
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.21.0
github.com/aws/aws-sdk-go-v2/service/s3 v1.51.0
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.28.0
github.com/aws/aws-sdk-go-v2/service/sts v1.28.0
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
@ -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.166.0
google.golang.org/api v0.167.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
)
@ -94,8 +94,8 @@ require (
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/aws-sdk-go-v2/service/sso v1.20.0 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.0 // 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
@ -146,7 +146,7 @@ require (
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/prometheus/client_model v0.6.0 // indirect
github.com/prometheus/common v0.47.0 // indirect
github.com/prometheus/common v0.48.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
@ -162,11 +162,11 @@ require (
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.48.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0 // indirect
go.opentelemetry.io/otel v1.23.1 // indirect
go.opentelemetry.io/otel/metric v1.23.1 // indirect
go.opentelemetry.io/otel/trace v1.23.1 // indirect
go.opentelemetry.io/otel v1.24.0 // indirect
go.opentelemetry.io/otel/metric v1.24.0 // indirect
go.opentelemetry.io/otel/trace v1.24.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/exp v0.0.0-20240213143201-ec583247a57a // indirect
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect
golang.org/x/mod v0.15.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/text v0.14.0 // indirect

60
go.sum
View file

@ -37,14 +37,14 @@ github.com/aws/aws-sdk-go-v2 v1.25.1 h1:P7hU6A5qEdmajGwvae/zDkOq+ULLC9tQBTwqqiwF
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/config v1.27.3 h1:0PRdb/q5a77HVYj+2rvPiCObfMfl/pWhwa5cs3cnl3c=
github.com/aws/aws-sdk-go-v2/config v1.27.3/go.mod h1:WeRAr9ENap9NAegbfNsLqGQd8ERz5ypdIUx4j0/ZgKI=
github.com/aws/aws-sdk-go-v2/credentials v1.17.3 h1:dDM5wrgwOL5gTZ0Gv/bvewPldjBcJywoaO5ClERrOGE=
github.com/aws/aws-sdk-go-v2/credentials v1.17.3/go.mod h1:G96Nuaw9qJS+s3OnK8RW8VEKEOjXi8H5Jk4lC/ZyZbw=
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/feature/s3/manager v1.16.5 h1:IEv6homMJMnedG/2VWfNuV34ouXUmK8E7y4rAl59Fhs=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.5/go.mod h1:a+wq9mSuG13iSkVMR1O8VApmAISm1ca+E2RQpcB3flw=
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=
@ -61,18 +61,18 @@ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.1 h1:cVP8mng1R
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/aws-sdk-go-v2/service/marketplacemetering v1.21.0 h1:NEWfVJgrDwEXrKDNrrX+2LtWhKVLYpJ2d7/gr1N1B54=
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.21.0/go.mod h1:TzgisXFoXgCssgP11SzC6KrvcyCErz5c3w++m3xFOfo=
github.com/aws/aws-sdk-go-v2/service/s3 v1.51.0 h1:rNVsCe3bqTAhG+qjnHJKgYKdHEsqqo/GMK3gEYY8W6g=
github.com/aws/aws-sdk-go-v2/service/s3 v1.51.0/go.mod h1:lTW7O4iMAnO2o7H3XJTvqaWFZCH6zIPs+eP7RdG/yp0=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.28.0 h1:Xf3s55N9cqKvFK6D70zCXvXXN4ZovTCy7glL+gUhLEc=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.28.0/go.mod h1:RA3ERghFSivbTf0Sbsxv/grUuLMcyAjm0F/PylJMmEs=
github.com/aws/aws-sdk-go-v2/service/sso v1.20.0 h1:6YL8G91QZ52KlPrLkEgEez5kejIVwChVCgND3qgY5j0=
github.com/aws/aws-sdk-go-v2/service/sso v1.20.0/go.mod h1:x6/tCd1o/AOKQR+iYnjrzhJxD+w0xRN34asGPaSV7ew=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.0 h1:+DqIa5Ll7W311QLUvGFDdVit9uC4G0VioDdw08cXcow=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.0/go.mod h1:lZB123q0SVQ3dfIbEOcGzhQHrwVBcHVReNS9tm20oU4=
github.com/aws/aws-sdk-go-v2/service/sts v1.28.0 h1:F7tQr61zYnTaeY50Rn4jwfVQbtcqJuBRwN/nGGNwzb0=
github.com/aws/aws-sdk-go-v2/service/sts v1.28.0/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=
@ -333,8 +333,8 @@ github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlk
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos=
github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8=
github.com/prometheus/common v0.47.0 h1:p5Cz0FNHo7SnWOmWmoRozVcjEp0bIVU8cV7OShpjL1k=
github.com/prometheus/common v0.47.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
@ -419,14 +419,14 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.4
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.48.0/go.mod h1:tIKj3DbO8N9Y2xo52og3irLsPI4GW02DSMtrVgNMgxg=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0 h1:doUP+ExOpH3spVTLS0FcWGLnQrPct/hD/bCPbDRUEAU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0/go.mod h1:rdENBZMT2OE6Ne/KLwpiXudnAsbdrdBaqBvTN8M8BgA=
go.opentelemetry.io/otel v1.23.1 h1:Za4UzOqJYS+MUczKI320AtqZHZb7EqxO00jAHE0jmQY=
go.opentelemetry.io/otel v1.23.1/go.mod h1:Td0134eafDLcTS4y+zQ26GE8u3dEuRBiBCTUIRHaikA=
go.opentelemetry.io/otel/metric v1.23.1 h1:PQJmqJ9u2QaJLBOELl1cxIdPcpbwzbkjfEyelTl2rlo=
go.opentelemetry.io/otel/metric v1.23.1/go.mod h1:mpG2QPlAfnK8yNhNJAxDZruU9Y1/HubbC+KyH8FaCWI=
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8=
go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E=
go.opentelemetry.io/otel/trace v1.23.1 h1:4LrmmEd8AU2rFvU1zegmvqW7+kWarxtNOPyeL6HmYY8=
go.opentelemetry.io/otel/trace v1.23.1/go.mod h1:4IpnpJFwr1mo/6HL8XIPJaE9y0+u1KcVmuW7dwFSVrI=
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8=
go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
@ -434,8 +434,8 @@ go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN8
gocloud.dev v0.36.0 h1:q5zoXux4xkOZP473e1EZbG8Gq9f0vlg1VNH5Du/ybus=
gocloud.dev v0.36.0/go.mod h1:bLxah6JQVKBaIxzsr5BQLYB4IYdWHkMZdzCXlo6F0gg=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20240213143201-ec583247a57a h1:HinSgX1tJRX3KsL//Gxynpw5CTOAIPhgL4W8PNiIpVE=
golang.org/x/exp v0.0.0-20240213143201-ec583247a57a/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ=
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
@ -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.166.0 h1:6m4NUwrZYhAaVIHZWxaKjw1L1vNAjtMwORmKRyEEo24=
google.golang.org/api v0.166.0/go.mod h1:4FcBc686KFi7QI/U51/2GKKevfZMpM17sCdibqe/bSA=
google.golang.org/api v0.167.0 h1:CKHrQD1BLRii6xdkatBDXyKzM0mkawt2QP+H3LtPmSE=
google.golang.org/api v0.167.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=

View file

@ -58,6 +58,7 @@ const (
PermAdminManageEventRules = "manage_event_rules"
PermAdminManageRoles = "manage_roles"
PermAdminManageIPLists = "manage_ip_lists"
PermAdminDisableMFA = "disable_mfa"
)
const (
@ -75,7 +76,7 @@ var (
PermAdminCloseConnections, PermAdminViewServerStatus, PermAdminManageAdmins, PermAdminManageRoles,
PermAdminManageEventRules, PermAdminManageAPIKeys, PermAdminQuotaScans, PermAdminManageSystem,
PermAdminManageDefender, PermAdminViewDefender, PermAdminManageIPLists, PermAdminRetentionChecks,
PermAdminViewEvents}
PermAdminViewEvents, PermAdminDisableMFA}
forbiddenPermsForRoleAdmins = []string{PermAdminAny, PermAdminManageAdmins, PermAdminManageSystem,
PermAdminManageEventRules, PermAdminManageIPLists, PermAdminManageRoles}
)

View file

@ -100,6 +100,10 @@ func disableAdmin2FA(w http.ResponseWriter, r *http.Request) {
sendAPIResponse(w, r, err, "", getRespStatus(err))
return
}
if !admin.Filters.TOTPConfig.Enabled {
sendAPIResponse(w, r, nil, "two-factor authentication is not enabled", http.StatusBadRequest)
return
}
if admin.Username == claims.Username {
if admin.Filters.RequireTwoFactor {
err := util.NewValidationError("two-factor authentication must be enabled")

View file

@ -134,6 +134,10 @@ func disableUser2FA(w http.ResponseWriter, r *http.Request) {
sendAPIResponse(w, r, err, "", getRespStatus(err))
return
}
if !user.Filters.TOTPConfig.Enabled {
sendAPIResponse(w, r, nil, "two-factor authentication is not enabled", http.StatusBadRequest)
return
}
user.Filters.RecoveryCodes = nil
user.Filters.TOTPConfig = dataprovider.UserTOTPConfig{
Enabled: false,

View file

@ -6812,14 +6812,6 @@ func TestNamingRules(t *testing.T) {
rr = executeRequest(req)
checkResponseCode(t, http.StatusBadRequest, rr)
assert.Contains(t, rr.Body.String(), "the following characters are allowed")
req, err = http.NewRequest(http.MethodPut, adminPath+"/"+admin.Username+"/2fa/disable", nil)
assert.NoError(t, err)
setBearerForReq(req, adminAPIToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusBadRequest, rr)
assert.Contains(t, rr.Body.String(), "the following characters are allowed")
// test admin reset password
form = make(url.Values)
form.Set("username", admin.Username)
@ -9086,6 +9078,18 @@ func TestAdminTwoFactorLogin(t *testing.T) {
admin.Password = altAdminPassword
admin, _, err := httpdtest.AddAdmin(admin, http.StatusCreated)
assert.NoError(t, err)
admin1 := getTestAdmin()
admin1.Username = altAdminUsername + "1"
admin1.Password = altAdminPassword
var permissions []string
for _, p := range admin1.GetValidPerms() {
if p != dataprovider.PermAdminAny && p != dataprovider.PermAdminDisableMFA {
permissions = append(permissions, p)
}
}
admin1.Permissions = permissions
admin1, _, err = httpdtest.AddAdmin(admin1, http.StatusCreated)
assert.NoError(t, err)
// enable two factor authentication
configName, key, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], admin.Username)
assert.NoError(t, err)
@ -9369,12 +9373,27 @@ func TestAdminTwoFactorLogin(t *testing.T) {
assert.NoError(t, err)
// disable TOTP
altToken1, err := getJWTAPITokenFromTestServer(admin1.Username, altAdminPassword)
assert.NoError(t, err)
req, err = http.NewRequest(http.MethodPut, adminPath+"/"+altAdminUsername+"/2fa/disable", nil)
assert.NoError(t, err)
setBearerForReq(req, altToken1)
rr = executeRequest(req)
checkResponseCode(t, http.StatusForbidden, rr)
req, err = http.NewRequest(http.MethodPut, adminPath+"/"+altAdminUsername+"/2fa/disable", nil)
assert.NoError(t, err)
setBearerForReq(req, altToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
req, err = http.NewRequest(http.MethodPut, adminPath+"/"+altAdminUsername+"/2fa/disable", nil)
assert.NoError(t, err)
setBearerForReq(req, altToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusBadRequest, rr)
assert.Contains(t, rr.Body.String(), "two-factor authentication is not enabled")
form = make(url.Values)
form.Set("recovery_code", recoveryCode)
form.Set(csrfFormToken, csrfToken)
@ -9399,6 +9418,8 @@ func TestAdminTwoFactorLogin(t *testing.T) {
_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)
assert.NoError(t, err)
_, err = httpdtest.RemoveAdmin(admin1, http.StatusOK)
assert.NoError(t, err)
req, err = http.NewRequest(http.MethodPost, webAdminTwoFactorRecoveryPath, bytes.NewBuffer([]byte(form.Encode())))
assert.NoError(t, err)
@ -10107,6 +10128,13 @@ func TestWebUserTwoFactorLogin(t *testing.T) {
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
req, err = http.NewRequest(http.MethodPut, userPath+"/"+user.Username+"/2fa/disable", nil)
assert.NoError(t, err)
setBearerForReq(req, adminToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusBadRequest, rr)
assert.Contains(t, rr.Body.String(), "two-factor authentication is not enabled")
form = make(url.Values)
form.Set("recovery_code", recoveryCode)
form.Set("passcode", passcode)

View file

@ -1364,7 +1364,7 @@ func (s *httpdServer) initializeRouter() {
router.With(s.checkPerm(dataprovider.PermAdminViewUsers)).Get(userPath+"/{username}", getUserByUsername) //nolint:goconst
router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Put(userPath+"/{username}", updateUser)
router.With(s.checkPerm(dataprovider.PermAdminDeleteUsers)).Delete(userPath+"/{username}", deleteUser)
router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Put(userPath+"/{username}/2fa/disable", disableUser2FA)
router.With(s.checkPerm(dataprovider.PermAdminDisableMFA)).Put(userPath+"/{username}/2fa/disable", disableUser2FA) //nolint:goconst
router.With(s.checkPerm(dataprovider.PermAdminManageFolders)).Get(folderPath, getFolders)
router.With(s.checkPerm(dataprovider.PermAdminManageFolders)).Get(folderPath+"/{name}", getFolderByName) //nolint:goconst
router.With(s.checkPerm(dataprovider.PermAdminManageFolders)).Post(folderPath, addFolder)
@ -1392,7 +1392,7 @@ func (s *httpdServer) initializeRouter() {
router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Get(adminPath+"/{username}", getAdminByUsername)
router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Put(adminPath+"/{username}", updateAdmin)
router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Delete(adminPath+"/{username}", deleteAdmin)
router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Put(adminPath+"/{username}/2fa/disable", disableAdmin2FA)
router.With(s.checkPerm(dataprovider.PermAdminDisableMFA)).Put(adminPath+"/{username}/2fa/disable", disableAdmin2FA)
router.With(s.checkPerm(dataprovider.PermAdminRetentionChecks)).Get(retentionChecksPath, getRetentionChecks)
router.With(s.checkPerm(dataprovider.PermAdminRetentionChecks)).Post(retentionBasePath+"/{username}/check",
startRetentionCheck)
@ -1752,6 +1752,8 @@ func (s *httpdServer) setupWebAdminRoutes() {
s.handleWebUpdateAdminPost)
router.With(s.checkPerm(dataprovider.PermAdminManageAdmins), verifyCSRFHeader).
Delete(webAdminPath+"/{username}", deleteAdmin)
router.With(s.checkPerm(dataprovider.PermAdminDisableMFA), verifyCSRFHeader).
Put(webAdminPath+"/{username}/2fa/disable", disableAdmin2FA)
router.With(s.checkPerm(dataprovider.PermAdminCloseConnections), verifyCSRFHeader).
Delete(webConnectionsPath+"/{connectionID}", handleCloseConnection)
router.With(s.checkPerm(dataprovider.PermAdminManageFolders), s.refreshCookie).
@ -1764,6 +1766,8 @@ func (s *httpdServer) setupWebAdminRoutes() {
Post(webScanVFolderPath+"/{name}", startFolderQuotaScan)
router.With(s.checkPerm(dataprovider.PermAdminDeleteUsers), verifyCSRFHeader).
Delete(webUserPath+"/{username}", deleteUser)
router.With(s.checkPerm(dataprovider.PermAdminDisableMFA), verifyCSRFHeader).
Put(webUserPath+"/{username}/2fa/disable", disableUser2FA)
router.With(s.checkPerm(dataprovider.PermAdminQuotaScans), verifyCSRFHeader).
Post(webQuotaScanPath+"/{username}", startUserQuotaScan)
router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Get(webMaintenancePath, s.handleWebMaintenance)

View file

@ -4948,6 +4948,7 @@ components:
- manage_event_rules
- manage_roles
- manage_ip_lists
- disable_mfa
description: |
Admin permissions:
* `*` - all permissions are granted
@ -4971,6 +4972,7 @@ components:
* `manage_event_rules` - manage event actions and rules is allowed
* `manage_roles` - manage roles is allowed
* `manage_ip_lists` - manage global and ratelimter allow lists and defender block and safe lists is allowed
* `disable_mfa` - allow to disable two-factor authentication for users and admins
FsProviders:
type: integer
enum:

View file

@ -132,6 +132,7 @@
"add": "Add",
"enable": "Enable",
"disable": "Disable",
"disable_confirm_btn": "Yes, disable",
"close": "Close",
"search": "Search",
"configuration": "Configuration",
@ -403,6 +404,8 @@
"setup_title": "Set up two-factor authentication",
"setup_msg": "Use your preferred Authenticator App (e.g. Microsoft Authenticator, Google Authenticator, Authy, 1Password etc. ) to scan the QR code. It will generate an authentication code for you to enter below.",
"setup_help": "If you have trouble using the QR code, select manual entry on your app, and enter the code:",
"disable_msg": "Disable 2FA",
"disable_confirm": "Do you want to disable two-factor authentication for the selected user? This action is generally only required if the user has lost access to the second authentication factor",
"disable_question": "Do you want to disable two-factor authentication?",
"generate_question": "Do you want to generate a new secret key and invalidate the previous one? Any registered Authenticator App will stop working",
"disabled": "Two-factor authentication is disabled",
@ -410,7 +413,7 @@
"recovery_codes_get_err": "Unable to obtain recovery codes",
"auth_code_invalid": "Failed to validate the provided authentication code",
"auth_secret_gen_err": "Failed to generate authentication secret",
"save_err": "Failed to save configuration",
"save_err": "Failed to save two-factor authentication configuration",
"auth_code_required": "The authentication code is required",
"no_protocol": "Please select at least a protocol",
"required_protocols": "Unable to disable two-factor authentication. The security policy configured for your account requires two-factor authentication for the following protocols: {{val}}",

View file

@ -132,6 +132,7 @@
"add": "Aggiungi",
"enable": "Abilita",
"disable": "Disabilita",
"disable_confirm_btn": "Si, disabilita",
"close": "Chiudi",
"search": "Cerca",
"configuration": "Configurazione",
@ -403,6 +404,8 @@
"setup_title": "Configura l'autenticazione a due fattori",
"setup_msg": "Utilizza la tua app di autenticazione preferita (ad esempio Microsoft Authenticator, Google Authenticator, Authy, 1Password ecc.) per scansionare il codice QR. Verrà generato un codice di autenticazione da inserire di seguito.",
"setup_help": "Se hai problemi con l'utilizzo del codice QR, seleziona l'inserimento manuale sulla tua app e inserisci il codice:",
"disable_msg": "Disabilita 2FA",
"disable_confirm": "Vuoi disabilitare l'autenticazione a due fattori per l'utente selezionato? Questa azione è generalmente necessaria solo se l'utente ha perso l'accesso al secondo fattore di autenticazione",
"disable_question": "Vuoi disattivare l'autenticazione a due fattori?",
"generate_question": "Vuoi generare un nuova chiave segreta e invalidare quella precedente? Qualsiasi app di autenticazione registrata smetterà di funzionare",
"disabled": "L'autenticazione a due fattori è disabilitata",
@ -410,7 +413,7 @@
"recovery_codes_get_err": "Impossibile ottenere i codici di ripristino",
"auth_code_invalid": "Impossibile convalidare il codice di autenticazione fornito",
"auth_secret_gen_err": "Impossibile generare il segreto di autenticazione",
"save_err": "Impossibile salvare la configurazione",
"save_err": "Impossibile salvare la configurazione dell'autenticazione a due fattori",
"auth_code_required": "Il codice di autenticazione è obbligatorio",
"no_protocol": "Seleziona almeno un protocollo",
"required_protocols": "Impossibile disabilitare l'autenticazione a due fattori. La politica di sicurezza configurata per il tuo account richiede l'autenticazione a due fattori per i seguenti protocolli: {{val}}",

View file

@ -175,6 +175,47 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
});
}
function disableSeconFactorAction(username) {
ModalAlert.fire({
text: $.t('2fa.disable_confirm'),
icon: "warning",
confirmButtonText: $.t('general.disable_confirm_btn'),
cancelButtonText: $.t('general.cancel'),
customClass: {
confirmButton: "btn btn-danger",
cancelButton: 'btn btn-secondary'
}
}).then((result) => {
if (result.isConfirmed){
$('#loading_message').text("");
KTApp.showPageLoading();
let path = '{{.AdminURL}}' + "/" + encodeURIComponent(username)+"/2fa/disable";
axios.put(path, null, {
timeout: 15000,
headers: {
'X-CSRF-TOKEN': '{{.CSRFToken}}'
},
validateStatus: function (status) {
return status == 200;
}
}).then(function(response){
location.reload();
}).catch(function(error){
KTApp.hidePageLoading();
ModalAlert.fire({
text: $.t('2fa.save_err'),
icon: "warning",
confirmButtonText: $.t('general.ok'),
customClass: {
confirmButton: "btn btn-primary"
}
});
});
}
});
}
var datatable = function(){
var dt;
@ -273,17 +314,17 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
}
},
{
data: "filters.totp_config.enabled",
data: "filters.totp_config",
visible: false,
defaultContent: false,
render: function(data, type, row) {
if (type === 'display') {
if (data){
if (data && data.enabled){
return $.t('general.active');
}
return $.t('general.inactive')
}
return data;
return data && data.enabled;
}
},
{
@ -307,20 +348,39 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
className: 'text-end',
render: function (data, type, row) {
if (type === 'display') {
//{{- if .LoggedUser.HasPermission "manage_admins"}}
return `<button class="btn btn-light btn-active-light-primary btn-flex btn-center btn-sm rotate" data-kt-menu-trigger="click" data-kt-menu-placement="bottom-end">
let numActions = 0;
let username = "{{.LoggedUser.Username}}";
let actions = `<button class="btn btn-light btn-active-light-primary btn-flex btn-center btn-sm rotate" data-kt-menu-trigger="click" data-kt-menu-placement="bottom-end">
<span data-i18n="general.actions" class="fs-6">Actions</span>
<i class="ki-duotone ki-down fs-5 ms-1 rotate-180"></i>
</button>
<div class="menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-700 menu-state-bg-light-primary fw-semibold fs-6 w-200px py-4" data-kt-menu="true">
<div class="menu-item px-3">
<div class="menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-700 menu-state-bg-light-primary fw-semibold fs-6 w-200px py-4" data-kt-menu="true">`;
//{{- if .LoggedUser.HasPermission "manage_admins"}}
numActions++;
actions+=`<div class="menu-item px-3">
<a data-i18n="general.edit" href="#" class="menu-link px-3" data-table-action="edit_row">Edit</a>
</div>
<div class="menu-item px-3">
<a data-i18n="general.delete" href="#" class="menu-link text-danger px-3" data-table-action="delete_row">Delete</a>
</div>
</div>`;
//{{- end}}
//{{- if .LoggedUser.HasPermission "disable_mfa"}}
if (row.filters.totp_config && row.filters.totp_config.enabled && username != row.username){
numActions++;
actions+=`<div class="menu-item px-3">
<a data-i18n="2fa.disable_msg" href="#" class="menu-link text-danger px-3" data-table-action="disable_2fa_row">Disable 2FA</a>
</div>`;
}
//{{- end}}
//{{- if .LoggedUser.HasPermission "manage_admins"}}
if (username != row.username){
numActions++;
actions+=`<div class="menu-item px-3">
<a data-i18n="general.delete" href="#" class="menu-link text-danger px-3" data-table-action="delete_row">Delete</a>
</div>`;
}
//{{- end}}
if (numActions > 0){
actions+=`</div>`;
return actions;
}
}
return "";
}
@ -406,6 +466,17 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
});
});
const diable2FAButtons = document.querySelectorAll('[data-table-action="disable_2fa_row"]');
diable2FAButtons.forEach(d => {
let el = $(d);
el.off("click");
el.on("click", function(e){
e.preventDefault();
let rowData = dt.row(e.target.closest('tr')).data();
disableSeconFactorAction(rowData['username']);
});
});
const deleteButtons = document.querySelectorAll('[data-table-action="delete_row"]');
deleteButtons.forEach(d => {
let el = $(d);

View file

@ -230,11 +230,11 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
numActions++;
actions+=`<div class="menu-item px-3">
<a data-i18n="general.edit" href="#" class="menu-link px-3" data-table-action="edit_row">Edit</a>
</div>`
</div>`;
numActions++;
actions+=`<div class="menu-item px-3">
<a data-i18n="general.delete" href="#" class="menu-link text-danger px-3" data-table-action="delete_row">Delete</a>
</div>`
</div>`;
//{{- end}}
if (numActions > 0){
actions+=`</div>`;

View file

@ -286,17 +286,17 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
numActions++;
actions+=`<div class="menu-item px-3">
<a data-i18n="general.edit" href="#" class="menu-link px-3" data-table-action="edit_row">Edit</a>
</div>`
</div>`;
numActions++;
if (row.trigger === 6){
actions+=`<div class="menu-item px-3">
<a data-i18n="rules.run" href="#" class="menu-link px-3" data-table-action="run_row">Run</a>
</div>`
</div>`;
numActions++;
}
actions+=`<div class="menu-item px-3">
<a data-i18n="general.delete" href="#" class="menu-link text-danger px-3" data-table-action="delete_row">Delete</a>
</div>`
</div>`;
//{{- end}}
if (numActions > 0){
actions+=`</div>`;

View file

@ -287,19 +287,19 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
numActions++;
actions+=`<div class="menu-item px-3">
<a data-i18n="general.edit" href="#" class="menu-link px-3" data-table-action="edit_row">Edit</a>
</div>`
</div>`;
//{{- end}}
//{{- if .LoggedUser.HasPermission "manage_system"}}
numActions++;
actions+=`<div class="menu-item px-3">
<a data-i18n="general.template" href="#" class="menu-link px-3" data-table-action="template_row">Template</a>
</div>`
</div>`;
//{{- end}}
//{{- if .LoggedUser.HasPermission "manage_folders"}}
numActions++;
actions+=`<div class="menu-item px-3">
<a data-i18n="general.delete" href="#" class="menu-link text-danger px-3" data-table-action="delete_row">Delete</a>
</div>`
</div>`;
//{{- end}}
if (numActions > 0){
actions+=`</div>`;

View file

@ -228,11 +228,11 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
numActions++;
actions+=`<div class="menu-item px-3">
<a data-i18n="general.edit" href="#" class="menu-link px-3" data-table-action="edit_row">Edit</a>
</div>`
</div>`;
numActions++;
actions+=`<div class="menu-item px-3">
<a data-i18n="general.delete" href="#" class="menu-link text-danger px-3" data-table-action="delete_row">Delete</a>
</div>`
</div>`;
//{{- end}}
if (numActions > 0){
actions+=`</div>`;

View file

@ -229,11 +229,11 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
numActions++;
actions+=`<div class="menu-item px-3">
<a data-i18n="general.edit" href="#" class="menu-link px-3" data-table-action="edit_row">Edit</a>
</div>`
</div>`;
numActions++;
actions+=`<div class="menu-item px-3">
<a data-i18n="general.delete" href="#" class="menu-link text-danger px-3" data-table-action="delete_row">Delete</a>
</div>`
</div>`;
//{{- end}}
if (numActions > 0){
actions+=`</div>`;

View file

@ -188,6 +188,47 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
});
}
function disableSeconFactorAction(username) {
ModalAlert.fire({
text: $.t('2fa.disable_confirm'),
icon: "warning",
confirmButtonText: $.t('general.disable_confirm_btn'),
cancelButtonText: $.t('general.cancel'),
customClass: {
confirmButton: "btn btn-danger",
cancelButton: 'btn btn-secondary'
}
}).then((result) => {
if (result.isConfirmed){
$('#loading_message').text("");
KTApp.showPageLoading();
let path = '{{.UserURL}}' + "/" + encodeURIComponent(username)+"/2fa/disable";
axios.put(path, null, {
timeout: 15000,
headers: {
'X-CSRF-TOKEN': '{{.CSRFToken}}'
},
validateStatus: function (status) {
return status == 200;
}
}).then(function(response){
location.reload();
}).catch(function(error){
KTApp.hidePageLoading();
ModalAlert.fire({
text: $.t('2fa.save_err'),
icon: "warning",
confirmButtonText: $.t('general.ok'),
customClass: {
confirmButton: "btn btn-primary"
}
});
});
}
});
}
function quotaScanAction(username) {
$('#loading_message').text("");
KTApp.showPageLoading();
@ -359,17 +400,20 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
}
},
{
data: "filters.two_factor_protocols",
data: "filters.totp_config",
visible: false,
defaultContent: "",
render: function(data, type, row) {
let protocols = "";
if (data && data.enabled){
protocols = data.protocols.join(', ');
}
if (type === 'display') {
if (data){
return escapeHTML(data.join(', '));
if (protocols){
return escapeHTML(protocols);
}
return ""
}
return data;
return protocols;
}
},
{
@ -486,25 +530,33 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
numActions++;
actions+=`<div class="menu-item px-3">
<a data-i18n="general.edit" href="#" class="menu-link px-3" data-table-action="edit_row">Edit</a>
</div>`
</div>`;
//{{- end}}
//{{- if .LoggedUser.HasPermission "manage_system"}}
numActions++;
actions+=`<div class="menu-item px-3">
<a data-i18n="general.template" href="#" class="menu-link px-3" data-table-action="template_row">Template</a>
</div>`
</div>`;
//{{- end}}
//{{- if .LoggedUser.HasPermission "quota_scans"}}
numActions++;
actions+=`<div class="menu-item px-3">
<a data-i18n="general.quota_scan" href="#" class="menu-link px-3" data-table-action="quota_scan_row">Quota scan</a>
</div>`
</div>`;
//{{- end}}
//{{- if .LoggedUser.HasPermission "disable_mfa"}}
if (row.filters.totp_config && row.filters.totp_config.enabled){
numActions++;
actions+=`<div class="menu-item px-3">
<a data-i18n="2fa.disable_msg" href="#" class="menu-link text-danger px-3" data-table-action="disable_2fa_row">Disable 2FA</a>
</div>`;
}
//{{- end}}
//{{- if .LoggedUser.HasPermission "del_users"}}
numActions++;
actions+=`<div class="menu-item px-3">
<a data-i18n="general.delete" href="#" class="menu-link text-danger px-3" data-table-action="delete_row">Delete</a>
</div>`
</div>`;
//{{- end}}
if (numActions > 0){
actions+=`</div>`;
@ -620,6 +672,17 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
});
});
const diable2FAButtons = document.querySelectorAll('[data-table-action="disable_2fa_row"]');
diable2FAButtons.forEach(d => {
let el = $(d);
el.off("click");
el.on("click", function(e){
e.preventDefault();
let rowData = dt.row(e.target.closest('tr')).data();
disableSeconFactorAction(rowData['username']);
});
});
const deleteButtons = document.querySelectorAll('[data-table-action="delete_row"]');
deleteButtons.forEach(d => {
let el = $(d);