diff --git a/.github/workflows/development.yml b/.github/workflows/development.yml index 9fe8901e..91a9b126 100644 --- a/.github/workflows/development.yml +++ b/.github/workflows/development.yml @@ -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 diff --git a/dataprovider/user.go b/dataprovider/user.go index 39c51ff3..4d48663e 100644 --- a/dataprovider/user.go +++ b/dataprovider/user.go @@ -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 { diff --git a/docs/full-configuration.md b/docs/full-configuration.md index 522ec1f3..ea1b68cd 100644 --- a/docs/full-configuration.md +++ b/docs/full-configuration.md @@ -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`. diff --git a/go.mod b/go.mod index 4f99c08c..d21ed9dd 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 0dbd2e61..2b8567da 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/httpd/api_admin.go b/httpd/api_admin.go index a99c2a1c..c0450529 100644 --- a/httpd/api_admin.go +++ b/httpd/api_admin.go @@ -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) { diff --git a/httpd/api_http_user.go b/httpd/api_http_user.go index 0f465d81..c752a59f 100644 --- a/httpd/api_http_user.go +++ b/httpd/api_http_user.go @@ -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) { diff --git a/httpd/api_utils.go b/httpd/api_utils.go index f7d442cb..e5492de1 100644 --- a/httpd/api_utils.go +++ b/httpd/api_utils.go @@ -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) { diff --git a/httpd/httpd.go b/httpd/httpd.go index b5d953ac..7bf74a88 100644 --- a/httpd/httpd.go +++ b/httpd/httpd.go @@ -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) diff --git a/httpd/httpd_test.go b/httpd/httpd_test.go index fe69bb85..b4eae339 100644 --- a/httpd/httpd_test.go +++ b/httpd/httpd_test.go @@ -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) diff --git a/httpd/internal_test.go b/httpd/internal_test.go index c32df4a8..d963900f 100644 --- a/httpd/internal_test.go +++ b/httpd/internal_test.go @@ -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) } diff --git a/httpd/schema/openapi.yaml b/httpd/schema/openapi.yaml index dddfe044..bbfc3995 100644 --- a/httpd/schema/openapi.yaml +++ b/httpd/schema/openapi.yaml @@ -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: diff --git a/httpd/server.go b/httpd/server.go index e329e853..791f13b7 100644 --- a/httpd/server.go +++ b/httpd/server.go @@ -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) diff --git a/httpd/webadmin.go b/httpd/webadmin.go index fc0c62de..c71d5e1c 100644 --- a/httpd/webadmin.go +++ b/httpd/webadmin.go @@ -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) { diff --git a/httpd/webclient.go b/httpd/webclient.go index 25c0233e..bca693b9 100644 --- a/httpd/webclient.go +++ b/httpd/webclient.go @@ -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) { diff --git a/sdk/user.go b/sdk/user.go index 8db50e0b..f60cd6c5 100644 --- a/sdk/user.go +++ b/sdk/user.go @@ -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)} ) diff --git a/smtp/smtp.go b/smtp/smtp.go index c71291ea..b5c65e40 100644 --- a/smtp/smtp.go +++ b/smtp/smtp.go @@ -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") diff --git a/templates/webadmin/admin.html b/templates/webadmin/admin.html index df75fef3..b704082f 100644 --- a/templates/webadmin/admin.html +++ b/templates/webadmin/admin.html @@ -22,6 +22,14 @@ +