WebClient: allow to set TLS certificates

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2024-05-03 18:30:03 +02:00
parent 58a8b2b860
commit a1af33c6aa
No known key found for this signature in database
GPG key ID: 935D2952DEC4EECF
13 changed files with 250 additions and 97 deletions

8
go.mod
View file

@ -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

20
go.sum
View file

@ -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=

View file

@ -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))

View file

@ -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 {

View file

@ -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
}

View file

@ -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) {

View file

@ -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)

View file

@ -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") != ""
}

View file

@ -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

View file

@ -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",

View file

@ -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",

View file

@ -149,6 +149,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</div>
<div class="card-body">
<div id="public_keys">
{{- template "infomsg-no-mb" "general.pub_keys_help"}}
<div class="form-group">
<div data-repeater-list="public_keys">
{{- range $idx, $val := .User.PublicKeys}}
@ -208,6 +209,71 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</div>
</div>
</div>
<div class="card mt-10">
<div class="card-header bg-light">
<h3 data-i18n="user.tls_certs" class="card-title section-title-inner">TLS certificates</h3>
</div>
<div class="card-body">
<div id="tls_certs">
{{- template "infomsg-no-mb" "user.tls_certs_help"}}
<div class="form-group">
<div data-repeater-list="tls_certs">
{{- range $idx, $val := .User.Filters.TLSCerts}}
<div data-repeater-item>
<div class="form-group row">
<div class="col-md-9 mt-3 mt-md-8">
<textarea data-i18n="[placeholder]user.tls_cert_help" class="form-control" name="tls_cert" rows="4">{{$val}}</textarea>
</div>
<div class="col-md-3 mt-3 mt-md-8">
<a href="#" data-repeater-delete
class="btn btn-light-danger">
<i class="ki-duotone ki-trash fs-5">
<span class="path1"></span>
<span class="path2"></span>
<span class="path3"></span>
<span class="path4"></span>
<span class="path5"></span>
</i>
<span data-i18n="general.delete">Delete</span>
</a>
</div>
</div>
</div>
{{- else}}
<div data-repeater-item>
<div class="form-group row">
<div class="col-md-9 mt-3 mt-md-8">
<textarea data-i18n="[placeholder]user.tls_cert_help" class="form-control" name="tls_cert" rows="4"></textarea>
</div>
<div class="col-md-3 mt-3 mt-md-8">
<a href="#" data-repeater-delete
class="btn btn-light-danger">
<i class="ki-duotone ki-trash fs-5">
<span class="path1"></span>
<span class="path2"></span>
<span class="path3"></span>
<span class="path4"></span>
<span class="path5"></span>
</i>
<span data-i18n="general.delete">Delete</span>
</a>
</div>
</div>
</div>
{{- end}}
</div>
</div>
<div class="form-group mt-5">
<a href="#" data-repeater-create class="btn btn-light-primary">
<i class="ki-duotone ki-plus fs-3"></i>
<span data-i18n="general.add">Add</span>
</a>
</div>
</div>
</div>
</div>
{{- end}}
{{- if .Groups}}
@ -626,70 +692,6 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<div id="collapseAdvanced" class="accordion-collapse collapse" aria-labelledby="headingAdvanced" data-bs-parent="#accordionUser">
<div class="accordion-body">
<div class="card mt-10">
<div class="card-header bg-light">
<h3 data-i18n="user.tls_certs" class="card-title section-title-inner">TLS certificates</h3>
</div>
<div class="card-body">
<div id="tls_certs">
<div class="form-group">
<div data-repeater-list="tls_certs">
{{- range $idx, $val := .User.Filters.TLSCerts}}
<div data-repeater-item>
<div class="form-group row">
<div class="col-md-9 mt-3 mt-md-8">
<textarea data-i18n="[placeholder]user.tls_cert_help" class="form-control" name="tls_cert" rows="4">{{$val}}</textarea>
</div>
<div class="col-md-3 mt-3 mt-md-8">
<a href="#" data-repeater-delete
class="btn btn-light-danger">
<i class="ki-duotone ki-trash fs-5">
<span class="path1"></span>
<span class="path2"></span>
<span class="path3"></span>
<span class="path4"></span>
<span class="path5"></span>
</i>
<span data-i18n="general.delete">Delete</span>
</a>
</div>
</div>
</div>
{{- else}}
<div data-repeater-item>
<div class="form-group row">
<div class="col-md-9 mt-3 mt-md-8">
<textarea data-i18n="[placeholder]user.tls_cert_help" class="form-control" name="tls_cert" rows="4"></textarea>
</div>
<div class="col-md-3 mt-3 mt-md-8">
<a href="#" data-repeater-delete
class="btn btn-light-danger">
<i class="ki-duotone ki-trash fs-5">
<span class="path1"></span>
<span class="path2"></span>
<span class="path3"></span>
<span class="path4"></span>
<span class="path5"></span>
</i>
<span data-i18n="general.delete">Delete</span>
</a>
</div>
</div>
</div>
{{- end}}
</div>
</div>
<div class="form-group mt-5">
<a href="#" data-repeater-create class="btn btn-light-primary">
<i class="ki-duotone ki-plus fs-3"></i>
<span data-i18n="general.add">Add</span>
</a>
</div>
</div>
</div>
</div>
{{template "user_group_advanced" .User.Filters}}
<div class="form-group row mt-10 {{if not .User.HasExternalAuth}}d-none{{end}}">

View file

@ -58,6 +58,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</div>
<div class="card-body">
<div id="public_keys">
{{- template "infomsg-no-mb" "general.pub_keys_help"}}
<div class="form-group">
<div data-repeater-list="public_keys">
{{- range $idx, $val := .PublicKeys}}
@ -118,6 +119,72 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</div>
</div>
{{- end}}
{{- if .LoggedUser.CanManageTLSCerts}}
<div class="card mt-10">
<div class="card-header bg-light">
<h3 data-i18n="user.tls_certs" class="card-title section-title-inner">TLS certificates</h3>
</div>
<div class="card-body">
<div id="tls_certs">
{{- template "infomsg-no-mb" "user.tls_certs_help"}}
<div class="form-group">
<div data-repeater-list="tls_certs">
{{- range $idx, $val := .TLSCerts}}
<div data-repeater-item>
<div class="form-group row">
<div class="col-md-9 mt-3 mt-md-8">
<textarea data-i18n="[placeholder]user.tls_cert_help" class="form-control" name="tls_cert" rows="4">{{$val}}</textarea>
</div>
<div class="col-md-3 mt-3 mt-md-8">
<a href="#" data-repeater-delete
class="btn btn-light-danger">
<i class="ki-duotone ki-trash fs-5">
<span class="path1"></span>
<span class="path2"></span>
<span class="path3"></span>
<span class="path4"></span>
<span class="path5"></span>
</i>
<span data-i18n="general.delete">Delete</span>
</a>
</div>
</div>
</div>
{{- else}}
<div data-repeater-item>
<div class="form-group row">
<div class="col-md-9 mt-3 mt-md-8">
<textarea data-i18n="[placeholder]user.tls_cert_help" class="form-control" name="tls_cert" rows="4"></textarea>
</div>
<div class="col-md-3 mt-3 mt-md-8">
<a href="#" data-repeater-delete
class="btn btn-light-danger">
<i class="ki-duotone ki-trash fs-5">
<span class="path1"></span>
<span class="path2"></span>
<span class="path3"></span>
<span class="path4"></span>
<span class="path5"></span>
</i>
<span data-i18n="general.delete">Delete</span>
</a>
</div>
</div>
</div>
{{- end}}
</div>
</div>
<div class="form-group mt-5">
<a href="#" data-repeater-create class="btn btn-light-primary">
<i class="ki-duotone ki-plus fs-3"></i>
<span data-i18n="general.add">Add</span>
</a>
</div>
</div>
</div>
</div>
{{- end}}
<div class="d-flex justify-content-end mt-12">
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<button type="submit" id="form_submit" class="btn btn-primary px-10">
@ -136,13 +203,18 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
{{- end}}
{{- define "extra_js"}}
{{- if .LoggedUser.CanManagePublicKeys}}
{{- if or .LoggedUser.CanManagePublicKeys .LoggedUser.CanManageTLSCerts}}
<script {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}} src="{{.StaticURL}}/assets/plugins/custom/formrepeater/formrepeater.bundle.js"></script>
{{- end}}
<script type="text/javascript" {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}}>
KTUtil.onDOMContentLoaded(function () {
//{{- if or .LoggedUser.CanManagePublicKeys .LoggedUser.CanManageTLSCerts}}
//{{- if .LoggedUser.CanManagePublicKeys}}
initRepeater('#public_keys');
//{{- end}}
//{{- if .LoggedUser.CanManageTLSCerts}}
initRepeater('#tls_certs');
//{{- end}}
initRepeaterItems();
//{{- end}}