diff --git a/go.mod b/go.mod index e6444636..e82f3326 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( github.com/bmatcuk/doublestar/v4 v4.6.1 github.com/cockroachdb/cockroach-go/v2 v2.3.7 github.com/coreos/go-oidc/v3 v3.10.0 - github.com/drakkan/webdav v0.0.0-20240414072657-7c19d3cb5103 + github.com/drakkan/webdav v0.0.0-20240503091431-218ec83910bb github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001 github.com/fclairamb/ftpserverlib v0.24.0 github.com/fclairamb/go-log v0.5.0 @@ -52,7 +52,7 @@ require ( github.com/rs/cors v1.11.0 github.com/rs/xid v1.5.0 github.com/rs/zerolog v1.32.0 - github.com/sftpgo/sdk v0.1.6-0.20240502175518-0e29cf9357a3 + github.com/sftpgo/sdk v0.1.6-0.20240503162435-c5606dbe6084 github.com/shirou/gopsutil/v3 v3.24.4 github.com/spf13/afero v1.11.0 github.com/spf13/cobra v1.8.0 @@ -84,7 +84,7 @@ require ( cloud.google.com/go/compute/metadata v0.3.0 // indirect cloud.google.com/go/iam v1.1.8 // indirect filippo.io/edwards25519 v1.1.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/internal v1.6.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.7.0 // indirect github.com/ajg/form v1.5.1 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 // indirect @@ -118,7 +118,7 @@ require ( github.com/golang/protobuf v1.5.4 // indirect github.com/google/s2a-go v0.1.7 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect - github.com/googleapis/gax-go/v2 v2.12.3 // indirect + github.com/googleapis/gax-go/v2 v2.12.4 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect diff --git a/go.sum b/go.sum index bdb88351..551c07e0 100644 --- a/go.sum +++ b/go.sum @@ -20,8 +20,8 @@ github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 h1:E+OJmp2tPvt1W+amx48v1eqb github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1 h1:sO0/P7g68FrryJzljemN+6GTssUXdANk6aJ7T1ZxnsQ= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1/go.mod h1:h8hyGFDsU5HMivxiS2iYFZsgDbU9OnnJ163x5UGVKYo= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.6.0 h1:sUFnFjzDUie80h24I7mrKtwCKgLY9L8h5Tp2x9+TWqk= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.6.0/go.mod h1:52JbnQTp15qg5mRkMBHwp0j0ZFwHJ42Sx3zVV5RE9p0= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.7.0 h1:rTfKOCZGy5ViVrlA74ZPE99a+SgoEE2K/yg3RyW9dFA= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.7.0/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.5.0 h1:AifHbc4mg0x9zW52WOpKbsHaDKuRhlI7TVl47thgQ70= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.5.0/go.mod h1:T5RfihdXtBDxt1Ch2wobif3TvzTdumDy29kahv6AV9A= github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.2 h1:YUUxeiOWgdAQE3pXt2H7QXzZs0q8UBjgRbl56qo8GYM= @@ -110,8 +110,6 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= -github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= -github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= github.com/drakkan/cron/v3 v3.0.0-20230222140221-217a1e4d96c0 h1:EW9gIJRmt9lzk66Fhh4S8VEtURA6QHZqGeSRE9Nb2/U= github.com/drakkan/cron/v3 v3.0.0-20230222140221-217a1e4d96c0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/drakkan/crypto v0.0.0-20240405104909-a6b14455cac6 h1:XzaQu+jRDZWu+CroSbYeNj87kvU73lTEUNbUZt1xjAo= @@ -120,8 +118,8 @@ github.com/drakkan/ftp v0.0.0-20240430173938-7ba8270c8e7f h1:S9JUlrOzjK58UKoLqqb github.com/drakkan/ftp v0.0.0-20240430173938-7ba8270c8e7f/go.mod h1:4p8lUl4vQ80L598CygL+3IFtm+3nggvvW/palOlViwE= github.com/drakkan/ftpserverlib v0.0.0-20240502162317-7bc57ede068a h1:IULpJkoPn+DlpbF0owSZDao1yCFuOaDrSnIEpvdxXM8= github.com/drakkan/ftpserverlib v0.0.0-20240502162317-7bc57ede068a/go.mod h1:+9afJRWESpCq4/O8Vr00Q2jfinRxP6PiCpXph6CgGuc= -github.com/drakkan/webdav v0.0.0-20240414072657-7c19d3cb5103 h1:jhcR8ixhpd3f8iBeH/6pJ9V3BNjY3Yjrb7Ipp8Cezmw= -github.com/drakkan/webdav v0.0.0-20240414072657-7c19d3cb5103/go.mod h1:zOVb1QDhwwqWn2L2qZ0U3swMSO4GTSNyIwXCGO/UGWE= +github.com/drakkan/webdav v0.0.0-20240503091431-218ec83910bb h1:067/Uo8cfeY7QC0yzWCr/RImuNcM0rLWAsBUyMks59o= +github.com/drakkan/webdav v0.0.0-20240503091431-218ec83910bb/go.mod h1:zOVb1QDhwwqWn2L2qZ0U3swMSO4GTSNyIwXCGO/UGWE= github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001 h1:/ZshrfQzayqRSBDodmp3rhNCHJCff+utvgBuWRbiqu4= github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001/go.mod h1:kltMsfRMTHSFdMbK66XdS8mfMW77+FZA1fGY1xYMF84= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -212,6 +210,8 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfF github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= github.com/googleapis/gax-go/v2 v2.12.3 h1:5/zPPDvw8Q1SuXjrqrZslrqT7dL/uJT2CQii/cLCKqA= github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4= +github.com/googleapis/gax-go/v2 v2.12.4 h1:9gWcmF85Wvq4ryPFvGFaOgPIs1AQX0d0bcbGw4Z96qg= +github.com/googleapis/gax-go/v2 v2.12.4/go.mod h1:KYEYLorsnIGDi/rPC8b5TdlB9kbKoFubselGIoBMCwI= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -333,8 +333,8 @@ github.com/prometheus/common v0.53.0 h1:U2pL9w9nmJwJDa4qqLQ3ZaePJ6ZTwt7cMD3AG3+a github.com/prometheus/common v0.53.0/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U= github.com/prometheus/procfs v0.14.0 h1:Lw4VdGGoKEZilJsayHf0B+9YgLGREba2C6xr+Fdfq6s= github.com/prometheus/procfs v0.14.0/go.mod h1:XL+Iwz8k8ZabyZfMFHPiilCniixqQarAy5Mu67pHlNQ= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po= github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= @@ -351,8 +351,8 @@ github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4 h1:PT+ElG/UUFMfqy5HrxJ github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4/go.mod h1:MnkX001NG75g3p8bhFycnyIjeQoOjGL6CEIsdE/nKSY= github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= -github.com/sftpgo/sdk v0.1.6-0.20240502175518-0e29cf9357a3 h1:EsC1qh/9YS+vybUPOJNcHRwSNTGGUSqsFlDL1wkzO+Y= -github.com/sftpgo/sdk v0.1.6-0.20240502175518-0e29cf9357a3/go.mod h1:ler/KG6kMLlsOs/8s6dVN3oom+z+NkbXBVWO//Cv/WA= +github.com/sftpgo/sdk v0.1.6-0.20240503162435-c5606dbe6084 h1:oGSw0jSxIvjQ2TIsh7MEjAe98vSne6RSTrTChACcIyM= +github.com/sftpgo/sdk v0.1.6-0.20240503162435-c5606dbe6084/go.mod h1:ler/KG6kMLlsOs/8s6dVN3oom+z+NkbXBVWO//Cv/WA= github.com/shirou/gopsutil/v3 v3.24.4 h1:dEHgzZXt4LMNm+oYELpzl9YCqV65Yr/6SfrvgRBtXeU= github.com/shirou/gopsutil/v3 v3.24.4/go.mod h1:lTd2mdiOspcqLgAnr9/nGi71NkeMpWKdmhuxm9GusH8= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= diff --git a/internal/dataprovider/dataprovider.go b/internal/dataprovider/dataprovider.go index 8252b0de..11bf52a6 100644 --- a/internal/dataprovider/dataprovider.go +++ b/internal/dataprovider/dataprovider.go @@ -3037,27 +3037,31 @@ func validateFilterProtocols(filters *sdk.BaseUserFilters) error { return nil } -func validateTLSCerts(certs []string) error { +func validateTLSCerts(certs []string) ([]string, error) { + var validateCerts []string for idx, cert := range certs { + if cert == "" { + continue + } derBlock, _ := pem.Decode([]byte(cert)) if derBlock == nil { - return util.NewI18nError( + return nil, util.NewI18nError( util.NewValidationError(fmt.Sprintf("invalid TLS certificate %d", idx)), util.I18nErrorInvalidTLSCert, ) } - cert, err := x509.ParseCertificate(derBlock.Bytes) + crt, err := x509.ParseCertificate(derBlock.Bytes) if err != nil { - return util.NewI18nError( + return nil, util.NewI18nError( util.NewValidationError(fmt.Sprintf("error parsing TLS certificate %d", idx)), util.I18nErrorInvalidTLSCert, ) } - if cert.PublicKeyAlgorithm == x509.RSA { - if rsaCert, ok := cert.PublicKey.(*rsa.PublicKey); ok { + if crt.PublicKeyAlgorithm == x509.RSA { + if rsaCert, ok := crt.PublicKey.(*rsa.PublicKey); ok { if size := rsaCert.N.BitLen(); size < 2048 { providerLog(logger.LevelError, "rsa cert with size %d not accepted, minimum 2048", size) - return util.NewI18nError( + return nil, util.NewI18nError( util.NewValidationError(fmt.Sprintf("invalid size %d for rsa cert at position %d, minimum 2048", size, idx)), util.I18nErrorKeySizeInvalid, @@ -3065,8 +3069,9 @@ func validateTLSCerts(certs []string) error { } } } + validateCerts = append(validateCerts, cert) } - return nil + return validateCerts, nil } func validateBaseFilters(filters *sdk.BaseUserFilters) error { @@ -3093,9 +3098,11 @@ func validateBaseFilters(filters *sdk.BaseUserFilters) error { return util.NewValidationError(fmt.Sprintf("invalid TLS username: %q", filters.TLSUsername)) } } - if err := validateTLSCerts(filters.TLSCerts); err != nil { + certs, err := validateTLSCerts(filters.TLSCerts) + if err != nil { return err } + filters.TLSCerts = certs for _, opts := range filters.WebClient { if !util.Contains(sdk.WebClientOptions, opts) { return util.NewValidationError(fmt.Sprintf("invalid web client options %q", opts)) diff --git a/internal/dataprovider/user.go b/internal/dataprovider/user.go index d45d8cbc..80970c26 100644 --- a/internal/dataprovider/user.go +++ b/internal/dataprovider/user.go @@ -1097,11 +1097,23 @@ func (u *User) CanChangeInfo() bool { } // CanManagePublicKeys returns true if this user is allowed to manage public keys -// from the web client. Used in web client UI +// from the WebClient. Used in WebClient UI func (u *User) CanManagePublicKeys() bool { return !util.Contains(u.Filters.WebClient, sdk.WebClientPubKeyChangeDisabled) } +// CanManageTLSCerts returns true if this user is allowed to manage TLS certificates +// from the WebClient. Used in WebClient UI +func (u *User) CanManageTLSCerts() bool { + return !util.Contains(u.Filters.WebClient, sdk.WebClientTLSCertChangeDisabled) +} + +// CanUpdateProfile returns true if the user is allowed to update the profile. +// Used in WebClient UI +func (u *User) CanUpdateProfile() bool { + return u.CanManagePublicKeys() || u.CanChangeAPIKeyAuth() || u.CanChangeInfo() || u.CanManageTLSCerts() +} + // CanAddFilesFromWeb returns true if the client can add files from the web UI. // The specified target is the directory where the files must be uploaded func (u *User) CanAddFilesFromWeb(target string) bool { diff --git a/internal/httpd/api_http_user.go b/internal/httpd/api_http_user.go index 53416763..a65ddda5 100644 --- a/internal/httpd/api_http_user.go +++ b/internal/httpd/api_http_user.go @@ -470,6 +470,7 @@ func getUserProfile(w http.ResponseWriter, r *http.Request) { AllowAPIKeyAuth: user.Filters.AllowAPIKeyAuth, }, PublicKeys: user.PublicKeys, + TLSCerts: user.Filters.TLSCerts, } render.JSON(w, r, resp) } @@ -492,13 +493,16 @@ func updateUserProfile(w http.ResponseWriter, r *http.Request) { sendAPIResponse(w, r, err, "", getRespStatus(err)) return } - if !userMerged.CanManagePublicKeys() && !userMerged.CanChangeAPIKeyAuth() && !userMerged.CanChangeInfo() { + if !userMerged.CanUpdateProfile() { sendAPIResponse(w, r, nil, "You are not allowed to change anything", http.StatusForbidden) return } if userMerged.CanManagePublicKeys() { user.PublicKeys = req.PublicKeys } + if userMerged.CanManageTLSCerts() { + user.Filters.TLSCerts = req.TLSCerts + } if userMerged.CanChangeAPIKeyAuth() { user.Filters.AllowAPIKeyAuth = req.AllowAPIKeyAuth } diff --git a/internal/httpd/api_utils.go b/internal/httpd/api_utils.go index b0fd3eb4..78d7219f 100644 --- a/internal/httpd/api_utils.go +++ b/internal/httpd/api_utils.go @@ -71,6 +71,7 @@ type adminProfile struct { type userProfile struct { baseProfile PublicKeys []string `json:"public_keys,omitempty"` + TLSCerts []string `json:"tls_certs,omitempty"` } func sendAPIResponse(w http.ResponseWriter, r *http.Request, err error, message string, code int) { diff --git a/internal/httpd/httpd_test.go b/internal/httpd/httpd_test.go index 54f42013..c22d6075 100644 --- a/internal/httpd/httpd_test.go +++ b/internal/httpd/httpd_test.go @@ -11135,6 +11135,7 @@ func TestWebAPIChangeUserProfileMock(t *testing.T) { profileReq["email"] = email profileReq["description"] = description profileReq["public_keys"] = []string{testPubKey, testPubKey1} + profileReq["tls_certs"] = []string{httpsCert} asJSON, err := json.Marshal(profileReq) assert.NoError(t, err) req, err = http.NewRequest(http.MethodPut, userProfilePath, bytes.NewBuffer(asJSON)) @@ -11158,6 +11159,10 @@ func TestWebAPIChangeUserProfileMock(t *testing.T) { if assert.True(t, ok, profileReq) { assert.Len(t, val, 2) } + val, ok = profileReq["tls_certs"].([]any) + if assert.True(t, ok, profileReq) { + assert.Len(t, val, 1) + } // set an invalid email profileReq = make(map[string]any) profileReq["email"] = "notavalidemail" @@ -11180,10 +11185,22 @@ func TestWebAPIChangeUserProfileMock(t *testing.T) { rr = executeRequest(req) checkResponseCode(t, http.StatusBadRequest, rr) assert.Contains(t, rr.Body.String(), "Validation error: error parsing public key") + // set an invalid TLS certificate + profileReq = make(map[string]any) + profileReq["tls_certs"] = []string{"not a TLS cert"} + 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: invalid TLS certificate") user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) - user.Filters.WebClient = []string{sdk.WebClientAPIKeyAuthChangeDisabled, sdk.WebClientPubKeyChangeDisabled} + user.Filters.WebClient = []string{sdk.WebClientAPIKeyAuthChangeDisabled, sdk.WebClientPubKeyChangeDisabled, + sdk.WebClientTLSCertChangeDisabled} user.Email = email user.Description = description user.Filters.AllowAPIKeyAuth = true @@ -11197,6 +11214,7 @@ func TestWebAPIChangeUserProfileMock(t *testing.T) { profileReq["email"] = email profileReq["description"] = description + "_mod" //nolint:goconst profileReq["public_keys"] = []string{testPubKey} + profileReq["tls_certs"] = []string{} asJSON, err = json.Marshal(profileReq) assert.NoError(t, err) req, err = http.NewRequest(http.MethodPut, userProfilePath, bytes.NewBuffer(asJSON)) @@ -11221,6 +11239,10 @@ func TestWebAPIChangeUserProfileMock(t *testing.T) { if assert.True(t, ok, profileReq) { assert.Len(t, val, 2) } + val, ok = profileReq["tls_certs"].([]any) + if assert.True(t, ok, profileReq) { + assert.Len(t, val, 1) + } user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) @@ -11257,7 +11279,7 @@ func TestWebAPIChangeUserProfileMock(t *testing.T) { assert.Len(t, profileReq["public_keys"].([]any), 1) // finally disable all profile permissions user.Filters.WebClient = []string{sdk.WebClientAPIKeyAuthChangeDisabled, sdk.WebClientInfoChangeDisabled, - sdk.WebClientPubKeyChangeDisabled} + sdk.WebClientPubKeyChangeDisabled, sdk.WebClientTLSCertChangeDisabled} _, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) req, err = http.NewRequest(http.MethodPut, userProfilePath, bytes.NewBuffer(asJSON)) @@ -19288,6 +19310,7 @@ func TestWebUserProfile(t *testing.T) { form.Set("description", description) form.Set("public_keys[0][public_key]", testPubKey) form.Set("public_keys[1][public_key]", testPubKey1) + form.Set("tls_certs[0][tls_cert]", httpsCert) // no csrf token req, err := http.NewRequest(http.MethodPost, webClientProfilePath, bytes.NewBuffer([]byte(form.Encode()))) assert.NoError(t, err) @@ -19311,6 +19334,7 @@ func TestWebUserProfile(t *testing.T) { assert.NoError(t, err) assert.True(t, user.Filters.AllowAPIKeyAuth) assert.Len(t, user.PublicKeys, 2) + assert.Len(t, user.Filters.TLSCerts, 1) assert.Equal(t, email, user.Email) assert.Equal(t, description, user.Description) @@ -19323,8 +19347,18 @@ func TestWebUserProfile(t *testing.T) { rr = executeRequest(req) checkResponseCode(t, http.StatusOK, rr) assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidEmail) - // invalid public key + // invalid tls cert form.Set("email", email) + form.Set("tls_certs[0][tls_cert]", "not a TLS cert") + req, _ = http.NewRequest(http.MethodPost, webClientProfilePath, bytes.NewBuffer([]byte(form.Encode()))) + req.RemoteAddr = defaultRemoteAddr + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + setJWTCookieForReq(req, token) + rr = executeRequest(req) + checkResponseCode(t, http.StatusOK, rr) + assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidTLSCert) + // invalid public key + form.Set("tls_certs[0][tls_cert]", httpsCert) form.Set("public_keys[0][public_key]", "invalid") req, _ = http.NewRequest(http.MethodPost, webClientProfilePath, bytes.NewBuffer([]byte(form.Encode()))) req.RemoteAddr = defaultRemoteAddr @@ -19355,16 +19389,19 @@ func TestWebUserProfile(t *testing.T) { assert.NoError(t, err) assert.True(t, user.Filters.AllowAPIKeyAuth) assert.Len(t, user.PublicKeys, 1) + assert.Len(t, user.Filters.TLSCerts, 1) assert.Equal(t, email, user.Email) assert.Equal(t, description, user.Description) - user.Filters.WebClient = []string{sdk.WebClientAPIKeyAuthChangeDisabled, sdk.WebClientPubKeyChangeDisabled} + user.Filters.WebClient = []string{sdk.WebClientAPIKeyAuthChangeDisabled, + sdk.WebClientPubKeyChangeDisabled, sdk.WebClientTLSCertChangeDisabled} _, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) token, err = getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword) assert.NoError(t, err) form.Set("public_keys[0][public_key]", testPubKey) form.Set("public_keys[1][public_key]", testPubKey1) + form.Set("tls_certs[0][tls_cert]", "") req, _ = http.NewRequest(http.MethodPost, webClientProfilePath, bytes.NewBuffer([]byte(form.Encode()))) req.RemoteAddr = defaultRemoteAddr req.Header.Set("Content-Type", "application/x-www-form-urlencoded") @@ -19376,6 +19413,7 @@ func TestWebUserProfile(t *testing.T) { assert.NoError(t, err) assert.True(t, user.Filters.AllowAPIKeyAuth) assert.Len(t, user.PublicKeys, 1) + assert.Len(t, user.Filters.TLSCerts, 1) assert.Equal(t, email, user.Email) assert.Equal(t, description, user.Description) @@ -19397,11 +19435,12 @@ func TestWebUserProfile(t *testing.T) { assert.NoError(t, err) assert.True(t, user.Filters.AllowAPIKeyAuth) assert.Len(t, user.PublicKeys, 2) + assert.Len(t, user.Filters.TLSCerts, 0) 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} + sdk.WebClientPubKeyChangeDisabled, sdk.WebClientTLSCertChangeDisabled} _, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) token, err = getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword) diff --git a/internal/httpd/webclient.go b/internal/httpd/webclient.go index 4699ecf1..6a1d018d 100644 --- a/internal/httpd/webclient.go +++ b/internal/httpd/webclient.go @@ -173,6 +173,7 @@ type clientMessagePage struct { type clientProfilePage struct { baseClientPage PublicKeys []string + TLSCerts []string CanSubmit bool AllowAPIKeyAuth bool Email string @@ -821,10 +822,11 @@ func (s *httpdServer) renderClientProfilePage(w http.ResponseWriter, r *http.Req return } data.PublicKeys = user.PublicKeys + data.TLSCerts = user.Filters.TLSCerts data.AllowAPIKeyAuth = user.Filters.AllowAPIKeyAuth data.Email = user.Email data.Description = user.Description - data.CanSubmit = userMerged.CanChangeAPIKeyAuth() || userMerged.CanManagePublicKeys() || userMerged.CanChangeInfo() + data.CanSubmit = userMerged.CanUpdateProfile() renderClientTemplate(w, templateClientProfile, data) } @@ -1615,7 +1617,7 @@ func (s *httpdServer) handleWebClientProfilePost(w http.ResponseWriter, r *http. s.renderClientProfilePage(w, r, util.NewI18nError(err, util.I18nErrorGetUser)) return } - if !userMerged.CanManagePublicKeys() && !userMerged.CanChangeAPIKeyAuth() && !userMerged.CanChangeInfo() { + if !userMerged.CanUpdateProfile() { s.renderClientForbiddenPage(w, r, util.NewI18nError( errors.New("you are not allowed to change anything"), util.I18nErrorNoPermissions, @@ -1630,6 +1632,14 @@ func (s *httpdServer) handleWebClientProfilePost(w http.ResponseWriter, r *http. } user.PublicKeys = r.Form["public_keys"] } + if userMerged.CanManageTLSCerts() { + for k := range r.Form { + if hasPrefixAndSuffix(k, "tls_certs[", "][tls_cert]") { + r.Form.Add("tls_certs", r.Form.Get(k)) + } + } + user.Filters.TLSCerts = r.Form["tls_certs"] + } if userMerged.CanChangeAPIKeyAuth() { user.Filters.AllowAPIKeyAuth = r.Form.Get("allow_api_key_auth") != "" } diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 0cb74887..84063bde 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -5133,6 +5133,7 @@ components: type: string enum: - publickey-change-disabled + - tls-cert-change-disabled - write-disabled - mfa-disabled - password-change-disabled @@ -5144,6 +5145,7 @@ components: description: | Options: * `publickey-change-disabled` - changing SSH public keys is not allowed + * `tls-cert-change-disabled` - changing TLS certificates is not allowed * `write-disabled` - upload, rename, delete are not allowed even if the user has permissions for these actions * `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 diff --git a/static/locales/en/translation.json b/static/locales/en/translation.json index 5d64c84f..9b9e8848 100644 --- a/static/locales/en/translation.json +++ b/static/locales/en/translation.json @@ -170,6 +170,7 @@ "api_key_auth_help": "Allow to impersonate yourself, in REST API, using an API key", "pub_keys": "Public keys", "pub_key_placeholder": "Paste a public key here", + "pub_keys_help": "Public keys can be used for SFTP authentication", "verify": "Verify", "problems": "Having problems?", "allowed_ip_mask": "Allowed IP/Mask", @@ -539,6 +540,7 @@ "expires_in": "Expires in", "expires_in_help": "Account expiration as number of days from the creation. 0 means no expiration", "tls_certs": "TLS certificates", + "tls_certs_help": "TLS certificates can be used for FTP and/or WebDAV authentication", "tls_cert_help": "Paste a PEM encoded TLS certificate here", "tls_cert_invalid": "Invalid TLS certificate", "template_title": "Create one or more new users from this template", diff --git a/static/locales/it/translation.json b/static/locales/it/translation.json index 45904025..9f223de1 100644 --- a/static/locales/it/translation.json +++ b/static/locales/it/translation.json @@ -170,6 +170,7 @@ "api_key_auth_help": "Permetti di impersonarti nelle API REST utilizzando una chiave API", "pub_keys": "Chiavi pubbliche", "pub_key_placeholder": "Incolla qui una chiave pubblica", + "pub_keys_help": "Le chiavi pubbliche possono essere utilizzate per l'autenticazione SFTP", "verify": "Verifica", "problems": "Hai problemi?", "allowed_ip_mask": "IP/Reti permesse", @@ -539,6 +540,7 @@ "expires_in": "Scadenza", "expires_in_help": "Scadenza dell'account espressa in numero di giorni dalla creazione. 0 significa nessuna scadenza", "tls_certs": "Certificati TLS", + "tls_certs_help": "I certificati TLS possono essere utilizzati per l'autenticazione FTP e/o WebDAV", "tls_cert_help": "Incolla qui un tuo certificato TLS codificato PEM", "tls_cert_invalid": "Certificato TLS non valido", "template_title": "Crea uno o piĆ¹ nuovi utenti da questo modello", diff --git a/templates/webadmin/user.html b/templates/webadmin/user.html index dfa3c3aa..ccaeb537 100644 --- a/templates/webadmin/user.html +++ b/templates/webadmin/user.html @@ -149,6 +149,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
+ {{- template "infomsg-no-mb" "general.pub_keys_help"}}
{{- range $idx, $val := .User.PublicKeys}} @@ -208,6 +209,71 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
+ +
+
+

TLS certificates

+
+
+
+ {{- template "infomsg-no-mb" "user.tls_certs_help"}} +
+
+ {{- range $idx, $val := .User.Filters.TLSCerts}} +
+
+
+ +
+ +
+
+ {{- else}} +
+ +
+ {{- end}} +
+
+ + +
+
+
{{- end}} {{- if .Groups}} @@ -626,70 +692,6 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
-
-
-

TLS certificates

-
-
-
-
-
- {{- range $idx, $val := .User.Filters.TLSCerts}} -
-
-
- -
- -
-
- {{- else}} -
- -
- {{- end}} -
-
- - -
-
-
- {{template "user_group_advanced" .User.Filters}}
diff --git a/templates/webclient/profile.html b/templates/webclient/profile.html index 23ec3e26..2f0f6ab3 100644 --- a/templates/webclient/profile.html +++ b/templates/webclient/profile.html @@ -58,6 +58,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
+ {{- template "infomsg-no-mb" "general.pub_keys_help"}}
{{- range $idx, $val := .PublicKeys}} @@ -118,6 +119,72 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
{{- end}} + {{- if .LoggedUser.CanManageTLSCerts}} +
+
+

TLS certificates

+
+
+
+ {{- template "infomsg-no-mb" "user.tls_certs_help"}} +
+
+ {{- range $idx, $val := .TLSCerts}} +
+
+
+ +
+ +
+
+ {{- else}} +
+ +
+ {{- end}} +
+
+ + +
+
+
+ {{- end}}