WebClient: allow to set TLS certificates
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
58a8b2b860
commit
a1af33c6aa
13 changed files with 250 additions and 97 deletions
8
go.mod
8
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
|
||||
|
|
20
go.sum
20
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=
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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") != ""
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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}}">
|
||||
|
|
|
@ -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}}
|
||||
|
||||
|
|
Loading…
Reference in a new issue