rework user and admin profiles

users and admins can now also update their email and description
This commit is contained in:
Nicola Murino 2021-09-29 18:46:15 +02:00
parent af8fa7ff81
commit ba1febba73
No known key found for this signature in database
GPG key ID: 2F1FB59433D5A8CB
25 changed files with 1038 additions and 798 deletions

View file

@ -45,7 +45,7 @@ jobs:
go build -trimpath -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/version.commit=$GIT_COMMIT -X github.com/drakkan/sftpgo/v2/version.date=$DATE_TIME" -o sftpgo.exe
- name: Run test cases using SQLite provider
run: go test -v -p 1 -timeout 10m ./... -coverprofile=coverage.txt -covermode=atomic
run: go test -v -p 1 -timeout 15m ./... -coverprofile=coverage.txt -covermode=atomic
- name: Upload coverage to Codecov
if: ${{ matrix.upload-coverage }}
@ -69,7 +69,7 @@ jobs:
SFTPGO_DATA_PROVIDER__NAME: 'sftpgo_bolt.db'
- name: Run test cases using memory provider
run: go test -v -p 1 -timeout 10m ./... -covermode=atomic
run: go test -v -p 1 -timeout 15m ./... -covermode=atomic
env:
SFTPGO_DATA_PROVIDER__DRIVER: memory
SFTPGO_DATA_PROVIDER__NAME: ''
@ -108,7 +108,7 @@ jobs:
path: output
test-goarch-386:
name: Run test cases on 32 bit arch
name: Run test cases on 32-bit arch
runs-on: ubuntu-latest
steps:
@ -125,7 +125,7 @@ jobs:
GOARCH: 386
- name: Run test cases
run: go test -v -p 1 -timeout 10m ./... -covermode=atomic
run: go test -v -p 1 -timeout 15m ./... -covermode=atomic
env:
SFTPGO_DATA_PROVIDER__DRIVER: memory
SFTPGO_DATA_PROVIDER__NAME: ''
@ -177,7 +177,7 @@ jobs:
- name: Run tests using PostgreSQL provider
run: |
go test -v -p 1 -timeout 10m ./... -covermode=atomic
go test -v -p 1 -timeout 15m ./... -covermode=atomic
env:
SFTPGO_DATA_PROVIDER__DRIVER: postgresql
SFTPGO_DATA_PROVIDER__NAME: sftpgo
@ -188,7 +188,7 @@ jobs:
- name: Run tests using MySQL provider
run: |
go test -v -p 1 -timeout 10m ./... -covermode=atomic
go test -v -p 1 -timeout 15m ./... -covermode=atomic
env:
SFTPGO_DATA_PROVIDER__DRIVER: mysql
SFTPGO_DATA_PROVIDER__NAME: sftpgo
@ -201,7 +201,7 @@ jobs:
run: |
docker run --rm --name crdb --health-cmd "curl -I http://127.0.0.1:8080" --health-interval 10s --health-timeout 5s --health-retries 6 -p 26257:26257 -d cockroachdb/cockroach:latest start-single-node --insecure --listen-addr 0.0.0.0:26257
docker exec crdb cockroach sql --insecure -e 'create database "sftpgo"'
go test -v -p 1 -timeout 10m ./... -covermode=atomic
go test -v -p 1 -timeout 15m ./... -covermode=atomic
docker stop crdb
env:
SFTPGO_DATA_PROVIDER__DRIVER: cockroachdb

View file

@ -736,6 +736,11 @@ func (u *User) CanChangeAPIKeyAuth() bool {
return !util.IsStringInSlice(sdk.WebClientAPIKeyAuthChangeDisabled, u.Filters.WebClient)
}
// CanChangeInfo returns true if this user is allowed to change its info such as email and description
func (u *User) CanChangeInfo() bool {
return !util.IsStringInSlice(sdk.WebClientInfoChangeDisabled, u.Filters.WebClient)
}
// CanManagePublicKeys returns true if this user is allowed to manage public keys
// from the web client. Used in web client UI
func (u *User) CanManagePublicKeys() bool {

View file

@ -252,9 +252,9 @@ The configuration file contains the following sections:
- `url`, string, optional. If not empty, the header will be added only if the request URL starts with the one specified here
- **kms**, configuration for the Key Management Service, more details can be found [here](./kms.md)
- `secrets`
- `url`, string. Defines the URI to the KMS service. Default empty.
- `master_key`, string. Defines the master encryption key as string. If not empty, it takes precedence over `master_key_path`. Default empty.
- `master_key_path, string. Defines the absolute path to a file containing the master encryption key. Default empty.
- `url`, string. Defines the URI to the KMS service. Default: empty.
- `master_key`, string. Defines the master encryption key as string. If not empty, it takes precedence over `master_key_path`. Default: empty.
- `master_key_path, string. Defines the absolute path to a file containing the master encryption key. Default: empty.
- **mfa**, multi-factor authentication settings
- `totp`, list of struct that define settings for time-based one time passwords (RFC 6238). Each struct has the following fields:
- `name`, string. Unique configuration name. This name should not be changed if there are users or admins using the configuration. The name is not exposed to the authentication apps. Default: `Default`.

16
go.mod
View file

@ -7,8 +7,8 @@ require (
github.com/Azure/azure-storage-blob-go v0.14.0
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
github.com/alexedwards/argon2id v0.0.0-20210511081203-7d35d68092b8
github.com/aws/aws-sdk-go v1.40.49
github.com/cockroachdb/cockroach-go/v2 v2.1.1
github.com/aws/aws-sdk-go v1.40.51
github.com/cockroachdb/cockroach-go/v2 v2.2.0
github.com/eikenb/pipeat v0.0.0-20210603033007-44fc3ffce52b
github.com/fatih/color v1.13.0 // indirect
github.com/fclairamb/ftpserverlib v0.16.0
@ -43,7 +43,7 @@ require (
github.com/pkg/sftp v1.13.4
github.com/pquerna/otp v1.3.0
github.com/prometheus/client_golang v1.11.0
github.com/prometheus/common v0.31.0 // indirect
github.com/prometheus/common v0.31.1 // indirect
github.com/rs/cors v1.8.0
github.com/rs/xid v1.3.0
github.com/rs/zerolog v1.25.0
@ -62,17 +62,17 @@ require (
gocloud.dev v0.24.0
golang.org/x/crypto v0.0.0-20210915214749-c084706c2272
golang.org/x/net v0.0.0-20210924151903-3ad01bbaa167
golang.org/x/sys v0.0.0-20210927052749-1cf2251ac284
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac
google.golang.org/api v0.57.0
google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0 // indirect
google.golang.org/api v0.58.0
google.golang.org/genproto v0.0.0-20210928142010-c7af6a1a74c9 // indirect
google.golang.org/grpc v1.41.0
google.golang.org/protobuf v1.27.1
gopkg.in/natefinch/lumberjack.v2 v2.0.0
)
require (
cloud.google.com/go v0.95.0 // indirect
cloud.google.com/go v0.96.0 // indirect
github.com/Azure/azure-pipeline-go v0.2.3 // indirect
github.com/StackExchange/wmi v1.2.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
@ -85,7 +85,7 @@ require (
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0 // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/go-ole/go-ole v1.2.5 // indirect
github.com/goccy/go-json v0.7.8 // indirect
github.com/goccy/go-json v0.7.9 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.6 // indirect

34
go.sum
View file

@ -32,8 +32,8 @@ cloud.google.com/go v0.92.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+Y
cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=
cloud.google.com/go v0.94.0/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=
cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=
cloud.google.com/go v0.95.0 h1:JVWssQIj9cLwHmLjqWLptFa83o7HgqUictM6eyvGWJE=
cloud.google.com/go v0.95.0/go.mod h1:MzZUAH870Y7E+c14j23Ir66FC1+PK8WLG7OG4SjP+0k=
cloud.google.com/go v0.96.0 h1:r9XIwQ9FrJspMjHulRm1kl1uanw5gSolzSK+dukeH0E=
cloud.google.com/go v0.96.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
@ -136,8 +136,8 @@ github.com/aws/aws-sdk-go v1.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZo
github.com/aws/aws-sdk-go v1.37.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
github.com/aws/aws-sdk-go v1.38.68/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
github.com/aws/aws-sdk-go v1.40.34/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
github.com/aws/aws-sdk-go v1.40.49 h1:kIbJYc4FZA2r4yxNU5giIR4HHLRkG9roFReWAsk0ZVQ=
github.com/aws/aws-sdk-go v1.40.49/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
github.com/aws/aws-sdk-go v1.40.51 h1:FfxDcjWqhMGwy+raf5Zf6AH8qsHIl9YG2dvJIBx1Aw4=
github.com/aws/aws-sdk-go v1.40.51/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
github.com/aws/aws-sdk-go-v2 v1.7.0/go.mod h1:tb9wi5s61kTDA5qCkcDbt3KRVV74GGslQkl/DRdX/P4=
github.com/aws/aws-sdk-go-v2 v1.9.0/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4=
github.com/aws/aws-sdk-go-v2/config v1.7.0/go.mod h1:w9+nMZ7soXCe5nT46Ri354SNhXDQ6v+V5wqDjnZE+GY=
@ -184,8 +184,8 @@ github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnht
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/cockroachdb/cockroach-go/v2 v2.1.1 h1:3XzfSMuUT0wBe1a3o5C0eOTcArhmmFAg2Jzh/7hhKqo=
github.com/cockroachdb/cockroach-go/v2 v2.1.1/go.mod h1:7NtUnP6eK+l6k483WSYNrq3Kb23bWV10IRV1TyeSpwM=
github.com/cockroachdb/cockroach-go/v2 v2.2.0 h1:/5znzg5n373N/3ESjHF5SMLxiW4RKB05Ql//KWfeTFs=
github.com/cockroachdb/cockroach-go/v2 v2.2.0/go.mod h1:u3MiKYGupPPjkn3ozknpMUpxPaNLTFWAya419/zv6eI=
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
@ -288,9 +288,12 @@ github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
github.com/goccy/go-json v0.7.6/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.7.8 h1:CvMH7LotYymYuLGEohBM1lTZWX4g6jzWUUl2aLFuBoE=
github.com/goccy/go-json v0.7.8/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.7.9 h1:mSp3uo1tr6MXQTYopSNhHTUnJhd2zQ4Yk+HdJZP+ZRY=
github.com/goccy/go-json v0.7.9/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
@ -681,8 +684,8 @@ github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6T
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/common v0.31.0 h1:FTJdLTjtrh4dXlCjpzdZJXMnejSTL5F/nVQm5sNwD34=
github.com/prometheus/common v0.31.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
github.com/prometheus/common v0.31.1 h1:d18hG4PkHnNAKNMOmFuXFaiY8Us0nird/2m60uS1AMs=
github.com/prometheus/common v0.31.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
@ -954,8 +957,9 @@ golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927052749-1cf2251ac284 h1:lBPNCmq8u4zFP3huKCmUQ2Fx8kcY4X+O12UgGnyKsrg=
golang.org/x/sys v0.0.0-20210927052749-1cf2251ac284/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 h1:foEbQz/B0Oz6YIqu/69kfXPYeFQAuuMYFkjaqXzl5Wo=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -1079,8 +1083,9 @@ google.golang.org/api v0.52.0/go.mod h1:Him/adpjt0sxtkWViy0b6xyKW/SD71CwdJ7HqJo7
google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k=
google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
google.golang.org/api v0.57.0 h1:4t9zuDlHLcIx0ZEhmXEeFVCRsiOgpgn2QOH9N0MNjPI=
google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI=
google.golang.org/api v0.58.0 h1:MDkAbYIB1JpSgCTOCYYoIec/coMlKK4oVbpnBLLcyT0=
google.golang.org/api v0.58.0/go.mod h1:cAbP2FsxoGVNwtgNAmmn3y5G1TWAiVYRmg4yku3lv+E=
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.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@ -1152,9 +1157,10 @@ google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEc
google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210921142501-181ce0d877f6/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0 h1:5Tbluzus3QxoAJx4IefGt1W0HQZW4nuMrVk684jI74Q=
google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20210928142010-c7af6a1a74c9 h1:XTH066D35LyHehRwlYhoK3qA+Hcgvg5xREG4kFQEW1Y=
google.golang.org/genproto v0.0.0-20210928142010-c7af6a1a74c9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=

View file

@ -155,7 +155,7 @@ func deleteAdmin(w http.ResponseWriter, r *http.Request) {
sendAPIResponse(w, r, err, "Admin deleted", http.StatusOK)
}
func getAdminAPIKeyAuthStatus(w http.ResponseWriter, r *http.Request) {
func getAdminProfile(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
claims, err := getTokenClaims(r)
if err != nil || claims.Username == "" {
@ -167,13 +167,17 @@ func getAdminAPIKeyAuthStatus(w http.ResponseWriter, r *http.Request) {
sendAPIResponse(w, r, err, "", getRespStatus(err))
return
}
resp := apiKeyAuth{
AllowAPIKeyAuth: admin.Filters.AllowAPIKeyAuth,
resp := adminProfile{
baseProfile: baseProfile{
Email: admin.Email,
Description: admin.Description,
AllowAPIKeyAuth: admin.Filters.AllowAPIKeyAuth,
},
}
render.JSON(w, r, resp)
}
func changeAdminAPIKeyAuthStatus(w http.ResponseWriter, r *http.Request) {
func updateAdminProfile(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
claims, err := getTokenClaims(r)
if err != nil || claims.Username == "" {
@ -185,18 +189,20 @@ func changeAdminAPIKeyAuthStatus(w http.ResponseWriter, r *http.Request) {
sendAPIResponse(w, r, err, "", getRespStatus(err))
return
}
var req apiKeyAuth
var req adminProfile
err = render.DecodeJSON(r.Body, &req)
if err != nil {
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
return
}
admin.Email = req.Email
admin.Description = req.Description
admin.Filters.AllowAPIKeyAuth = req.AllowAPIKeyAuth
if err := dataprovider.UpdateAdmin(&admin); err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err))
return
}
sendAPIResponse(w, r, err, "API key authentication status updated", http.StatusOK)
sendAPIResponse(w, r, err, "Profile updated", http.StatusOK)
}
func changeAdminPassword(w http.ResponseWriter, r *http.Request) {

View file

@ -349,7 +349,7 @@ func setUserPublicKeys(w http.ResponseWriter, r *http.Request) {
sendAPIResponse(w, r, err, "Public keys updated", http.StatusOK)
}
func getUserAPIKeyAuthStatus(w http.ResponseWriter, r *http.Request) {
func getUserProfile(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
claims, err := getTokenClaims(r)
if err != nil || claims.Username == "" {
@ -361,20 +361,25 @@ func getUserAPIKeyAuthStatus(w http.ResponseWriter, r *http.Request) {
sendAPIResponse(w, r, err, "", getRespStatus(err))
return
}
resp := apiKeyAuth{
AllowAPIKeyAuth: user.Filters.AllowAPIKeyAuth,
resp := userProfile{
baseProfile: baseProfile{
Email: user.Email,
Description: user.Description,
AllowAPIKeyAuth: user.Filters.AllowAPIKeyAuth,
},
PublicKeys: user.PublicKeys,
}
render.JSON(w, r, resp)
}
func changeUserAPIKeyAuthStatus(w http.ResponseWriter, r *http.Request) {
func updateUserProfile(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
claims, err := getTokenClaims(r)
if err != nil || claims.Username == "" {
sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
return
}
var req apiKeyAuth
var req userProfile
err = render.DecodeJSON(r.Body, &req)
if err != nil {
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
@ -385,12 +390,25 @@ func changeUserAPIKeyAuthStatus(w http.ResponseWriter, r *http.Request) {
sendAPIResponse(w, r, err, "", getRespStatus(err))
return
}
user.Filters.AllowAPIKeyAuth = req.AllowAPIKeyAuth
if !user.CanManagePublicKeys() && !user.CanChangeAPIKeyAuth() && !user.CanChangeInfo() {
sendAPIResponse(w, r, nil, "You are not allowed to change anything", http.StatusForbidden)
return
}
if user.CanManagePublicKeys() {
user.PublicKeys = req.PublicKeys
}
if user.CanChangeAPIKeyAuth() {
user.Filters.AllowAPIKeyAuth = req.AllowAPIKeyAuth
}
if user.CanChangeInfo() {
user.Email = req.Email
user.Description = req.Description
}
if err := dataprovider.UpdateUser(&user); err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err))
return
}
sendAPIResponse(w, r, err, "API key authentication status updated", http.StatusOK)
sendAPIResponse(w, r, err, "Profile updated", http.StatusOK)
}
func changeUserPassword(w http.ResponseWriter, r *http.Request) {

View file

@ -28,8 +28,19 @@ type pwdChange struct {
NewPassword string `json:"new_password"`
}
type apiKeyAuth struct {
AllowAPIKeyAuth bool `json:"allow_api_key_auth"`
type baseProfile struct {
Email string `json:"email,omitempty"`
Description string `json:"description,omitempty"`
AllowAPIKeyAuth bool `json:"allow_api_key_auth"`
}
type adminProfile struct {
baseProfile
}
type userProfile struct {
baseProfile
PublicKeys []string `json:"public_keys,omitempty"`
}
func sendAPIResponse(w http.ResponseWriter, r *http.Request, err error, message string, code int) {

View file

@ -31,104 +31,101 @@ import (
)
const (
logSender = "httpd"
tokenPath = "/api/v2/token"
logoutPath = "/api/v2/logout"
userTokenPath = "/api/v2/user/token"
userLogoutPath = "/api/v2/user/logout"
activeConnectionsPath = "/api/v2/connections"
quotasBasePath = "/api/v2/quotas"
quotaScanPath = "/api/v2/quota-scans"
quotaScanVFolderPath = "/api/v2/folder-quota-scans"
userPath = "/api/v2/users"
versionPath = "/api/v2/version"
folderPath = "/api/v2/folders"
serverStatusPath = "/api/v2/status"
dumpDataPath = "/api/v2/dumpdata"
loadDataPath = "/api/v2/loaddata"
updateUsedQuotaPath = "/api/v2/quota-update"
updateFolderUsedQuotaPath = "/api/v2/folder-quota-update"
defenderHosts = "/api/v2/defender/hosts"
defenderBanTime = "/api/v2/defender/bantime"
defenderUnban = "/api/v2/defender/unban"
defenderScore = "/api/v2/defender/score"
adminPath = "/api/v2/admins"
adminPwdPath = "/api/v2/admin/changepwd"
adminPwdCompatPath = "/api/v2/changepwd/admin"
adminManageAPIKeyPath = "/api/v2/admin/apikeyauth"
userPwdPath = "/api/v2/user/changepwd"
userPublicKeysPath = "/api/v2/user/publickeys"
userFolderPath = "/api/v2/user/folder"
userDirsPath = "/api/v2/user/dirs"
userFilePath = "/api/v2/user/file"
userFilesPath = "/api/v2/user/files"
userStreamZipPath = "/api/v2/user/streamzip"
apiKeysPath = "/api/v2/apikeys"
adminTOTPConfigsPath = "/api/v2/admin/totp/configs"
adminTOTPGeneratePath = "/api/v2/admin/totp/generate"
adminTOTPValidatePath = "/api/v2/admin/totp/validate"
adminTOTPSavePath = "/api/v2/admin/totp/save"
admin2FARecoveryCodesPath = "/api/v2/admin/2fa/recoverycodes"
userTOTPConfigsPath = "/api/v2/user/totp/configs"
userTOTPGeneratePath = "/api/v2/user/totp/generate"
userTOTPValidatePath = "/api/v2/user/totp/validate"
userTOTPSavePath = "/api/v2/user/totp/save"
user2FARecoveryCodesPath = "/api/v2/user/2fa/recoverycodes"
userManageAPIKeyPath = "/api/v2/user/apikeyauth"
retentionBasePath = "/api/v2/retention/users"
retentionChecksPath = "/api/v2/retention/users/checks"
healthzPath = "/healthz"
webRootPathDefault = "/"
webBasePathDefault = "/web"
webBasePathAdminDefault = "/web/admin"
webBasePathClientDefault = "/web/client"
webAdminSetupPathDefault = "/web/admin/setup"
webLoginPathDefault = "/web/admin/login"
webAdminTwoFactorPathDefault = "/web/admin/twofactor"
webAdminTwoFactorRecoveryPathDefault = "/web/admin/twofactor-recovery"
webLogoutPathDefault = "/web/admin/logout"
webUsersPathDefault = "/web/admin/users"
webUserPathDefault = "/web/admin/user"
webConnectionsPathDefault = "/web/admin/connections"
webFoldersPathDefault = "/web/admin/folders"
webFolderPathDefault = "/web/admin/folder"
webStatusPathDefault = "/web/admin/status"
webAdminsPathDefault = "/web/admin/managers"
webAdminPathDefault = "/web/admin/manager"
webMaintenancePathDefault = "/web/admin/maintenance"
webBackupPathDefault = "/web/admin/backup"
webRestorePathDefault = "/web/admin/restore"
webScanVFolderPathDefault = "/web/admin/quotas/scanfolder"
webQuotaScanPathDefault = "/web/admin/quotas/scanuser"
webChangeAdminPwdPathDefault = "/web/admin/changepwd"
webAdminCredentialsPathDefault = "/web/admin/credentials"
webAdminMFAPathDefault = "/web/admin/mfa"
webAdminTOTPGeneratePathDefault = "/web/admin/totp/generate"
webAdminTOTPValidatePathDefault = "/web/admin/totp/validate"
webAdminTOTPSavePathDefault = "/web/admin/totp/save"
webAdminRecoveryCodesPathDefault = "/web/admin/recoverycodes"
webChangeAdminAPIKeyAccessPathDefault = "/web/admin/apikeyaccess"
webTemplateUserDefault = "/web/admin/template/user"
webTemplateFolderDefault = "/web/admin/template/folder"
webDefenderPathDefault = "/web/admin/defender"
webDefenderHostsPathDefault = "/web/admin/defender/hosts"
webClientLoginPathDefault = "/web/client/login"
webClientTwoFactorPathDefault = "/web/client/twofactor"
webClientTwoFactorRecoveryPathDefault = "/web/client/twofactor-recovery"
webClientFilesPathDefault = "/web/client/files"
webClientDirsPathDefault = "/web/client/dirs"
webClientDownloadZipPathDefault = "/web/client/downloadzip"
webClientCredentialsPathDefault = "/web/client/credentials"
webClientMFAPathDefault = "/web/client/mfa"
webClientTOTPGeneratePathDefault = "/web/client/totp/generate"
webClientTOTPValidatePathDefault = "/web/client/totp/validate"
webClientTOTPSavePathDefault = "/web/client/totp/save"
webClientRecoveryCodesPathDefault = "/web/client/recoverycodes"
webChangeClientPwdPathDefault = "/web/client/changepwd"
webChangeClientKeysPathDefault = "/web/client/managekeys"
webChangeClientAPIKeyAccessPathDefault = "/web/client/apikeyaccess"
webClientLogoutPathDefault = "/web/client/logout"
webStaticFilesPathDefault = "/static"
logSender = "httpd"
tokenPath = "/api/v2/token"
logoutPath = "/api/v2/logout"
userTokenPath = "/api/v2/user/token"
userLogoutPath = "/api/v2/user/logout"
activeConnectionsPath = "/api/v2/connections"
quotasBasePath = "/api/v2/quotas"
quotaScanPath = "/api/v2/quota-scans"
quotaScanVFolderPath = "/api/v2/folder-quota-scans"
userPath = "/api/v2/users"
versionPath = "/api/v2/version"
folderPath = "/api/v2/folders"
serverStatusPath = "/api/v2/status"
dumpDataPath = "/api/v2/dumpdata"
loadDataPath = "/api/v2/loaddata"
updateUsedQuotaPath = "/api/v2/quota-update"
updateFolderUsedQuotaPath = "/api/v2/folder-quota-update"
defenderHosts = "/api/v2/defender/hosts"
defenderBanTime = "/api/v2/defender/bantime"
defenderUnban = "/api/v2/defender/unban"
defenderScore = "/api/v2/defender/score"
adminPath = "/api/v2/admins"
adminPwdPath = "/api/v2/admin/changepwd"
adminPwdCompatPath = "/api/v2/changepwd/admin"
adminProfilePath = "/api/v2/admin/profile"
userPwdPath = "/api/v2/user/changepwd"
userPublicKeysPath = "/api/v2/user/publickeys"
userFolderPath = "/api/v2/user/folder"
userDirsPath = "/api/v2/user/dirs"
userFilePath = "/api/v2/user/file"
userFilesPath = "/api/v2/user/files"
userStreamZipPath = "/api/v2/user/streamzip"
apiKeysPath = "/api/v2/apikeys"
adminTOTPConfigsPath = "/api/v2/admin/totp/configs"
adminTOTPGeneratePath = "/api/v2/admin/totp/generate"
adminTOTPValidatePath = "/api/v2/admin/totp/validate"
adminTOTPSavePath = "/api/v2/admin/totp/save"
admin2FARecoveryCodesPath = "/api/v2/admin/2fa/recoverycodes"
userTOTPConfigsPath = "/api/v2/user/totp/configs"
userTOTPGeneratePath = "/api/v2/user/totp/generate"
userTOTPValidatePath = "/api/v2/user/totp/validate"
userTOTPSavePath = "/api/v2/user/totp/save"
user2FARecoveryCodesPath = "/api/v2/user/2fa/recoverycodes"
userProfilePath = "/api/v2/user/profile"
retentionBasePath = "/api/v2/retention/users"
retentionChecksPath = "/api/v2/retention/users/checks"
healthzPath = "/healthz"
webRootPathDefault = "/"
webBasePathDefault = "/web"
webBasePathAdminDefault = "/web/admin"
webBasePathClientDefault = "/web/client"
webAdminSetupPathDefault = "/web/admin/setup"
webLoginPathDefault = "/web/admin/login"
webAdminTwoFactorPathDefault = "/web/admin/twofactor"
webAdminTwoFactorRecoveryPathDefault = "/web/admin/twofactor-recovery"
webLogoutPathDefault = "/web/admin/logout"
webUsersPathDefault = "/web/admin/users"
webUserPathDefault = "/web/admin/user"
webConnectionsPathDefault = "/web/admin/connections"
webFoldersPathDefault = "/web/admin/folders"
webFolderPathDefault = "/web/admin/folder"
webStatusPathDefault = "/web/admin/status"
webAdminsPathDefault = "/web/admin/managers"
webAdminPathDefault = "/web/admin/manager"
webMaintenancePathDefault = "/web/admin/maintenance"
webBackupPathDefault = "/web/admin/backup"
webRestorePathDefault = "/web/admin/restore"
webScanVFolderPathDefault = "/web/admin/quotas/scanfolder"
webQuotaScanPathDefault = "/web/admin/quotas/scanuser"
webChangeAdminPwdPathDefault = "/web/admin/changepwd"
webAdminProfilePathDefault = "/web/admin/profile"
webAdminMFAPathDefault = "/web/admin/mfa"
webAdminTOTPGeneratePathDefault = "/web/admin/totp/generate"
webAdminTOTPValidatePathDefault = "/web/admin/totp/validate"
webAdminTOTPSavePathDefault = "/web/admin/totp/save"
webAdminRecoveryCodesPathDefault = "/web/admin/recoverycodes"
webTemplateUserDefault = "/web/admin/template/user"
webTemplateFolderDefault = "/web/admin/template/folder"
webDefenderPathDefault = "/web/admin/defender"
webDefenderHostsPathDefault = "/web/admin/defender/hosts"
webClientLoginPathDefault = "/web/client/login"
webClientTwoFactorPathDefault = "/web/client/twofactor"
webClientTwoFactorRecoveryPathDefault = "/web/client/twofactor-recovery"
webClientFilesPathDefault = "/web/client/files"
webClientDirsPathDefault = "/web/client/dirs"
webClientDownloadZipPathDefault = "/web/client/downloadzip"
webClientProfilePathDefault = "/web/client/profile"
webClientMFAPathDefault = "/web/client/mfa"
webClientTOTPGeneratePathDefault = "/web/client/totp/generate"
webClientTOTPValidatePathDefault = "/web/client/totp/validate"
webClientTOTPSavePathDefault = "/web/client/totp/save"
webClientRecoveryCodesPathDefault = "/web/client/recoverycodes"
webChangeClientPwdPathDefault = "/web/client/changepwd"
webClientLogoutPathDefault = "/web/client/logout"
webStaticFilesPathDefault = "/static"
// MaxRestoreSize defines the max size for the loaddata input file
MaxRestoreSize = 10485760 // 10 MB
maxRequestSize = 1048576 // 1MB
@ -139,63 +136,60 @@ const (
)
var (
backupsPath string
certMgr *common.CertManager
cleanupTicker *time.Ticker
cleanupDone chan bool
invalidatedJWTTokens sync.Map
csrfTokenAuth *jwtauth.JWTAuth
webRootPath string
webBasePath string
webBaseAdminPath string
webBaseClientPath string
webAdminSetupPath string
webLoginPath string
webAdminTwoFactorPath string
webAdminTwoFactorRecoveryPath string
webLogoutPath string
webUsersPath string
webUserPath string
webConnectionsPath string
webFoldersPath string
webFolderPath string
webStatusPath string
webAdminsPath string
webAdminPath string
webMaintenancePath string
webBackupPath string
webRestorePath string
webScanVFolderPath string
webQuotaScanPath string
webAdminCredentialsPath string
webAdminMFAPath string
webAdminTOTPGeneratePath string
webAdminTOTPValidatePath string
webAdminTOTPSavePath string
webAdminRecoveryCodesPath string
webChangeAdminAPIKeyAccessPath string
webChangeAdminPwdPath string
webTemplateUser string
webTemplateFolder string
webDefenderPath string
webDefenderHostsPath string
webClientLoginPath string
webClientTwoFactorPath string
webClientTwoFactorRecoveryPath string
webClientFilesPath string
webClientDirsPath string
webClientDownloadZipPath string
webClientCredentialsPath string
webChangeClientPwdPath string
webChangeClientKeysPath string
webClientMFAPath string
webClientTOTPGeneratePath string
webClientTOTPValidatePath string
webClientTOTPSavePath string
webClientRecoveryCodesPath string
webChangeClientAPIKeyAccessPath string
webClientLogoutPath string
webStaticFilesPath string
backupsPath string
certMgr *common.CertManager
cleanupTicker *time.Ticker
cleanupDone chan bool
invalidatedJWTTokens sync.Map
csrfTokenAuth *jwtauth.JWTAuth
webRootPath string
webBasePath string
webBaseAdminPath string
webBaseClientPath string
webAdminSetupPath string
webLoginPath string
webAdminTwoFactorPath string
webAdminTwoFactorRecoveryPath string
webLogoutPath string
webUsersPath string
webUserPath string
webConnectionsPath string
webFoldersPath string
webFolderPath string
webStatusPath string
webAdminsPath string
webAdminPath string
webMaintenancePath string
webBackupPath string
webRestorePath string
webScanVFolderPath string
webQuotaScanPath string
webAdminProfilePath string
webAdminMFAPath string
webAdminTOTPGeneratePath string
webAdminTOTPValidatePath string
webAdminTOTPSavePath string
webAdminRecoveryCodesPath string
webChangeAdminPwdPath string
webTemplateUser string
webTemplateFolder string
webDefenderPath string
webDefenderHostsPath string
webClientLoginPath string
webClientTwoFactorPath string
webClientTwoFactorRecoveryPath string
webClientFilesPath string
webClientDirsPath string
webClientDownloadZipPath string
webClientProfilePath string
webChangeClientPwdPath string
webClientMFAPath string
webClientTOTPGeneratePath string
webClientTOTPValidatePath string
webClientTOTPSavePath string
webClientRecoveryCodesPath string
webClientLogoutPath string
webStaticFilesPath string
// max upload size for http clients, 1GB by default
maxUploadFileSize = int64(1048576000)
)
@ -530,10 +524,8 @@ func updateWebClientURLs(baseURL string) {
webClientFilesPath = path.Join(baseURL, webClientFilesPathDefault)
webClientDirsPath = path.Join(baseURL, webClientDirsPathDefault)
webClientDownloadZipPath = path.Join(baseURL, webClientDownloadZipPathDefault)
webClientCredentialsPath = path.Join(baseURL, webClientCredentialsPathDefault)
webClientProfilePath = path.Join(baseURL, webClientProfilePathDefault)
webChangeClientPwdPath = path.Join(baseURL, webChangeClientPwdPathDefault)
webChangeClientKeysPath = path.Join(baseURL, webChangeClientKeysPathDefault)
webChangeClientAPIKeyAccessPath = path.Join(baseURL, webChangeClientAPIKeyAccessPathDefault)
webClientLogoutPath = path.Join(baseURL, webClientLogoutPathDefault)
webClientMFAPath = path.Join(baseURL, webClientMFAPathDefault)
webClientTOTPGeneratePath = path.Join(baseURL, webClientTOTPGeneratePathDefault)
@ -568,13 +560,12 @@ func updateWebAdminURLs(baseURL string) {
webScanVFolderPath = path.Join(baseURL, webScanVFolderPathDefault)
webQuotaScanPath = path.Join(baseURL, webQuotaScanPathDefault)
webChangeAdminPwdPath = path.Join(baseURL, webChangeAdminPwdPathDefault)
webAdminCredentialsPath = path.Join(baseURL, webAdminCredentialsPathDefault)
webAdminProfilePath = path.Join(baseURL, webAdminProfilePathDefault)
webAdminMFAPath = path.Join(baseURL, webAdminMFAPathDefault)
webAdminTOTPGeneratePath = path.Join(baseURL, webAdminTOTPGeneratePathDefault)
webAdminTOTPValidatePath = path.Join(baseURL, webAdminTOTPValidatePathDefault)
webAdminTOTPSavePath = path.Join(baseURL, webAdminTOTPSavePathDefault)
webAdminRecoveryCodesPath = path.Join(baseURL, webAdminRecoveryCodesPathDefault)
webChangeAdminAPIKeyAccessPath = path.Join(baseURL, webChangeAdminAPIKeyAccessPathDefault)
webTemplateUser = path.Join(baseURL, webTemplateUserDefault)
webTemplateFolder = path.Join(baseURL, webTemplateFolderDefault)
webDefenderHostsPath = path.Join(baseURL, webDefenderHostsPathDefault)

View file

@ -92,13 +92,13 @@ const (
adminTOTPValidatePath = "/api/v2/admin/totp/validate"
adminTOTPSavePath = "/api/v2/admin/totp/save"
admin2FARecoveryCodesPath = "/api/v2/admin/2fa/recoverycodes"
adminManageAPIKeyPath = "/api/v2/admin/apikeyauth"
adminProfilePath = "/api/v2/admin/profile"
userTOTPConfigsPath = "/api/v2/user/totp/configs"
userTOTPGeneratePath = "/api/v2/user/totp/generate"
userTOTPValidatePath = "/api/v2/user/totp/validate"
userTOTPSavePath = "/api/v2/user/totp/save"
user2FARecoveryCodesPath = "/api/v2/user/2fa/recoverycodes"
userManageAPIKeyPath = "/api/v2/user/apikeyauth"
userProfilePath = "/api/v2/user/profile"
retentionBasePath = "/api/v2/retention/users"
healthzPath = "/healthz"
webBasePath = "/web"
@ -117,11 +117,10 @@ const (
webMaintenancePath = "/web/admin/maintenance"
webRestorePath = "/web/admin/restore"
webChangeAdminPwdPath = "/web/admin/changepwd"
webAdminCredentialsPath = "/web/admin/credentials"
webAdminProfilePath = "/web/admin/profile"
webTemplateUser = "/web/admin/template/user"
webTemplateFolder = "/web/admin/template/folder"
webDefenderPath = "/web/admin/defender"
webChangeAdminAPIKeyAccessPath = "/web/admin/apikeyaccess"
webAdminTwoFactorPath = "/web/admin/twofactor"
webAdminTwoFactorRecoveryPath = "/web/admin/twofactor-recovery"
webAdminMFAPath = "/web/admin/mfa"
@ -131,10 +130,8 @@ const (
webClientFilesPath = "/web/client/files"
webClientDirsPath = "/web/client/dirs"
webClientDownloadZipPath = "/web/client/downloadzip"
webClientCredentialsPath = "/web/client/credentials"
webChangeClientPwdPath = "/web/client/changepwd"
webChangeClientKeysPath = "/web/client/managekeys"
webChangeClientAPIKeyAccessPath = "/web/client/apikeyaccess"
webClientProfilePath = "/web/client/profile"
webClientTwoFactorPath = "/web/client/twofactor"
webClientTwoFactorRecoveryPath = "/web/client/twofactor-recovery"
webClientLogoutPath = "/web/client/logout"
@ -3408,7 +3405,7 @@ func TestSkipNaturalKeysValidation(t *testing.T) {
assert.NoError(t, err)
form := make(url.Values)
form.Set(csrfFormToken, csrfToken)
req, err := http.NewRequest(http.MethodPost, webChangeClientAPIKeyAccessPath, bytes.NewBuffer([]byte(form.Encode())))
req, err := http.NewRequest(http.MethodPost, webClientProfilePath, bytes.NewBuffer([]byte(form.Encode())))
assert.NoError(t, err)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
setJWTCookieForReq(req, token)
@ -3438,7 +3435,7 @@ func TestSkipNaturalKeysValidation(t *testing.T) {
apiKeyAuthReq["allow_api_key_auth"] = true
asJSON, err := json.Marshal(apiKeyAuthReq)
assert.NoError(t, err)
req, err = http.NewRequest(http.MethodPut, userManageAPIKeyPath, bytes.NewBuffer(asJSON))
req, err = http.NewRequest(http.MethodPut, userProfilePath, bytes.NewBuffer(asJSON))
assert.NoError(t, err)
setBearerForReq(req, userAPIToken)
rr = executeRequest(req)
@ -3456,7 +3453,7 @@ func TestSkipNaturalKeysValidation(t *testing.T) {
assert.NoError(t, err)
form = make(url.Values)
form.Set(csrfFormToken, csrfToken)
req, _ = http.NewRequest(http.MethodPost, webChangeAdminAPIKeyAccessPath, bytes.NewBuffer([]byte(form.Encode())))
req, _ = http.NewRequest(http.MethodPost, webAdminProfilePath, bytes.NewBuffer([]byte(form.Encode())))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
setJWTCookieForReq(req, token)
rr = executeRequest(req)
@ -3467,7 +3464,7 @@ func TestSkipNaturalKeysValidation(t *testing.T) {
apiKeyAuthReq["allow_api_key_auth"] = true
asJSON, err = json.Marshal(apiKeyAuthReq)
assert.NoError(t, err)
req, err = http.NewRequest(http.MethodPut, adminManageAPIKeyPath, bytes.NewBuffer(asJSON))
req, err = http.NewRequest(http.MethodPut, adminProfilePath, bytes.NewBuffer(asJSON))
assert.NoError(t, err)
setBearerForReq(req, adminAPIToken)
rr = executeRequest(req)
@ -5128,6 +5125,40 @@ func TestChangeAdminPwdInvalidJsonMock(t *testing.T) {
checkResponseCode(t, http.StatusBadRequest, rr)
}
func TestMFAPermission(t *testing.T) {
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
assert.NoError(t, err)
webToken, err := getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)
assert.NoError(t, err)
req, err := http.NewRequest(http.MethodGet, webClientMFAPath, nil)
assert.NoError(t, err)
req.RequestURI = webClientMFAPath
setJWTCookieForReq(req, webToken)
rr := executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
user.Filters.WebClient = []string{sdk.WebClientMFADisabled}
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
webToken, err = getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)
assert.NoError(t, err)
req, err = http.NewRequest(http.MethodGet, webClientMFAPath, nil)
assert.NoError(t, err)
req.RequestURI = webClientMFAPath
setJWTCookieForReq(req, webToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusForbidden, rr)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
}
func TestWebUserTwoFactorLogin(t *testing.T) {
u := getTestUser()
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
@ -5958,104 +5989,160 @@ func TestWebUserTOTP(t *testing.T) {
checkResponseCode(t, http.StatusNotFound, rr)
}
func TestWebAPIChangeUserAPIKeyAuth(t *testing.T) {
func TestWebAPIChangeUserProfileMock(t *testing.T) {
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
assert.NoError(t, err)
assert.False(t, user.Filters.AllowAPIKeyAuth)
token, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)
assert.NoError(t, err)
// invalid json
req, err := http.NewRequest(http.MethodPut, userManageAPIKeyPath, bytes.NewBuffer([]byte("{")))
req, err := http.NewRequest(http.MethodPut, userProfilePath, bytes.NewBuffer([]byte("{")))
assert.NoError(t, err)
setBearerForReq(req, token)
rr := executeRequest(req)
checkResponseCode(t, http.StatusBadRequest, rr)
apiKeyAuthReq := make(map[string]bool)
apiKeyAuthReq["allow_api_key_auth"] = true
asJSON, err := json.Marshal(apiKeyAuthReq)
email := "userapi@example.com"
description := "user API description"
profileReq := make(map[string]interface{})
profileReq["allow_api_key_auth"] = true
profileReq["email"] = email
profileReq["description"] = description
profileReq["public_keys"] = []string{testPubKey, testPubKey1}
asJSON, err := json.Marshal(profileReq)
assert.NoError(t, err)
req, err = http.NewRequest(http.MethodPut, userManageAPIKeyPath, bytes.NewBuffer(asJSON))
req, err = http.NewRequest(http.MethodPut, userProfilePath, bytes.NewBuffer(asJSON))
assert.NoError(t, err)
setBearerForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err)
assert.True(t, user.Filters.AllowAPIKeyAuth)
apiKeyAuthReq = make(map[string]bool)
req, err = http.NewRequest(http.MethodGet, userManageAPIKeyPath, nil)
profileReq = make(map[string]interface{})
req, err = http.NewRequest(http.MethodGet, userProfilePath, nil)
assert.NoError(t, err)
setBearerForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
err = json.Unmarshal(rr.Body.Bytes(), &apiKeyAuthReq)
err = json.Unmarshal(rr.Body.Bytes(), &profileReq)
assert.NoError(t, err)
assert.True(t, apiKeyAuthReq["allow_api_key_auth"])
assert.Equal(t, email, profileReq["email"].(string))
assert.Equal(t, description, profileReq["description"].(string))
assert.True(t, profileReq["allow_api_key_auth"].(bool))
assert.Len(t, profileReq["public_keys"].([]interface{}), 2)
// set an invalid email
profileReq = make(map[string]interface{})
profileReq["email"] = "notavalidemail"
asJSON, err = json.Marshal(profileReq)
assert.NoError(t, err)
req, err = http.NewRequest(http.MethodPut, userProfilePath, bytes.NewBuffer(asJSON))
assert.NoError(t, err)
setBearerForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusBadRequest, rr)
assert.Contains(t, rr.Body.String(), "Validation error: email")
// set an invalid public key
profileReq = make(map[string]interface{})
profileReq["public_keys"] = []string{"not a public key"}
asJSON, err = json.Marshal(profileReq)
assert.NoError(t, err)
req, err = http.NewRequest(http.MethodPut, userProfilePath, bytes.NewBuffer(asJSON))
assert.NoError(t, err)
setBearerForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusBadRequest, rr)
assert.Contains(t, rr.Body.String(), "Validation error: could not parse key")
apiKeyAuthReq["allow_api_key_auth"] = false
asJSON, err = json.Marshal(apiKeyAuthReq)
user.Filters.WebClient = []string{sdk.WebClientAPIKeyAuthChangeDisabled, sdk.WebClientPubKeyChangeDisabled}
user.Email = email
user.Description = description
user.Filters.AllowAPIKeyAuth = true
_, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
req, err = http.NewRequest(http.MethodPut, userManageAPIKeyPath, bytes.NewBuffer(asJSON))
token, err = getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)
assert.NoError(t, err)
profileReq = make(map[string]interface{})
profileReq["allow_api_key_auth"] = false
profileReq["email"] = email
profileReq["description"] = description + "_mod"
profileReq["public_keys"] = []string{testPubKey}
asJSON, err = json.Marshal(profileReq)
assert.NoError(t, err)
req, err = http.NewRequest(http.MethodPut, userProfilePath, bytes.NewBuffer(asJSON))
assert.NoError(t, err)
setBearerForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err)
assert.False(t, user.Filters.AllowAPIKeyAuth)
apiKeyAuthReq = make(map[string]bool)
req, err = http.NewRequest(http.MethodGet, userManageAPIKeyPath, nil)
assert.Contains(t, rr.Body.String(), "Profile updated")
// check that api key auth and public keys were not changed
profileReq = make(map[string]interface{})
req, err = http.NewRequest(http.MethodGet, userProfilePath, nil)
assert.NoError(t, err)
setBearerForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
err = json.Unmarshal(rr.Body.Bytes(), &apiKeyAuthReq)
err = json.Unmarshal(rr.Body.Bytes(), &profileReq)
assert.NoError(t, err)
assert.False(t, apiKeyAuthReq["allow_api_key_auth"])
assert.Equal(t, email, profileReq["email"].(string))
assert.Equal(t, description+"_mod", profileReq["description"].(string))
assert.True(t, profileReq["allow_api_key_auth"].(bool))
assert.Len(t, profileReq["public_keys"].([]interface{}), 2)
// remove the permission
user.Filters.WebClient = []string{sdk.WebClientAPIKeyAuthChangeDisabled}
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
user.Filters.WebClient = []string{sdk.WebClientAPIKeyAuthChangeDisabled, sdk.WebClientInfoChangeDisabled}
user.Description = description + "_mod"
_, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
assert.Len(t, user.Filters.WebClient, 1)
assert.Contains(t, user.Filters.WebClient, sdk.WebClientAPIKeyAuthChangeDisabled)
newToken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)
token, err = getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)
assert.NoError(t, err)
apiKeyAuthReq["allow_api_key_auth"] = true
asJSON, err = json.Marshal(apiKeyAuthReq)
profileReq = make(map[string]interface{})
profileReq["allow_api_key_auth"] = false
profileReq["email"] = "newemail@apiuser.com"
profileReq["description"] = description
profileReq["public_keys"] = []string{testPubKey}
asJSON, err = json.Marshal(profileReq)
assert.NoError(t, err)
req, err = http.NewRequest(http.MethodPut, userManageAPIKeyPath, bytes.NewBuffer(asJSON))
req, err = http.NewRequest(http.MethodPut, userProfilePath, bytes.NewBuffer(asJSON))
assert.NoError(t, err)
setBearerForReq(req, newToken)
setBearerForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
profileReq = make(map[string]interface{})
req, err = http.NewRequest(http.MethodGet, userProfilePath, nil)
assert.NoError(t, err)
setBearerForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
err = json.Unmarshal(rr.Body.Bytes(), &profileReq)
assert.NoError(t, err)
assert.Equal(t, email, profileReq["email"].(string))
assert.Equal(t, description+"_mod", profileReq["description"].(string))
assert.True(t, profileReq["allow_api_key_auth"].(bool))
assert.Len(t, profileReq["public_keys"].([]interface{}), 1)
// finally disable all profile permissions
user.Filters.WebClient = []string{sdk.WebClientAPIKeyAuthChangeDisabled, sdk.WebClientInfoChangeDisabled,
sdk.WebClientPubKeyChangeDisabled}
_, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
req, err = http.NewRequest(http.MethodPut, userProfilePath, bytes.NewBuffer(asJSON))
assert.NoError(t, err)
setBearerForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusForbidden, rr)
// get will still work
req, err = http.NewRequest(http.MethodGet, userManageAPIKeyPath, nil)
assert.NoError(t, err)
setBearerForReq(req, newToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "You are not allowed to change anything")
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
apiKeyAuthReq = make(map[string]bool)
req, err = http.NewRequest(http.MethodGet, userManageAPIKeyPath, nil)
req, err = http.NewRequest(http.MethodGet, userProfilePath, nil)
assert.NoError(t, err)
setBearerForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusNotFound, rr)
req, err = http.NewRequest(http.MethodPut, userManageAPIKeyPath, bytes.NewBuffer(asJSON))
req, err = http.NewRequest(http.MethodPut, userProfilePath, bytes.NewBuffer(asJSON))
assert.NoError(t, err)
setBearerForReq(req, token)
rr = executeRequest(req)
@ -6136,7 +6223,7 @@ func TestLoginInvalidPasswordMock(t *testing.T) {
assert.Equal(t, http.StatusUnauthorized, rr.Code)
}
func TestChangeAdminAPIKeyAuth(t *testing.T) {
func TestWebAPIChangeAdminProfileMock(t *testing.T) {
admin := getTestAdmin()
admin.Username = altAdminUsername
admin.Password = altAdminPassword
@ -6147,65 +6234,59 @@ func TestChangeAdminAPIKeyAuth(t *testing.T) {
token, err := getJWTAPITokenFromTestServer(altAdminUsername, altAdminPassword)
assert.NoError(t, err)
// invalid json
req, err := http.NewRequest(http.MethodPut, adminManageAPIKeyPath, bytes.NewBuffer([]byte("{")))
req, err := http.NewRequest(http.MethodPut, adminProfilePath, bytes.NewBuffer([]byte("{")))
assert.NoError(t, err)
setBearerForReq(req, token)
rr := executeRequest(req)
checkResponseCode(t, http.StatusBadRequest, rr)
apiKeyAuthReq := make(map[string]bool)
apiKeyAuthReq["allow_api_key_auth"] = true
asJSON, err := json.Marshal(apiKeyAuthReq)
email := "adminapi@example.com"
description := "admin API description"
profileReq := make(map[string]interface{})
profileReq["allow_api_key_auth"] = true
profileReq["email"] = email
profileReq["description"] = description
asJSON, err := json.Marshal(profileReq)
assert.NoError(t, err)
req, err = http.NewRequest(http.MethodPut, adminManageAPIKeyPath, bytes.NewBuffer(asJSON))
req, err = http.NewRequest(http.MethodPut, adminProfilePath, bytes.NewBuffer(asJSON))
assert.NoError(t, err)
setBearerForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "Profile updated")
admin, _, err = httpdtest.GetAdminByUsername(altAdminUsername, http.StatusOK)
assert.NoError(t, err)
assert.True(t, admin.Filters.AllowAPIKeyAuth)
apiKeyAuthReq = make(map[string]bool)
req, err = http.NewRequest(http.MethodGet, adminManageAPIKeyPath, nil)
profileReq = make(map[string]interface{})
req, err = http.NewRequest(http.MethodGet, adminProfilePath, nil)
assert.NoError(t, err)
setBearerForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
err = json.Unmarshal(rr.Body.Bytes(), &apiKeyAuthReq)
err = json.Unmarshal(rr.Body.Bytes(), &profileReq)
assert.NoError(t, err)
assert.True(t, apiKeyAuthReq["allow_api_key_auth"])
apiKeyAuthReq["allow_api_key_auth"] = false
asJSON, err = json.Marshal(apiKeyAuthReq)
assert.Equal(t, email, profileReq["email"].(string))
assert.Equal(t, description, profileReq["description"].(string))
assert.True(t, profileReq["allow_api_key_auth"].(bool))
// set an invalid email
profileReq["email"] = "admin_invalid_email"
asJSON, err = json.Marshal(profileReq)
assert.NoError(t, err)
req, err = http.NewRequest(http.MethodPut, adminManageAPIKeyPath, bytes.NewBuffer(asJSON))
req, err = http.NewRequest(http.MethodPut, adminProfilePath, bytes.NewBuffer(asJSON))
assert.NoError(t, err)
setBearerForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
apiKeyAuthReq = make(map[string]bool)
req, err = http.NewRequest(http.MethodGet, adminManageAPIKeyPath, nil)
assert.NoError(t, err)
setBearerForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
err = json.Unmarshal(rr.Body.Bytes(), &apiKeyAuthReq)
assert.NoError(t, err)
assert.False(t, apiKeyAuthReq["allow_api_key_auth"])
checkResponseCode(t, http.StatusBadRequest, rr)
assert.Contains(t, rr.Body.String(), "Validation error: email")
_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)
assert.NoError(t, err)
req, err = http.NewRequest(http.MethodGet, adminManageAPIKeyPath, nil)
req, err = http.NewRequest(http.MethodGet, adminProfilePath, nil)
assert.NoError(t, err)
setBearerForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusNotFound, rr)
req, err = http.NewRequest(http.MethodPut, adminManageAPIKeyPath, bytes.NewBuffer(asJSON))
req, err = http.NewRequest(http.MethodPut, adminProfilePath, bytes.NewBuffer(asJSON))
assert.NoError(t, err)
setBearerForReq(req, token)
rr = executeRequest(req)
@ -7473,13 +7554,13 @@ func TestWebAPILoginMock(t *testing.T) {
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
// API token is not valid for web usage
req, _ = http.NewRequest(http.MethodGet, webClientCredentialsPath, nil)
req, _ = http.NewRequest(http.MethodGet, webClientProfilePath, nil)
setJWTCookieForReq(req, apiToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusFound, rr)
assert.Equal(t, webClientLoginPath, rr.Header().Get("Location"))
req, _ = http.NewRequest(http.MethodGet, webClientCredentialsPath, nil)
req, _ = http.NewRequest(http.MethodGet, webClientProfilePath, nil)
setJWTCookieForReq(req, webToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
@ -7509,13 +7590,13 @@ func TestWebClientLoginMock(t *testing.T) {
checkResponseCode(t, http.StatusFound, rr)
assert.Equal(t, webLoginPath, rr.Header().Get("Location"))
// bearer should not work
req, _ = http.NewRequest(http.MethodGet, webClientCredentialsPath, nil)
req, _ = http.NewRequest(http.MethodGet, webClientProfilePath, nil)
setBearerForReq(req, webToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusFound, rr)
assert.Equal(t, webClientLoginPath, rr.Header().Get("Location"))
// now try to render client pages
req, _ = http.NewRequest(http.MethodGet, webClientCredentialsPath, nil)
req, _ = http.NewRequest(http.MethodGet, webClientProfilePath, nil)
setJWTCookieForReq(req, webToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
@ -7529,7 +7610,7 @@ func TestWebClientLoginMock(t *testing.T) {
rr = executeRequest(req)
checkResponseCode(t, http.StatusFound, rr)
assert.Equal(t, webClientLoginPath, rr.Header().Get("Location"))
req, _ = http.NewRequest(http.MethodGet, webClientCredentialsPath, nil)
req, _ = http.NewRequest(http.MethodGet, webClientProfilePath, nil)
setJWTCookieForReq(req, webToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusFound, rr)
@ -7545,7 +7626,7 @@ func TestWebClientLoginMock(t *testing.T) {
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
req, _ = http.NewRequest(http.MethodGet, webClientCredentialsPath, nil)
req, _ = http.NewRequest(http.MethodGet, webClientProfilePath, nil)
setJWTCookieForReq(req, webToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusInternalServerError, rr)
@ -7602,7 +7683,7 @@ func TestWebClientLoginMock(t *testing.T) {
form := make(url.Values)
form.Set("public_keys", testPubKey)
form.Set(csrfFormToken, csrfToken)
req, _ = http.NewRequest(http.MethodPost, webChangeClientKeysPath, bytes.NewBuffer([]byte(form.Encode())))
req, _ = http.NewRequest(http.MethodPost, webClientProfilePath, bytes.NewBuffer([]byte(form.Encode())))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
setJWTCookieForReq(req, webToken)
rr = executeRequest(req)
@ -7864,15 +7945,22 @@ func TestWebClientChangePwd(t *testing.T) {
csrfToken, err := getCSRFToken(httpBaseURL + webClientLoginPath)
assert.NoError(t, err)
req, err := http.NewRequest(http.MethodGet, webChangeClientPwdPath, nil)
assert.NoError(t, err)
setJWTCookieForReq(req, webToken)
rr := executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
form := make(url.Values)
form.Set("current_password", defaultPassword)
form.Set("new_password1", defaultPassword)
form.Set("new_password2", defaultPassword)
// no csrf token
req, _ := http.NewRequest(http.MethodPost, webChangeClientPwdPath, bytes.NewBuffer([]byte(form.Encode())))
req, err = http.NewRequest(http.MethodPost, webChangeClientPwdPath, bytes.NewBuffer([]byte(form.Encode())))
assert.NoError(t, err)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
setJWTCookieForReq(req, webToken)
rr := executeRequest(req)
rr = executeRequest(req)
checkResponseCode(t, http.StatusForbidden, rr)
assert.Contains(t, rr.Body.String(), "unable to verify form token")
@ -8004,64 +8092,6 @@ func TestWebAPIPublicKeys(t *testing.T) {
assert.NoError(t, err)
}
func TestWebClientChangePubKeys(t *testing.T) {
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
assert.NoError(t, err)
webToken, err := getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)
assert.NoError(t, err)
csrfToken, err := getCSRFToken(httpBaseURL + webClientLoginPath)
assert.NoError(t, err)
form := make(url.Values)
form.Set("public_keys", testPubKey)
form.Add("public_keys", testPubKey1)
// no csrf token
req, _ := http.NewRequest(http.MethodPost, webChangeClientKeysPath, bytes.NewBuffer([]byte(form.Encode())))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
setJWTCookieForReq(req, webToken)
rr := executeRequest(req)
checkResponseCode(t, http.StatusForbidden, rr)
assert.Contains(t, rr.Body.String(), "unable to verify form token")
form.Set(csrfFormToken, csrfToken)
req, _ = http.NewRequest(http.MethodPost, webChangeClientKeysPath, bytes.NewBuffer([]byte(form.Encode())))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
setJWTCookieForReq(req, webToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "Your public keys has been successfully updated")
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err)
assert.Len(t, user.PublicKeys, 2)
form.Set("public_keys", "invalid")
req, _ = http.NewRequest(http.MethodPost, webChangeClientKeysPath, bytes.NewBuffer([]byte(form.Encode())))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
setJWTCookieForReq(req, webToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "Validation error: could not parse key")
user.Filters.WebClient = append(user.Filters.WebClient, sdk.WebClientPubKeyChangeDisabled)
_, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
webToken, err = getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)
assert.NoError(t, err)
form.Set(csrfFormToken, csrfToken)
form.Set("public_keys", testPubKey)
req, _ = http.NewRequest(http.MethodPost, webChangeClientKeysPath, bytes.NewBuffer([]byte(form.Encode())))
req.RequestURI = webChangeClientKeysPath
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
setJWTCookieForReq(req, webToken)
rr = executeRequest(req)
checkResponseCode(t, http.StatusForbidden, rr)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
}
func TestPreDownloadHook(t *testing.T) {
if runtime.GOOS == osWindows {
t.Skip("this test is not available on Windows")
@ -9743,7 +9773,7 @@ func TestWebAdminLoginMock(t *testing.T) {
}
func TestAdminNoToken(t *testing.T) {
req, _ := http.NewRequest(http.MethodGet, webAdminCredentialsPath, nil)
req, _ := http.NewRequest(http.MethodGet, webAdminProfilePath, nil)
rr := executeRequest(req)
checkResponseCode(t, http.StatusFound, rr)
assert.Equal(t, webLoginPath, rr.Header().Get("Location"))
@ -9762,10 +9792,8 @@ func TestAdminNoToken(t *testing.T) {
checkResponseCode(t, http.StatusUnauthorized, rr)
}
func TestWebUserAllowAPIKeyAuth(t *testing.T) {
u := getTestUser()
u.Filters.AllowAPIKeyAuth = true
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
func TestWebUserProfile(t *testing.T) {
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
assert.NoError(t, err)
csrfToken, err := getCSRFToken(httpBaseURL + webClientLoginPath)
@ -9773,10 +9801,17 @@ func TestWebUserAllowAPIKeyAuth(t *testing.T) {
token, err := getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)
assert.NoError(t, err)
email := "user@user.com"
description := "User"
form := make(url.Values)
form.Set("allow_api_key_auth", "1")
form.Set("email", email)
form.Set("description", description)
form.Set("public_keys", testPubKey)
form.Add("public_keys", testPubKey1)
// no csrf token
req, err := http.NewRequest(http.MethodPost, webChangeClientAPIKeyAccessPath, bytes.NewBuffer([]byte(form.Encode())))
req, err := http.NewRequest(http.MethodPost, webClientProfilePath, bytes.NewBuffer([]byte(form.Encode())))
assert.NoError(t, err)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
setJWTCookieForReq(req, token)
@ -9785,43 +9820,109 @@ func TestWebUserAllowAPIKeyAuth(t *testing.T) {
assert.Contains(t, rr.Body.String(), "unable to verify form token")
form.Set(csrfFormToken, csrfToken)
req, _ = http.NewRequest(http.MethodPost, webChangeClientAPIKeyAccessPath, bytes.NewBuffer([]byte(form.Encode())))
req, _ = http.NewRequest(http.MethodPost, webClientProfilePath, bytes.NewBuffer([]byte(form.Encode())))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
setJWTCookieForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "API key authentication updated")
assert.Contains(t, rr.Body.String(), "Your profile has been successfully updated")
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err)
assert.True(t, user.Filters.AllowAPIKeyAuth)
assert.Len(t, user.PublicKeys, 2)
assert.Equal(t, email, user.Email)
assert.Equal(t, description, user.Description)
form = make(url.Values)
form.Set(csrfFormToken, csrfToken)
req, _ = http.NewRequest(http.MethodPost, webChangeClientAPIKeyAccessPath, bytes.NewBuffer([]byte(form.Encode())))
// set an invalid email
form.Set("email", "not an email")
req, _ = http.NewRequest(http.MethodPost, webClientProfilePath, bytes.NewBuffer([]byte(form.Encode())))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
setJWTCookieForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "API key authentication updated")
assert.Contains(t, rr.Body.String(), "Validation error: email")
// invalid public key
form.Set("email", email)
form.Set("public_keys", "invalid")
req, _ = http.NewRequest(http.MethodPost, webClientProfilePath, bytes.NewBuffer([]byte(form.Encode())))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
setJWTCookieForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "Validation error: could not parse key")
// now remove permissions
form.Set("public_keys", testPubKey)
user.Filters.WebClient = []string{sdk.WebClientAPIKeyAuthChangeDisabled}
_, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
token, err = getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)
assert.NoError(t, err)
form.Set("allow_api_key_auth", "0")
form.Set(csrfFormToken, csrfToken)
req, _ = http.NewRequest(http.MethodPost, webClientProfilePath, bytes.NewBuffer([]byte(form.Encode())))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
setJWTCookieForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "Your profile has been successfully updated")
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err)
assert.False(t, user.Filters.AllowAPIKeyAuth)
assert.True(t, user.Filters.AllowAPIKeyAuth)
assert.Len(t, user.PublicKeys, 1)
assert.Equal(t, email, user.Email)
assert.Equal(t, description, user.Description)
user.Filters.WebClient = []string{sdk.WebClientAPIKeyAuthChangeDisabled}
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
user.Filters.WebClient = []string{sdk.WebClientAPIKeyAuthChangeDisabled, sdk.WebClientPubKeyChangeDisabled}
_, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
assert.False(t, user.CanChangeAPIKeyAuth())
newToken, err := getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)
token, err = getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)
assert.NoError(t, err)
form = make(url.Values)
form.Set("allow_api_key_auth", "1")
form.Set(csrfFormToken, csrfToken)
req, _ = http.NewRequest(http.MethodPost, webChangeClientAPIKeyAccessPath, bytes.NewBuffer([]byte(form.Encode())))
form.Set("public_keys", testPubKey)
form.Add("public_keys", testPubKey1)
req, _ = http.NewRequest(http.MethodPost, webClientProfilePath, bytes.NewBuffer([]byte(form.Encode())))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
setJWTCookieForReq(req, newToken)
setJWTCookieForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "Your profile has been successfully updated")
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err)
assert.True(t, user.Filters.AllowAPIKeyAuth)
assert.Len(t, user.PublicKeys, 1)
assert.Equal(t, email, user.Email)
assert.Equal(t, description, user.Description)
user.Filters.WebClient = []string{sdk.WebClientAPIKeyAuthChangeDisabled, sdk.WebClientInfoChangeDisabled}
_, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
token, err = getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)
assert.NoError(t, err)
form.Set("email", "newemail@user.com")
form.Set("description", "new description")
req, _ = http.NewRequest(http.MethodPost, webClientProfilePath, bytes.NewBuffer([]byte(form.Encode())))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
setJWTCookieForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "Your profile has been successfully updated")
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err)
assert.True(t, user.Filters.AllowAPIKeyAuth)
assert.Len(t, user.PublicKeys, 2)
assert.Equal(t, email, user.Email)
assert.Equal(t, description, user.Description)
// finally disable all profile permissions
user.Filters.WebClient = []string{sdk.WebClientAPIKeyAuthChangeDisabled, sdk.WebClientInfoChangeDisabled,
sdk.WebClientPubKeyChangeDisabled}
_, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
token, err = getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)
assert.NoError(t, err)
req, _ = http.NewRequest(http.MethodPost, webClientProfilePath, bytes.NewBuffer([]byte(form.Encode())))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
setJWTCookieForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusForbidden, rr)
@ -9832,14 +9933,14 @@ func TestWebUserAllowAPIKeyAuth(t *testing.T) {
form = make(url.Values)
form.Set(csrfFormToken, csrfToken)
req, _ = http.NewRequest(http.MethodPost, webChangeClientAPIKeyAccessPath, bytes.NewBuffer([]byte(form.Encode())))
req, _ = http.NewRequest(http.MethodPost, webClientProfilePath, bytes.NewBuffer([]byte(form.Encode())))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
setJWTCookieForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusInternalServerError, rr)
}
func TestWebAdminAllowAPIKeyAuth(t *testing.T) {
func TestWebAdminProfile(t *testing.T) {
admin := getTestAdmin()
admin.Username = altAdminUsername
admin.Password = altAdminPassword
@ -9849,48 +9950,60 @@ func TestWebAdminAllowAPIKeyAuth(t *testing.T) {
assert.NoError(t, err)
csrfToken, err := getCSRFToken(httpBaseURL + webLoginPath)
assert.NoError(t, err)
req, err := http.NewRequest(http.MethodGet, webAdminProfilePath, nil)
assert.NoError(t, err)
setJWTCookieForReq(req, token)
rr := executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
form := make(url.Values)
form.Set("allow_api_key_auth", "1")
form.Set("email", "admin@example.com")
form.Set("description", "admin desc")
// no csrf token
req, err := http.NewRequest(http.MethodPost, webChangeAdminAPIKeyAccessPath, bytes.NewBuffer([]byte(form.Encode())))
req, err = http.NewRequest(http.MethodPost, webAdminProfilePath, bytes.NewBuffer([]byte(form.Encode())))
assert.NoError(t, err)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
setJWTCookieForReq(req, token)
rr := executeRequest(req)
rr = executeRequest(req)
checkResponseCode(t, http.StatusForbidden, rr)
assert.Contains(t, rr.Body.String(), "unable to verify form token")
form.Set(csrfFormToken, csrfToken)
req, _ = http.NewRequest(http.MethodPost, webChangeAdminAPIKeyAccessPath, bytes.NewBuffer([]byte(form.Encode())))
req, _ = http.NewRequest(http.MethodPost, webAdminProfilePath, bytes.NewBuffer([]byte(form.Encode())))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
setJWTCookieForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "API key authentication updated")
assert.Contains(t, rr.Body.String(), "Your profile has been successfully updated")
admin, _, err = httpdtest.GetAdminByUsername(admin.Username, http.StatusOK)
assert.NoError(t, err)
assert.True(t, admin.Filters.AllowAPIKeyAuth)
assert.Equal(t, "admin@example.com", admin.Email)
assert.Equal(t, "admin desc", admin.Description)
form = make(url.Values)
form.Set(csrfFormToken, csrfToken)
req, _ = http.NewRequest(http.MethodPost, webChangeAdminAPIKeyAccessPath, bytes.NewBuffer([]byte(form.Encode())))
req, _ = http.NewRequest(http.MethodPost, webAdminProfilePath, bytes.NewBuffer([]byte(form.Encode())))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
setJWTCookieForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "API key authentication updated")
assert.Contains(t, rr.Body.String(), "Your profile has been successfully updated")
admin, _, err = httpdtest.GetAdminByUsername(admin.Username, http.StatusOK)
assert.NoError(t, err)
assert.False(t, admin.Filters.AllowAPIKeyAuth)
assert.Empty(t, admin.Email)
assert.Empty(t, admin.Description)
_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)
assert.NoError(t, err)
form = make(url.Values)
form.Set(csrfFormToken, csrfToken)
req, _ = http.NewRequest(http.MethodPost, webChangeAdminAPIKeyAccessPath, bytes.NewBuffer([]byte(form.Encode())))
req, _ = http.NewRequest(http.MethodPost, webAdminProfilePath, bytes.NewBuffer([]byte(form.Encode())))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
setJWTCookieForReq(req, token)
rr = executeRequest(req)
@ -9908,7 +10021,7 @@ func TestWebAdminPwdChange(t *testing.T) {
assert.NoError(t, err)
csrfToken, err := getCSRFToken(httpBaseURL + webLoginPath)
assert.NoError(t, err)
req, err := http.NewRequest(http.MethodGet, webAdminCredentialsPath, nil)
req, err := http.NewRequest(http.MethodGet, webChangeAdminPwdPath, nil)
assert.NoError(t, err)
setJWTCookieForReq(req, token)
rr := executeRequest(req)

View file

@ -411,22 +411,22 @@ func TestInvalidToken(t *testing.T) {
assert.Contains(t, rr.Body.String(), "Invalid token claims")
rr = httptest.NewRecorder()
getUserAPIKeyAuthStatus(rr, req)
getUserProfile(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
assert.Contains(t, rr.Body.String(), "Invalid token claims")
rr = httptest.NewRecorder()
changeUserAPIKeyAuthStatus(rr, req)
updateUserProfile(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
assert.Contains(t, rr.Body.String(), "Invalid token claims")
rr = httptest.NewRecorder()
getAdminAPIKeyAuthStatus(rr, req)
getAdminProfile(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
assert.Contains(t, rr.Body.String(), "Invalid token claims")
rr = httptest.NewRecorder()
changeAdminAPIKeyAuthStatus(rr, req)
updateAdminProfile(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
assert.Contains(t, rr.Body.String(), "Invalid token claims")
@ -577,9 +577,8 @@ func TestCreateTokenError(t *testing.T) {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr = httptest.NewRecorder()
handleWebAdminChangePwdPost(rr, req)
// the claim is invalid so we fail to render the client page since
// we have to load the logged admin
assert.Equal(t, http.StatusInternalServerError, rr.Code, rr.Body.String())
assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
assert.Contains(t, rr.Body.String(), "invalid URL escape")
req, _ = http.NewRequest(http.MethodGet, webLoginPath+"?a=a%C3%A2%G3", nil)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
@ -596,24 +595,19 @@ func TestCreateTokenError(t *testing.T) {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr = httptest.NewRecorder()
handleWebClientChangePwdPost(rr, req)
assert.Equal(t, http.StatusInternalServerError, rr.Code, rr.Body.String())
assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
assert.Contains(t, rr.Body.String(), "invalid URL escape")
req, _ = http.NewRequest(http.MethodPost, webChangeClientKeysPath+"?a=a%C3%AO%GB", bytes.NewBuffer([]byte(form.Encode())))
req, _ = http.NewRequest(http.MethodPost, webClientProfilePath+"?a=a%C3%AO%GB", bytes.NewBuffer([]byte(form.Encode())))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr = httptest.NewRecorder()
handleWebClientManageKeysPost(rr, req)
handleWebClientProfilePost(rr, req)
assert.Equal(t, http.StatusInternalServerError, rr.Code, rr.Body.String())
req, _ = http.NewRequest(http.MethodPost, webChangeClientAPIKeyAccessPath+"?a=a%C3%AO%GA", bytes.NewBuffer([]byte(form.Encode())))
req, _ = http.NewRequest(http.MethodPost, webAdminProfilePath+"?a=a%C3%AO%GB", bytes.NewBuffer([]byte(form.Encode())))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr = httptest.NewRecorder()
handleWebClientManageAPIKeyPost(rr, req)
assert.Equal(t, http.StatusInternalServerError, rr.Code, rr.Body.String())
req, _ = http.NewRequest(http.MethodPost, webChangeAdminAPIKeyAccessPath+"?a=a%C3%AO%GB", bytes.NewBuffer([]byte(form.Encode())))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr = httptest.NewRecorder()
handleWebAdminManageAPIKeyPost(rr, req)
handleWebAdminProfilePost(rr, req)
assert.Equal(t, http.StatusInternalServerError, rr.Code, rr.Body.String())
req, _ = http.NewRequest(http.MethodPost, webAdminTwoFactorPath+"?a=a%C3%AO%GC", bytes.NewBuffer([]byte(form.Encode())))
@ -754,8 +748,8 @@ func TestJWTTokenValidation(t *testing.T) {
permClientFn := checkHTTPUserPerm(sdk.WebClientPubKeyChangeDisabled)
fn = permClientFn(r)
rr = httptest.NewRecorder()
req, _ = http.NewRequest(http.MethodPost, webChangeClientKeysPath, nil)
req.RequestURI = webChangeClientKeysPath
req, _ = http.NewRequest(http.MethodPost, webClientProfilePath, nil)
req.RequestURI = webClientProfilePath
ctx = jwtauth.NewContext(req.Context(), token, errTest)
fn.ServeHTTP(rr, req.WithContext(ctx))
assert.Equal(t, http.StatusBadRequest, rr.Code)
@ -1745,19 +1739,10 @@ func TestInvalidClaims(t *testing.T) {
form := make(url.Values)
form.Set(csrfFormToken, createCSRFToken())
form.Set("public_keys", "")
req, _ := http.NewRequest(http.MethodPost, webChangeClientKeysPath, bytes.NewBuffer([]byte(form.Encode())))
req, _ := http.NewRequest(http.MethodPost, webClientProfilePath, bytes.NewBuffer([]byte(form.Encode())))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
handleWebClientManageKeysPost(rr, req)
assert.Equal(t, http.StatusInternalServerError, rr.Code)
form = make(url.Values)
form.Set(csrfFormToken, createCSRFToken())
form.Set("allow_api_key_auth", "")
req, _ = http.NewRequest(http.MethodPost, webChangeClientKeysPath, bytes.NewBuffer([]byte(form.Encode())))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
handleWebClientManageAPIKeyPost(rr, req)
handleWebClientProfilePost(rr, req)
assert.Equal(t, http.StatusInternalServerError, rr.Code)
admin := dataprovider.Admin{
@ -1774,10 +1759,10 @@ func TestInvalidClaims(t *testing.T) {
form = make(url.Values)
form.Set(csrfFormToken, createCSRFToken())
form.Set("allow_api_key_auth", "")
req, _ = http.NewRequest(http.MethodPost, webChangeClientKeysPath, bytes.NewBuffer([]byte(form.Encode())))
req, _ = http.NewRequest(http.MethodPost, webAdminProfilePath, bytes.NewBuffer([]byte(form.Encode())))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
handleWebAdminManageAPIKeyPost(rr, req)
handleWebAdminProfilePost(rr, req)
assert.Equal(t, http.StatusInternalServerError, rr.Code)
}

View file

@ -243,25 +243,22 @@ paths:
$ref: '#/components/responses/InternalServerError'
default:
$ref: '#/components/responses/DefaultResponse'
/admin/apikeyauth:
/admin/profile:
get:
security:
- BearerAuth: []
tags:
- admins
summary: Get API key authentication status
description: 'Returns the API Key authentication status for the logged in admin'
operationId: get_admin_api_key_status
summary: Get profile
description: 'Returns the profile for the logged in admin'
operationId: get_admin_profile
responses:
'200':
description: successful operation
content:
application/json:
schema:
type: object
properties:
allow_api_key_auth:
type: boolean
$ref: '#/components/schemas/AdminProfile'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
@ -275,18 +272,15 @@ paths:
- BearerAuth: []
tags:
- admins
summary: Update API key auth status
description: 'Allows to enable/disable the API key authentication for the logged in admin. If enabled, you can impersonate this admin, in REST API, using an API key, otherwise your credentials, including two-factor authentication, if enabled, are required to use the REST API on your behalf'
operationId: update_admin_api_key_status
summary: Update admin profile
description: 'Allows to update the profile for the logged in admin'
operationId: update_admin_profile
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
allow_api_key_auth:
type: boolean
$ref: '#/components/schemas/AdminProfile'
responses:
'200':
description: successful operation
@ -2338,8 +2332,9 @@ paths:
- BearerAuth: []
tags:
- users API
deprecated: true
summary: Get the user's public keys
description: Returns the public keys for the logged in user
description: 'Returns the public keys for the logged in user. Deprecated please use "/user/profile" instead'
operationId: get_user_public_keys
responses:
'200':
@ -2365,8 +2360,9 @@ paths:
- BearerAuth: []
tags:
- users API
deprecated: true
summary: Set the user's public keys
description: Sets the public keys for the logged in user. Public keys must be in OpenSSH format
description: 'Sets the public keys for the logged in user. Public keys must be in OpenSSH format. Deprecated please use "/user/profile" instead'
operationId: set_user_public_keys
requestBody:
required: true
@ -2395,25 +2391,22 @@ paths:
$ref: '#/components/responses/InternalServerError'
default:
$ref: '#/components/responses/DefaultResponse'
/user/apikeyauth:
/user/profile:
get:
security:
- BearerAuth: []
tags:
- users API
summary: Get API key authentication status
description: 'Returns the API Key authentication status for the logged in user'
operationId: get_user_api_key_status
summary: Get user profile
description: 'Returns the profile for the logged in user'
operationId: get_user_profile
responses:
'200':
description: successful operation
content:
application/json:
schema:
type: object
properties:
allow_api_key_auth:
type: boolean
$ref: '#/components/schemas/UserProfile'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
@ -2427,18 +2420,15 @@ paths:
- BearerAuth: []
tags:
- users API
summary: Update API key auth status
description: 'Allows to enable/disable the API key authentication for the logged in user. If enabled, you can impersonate this user, in REST API, using an API key, otherwise your credentials, including two-factor authentication, if enabled, are required to use the REST API on your behalf'
operationId: update_user_api_key_status
summary: Update profile
description: 'Allows to update the profile for the logged in user'
operationId: update_user_profile
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
allow_api_key_auth:
type: boolean
$ref: '#/components/schemas/UserProfile'
responses:
'200':
description: successful operation
@ -3224,6 +3214,7 @@ components:
- mfa-disabled
- password-change-disabled
- api-key-auth-change-disabled
- info-change-disabled
description: |
Options:
* `publickey-change-disabled` - changing SSH public keys is not allowed
@ -3231,6 +3222,7 @@ components:
* `mfa-disabled` - enabling multi-factor authentication is not allowed. This option cannot be set if the user has MFA already enabled
* `password-change-disabled` - changing password is not allowed
* `api-key-auth-change-disabled` - enabling/disabling API key authentication is not allowed
* `info-change-disabled` - changing info such as email and description is not allowed
APIKeyScope:
type: integer
enum:
@ -3830,6 +3822,34 @@ components:
type: integer
format: int64
description: Last user login as unix timestamp in milliseconds. It is saved at most once every 10 minutes
AdminProfile:
type: object
properties:
email:
type: string
format: email
description:
type: string
allow_api_key_auth:
type: boolean
description: 'If enabled, you can impersonate this admin, in REST API, using an API key. If disabled admin credentials are required for impersonation'
UserProfile:
type: object
properties:
email:
type: string
format: email
description:
type: string
allow_api_key_auth:
type: boolean
description: 'If enabled, you can impersonate this user, in REST API, using an API key. If disabled user credentials are required for impersonation'
public_keys:
type: array
items:
type: string
example: ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEUWwDwEWhTbF0MqAsp/oXK1HR2cElhM8oo1uVmL3ZeDKDiTm4ljMr92wfTgIGDqIoxmVqgYIkAOAhuykAVWBzc= user@host
description: Public keys in OpenSSH format
APIKey:
type: object
properties:

View file

@ -909,8 +909,8 @@ func (s *httpdServer) initializeRouter() {
})
router.With(forbidAPIKeyAuthentication).Get(logoutPath, s.logout)
router.With(forbidAPIKeyAuthentication).Get(adminManageAPIKeyPath, getAdminAPIKeyAuthStatus)
router.With(forbidAPIKeyAuthentication).Put(adminManageAPIKeyPath, changeAdminAPIKeyAuthStatus)
router.With(forbidAPIKeyAuthentication).Get(adminProfilePath, getAdminProfile)
router.With(forbidAPIKeyAuthentication).Put(adminProfilePath, updateAdminProfile)
router.With(forbidAPIKeyAuthentication).Put(adminPwdPath, changeAdminPassword)
// compatibility layer to remove in v2.2
router.With(forbidAPIKeyAuthentication).Put(adminPwdCompatPath, changeAdminPassword)
@ -1003,9 +1003,8 @@ func (s *httpdServer) initializeRouter() {
Get(userPublicKeysPath, getUserPublicKeys)
router.With(forbidAPIKeyAuthentication, checkHTTPUserPerm(sdk.WebClientPubKeyChangeDisabled)).
Put(userPublicKeysPath, setUserPublicKeys)
router.With(forbidAPIKeyAuthentication).Get(userManageAPIKeyPath, getUserAPIKeyAuthStatus)
router.With(forbidAPIKeyAuthentication, checkHTTPUserPerm(sdk.WebClientAPIKeyAuthChangeDisabled)).
Put(userManageAPIKeyPath, changeUserAPIKeyAuthStatus)
router.With(forbidAPIKeyAuthentication).Get(userProfilePath, getUserProfile)
router.With(forbidAPIKeyAuthentication).Put(userProfilePath, updateUserProfile)
// user TOTP APIs
router.With(forbidAPIKeyAuthentication, checkHTTPUserPerm(sdk.WebClientMFADisabled)).
Get(userTOTPConfigsPath, getTOTPConfigs)
@ -1101,13 +1100,12 @@ func (s *httpdServer) initializeRouter() {
router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
Delete(webClientDirsPath, deleteUserDir)
router.With(s.refreshCookie).Get(webClientDownloadZipPath, handleWebClientDownloadZip)
router.With(s.refreshCookie).Get(webClientCredentialsPath, handleClientGetCredentials)
router.With(s.refreshCookie).Get(webClientProfilePath, handleClientGetProfile)
router.Post(webClientProfilePath, handleWebClientProfilePost)
router.With(checkHTTPUserPerm(sdk.WebClientPasswordChangeDisabled)).
Get(webChangeClientPwdPath, handleWebClientChangePwd)
router.With(checkHTTPUserPerm(sdk.WebClientPasswordChangeDisabled)).
Post(webChangeClientPwdPath, handleWebClientChangePwdPost)
router.With(checkHTTPUserPerm(sdk.WebClientAPIKeyAuthChangeDisabled)).
Post(webChangeClientAPIKeyAccessPath, handleWebClientManageAPIKeyPost)
router.With(checkHTTPUserPerm(sdk.WebClientPubKeyChangeDisabled)).
Post(webChangeClientKeysPath, handleWebClientManageKeysPost)
router.With(checkHTTPUserPerm(sdk.WebClientMFADisabled), s.refreshCookie).
Get(webClientMFAPath, handleWebClientMFA)
router.With(checkHTTPUserPerm(sdk.WebClientMFADisabled), verifyCSRFHeader).
@ -1150,9 +1148,11 @@ func (s *httpdServer) initializeRouter() {
router.Use(jwtAuthenticatorWebAdmin)
router.Get(webLogoutPath, handleWebLogout)
router.With(s.refreshCookie).Get(webAdminCredentialsPath, handleWebAdminCredentials)
router.With(s.refreshCookie).Get(webAdminProfilePath, handleWebAdminProfile)
router.Post(webAdminProfilePath, handleWebAdminProfilePost)
router.With(s.refreshCookie).Get(webChangeAdminPwdPath, handleWebAdminChangePwd)
router.Post(webChangeAdminPwdPath, handleWebAdminChangePwdPost)
router.Post(webChangeAdminAPIKeyAccessPath, handleWebAdminManageAPIKeyPost)
router.With(s.refreshCookie).Get(webAdminMFAPath, handleWebAdminMFA)
router.With(verifyCSRFHeader).Post(webAdminTOTPGeneratePath, generateTOTPSecret)
router.With(verifyCSRFHeader).Post(webAdminTOTPValidatePath, validateTOTPPasscode)

View file

@ -56,7 +56,8 @@ const (
templateStatus = "status.html"
templateLogin = "login.html"
templateDefender = "defender.html"
templateCredentials = "credentials.html"
templateProfile = "profile.html"
templateChangePwd = "changepassword.html"
templateMaintenance = "maintenance.html"
templateMFA = "mfa.html"
templateSetup = "adminsetup.html"
@ -65,7 +66,8 @@ const (
pageConnectionsTitle = "Connections"
pageStatusTitle = "Status"
pageFoldersTitle = "Folders"
pageCredentialsTitle = "Manage credentials"
pageProfileTitle = "My profile"
pageChangePwdTitle = "Change password"
pageMaintenanceTitle = "Maintenance"
pageDefenderTitle = "Defender"
pageSetupTitle = "Create first admin user"
@ -91,7 +93,8 @@ type basePage struct {
FolderTemplateURL string
DefenderURL string
LogoutURL string
CredentialsURL string
ProfileURL string
ChangePwdURL string
MFAURL string
FolderQuotaScanURL string
StatusURL string
@ -157,13 +160,17 @@ type adminPage struct {
IsAdd bool
}
type credentialsPage struct {
type profilePage struct {
basePage
Error string
AllowAPIKeyAuth bool
ChangePwdURL string
ManageAPIKeyURL string
APIKeyError string
Email string
Description string
}
type changePasswordPage struct {
basePage
Error string
}
type mfaPage struct {
@ -231,9 +238,13 @@ func loadAdminTemplates(templatesPath string) {
filepath.Join(templatesPath, templateAdminDir, templateBase),
filepath.Join(templatesPath, templateAdminDir, templateAdmin),
}
credentialsPaths := []string{
profilePaths := []string{
filepath.Join(templatesPath, templateAdminDir, templateBase),
filepath.Join(templatesPath, templateAdminDir, templateCredentials),
filepath.Join(templatesPath, templateAdminDir, templateProfile),
}
changePwdPaths := []string{
filepath.Join(templatesPath, templateAdminDir, templateBase),
filepath.Join(templatesPath, templateAdminDir, templateChangePwd),
}
connectionsPaths := []string{
filepath.Join(templatesPath, templateAdminDir, templateBase),
@ -298,7 +309,8 @@ func loadAdminTemplates(templatesPath string) {
folderTmpl := util.LoadTemplate(rootTpl, folderPath...)
statusTmpl := util.LoadTemplate(rootTpl, statusPath...)
loginTmpl := util.LoadTemplate(rootTpl, loginPath...)
credentialsTmpl := util.LoadTemplate(rootTpl, credentialsPaths...)
profileTmpl := util.LoadTemplate(rootTpl, profilePaths...)
changePwdTmpl := util.LoadTemplate(rootTpl, changePwdPaths...)
maintenanceTmpl := util.LoadTemplate(rootTpl, maintenancePath...)
defenderTmpl := util.LoadTemplate(rootTpl, defenderPath...)
mfaTmpl := util.LoadTemplate(nil, mfaPath...)
@ -316,7 +328,8 @@ func loadAdminTemplates(templatesPath string) {
adminTemplates[templateFolder] = folderTmpl
adminTemplates[templateStatus] = statusTmpl
adminTemplates[templateLogin] = loginTmpl
adminTemplates[templateCredentials] = credentialsTmpl
adminTemplates[templateProfile] = profileTmpl
adminTemplates[templateChangePwd] = changePwdTmpl
adminTemplates[templateMaintenance] = maintenanceTmpl
adminTemplates[templateDefender] = defenderTmpl
adminTemplates[templateMFA] = mfaTmpl
@ -343,7 +356,8 @@ func getBasePageData(title, currentURL string, r *http.Request) basePage {
FolderTemplateURL: webTemplateFolder,
DefenderURL: webDefenderPath,
LogoutURL: webLogoutPath,
CredentialsURL: webAdminCredentialsPath,
ProfileURL: webAdminProfilePath,
ChangePwdURL: webChangeAdminPwdPath,
MFAURL: webAdminMFAPath,
QuotaScanURL: webQuotaScanPath,
ConnectionsURL: webConnectionsPath,
@ -446,13 +460,10 @@ func renderMFAPage(w http.ResponseWriter, r *http.Request) {
renderAdminTemplate(w, templateMFA, data)
}
func renderCredentialsPage(w http.ResponseWriter, r *http.Request, pwdError, apiKeyError string) {
data := credentialsPage{
basePage: getBasePageData(pageCredentialsTitle, webAdminCredentialsPath, r),
ChangePwdURL: webChangeAdminPwdPath,
ManageAPIKeyURL: webChangeAdminAPIKeyAccessPath,
Error: pwdError,
APIKeyError: apiKeyError,
func renderProfilePage(w http.ResponseWriter, r *http.Request, error string) {
data := profilePage{
basePage: getBasePageData(pageProfileTitle, webAdminProfilePath, r),
Error: error,
}
admin, err := dataprovider.AdminExists(data.LoggedAdmin.Username)
if err != nil {
@ -460,8 +471,19 @@ func renderCredentialsPage(w http.ResponseWriter, r *http.Request, pwdError, api
return
}
data.AllowAPIKeyAuth = admin.Filters.AllowAPIKeyAuth
data.Email = admin.Email
data.Description = admin.Description
renderAdminTemplate(w, templateCredentials, data)
renderAdminTemplate(w, templateProfile, data)
}
func renderChangePasswordPage(w http.ResponseWriter, r *http.Request, error string) {
data := changePasswordPage{
basePage: getBasePageData(pageChangePwdTitle, webChangeAdminPwdPath, r),
Error: error,
}
renderAdminTemplate(w, templateChangePwd, data)
}
func renderMaintenancePage(w http.ResponseWriter, r *http.Request, error string) {
@ -1125,16 +1147,21 @@ func handleWebAdminMFA(w http.ResponseWriter, r *http.Request) {
renderMFAPage(w, r)
}
func handleWebAdminCredentials(w http.ResponseWriter, r *http.Request) {
func handleWebAdminProfile(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
renderCredentialsPage(w, r, "", "")
renderProfilePage(w, r, "")
}
func handleWebAdminChangePwd(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
renderChangePasswordPage(w, r, "")
}
func handleWebAdminChangePwdPost(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
err := r.ParseForm()
if err != nil {
renderCredentialsPage(w, r, err.Error(), "")
renderChangePasswordPage(w, r, err.Error())
return
}
if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
@ -1144,17 +1171,17 @@ func handleWebAdminChangePwdPost(w http.ResponseWriter, r *http.Request) {
err = doChangeAdminPassword(r, r.Form.Get("current_password"), r.Form.Get("new_password1"),
r.Form.Get("new_password2"))
if err != nil {
renderCredentialsPage(w, r, err.Error(), "")
renderChangePasswordPage(w, r, err.Error())
return
}
handleWebLogout(w, r)
}
func handleWebAdminManageAPIKeyPost(w http.ResponseWriter, r *http.Request) {
func handleWebAdminProfilePost(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
err := r.ParseForm()
if err != nil {
renderCredentialsPage(w, r, err.Error(), "")
renderProfilePage(w, r, err.Error())
return
}
if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
@ -1163,22 +1190,24 @@ func handleWebAdminManageAPIKeyPost(w http.ResponseWriter, r *http.Request) {
}
claims, err := getTokenClaims(r)
if err != nil || claims.Username == "" {
renderCredentialsPage(w, r, "", "Invalid token claims")
renderProfilePage(w, r, "Invalid token claims")
return
}
admin, err := dataprovider.AdminExists(claims.Username)
if err != nil {
renderCredentialsPage(w, r, "", err.Error())
renderProfilePage(w, r, err.Error())
return
}
admin.Filters.AllowAPIKeyAuth = len(r.Form.Get("allow_api_key_auth")) > 0
admin.Email = r.Form.Get("email")
admin.Description = r.Form.Get("description")
err = dataprovider.UpdateAdmin(&admin)
if err != nil {
renderCredentialsPage(w, r, "", err.Error())
renderProfilePage(w, r, err.Error())
return
}
renderMessagePage(w, r, "API key authentication updated", "", http.StatusOK, nil,
"Your API key access permission has been successfully updated")
renderMessagePage(w, r, "Profile updated", "", http.StatusOK, nil,
"Your profile has been successfully updated")
}
func handleWebLogout(w http.ResponseWriter, r *http.Request) {

View file

@ -30,12 +30,15 @@ const (
templateClientLogin = "login.html"
templateClientFiles = "files.html"
templateClientMessage = "message.html"
templateClientCredentials = "credentials.html"
templateClientProfile = "profile.html"
templateClientChangePwd = "changepassword.html"
templateClientTwoFactor = "twofactor.html"
templateClientTwoFactorRecovery = "twofactor-recovery.html"
templateClientMFA = "mfa.html"
pageClientFilesTitle = "My Files"
pageClientCredentialsTitle = "Credentials"
pageClientProfileTitle = "My Profile"
pageClientChangePwdTitle = "Change password"
pageClient2FATitle = "Two-factor auth"
)
// condResult is the result of an HTTP request precondition check.
@ -59,19 +62,20 @@ func isZeroTime(t time.Time) bool {
}
type baseClientPage struct {
Title string
CurrentURL string
FilesURL string
CredentialsURL string
StaticURL string
LogoutURL string
MFAURL string
MFATitle string
FilesTitle string
CredentialsTitle string
Version string
CSRFToken string
LoggedUser *dataprovider.User
Title string
CurrentURL string
FilesURL string
ProfileURL string
ChangePwdURL string
StaticURL string
LogoutURL string
MFAURL string
MFATitle string
FilesTitle string
ProfileTitle string
Version string
CSRFToken string
LoggedUser *dataprovider.User
}
type dirMapping struct {
@ -99,16 +103,19 @@ type clientMessagePage struct {
Success string
}
type clientCredentialsPage struct {
type clientProfilePage struct {
baseClientPage
PublicKeys []string
CanSubmit bool
AllowAPIKeyAuth bool
ChangePwdURL string
ManageKeysURL string
ManageAPIKeyURL string
PwdError string
KeyError string
APIKeyError string
Email string
Description string
Error string
}
type changeClientPasswordPage struct {
baseClientPage
Error string
}
type clientMFAPage struct {
@ -138,9 +145,13 @@ func loadClientTemplates(templatesPath string) {
filepath.Join(templatesPath, templateClientDir, templateClientBase),
filepath.Join(templatesPath, templateClientDir, templateClientFiles),
}
credentialsPaths := []string{
profilePaths := []string{
filepath.Join(templatesPath, templateClientDir, templateClientBase),
filepath.Join(templatesPath, templateClientDir, templateClientCredentials),
filepath.Join(templatesPath, templateClientDir, templateClientProfile),
}
changePwdPaths := []string{
filepath.Join(templatesPath, templateClientDir, templateClientBase),
filepath.Join(templatesPath, templateClientDir, templateClientChangePwd),
}
loginPath := []string{
filepath.Join(templatesPath, templateClientDir, templateClientBaseLogin),
@ -164,7 +175,8 @@ func loadClientTemplates(templatesPath string) {
}
filesTmpl := util.LoadTemplate(nil, filesPaths...)
credentialsTmpl := util.LoadTemplate(nil, credentialsPaths...)
profileTmpl := util.LoadTemplate(nil, profilePaths...)
changePwdTmpl := util.LoadTemplate(nil, changePwdPaths...)
loginTmpl := util.LoadTemplate(nil, loginPath...)
messageTmpl := util.LoadTemplate(nil, messagePath...)
mfaTmpl := util.LoadTemplate(nil, mfaPath...)
@ -172,7 +184,8 @@ func loadClientTemplates(templatesPath string) {
twoFactorRecoveryTmpl := util.LoadTemplate(nil, twoFactorRecoveryPath...)
clientTemplates[templateClientFiles] = filesTmpl
clientTemplates[templateClientCredentials] = credentialsTmpl
clientTemplates[templateClientProfile] = profileTmpl
clientTemplates[templateClientChangePwd] = changePwdTmpl
clientTemplates[templateClientLogin] = loginTmpl
clientTemplates[templateClientMessage] = messageTmpl
clientTemplates[templateClientMFA] = mfaTmpl
@ -188,19 +201,20 @@ func getBaseClientPageData(title, currentURL string, r *http.Request) baseClient
v := version.Get()
return baseClientPage{
Title: title,
CurrentURL: currentURL,
FilesURL: webClientFilesPath,
CredentialsURL: webClientCredentialsPath,
StaticURL: webStaticFilesPath,
LogoutURL: webClientLogoutPath,
MFAURL: webClientMFAPath,
MFATitle: "Two-factor auth",
FilesTitle: pageClientFilesTitle,
CredentialsTitle: pageClientCredentialsTitle,
Version: fmt.Sprintf("%v-%v", v.Version, v.CommitHash),
CSRFToken: csrfToken,
LoggedUser: getUserFromToken(r),
Title: title,
CurrentURL: currentURL,
FilesURL: webClientFilesPath,
ProfileURL: webClientProfilePath,
ChangePwdURL: webChangeClientPwdPath,
StaticURL: webStaticFilesPath,
LogoutURL: webClientLogoutPath,
MFAURL: webClientMFAPath,
MFATitle: pageClient2FATitle,
FilesTitle: pageClientFilesTitle,
ProfileTitle: pageClientProfileTitle,
Version: fmt.Sprintf("%v-%v", v.Version, v.CommitHash),
CSRFToken: csrfToken,
LoggedUser: getUserFromToken(r),
}
}
@ -320,15 +334,10 @@ func renderFilesPage(w http.ResponseWriter, r *http.Request, dirName, error stri
renderClientTemplate(w, templateClientFiles, data)
}
func renderClientCredentialsPage(w http.ResponseWriter, r *http.Request, pwdError, keyError, apiKeyError string) {
data := clientCredentialsPage{
baseClientPage: getBaseClientPageData(pageClientCredentialsTitle, webClientCredentialsPath, r),
ChangePwdURL: webChangeClientPwdPath,
ManageKeysURL: webChangeClientKeysPath,
ManageAPIKeyURL: webChangeClientAPIKeyAccessPath,
PwdError: pwdError,
KeyError: keyError,
APIKeyError: apiKeyError,
func renderClientProfilePage(w http.ResponseWriter, r *http.Request, error string) {
data := clientProfilePage{
baseClientPage: getBaseClientPageData(pageClientProfileTitle, webClientProfilePath, r),
Error: error,
}
user, err := dataprovider.UserExists(data.LoggedUser.Username)
if err != nil {
@ -337,7 +346,19 @@ func renderClientCredentialsPage(w http.ResponseWriter, r *http.Request, pwdErro
}
data.PublicKeys = user.PublicKeys
data.AllowAPIKeyAuth = user.Filters.AllowAPIKeyAuth
renderClientTemplate(w, templateClientCredentials, data)
data.Email = user.Email
data.Description = user.Description
data.CanSubmit = user.CanChangeAPIKeyAuth() || user.CanManagePublicKeys() || user.CanChangeInfo()
renderClientTemplate(w, templateClientProfile, data)
}
func renderClientChangePasswordPage(w http.ResponseWriter, r *http.Request, error string) {
data := changeClientPasswordPage{
baseClientPage: getBaseClientPageData(pageClientChangePwdTitle, webChangeClientPwdPath, r),
Error: error,
}
renderClientTemplate(w, templateClientChangePwd, data)
}
func handleWebClientLogout(w http.ResponseWriter, r *http.Request) {
@ -513,16 +534,21 @@ func handleClientGetFiles(w http.ResponseWriter, r *http.Request) {
}
}
func handleClientGetCredentials(w http.ResponseWriter, r *http.Request) {
func handleClientGetProfile(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
renderClientCredentialsPage(w, r, "", "", "")
renderClientProfilePage(w, r, "")
}
func handleWebClientChangePwd(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
renderClientChangePasswordPage(w, r, "")
}
func handleWebClientChangePwdPost(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
err := r.ParseForm()
if err != nil {
renderClientCredentialsPage(w, r, err.Error(), "", "")
renderClientChangePasswordPage(w, r, err.Error())
return
}
if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
@ -532,17 +558,17 @@ func handleWebClientChangePwdPost(w http.ResponseWriter, r *http.Request) {
err = doChangeUserPassword(r, r.Form.Get("current_password"), r.Form.Get("new_password1"),
r.Form.Get("new_password2"))
if err != nil {
renderClientCredentialsPage(w, r, err.Error(), "", "")
renderClientChangePasswordPage(w, r, err.Error())
return
}
handleWebClientLogout(w, r)
}
func handleWebClientManageKeysPost(w http.ResponseWriter, r *http.Request) {
func handleWebClientProfilePost(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
err := r.ParseForm()
if err != nil {
renderClientCredentialsPage(w, r, "", err.Error(), "")
renderClientProfilePage(w, r, err.Error())
return
}
if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
@ -551,53 +577,35 @@ func handleWebClientManageKeysPost(w http.ResponseWriter, r *http.Request) {
}
claims, err := getTokenClaims(r)
if err != nil || claims.Username == "" {
renderClientCredentialsPage(w, r, "", "Invalid token claims", "")
renderClientProfilePage(w, r, "Invalid token claims")
return
}
user, err := dataprovider.UserExists(claims.Username)
if err != nil {
renderClientCredentialsPage(w, r, "", err.Error(), "")
renderClientProfilePage(w, r, err.Error())
return
}
user.PublicKeys = r.Form["public_keys"]
if !user.CanManagePublicKeys() && !user.CanChangeAPIKeyAuth() && !user.CanChangeInfo() {
renderClientForbiddenPage(w, r, "You are not allowed to change anything")
return
}
if user.CanManagePublicKeys() {
user.PublicKeys = r.Form["public_keys"]
}
if user.CanChangeAPIKeyAuth() {
user.Filters.AllowAPIKeyAuth = len(r.Form.Get("allow_api_key_auth")) > 0
}
if user.CanChangeInfo() {
user.Email = r.Form.Get("email")
user.Description = r.Form.Get("description")
}
err = dataprovider.UpdateUser(&user)
if err != nil {
renderClientCredentialsPage(w, r, "", err.Error(), "")
renderClientProfilePage(w, r, err.Error())
return
}
renderClientMessagePage(w, r, "Public keys updated", "", http.StatusOK, nil,
"Your public keys has been successfully updated")
}
func handleWebClientManageAPIKeyPost(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
err := r.ParseForm()
if err != nil {
renderClientCredentialsPage(w, r, "", "", err.Error())
return
}
if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
renderClientForbiddenPage(w, r, err.Error())
return
}
claims, err := getTokenClaims(r)
if err != nil || claims.Username == "" {
renderClientCredentialsPage(w, r, "", "", "Invalid token claims")
return
}
user, err := dataprovider.UserExists(claims.Username)
if err != nil {
renderClientCredentialsPage(w, r, "", "", err.Error())
return
}
user.Filters.AllowAPIKeyAuth = len(r.Form.Get("allow_api_key_auth")) > 0
err = dataprovider.UpdateUser(&user)
if err != nil {
renderClientCredentialsPage(w, r, "", "", err.Error())
return
}
renderClientMessagePage(w, r, "API key authentication updated", "", http.StatusOK, nil,
"Your API key access permission has been successfully updated")
renderClientMessagePage(w, r, "Profile updated", "", http.StatusOK, nil,
"Your profile has been successfully updated")
}
func handleWebClientMFA(w http.ResponseWriter, r *http.Request) {

View file

@ -14,12 +14,13 @@ const (
WebClientMFADisabled = "mfa-disabled"
WebClientPasswordChangeDisabled = "password-change-disabled"
WebClientAPIKeyAuthChangeDisabled = "api-key-auth-change-disabled"
WebClientInfoChangeDisabled = "info-change-disabled"
)
var (
// WebClientOptions defines the available options for the web client interface/user REST API
WebClientOptions = []string{WebClientPubKeyChangeDisabled, WebClientWriteDisabled, WebClientMFADisabled,
WebClientPasswordChangeDisabled, WebClientAPIKeyAuthChangeDisabled}
WebClientOptions = []string{WebClientWriteDisabled, WebClientPasswordChangeDisabled, WebClientPubKeyChangeDisabled,
WebClientMFADisabled, WebClientAPIKeyAuthChangeDisabled, WebClientInfoChangeDisabled}
// UserTypes defines the supported user type hints for auth plugins
UserTypes = []string{string(UserTypeLDAP), string(UserTypeOS)}
)

View file

@ -115,7 +115,6 @@ func (c *Config) getAuthType() mail.AuthType {
}
// SendEmail tries to send an email using the specified parameters.
// If the contentType is 0 text/plain is assumed, otherwise text/html
func SendEmail(to, subject, body string, contentType EmailContentType) error {
if smtpServer == nil {
return errors.New("smtp: not configured")

View file

@ -22,6 +22,14 @@
</div>
</div>
<div class="form-group row">
<label for="idEmail" class="col-sm-2 col-form-label">Email</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="idEmail" name="email" placeholder=""
value="{{.Admin.Email}}" maxlength="255">
</div>
</div>
<div class="form-group row">
<label for="idDescription" class="col-sm-2 col-form-label">Description</label>
<div class="col-sm-10">
@ -69,14 +77,6 @@
</div>
</div>
<div class="form-group row">
<label for="idEmail" class="col-sm-2 col-form-label">Email</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="idEmail" name="email" placeholder=""
value="{{.Admin.Email}}" maxlength="255">
</div>
</div>
<div class="form-group row">
<label for="idAllowedIP" class="col-sm-2 col-form-label">Allowed IP/Mask</label>
<div class="col-sm-10">

View file

@ -167,9 +167,13 @@
</a>
<!-- Dropdown - User Information -->
<div class="dropdown-menu dropdown-menu-right shadow animated--grow-in" aria-labelledby="userDropdown">
<a class="dropdown-item" href="{{.CredentialsURL}}">
<a class="dropdown-item" href="{{.ProfileURL}}">
<i class="fas fa-user fa-sm fa-fw mr-2 text-gray-400"></i>
Profile
</a>
<a class="dropdown-item" href="{{.ChangePwdURL}}">
<i class="fas fa-key fa-sm fa-fw mr-2 text-gray-400"></i>
Credentials
Change password
</a>
{{if .LoggedAdmin.CanManageMFA}}
<a class="dropdown-item" href="{{.MFAURL}}">

View file

@ -14,7 +14,7 @@
<div class="card-body text-form-error">{{.Error}}</div>
</div>
{{end}}
<form id="user_form" action="{{.ChangePwdURL}}" method="POST" autocomplete="off">
<form id="user_form" action="{{.CurrentURL}}" method="POST" autocomplete="off">
<div class="form-group row">
<label for="idCurrentPassword" class="col-sm-2 col-form-label">Current password</label>
<div class="col-sm-10">
@ -41,31 +41,4 @@
</form>
</div>
</div>
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">REST API access</h6>
</div>
<div class="card-body">
{{if .APIKeyError}}
<div class="card mb-4 border-left-warning">
<div class="card-body text-form-error">{{.APIKeyError}}</div>
</div>
{{end}}
<form id="key_form" action="{{.ManageAPIKeyURL}}" method="POST">
<div class="form-group">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="idAllowAPIKeyAuth" name="allow_api_key_auth"
{{if .AllowAPIKeyAuth}}checked{{end}} aria-describedby="allowAPIKeyAuthHelpBlock">
<label for="idAllowAPIKeyAuth" class="form-check-label">Allow API key authentication</label>
<small id="allowAPIKeyAuthHelpBlock" class="form-text text-muted">
Allow to impersonate yourself, in REST API, with an API key. If this permission is not granted, your credentials are required to use the REST API on your behalf
</small>
</div>
</div>
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<button type="submit" class="btn btn-primary float-right mt-3 px-5 px-3">Submit</button>
</form>
</div>
</div>
{{end}}

View file

@ -0,0 +1,50 @@
{{template "base" .}}
{{define "title"}}{{.Title}}{{end}}
{{define "page_body"}}
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">My profile - {{.LoggedAdmin.Username}}</h6>
</div>
<div class="card-body">
{{if .Error}}
<div class="card mb-4 border-left-warning">
<div class="card-body text-form-error">{{.Error}}</div>
</div>
{{end}}
<form id="profile_form" action="{{.CurrentURL}}" method="POST">
<div class="form-group row">
<label for="idEmail" class="col-sm-2 col-form-label">Email</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="idEmail" name="email" placeholder=""
value="{{.Email}}" maxlength="255">
</div>
</div>
<div class="form-group row">
<label for="idDescription" class="col-sm-2 col-form-label">Description</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="idDescription" name="description" placeholder=""
value="{{.Description}}" maxlength="255">
</div>
</div>
<div class="form-group">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="idAllowAPIKeyAuth" name="allow_api_key_auth"
{{if .AllowAPIKeyAuth}}checked{{end}} aria-describedby="allowAPIKeyAuthHelpBlock">
<label for="idAllowAPIKeyAuth" class="form-check-label">Allow API key authentication</label>
<small id="allowAPIKeyAuthHelpBlock" class="form-text text-muted">
Allow to impersonate yourself, in REST API, with an API key. If this permission is not granted, your credentials are required to use the REST API on your behalf
</small>
</div>
</div>
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<button type="submit" class="btn btn-primary float-right mt-3 px-5 px-3">Submit</button>
</form>
</div>
</div>
{{end}}

View file

@ -79,10 +79,10 @@
</a>
</li>
<li class="nav-item {{if eq .CurrentURL .CredentialsURL}}active{{end}}">
<a class="nav-link" href="{{.CredentialsURL}}">
<i class="fas fa-key"></i>
<span>{{.CredentialsTitle}}</span></a>
<li class="nav-item {{if eq .CurrentURL .ProfileURL}}active{{end}}">
<a class="nav-link" href="{{.ProfileURL}}">
<i class="fas fa-user"></i>
<span>{{.ProfileTitle}}</span></a>
</li>
{{if .LoggedUser.CanManageMFA}}
<li class="nav-item {{if eq .CurrentURL .MFAURL}}active{{end}}">
@ -129,6 +129,13 @@
</a>
<!-- Dropdown - User Information -->
<div class="dropdown-menu dropdown-menu-right shadow animated--grow-in" aria-labelledby="userDropdown">
{{if .LoggedUser.CanChangePassword}}
<a class="dropdown-item" href="{{.ChangePwdURL}}">
<i class="fas fa-key fa-sm fa-fw mr-2 text-gray-400"></i>
Change password
</a>
<div class="dropdown-divider"></div>
{{end}}
<a class="dropdown-item" href="#" data-toggle="modal" data-target="#logoutModal">
<i class="fas fa-sign-out-alt fa-sm fa-fw mr-2 text-gray-400"></i>
Logout

View file

@ -0,0 +1,44 @@
{{template "base" .}}
{{define "title"}}{{.Title}}{{end}}
{{define "page_body"}}
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Change password</h6>
</div>
<div class="card-body">
{{if .Error}}
<div class="card mb-4 border-left-warning">
<div class="card-body text-form-error">{{.Error}}</div>
</div>
{{end}}
<form id="user_form" action="{{.CurrentURL}}" method="POST" autocomplete="off">
<div class="form-group row">
<label for="idCurrentPassword" class="col-sm-2 col-form-label">Current password</label>
<div class="col-sm-10">
<input type="password" class="form-control" id="idCurrentPassword" name="current_password" required>
</div>
</div>
<div class="form-group row">
<label for="idNewPassword1" class="col-sm-2 col-form-label">New password</label>
<div class="col-sm-10">
<input type="password" class="form-control" id="idNewPassword1" name="new_password1" required>
</div>
</div>
<div class="form-group row">
<label for="idNewPassword2" class="col-sm-2 col-form-label">Confirm password</label>
<div class="col-sm-10">
<input type="password" class="form-control" id="idNewPassword2" name="new_password2" required>
</div>
</div>
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<button type="submit" class="btn btn-primary float-right mt-3 px-5 px-3">Change my password</button>
</form>
</div>
</div>
{{end}}

View file

@ -1,158 +0,0 @@
{{template "base" .}}
{{define "title"}}{{.Title}}{{end}}
{{define "page_body"}}
{{if .LoggedUser.CanChangePassword}}
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Change password</h6>
</div>
<div class="card-body">
{{if .PwdError}}
<div class="card mb-4 border-left-warning">
<div class="card-body text-form-error">{{.PwdError}}</div>
</div>
{{end}}
<form id="user_form" action="{{.ChangePwdURL}}" method="POST" autocomplete="off">
<div class="form-group row">
<label for="idCurrentPassword" class="col-sm-2 col-form-label">Current password</label>
<div class="col-sm-10">
<input type="password" class="form-control" id="idCurrentPassword" name="current_password" required>
</div>
</div>
<div class="form-group row">
<label for="idNewPassword1" class="col-sm-2 col-form-label">New password</label>
<div class="col-sm-10">
<input type="password" class="form-control" id="idNewPassword1" name="new_password1" required>
</div>
</div>
<div class="form-group row">
<label for="idNewPassword2" class="col-sm-2 col-form-label">Confirm password</label>
<div class="col-sm-10">
<input type="password" class="form-control" id="idNewPassword2" name="new_password2" required>
</div>
</div>
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<button type="submit" class="btn btn-primary float-right mt-3 px-5 px-3">Change my password</button>
</form>
</div>
</div>
{{end}}
{{if .LoggedUser.CanManagePublicKeys}}
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Manage public keys</h6>
</div>
<div class="card-body">
{{if .KeyError}}
<div class="card mb-4 border-left-warning">
<div class="card-body text-form-error">{{.KeyError}}</div>
</div>
{{end}}
<form id="key_form" action="{{.ManageKeysURL}}" method="POST">
<div class="form-group row">
<div class="col-md-12 form_field_pk_outer">
{{range $idx, $val := .PublicKeys}}
<div class="row form_field_pk_outer_row">
<div class="form-group col-md-11">
<textarea class="form-control" id="idPublicKey{{$idx}}" name="public_keys" rows="4"
placeholder="Paste your public key here">{{$val}}</textarea>
</div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_pk_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
{{else}}
<div class="row form_field_pk_outer_row">
<div class="form-group col-md-11">
<textarea class="form-control" id="idPublicKey0" name="public_keys" rows="4"
placeholder="Paste your public key here"></textarea>
</div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_pk_btn_frm_field" disabled>
<i class="fas fa-trash"></i>
</button>
</div>
</div>
{{end}}
</div>
</div>
<div class="row mx-1">
<button type="button" class="btn btn-secondary add_new_pk_field_btn">
<i class="fas fa-plus"></i> Add new public key
</button>
</div>
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<button type="submit" class="btn btn-primary float-right mt-3 px-5 px-3">Submit</button>
</form>
</div>
</div>
{{end}}
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">REST API access</h6>
</div>
<div class="card-body">
{{if .APIKeyError}}
<div class="card mb-4 border-left-warning">
<div class="card-body text-form-error">{{.APIKeyError}}</div>
</div>
{{end}}
<form id="key_form" action="{{.ManageAPIKeyURL}}" method="POST">
<div class="form-group">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="idAllowAPIKeyAuth" name="allow_api_key_auth" {{if not .LoggedUser.CanChangeAPIKeyAuth}}disabled="disabled"{{end}}
{{if .AllowAPIKeyAuth}}checked{{end}} aria-describedby="allowAPIKeyAuthHelpBlock">
<label for="idAllowAPIKeyAuth" class="form-check-label">Allow API key authentication</label>
<small id="allowAPIKeyAuthHelpBlock" class="form-text text-muted">
Allow to impersonate yourself, in REST API, with an API key. If this permission is not granted, your credentials are required to use the REST API on your behalf
</small>
</div>
</div>
{{if .LoggedUser.CanChangeAPIKeyAuth}}
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<button type="submit" class="btn btn-primary float-right mt-3 px-5 px-3">Submit</button>
{{end}}
</form>
</div>
</div>
{{end}}
{{define "extra_js"}}
<script type="text/javascript">
$(document).ready(function () {
$("body").on("click", ".add_new_pk_field_btn", function () {
var index = $(".form_field_pk_outer").find(".form_field_pk_outer_row").length;
while (document.getElementById("idPublicKey"+index) != null){
index++;
}
$(".form_field_pk_outer").append(`
<div class="row form_field_pk_outer_row">
<div class="form-group col-md-11">
<textarea class="form-control" id="idPublicKey${index}" name="public_keys" rows="4"
placeholder="Paste your public key here"></textarea>
</div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_pk_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
`);
});
$("body").on("click", ".remove_pk_btn_frm_field", function () {
$(this).closest(".form_field_pk_outer_row").remove();
});
});
</script>
{{end}}

View file

@ -0,0 +1,128 @@
{{template "base" .}}
{{define "title"}}{{.Title}}{{end}}
{{define "page_body"}}
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">My profile - {{.LoggedUser.Username}}</h6>
</div>
<div class="card-body">
{{if .Error}}
<div class="card mb-4 border-left-warning">
<div class="card-body text-form-error">{{.Error}}</div>
</div>
{{end}}
<form id="profile_form" action="{{.CurrentURL}}" method="POST">
<div class="form-group row">
<label for="idEmail" class="col-sm-2 col-form-label">Email</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="idEmail" name="email" placeholder=""
value="{{.Email}}" maxlength="255" autocomplete="nope" {{if not .LoggedUser.CanChangeInfo}}readonly{{end}}>
</div>
</div>
<div class="form-group row">
<label for="idDescription" class="col-sm-2 col-form-label">Description</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="idDescription" name="description" placeholder=""
value="{{.Description}}" maxlength="255" {{if not .LoggedUser.CanChangeInfo}}readonly{{end}}>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="idAllowAPIKeyAuth" name="allow_api_key_auth" {{if not .LoggedUser.CanChangeAPIKeyAuth}}disabled="disabled"{{end}}
{{if .AllowAPIKeyAuth}}checked{{end}} aria-describedby="allowAPIKeyAuthHelpBlock">
<label for="idAllowAPIKeyAuth" class="form-check-label">Allow API key authentication</label>
<small id="allowAPIKeyAuthHelpBlock" class="form-text text-muted">
Allow to impersonate yourself, in REST API, with an API key. If this permission is not granted, your credentials are required to use the REST API on your behalf
</small>
</div>
</div>
{{if .LoggedUser.CanManagePublicKeys}}
<div class="card bg-light mb-3">
<div class="card-header">
Public keys
</div>
<div class="card-body">
<div class="form-group row">
<div class="col-md-12 form_field_pk_outer">
{{range $idx, $val := .PublicKeys}}
<div class="row form_field_pk_outer_row">
<div class="form-group col-md-11">
<textarea class="form-control" id="idPublicKey{{$idx}}" name="public_keys" rows="4"
placeholder="Paste your public key here">{{$val}}</textarea>
</div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_pk_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
{{else}}
<div class="row form_field_pk_outer_row">
<div class="form-group col-md-11">
<textarea class="form-control" id="idPublicKey0" name="public_keys" rows="4"
placeholder="Paste your public key here"></textarea>
</div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_pk_btn_frm_field" disabled>
<i class="fas fa-trash"></i>
</button>
</div>
</div>
{{end}}
</div>
</div>
<div class="row mx-1">
<button type="button" class="btn btn-secondary add_new_pk_field_btn">
<i class="fas fa-plus"></i> Add new public key
</button>
</div>
</div>
</div>
{{end}}
{{if .CanSubmit}}
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<button type="submit" class="btn btn-primary float-right mt-3 px-5 px-3">Submit</button>
{{end}}
</form>
</div>
</div>
{{end}}
{{define "extra_js"}}
{{if .LoggedUser.CanManagePublicKeys}}
<script type="text/javascript">
$(document).ready(function () {
$("body").on("click", ".add_new_pk_field_btn", function () {
var index = $(".form_field_pk_outer").find(".form_field_pk_outer_row").length;
while (document.getElementById("idPublicKey"+index) != null){
index++;
}
$(".form_field_pk_outer").append(`
<div class="row form_field_pk_outer_row">
<div class="form-group col-md-11">
<textarea class="form-control" id="idPublicKey${index}" name="public_keys" rows="4"
placeholder="Paste your public key here"></textarea>
</div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_pk_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
`);
});
$("body").on("click", ".remove_pk_btn_frm_field", function () {
$(this).closest(".form_field_pk_outer_row").remove();
});
});
</script>
{{end}}
{{end}}