web admin: simplify user page

The page to add/edit users should be less less intimidating now.
All the advanced settings are hidden by default. Permissions are set
to any, so if you also have a users base dir set, to add a user
you have to simply set username, password or public key and save

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2022-01-10 19:44:16 +01:00
parent f61456ce87
commit ef626befb1
No known key found for this signature in database
GPG key ID: 2F1FB59433D5A8CB
9 changed files with 574 additions and 448 deletions

View file

@ -831,7 +831,10 @@ func TestParseAllowedIPAndRanges(t *testing.T) {
} }
func TestHideConfidentialData(t *testing.T) { func TestHideConfidentialData(t *testing.T) {
for _, provider := range sdk.ListProviders() { for _, provider := range []sdk.FilesystemProvider{sdk.LocalFilesystemProvider,
sdk.CryptedFilesystemProvider, sdk.S3FilesystemProvider, sdk.GCSFilesystemProvider,
sdk.AzureBlobFilesystemProvider, sdk.SFTPFilesystemProvider,
} {
u := dataprovider.User{ u := dataprovider.User{
FsConfig: vfs.Filesystem{ FsConfig: vfs.Filesystem{
Provider: provider, Provider: provider,

View file

@ -457,6 +457,11 @@ func GetQuotaTracking() int {
return config.TrackQuota return config.TrackQuota
} }
// HasUsersBaseDir returns true if users base dir is set
func HasUsersBaseDir() bool {
return config.UsersBaseDir != ""
}
// Provider defines the interface that data providers must implement. // Provider defines the interface that data providers must implement.
type Provider interface { type Provider interface {
validateUserAndPass(username, password, ip, protocol string) (User, error) validateUserAndPass(username, password, ip, protocol string) (User, error)

View file

@ -561,19 +561,25 @@ func (u *User) AddVirtualDirs(list []os.FileInfo, virtualPath string) []os.FileI
return list return list
} }
vdirs := make(map[string]bool)
for dir := range u.GetVirtualFoldersInPath(virtualPath) { for dir := range u.GetVirtualFoldersInPath(virtualPath) {
fi := vfs.NewFileInfo(dir, true, 0, time.Now(), false) vdirs[path.Base(dir)] = true
found := false }
for index := range list { if len(vdirs) == 0 {
if list[index].Name() == fi.Name() { return list
list[index] = fi }
found = true
break for index := range list {
for dir := range vdirs {
if list[index].Name() == dir {
delete(vdirs, dir)
} }
} }
if !found { }
list = append(list, fi)
} for dir := range vdirs {
fi := vfs.NewFileInfo(dir, true, 0, time.Now(), false)
list = append(list, fi)
} }
return list return list
} }

2
go.mod
View file

@ -39,7 +39,7 @@ require (
github.com/rs/cors v1.8.2 github.com/rs/cors v1.8.2
github.com/rs/xid v1.3.0 github.com/rs/xid v1.3.0
github.com/rs/zerolog v1.26.2-0.20211219225053-665519c4da50 github.com/rs/zerolog v1.26.2-0.20211219225053-665519c4da50
github.com/sftpgo/sdk v0.0.0-20220106101837-50e87c59705a github.com/sftpgo/sdk v0.0.0-20220110174344-ecf586dd8941
github.com/shirou/gopsutil/v3 v3.21.13-0.20220106132423-a3ae4bc40d26 github.com/shirou/gopsutil/v3 v3.21.13-0.20220106132423-a3ae4bc40d26
github.com/spf13/afero v1.8.0 github.com/spf13/afero v1.8.0
github.com/spf13/cobra v1.3.0 github.com/spf13/cobra v1.3.0

4
go.sum
View file

@ -747,8 +747,8 @@ github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdh
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4 h1:PT+ElG/UUFMfqy5HrxJxNzj3QBOf7dZwupeVC+mG1Lo= github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4 h1:PT+ElG/UUFMfqy5HrxJxNzj3QBOf7dZwupeVC+mG1Lo=
github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4/go.mod h1:MnkX001NG75g3p8bhFycnyIjeQoOjGL6CEIsdE/nKSY= github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4/go.mod h1:MnkX001NG75g3p8bhFycnyIjeQoOjGL6CEIsdE/nKSY=
github.com/sftpgo/sdk v0.0.0-20220106101837-50e87c59705a h1:JJc19rE0eW2knPa/KIFYvqyu25CwzKltJ5Cw1kK3o4A= github.com/sftpgo/sdk v0.0.0-20220110174344-ecf586dd8941 h1:CxKFDSYekL6+dOZ9rSglYGwcXyhM4Aki6yDsdiPlJ5Y=
github.com/sftpgo/sdk v0.0.0-20220106101837-50e87c59705a/go.mod h1:Bhgac6kiwIziILXLzH4wepT8lQXyhF83poDXqZorN6Q= github.com/sftpgo/sdk v0.0.0-20220110174344-ecf586dd8941/go.mod h1:Bhgac6kiwIziILXLzH4wepT8lQXyhF83poDXqZorN6Q=
github.com/shirou/gopsutil/v3 v3.21.13-0.20220106132423-a3ae4bc40d26 h1:nkvraEu1xs6D3AimiR9SkIOCG6lVvVZRfwbbQ7fX1DY= github.com/shirou/gopsutil/v3 v3.21.13-0.20220106132423-a3ae4bc40d26 h1:nkvraEu1xs6D3AimiR9SkIOCG6lVvVZRfwbbQ7fX1DY=
github.com/shirou/gopsutil/v3 v3.21.13-0.20220106132423-a3ae4bc40d26/go.mod h1:BToYZVTlSVlfazpDDYFnsVZLaoRG+g8ufT6fPQLdJzA= github.com/shirou/gopsutil/v3 v3.21.13-0.20220106132423-a3ae4bc40d26/go.mod h1:BToYZVTlSVlfazpDDYFnsVZLaoRG+g8ufT6fPQLdJzA=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=

View file

@ -7,6 +7,7 @@ import (
"io" "io"
"net/http" "net/http"
"net/url" "net/url"
"os"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
@ -142,6 +143,13 @@ type statusPage struct {
Status ServicesStatus Status ServicesStatus
} }
type fsWrapper struct {
vfs.Filesystem
IsUserPage bool
HasUsersBaseDir bool
DirPath string
}
type userPage struct { type userPage struct {
basePage basePage
User *dataprovider.User User *dataprovider.User
@ -155,6 +163,8 @@ type userPage struct {
RedactedSecret string RedactedSecret string
Mode userPageMode Mode userPageMode
VirtualFolders []vfs.BaseVirtualFolder VirtualFolders []vfs.BaseVirtualFolder
CanImpersonate bool
FsWrapper fsWrapper
} }
type adminPage struct { type adminPage struct {
@ -207,9 +217,10 @@ type setupPage struct {
type folderPage struct { type folderPage struct {
basePage basePage
Folder vfs.BaseVirtualFolder Folder vfs.BaseVirtualFolder
Error string Error string
Mode folderPageMode Mode folderPageMode
FsWrapper fsWrapper
} }
type messagePage struct { type messagePage struct {
@ -307,7 +318,12 @@ func loadAdminTemplates(templatesPath string) {
} }
fsBaseTpl := template.New("fsBaseTemplate").Funcs(template.FuncMap{ fsBaseTpl := template.New("fsBaseTemplate").Funcs(template.FuncMap{
"ListFSProviders": sdk.ListProviders, "ListFSProviders": func() []sdk.FilesystemProvider {
return []sdk.FilesystemProvider{sdk.LocalFilesystemProvider, sdk.CryptedFilesystemProvider,
sdk.S3FilesystemProvider, sdk.GCSFilesystemProvider,
sdk.AzureBlobFilesystemProvider, sdk.SFTPFilesystemProvider,
}
},
}) })
usersTmpl := util.LoadTemplate(nil, usersPaths...) usersTmpl := util.LoadTemplate(nil, usersPaths...)
userTmpl := util.LoadTemplate(fsBaseTpl, userPaths...) userTmpl := util.LoadTemplate(fsBaseTpl, userPaths...)
@ -594,6 +610,13 @@ func renderUserPage(w http.ResponseWriter, r *http.Request, user *dataprovider.U
WebClientOptions: sdk.WebClientOptions, WebClientOptions: sdk.WebClientOptions,
RootDirPerms: user.GetPermissionsForPath("/"), RootDirPerms: user.GetPermissionsForPath("/"),
VirtualFolders: folders, VirtualFolders: folders,
CanImpersonate: os.Getuid() == 0,
FsWrapper: fsWrapper{
Filesystem: user.FsConfig,
IsUserPage: true,
HasUsersBaseDir: dataprovider.HasUsersBaseDir(),
DirPath: user.HomeDir,
},
} }
renderAdminTemplate(w, templateUser, data) renderAdminTemplate(w, templateUser, data)
} }
@ -619,6 +642,12 @@ func renderFolderPage(w http.ResponseWriter, r *http.Request, folder vfs.BaseVir
Error: error, Error: error,
Folder: folder, Folder: folder,
Mode: mode, Mode: mode,
FsWrapper: fsWrapper{
Filesystem: folder.FsConfig,
IsUserPage: false,
HasUsersBaseDir: false,
DirPath: folder.MappedPath,
},
} }
renderAdminTemplate(w, templateFolder, data) renderAdminTemplate(w, templateFolder, data)
} }
@ -1708,6 +1737,7 @@ func handleWebAddUserGet(w http.ResponseWriter, r *http.Request) {
user.ID = 0 user.ID = 0
user.Username = "" user.Username = ""
user.Password = "" user.Password = ""
user.PublicKeys = nil
user.SetEmptySecrets() user.SetEmptySecrets()
renderUserPage(w, r, &user, userPageModeAdd, "") renderUserPage(w, r, &user, userPageModeAdd, "")
} else if _, ok := err.(*util.RecordNotFoundError); ok { } else if _, ok := err.(*util.RecordNotFoundError); ok {
@ -1716,7 +1746,12 @@ func handleWebAddUserGet(w http.ResponseWriter, r *http.Request) {
renderInternalServerErrorPage(w, r, err) renderInternalServerErrorPage(w, r, err)
} }
} else { } else {
user := dataprovider.User{BaseUser: sdk.BaseUser{Status: 1}} user := dataprovider.User{BaseUser: sdk.BaseUser{
Status: 1,
Permissions: map[string][]string{
"/": {dataprovider.PermAny},
},
}}
renderUserPage(w, r, &user, userPageModeAdd, "") renderUserPage(w, r, &user, userPageModeAdd, "")
} }
} }

View file

@ -76,18 +76,8 @@
</small> </small>
</div> </div>
</div> </div>
<div class="form-group row">
<label for="idMappedPath" class="col-sm-2 col-form-label">Absolute Path</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="idMappedPath" name="mapped_path" placeholder=""
value="{{.Folder.MappedPath}}" maxlength="512" aria-describedby="mappedPathHelpBlock">
<small id="descriptionHelpBlock" class="form-text text-muted">
Required for local providers. For Cloud providers, if set, it will store temporary files
</small>
</div>
</div>
{{template "fshtml" .Folder.FsConfig}} {{template "fshtml" .FsWrapper}}
<input type="hidden" name="_form_token" value="{{.CSRFToken}}"> <input type="hidden" name="_form_token" value="{{.CSRFToken}}">
<button type="submit" class="btn btn-primary float-right mt-3 px-5 px-3">{{if eq .Mode 3}}Generate and export folders{{else}}Submit{{end}}</button> <button type="submit" class="btn btn-primary float-right mt-3 px-5 px-3">{{if eq .Mode 3}}Generate and export folders{{else}}Submit{{end}}</button>

View file

@ -1,5 +1,8 @@
{{define "fshtml"}} {{define "fshtml"}}
<div class="card bg-light mb-3"> <div class="card bg-light mb-3">
<div class="card-header">
<b>Filesystem</b>
</div>
<div class="card-body pb-1"> <div class="card-body pb-1">
<div class="form-group row"> <div class="form-group row">
<label for="idFilesystem" class="col-sm-2 col-form-label">Storage</label> <label for="idFilesystem" class="col-sm-2 col-form-label">Storage</label>
@ -12,7 +15,29 @@
</select> </select>
</div> </div>
</div> </div>
{{if .IsUserPage}}
<div class="form-group row">
<label for="idHomeDir" class="col-sm-2 col-form-label">Home Dir</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="idHomeDir" name="home_dir" placeholder="Absolute path to a local directory"
value="{{.DirPath}}" aria-describedby="homeDirHelpBlock">
<small id="homeDirHelpBlock" class="form-text text-muted">
{{if not .DirPath}}{{if .HasUsersBaseDir}}Leave blank for an appropriate default{{else}}Required for local storage providers. For non-local filesystems it will store temporary files, you can leave blank for an appropriate default{{end}}{{end}}
</small>
</div>
</div>
{{else}}
<div class="form-group row">
<label for="idMappedPath" class="col-sm-2 col-form-label">Home Dir</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="idMappedPath" name="mapped_path" placeholder="Absolute path to a local directory"
value="{{.DirPath}}" aria-describedby="mappedPathHelpBlock">
<small id="mappedPathHelpBlock" class="form-text text-muted">
Required for local storage providers. For non-local filesystems it will store temporary files, you can leave blank for an appropriate default
</small>
</div>
</div>
{{end}}
<div class="form-group row fsconfig fsconfig-s3fs"> <div class="form-group row fsconfig fsconfig-s3fs">
<label for="idS3Bucket" class="col-sm-2 col-form-label">Bucket</label> <label for="idS3Bucket" class="col-sm-2 col-form-label">Bucket</label>
<div class="col-sm-3"> <div class="col-sm-3">
@ -123,7 +148,7 @@
<label for="idS3KeyPrefix" class="col-sm-2 col-form-label">Key Prefix</label> <label for="idS3KeyPrefix" class="col-sm-2 col-form-label">Key Prefix</label>
<div class="col-sm-10"> <div class="col-sm-10">
<input type="text" class="form-control" id="idS3KeyPrefix" name="s3_key_prefix" placeholder="" <input type="text" class="form-control" id="idS3KeyPrefix" name="s3_key_prefix" placeholder=""
value="{{.S3Config.KeyPrefix}}" maxlength="255" aria-describedby="S3KeyPrefixHelpBlock"> value="{{.S3Config.KeyPrefix}}" aria-describedby="S3KeyPrefixHelpBlock">
<small id="S3KeyPrefixHelpBlock" class="form-text text-muted"> <small id="S3KeyPrefixHelpBlock" class="form-text text-muted">
Similar to a chroot for local filesystem. Cannot start with "/". Example: "somedir/subdir/". Similar to a chroot for local filesystem. Cannot start with "/". Example: "somedir/subdir/".
</small> </small>
@ -176,7 +201,7 @@
<label for="idGCSKeyPrefix" class="col-sm-2 col-form-label">Key Prefix</label> <label for="idGCSKeyPrefix" class="col-sm-2 col-form-label">Key Prefix</label>
<div class="col-sm-3"> <div class="col-sm-3">
<input type="text" class="form-control" id="idGCSKeyPrefix" name="gcs_key_prefix" placeholder="" <input type="text" class="form-control" id="idGCSKeyPrefix" name="gcs_key_prefix" placeholder=""
value="{{.GCSConfig.KeyPrefix}}" maxlength="255" aria-describedby="GCSKeyPrefixHelpBlock"> value="{{.GCSConfig.KeyPrefix}}" aria-describedby="GCSKeyPrefixHelpBlock">
<small id="GCSKeyPrefixHelpBlock" class="form-text text-muted"> <small id="GCSKeyPrefixHelpBlock" class="form-text text-muted">
Similar to a chroot for local filesystem. Cannot start with "/". Example: "somedir/subdir/". Similar to a chroot for local filesystem. Cannot start with "/". Example: "somedir/subdir/".
</small> </small>
@ -268,7 +293,7 @@
<label for="idAzKeyPrefix" class="col-sm-2 col-form-label">Key Prefix</label> <label for="idAzKeyPrefix" class="col-sm-2 col-form-label">Key Prefix</label>
<div class="col-sm-10"> <div class="col-sm-10">
<input type="text" class="form-control" id="idAzKeyPrefix" name="az_key_prefix" placeholder="" <input type="text" class="form-control" id="idAzKeyPrefix" name="az_key_prefix" placeholder=""
value="{{.AzBlobConfig.KeyPrefix}}" maxlength="255" aria-describedby="AzKeyPrefixHelpBlock"> value="{{.AzBlobConfig.KeyPrefix}}" aria-describedby="AzKeyPrefixHelpBlock">
<small id="AzKeyPrefixHelpBlock" class="form-text text-muted"> <small id="AzKeyPrefixHelpBlock" class="form-text text-muted">
Similar to a chroot for local filesystem. Cannot start with "/". Example: "somedir/subdir/". Similar to a chroot for local filesystem. Cannot start with "/". Example: "somedir/subdir/".
</small> </small>
@ -352,7 +377,7 @@
<label for="idSFTPPrefix" class="col-sm-2 col-form-label">Prefix</label> <label for="idSFTPPrefix" class="col-sm-2 col-form-label">Prefix</label>
<div class="col-sm-10"> <div class="col-sm-10">
<input type="text" class="form-control" id="idSFTPPrefix" name="sftp_prefix" placeholder="" <input type="text" class="form-control" id="idSFTPPrefix" name="sftp_prefix" placeholder=""
value="{{.SFTPConfig.Prefix}}" maxlength="255" aria-describedby="SFTPPrefixHelpBlock"> value="{{.SFTPConfig.Prefix}}" aria-describedby="SFTPPrefixHelpBlock">
<small id="SFTPPrefixHelpBlock" class="form-text text-muted"> <small id="SFTPPrefixHelpBlock" class="form-text text-muted">
Similar to a chroot for local filesystem. Example: "/somedir/subdir". Similar to a chroot for local filesystem. Example: "/somedir/subdir".
</small> </small>

View file

@ -86,45 +86,6 @@
</div> </div>
{{end}} {{end}}
<div class="form-group row">
<label for="idEmail" class="col-sm-2 col-form-label">Email</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="idEmail" name="email" placeholder=""
value="{{.User.Email}}" maxlength="255" autocomplete="nope">
</div>
</div>
<div class="form-group row">
<label for="idDescription" class="col-sm-2 col-form-label">Description</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="idDescription" name="description" placeholder=""
value="{{.User.Description}}" maxlength="255" aria-describedby="descriptionHelpBlock">
<small id="descriptionHelpBlock" class="form-text text-muted">
Optional description, for example the user full name
</small>
</div>
</div>
<div class="form-group row">
<label for="idStatus" class="col-sm-2 col-form-label">Status</label>
<div class="col-sm-10">
<select class="form-control" id="idStatus" name="status">
<option value="1" {{if eq .User.Status 1 }}selected{{end}}>Active</option>
<option value="0" {{if eq .User.Status 0 }}selected{{end}}>Inactive</option>
</select>
</div>
</div>
<div class="form-group row">
<label for="idExpirationDate" class="col-sm-2 col-form-label">Expiration Date</label>
<div class="col-sm-10 input-group date" id="expirationDatePicker" data-target-input="nearest">
<input type="text" class="form-control datetimepicker-input" id="idExpirationDate"
data-target="#expirationDatePicker">
<div class="input-group-append" data-target="#expirationDatePicker" data-toggle="datetimepicker">
<div class="input-group-text"><i class="fas fa-calendar"></i></div>
</div>
</div>
</div>
{{if ne .Mode 3}} {{if ne .Mode 3}}
<div class="form-group row"> <div class="form-group row">
<label for="idPassword" class="col-sm-2 col-form-label">Password</label> <label for="idPassword" class="col-sm-2 col-form-label">Password</label>
@ -135,7 +96,7 @@
<div class="card bg-light mb-3"> <div class="card bg-light mb-3">
<div class="card-header"> <div class="card-header">
Public keys <b>Public keys</b>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="form-group row"> <div class="form-group row">
@ -177,43 +138,11 @@
</div> </div>
{{end}} {{end}}
<div class="form-group row"> {{template "fshtml" .FsWrapper}}
<label for="idTLSUsername" class="col-sm-2 col-form-label">TLS username</label>
<div class="col-sm-10">
<select class="form-control" id="idTLSUsername" name="tls_username" aria-describedby="tlsUsernameHelpBlock">
<option value="None" {{if eq .User.Filters.TLSUsername "None" }}selected{{end}}>None</option>
<option value="CommonName" {{if eq .User.Filters.TLSUsername "CommonName" }}selected{{end}}>Common Name</option>
</select>
<small id="tlsUsernameHelpBlock" class="form-text text-muted">
Defines the TLS certificate field to use as username. Ignored if mutual TLS is disabled
</small>
</div>
</div>
<div class="form-group row">
<label for="idHomeDir" class="col-sm-2 col-form-label">Home Dir</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="idHomeDir" name="home_dir" placeholder=""
value="{{.User.HomeDir}}" maxlength="255">
</div>
</div>
<div class="form-group row">
<label for="idPermissions" class="col-sm-2 col-form-label">Permissions</label>
<div class="col-sm-10">
<select class="form-control" id="idPermissions" name="permissions" required multiple>
{{range $validPerm := .ValidPerms}}
<option value="{{$validPerm}}" {{range $perm :=$.RootDirPerms }}{{if eq $perm $validPerm}}selected{{end}}{{end}}>{{$validPerm}}</option>
{{end}}
</select>
</div>
</div>
{{template "fshtml" .User.FsConfig}}
{{if .VirtualFolders}} {{if .VirtualFolders}}
<div class="card bg-light mb-3"> <div class="card bg-light mb-3">
<div class="card-header"> <div class="card-header">
Virtual folders <b>Virtual folders</b>
</div> </div>
<div class="card-body"> <div class="card-body">
<h6 class="card-title mb-4">Quota -1 means included within user quota, 0 unlimited. Don't set -1 for shared folders</h6> <h6 class="card-title mb-4">Quota -1 means included within user quota, 0 unlimited. Don't set -1 for shared folders</h6>
@ -298,385 +227,516 @@
</div> </div>
{{end}} {{end}}
<div class="card bg-light mb-3"> <div class="accordion" id="accordionUser">
<div class="card-header"> <div class="card">
Per-directory permissions <div class="card-header" id="headingProfile">
</div> <h2 class="mb-0">
<div class="card-body"> <button class="btn btn-link btn-block text-left" type="button" data-toggle="collapse"
<div class="form-group row"> data-target="#collapseProfile" aria-expanded="true" aria-controls="collapseProfile">
<div class="col-md-12 form_field_dirperms_outer"> <h6 class="m-0 font-weight-bold text-primary">Profile</h6>
{{range $idx, $dirPerms := .User.GetSubDirPermissions -}} </button>
<div class="row form_field_dirperms_outer_row"> </h2>
<div class="form-group col-md-8"> </div>
<input type="text" class="form-control" id="idSubDirPermsPath{{$idx}}" name="sub_perm_path{{$idx}}" placeholder="directory path, i.e. /dir" value="{{$dirPerms.Path}}" maxlength="255">
</div> <div id="collapseProfile" class="collapse" aria-labelledby="headingProfile" data-parent="#accordionUser">
<div class="form-group col-md-3"> <div class="card-body">
<select class="form-control" id="idSubDirPermissions{{$idx}}" name="sub_perm_permissions{{$idx}}" multiple> <div class="form-group row">
{{range $validPerm := $.ValidPerms}} <label for="idStatus" class="col-sm-2 col-form-label">Status</label>
<option value="{{$validPerm}}" {{range $perm := $dirPerms.Permissions }}{{if eq $perm $validPerm}}selected{{end}}{{end}}>{{$validPerm}}</option> <div class="col-sm-10">
{{end}} <select class="form-control" id="idStatus" name="status">
<option value="1" {{if eq .User.Status 1 }}selected{{end}}>Active</option>
<option value="0" {{if eq .User.Status 0 }}selected{{end}}>Inactive</option>
</select> </select>
</div> </div>
<div class="form-group col-md-1"> </div>
<button class="btn btn-circle btn-danger remove_dirperms_btn_frm_field">
<i class="fas fa-trash"></i> <div class="form-group row">
</button> <label for="idExpirationDate" class="col-sm-2 col-form-label">Expiration Date</label>
<div class="col-sm-10 input-group date" id="expirationDatePicker" data-target-input="nearest">
<input type="text" class="form-control datetimepicker-input" id="idExpirationDate"
data-target="#expirationDatePicker">
<div class="input-group-append" data-target="#expirationDatePicker" data-toggle="datetimepicker">
<div class="input-group-text"><i class="fas fa-calendar"></i></div>
</div>
</div> </div>
</div> </div>
{{else}}
<div class="row form_field_dirperms_outer_row"> <div class="form-group row">
<div class="form-group col-md-8"> <label for="idEmail" class="col-sm-2 col-form-label">Email</label>
<input type="text" class="form-control" id="idSubDirPermsPath0" name="sub_perm_path0" placeholder="directory path, i.e. /dir" value="" maxlength="255"> <div class="col-sm-10">
<input type="text" class="form-control" id="idEmail" name="email" placeholder=""
value="{{.User.Email}}" maxlength="255" autocomplete="nope">
</div> </div>
<div class="form-group col-md-3"> </div>
<select class="form-control" id="idSubDirPermissions0" name="sub_perm_permissions0" multiple>
<div class="form-group">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="idAllowAPIKeyAuth" name="allow_api_key_auth"
{{if .User.Filters.AllowAPIKeyAuth}}checked{{end}} aria-describedby="allowAPIKeyAuthHelpBlock">
<label for="idAllowAPIKeyAuth" class="form-check-label">Allow API key authentication</label>
<small id="allowAPIKeyAuthHelpBlock" class="form-text text-muted">
Allow to impersonate this user, in REST API, with an API key
</small>
</div>
</div>
<div class="form-group row">
<label for="idDescription" class="col-sm-2 col-form-label">Description</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="idDescription" name="description" placeholder=""
value="{{.User.Description}}" maxlength="255" aria-describedby="descriptionHelpBlock">
<small id="descriptionHelpBlock" class="form-text text-muted">
Optional description, for example the user full name
</small>
</div>
</div>
<div class="form-group row">
<label for="idAdditionalInfo" class="col-sm-2 col-form-label">Additional info</label>
<div class="col-sm-10">
<textarea class="form-control" id="idAdditionalInfo" name="additional_info" rows="3"
aria-describedby="additionalInfoHelpBlock">{{.User.AdditionalInfo}}</textarea>
<small id="additionalInfoHelpBlock" class="form-text text-muted">
Free form text field
</small>
</div>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header" id="headingPermissions">
<h2 class="mb-0">
<button class="btn btn-link btn-block text-left" type="button" data-toggle="collapse"
data-target="#collapsePermissions" aria-expanded="true" aria-controls="collapsePermissions">
<h6 class="m-0 font-weight-bold text-primary">ACLs</h6>
</button>
</h2>
</div>
<div id="collapsePermissions" class="collapse" aria-labelledby="headingPermissions" data-parent="#accordionUser">
<div class="card-body">
<div class="form-group row">
<label for="idPermissions" class="col-sm-2 col-form-label">Permissions</label>
<div class="col-sm-10">
<select class="form-control" id="idPermissions" name="permissions" required multiple>
{{range $validPerm := .ValidPerms}} {{range $validPerm := .ValidPerms}}
<option value="{{$validPerm}}">{{$validPerm}}</option> <option value="{{$validPerm}}" {{range $perm :=$.RootDirPerms }}{{if eq $perm $validPerm}}selected{{end}}{{end}}>{{$validPerm}}</option>
{{end}} {{end}}
</select> </select>
</div> </div>
<div class="form-group col-md-1"> </div>
<button class="btn btn-circle btn-danger remove_dirperms_btn_frm_field">
<i class="fas fa-trash"></i> <div class="card bg-light mb-3">
</button> <div class="card-header">
<b>Per-directory permissions</b>
</div>
<div class="card-body">
<div class="form-group row">
<div class="col-md-12 form_field_dirperms_outer">
{{range $idx, $dirPerms := .User.GetSubDirPermissions -}}
<div class="row form_field_dirperms_outer_row">
<div class="form-group col-md-8">
<input type="text" class="form-control" id="idSubDirPermsPath{{$idx}}" name="sub_perm_path{{$idx}}" placeholder="directory path, i.e. /dir" value="{{$dirPerms.Path}}" maxlength="255">
</div>
<div class="form-group col-md-3">
<select class="form-control" id="idSubDirPermissions{{$idx}}" name="sub_perm_permissions{{$idx}}" multiple>
{{range $validPerm := $.ValidPerms}}
<option value="{{$validPerm}}" {{range $perm := $dirPerms.Permissions }}{{if eq $perm $validPerm}}selected{{end}}{{end}}>{{$validPerm}}</option>
{{end}}
</select>
</div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_dirperms_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
{{else}}
<div class="row form_field_dirperms_outer_row">
<div class="form-group col-md-8">
<input type="text" class="form-control" id="idSubDirPermsPath0" name="sub_perm_path0" placeholder="directory path, i.e. /dir" value="" maxlength="255">
</div>
<div class="form-group col-md-3">
<select class="form-control" id="idSubDirPermissions0" name="sub_perm_permissions0" multiple>
{{range $validPerm := .ValidPerms}}
<option value="{{$validPerm}}">{{$validPerm}}</option>
{{end}}
</select>
</div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_dirperms_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
{{end}}
</div>
</div>
<div class="row mx-1">
<button type="button" class="btn btn-secondary add_new_dirperms_field_btn">
<i class="fas fa-plus"></i> Add new directory permissions
</button>
</div>
</div> </div>
</div> </div>
{{end}}
</div>
</div>
<div class="row mx-1"> <div class="card bg-light mb-3">
<button type="button" class="btn btn-secondary add_new_dirperms_field_btn"> <div class="card-header">
<i class="fas fa-plus"></i> Add new directory permissions <b>Per-directory file patterns</b>
</button> </div>
</div> <div class="card-body">
</div> <h6 class="card-title mb-4">Comma separated denied or allowed files, based on shell patterns</h6>
</div> <div class="form-group row">
<div class="col-md-12 form_field_patterns_outer">
{{range $idx, $pattern := .User.GetFlatFilePatterns -}}
<div class="row form_field_patterns_outer_row">
<div class="form-group col-md-4">
<input type="text" class="form-control" id="idPatternPath{{$idx}}" name="pattern_path{{$idx}}" placeholder="directory path, i.e. /dir" value="{{$pattern.Path}}" maxlength="255">
</div>
<div class="form-group col-md-5">
<input type="text" class="form-control" id="idPatterns{{$idx}}" name="patterns{{$idx}}" placeholder="*.zip,?.txt" value="{{$pattern.GetCommaSeparatedPatterns}}" maxlength="255">
</div>
<div class="form-group col-md-2">
<select class="form-control" id="idPatternType{{$idx}}" name="pattern_type{{$idx}}">
<option value="denied" {{if $pattern.IsDenied}}selected{{end}}>Denied</option>
<option value="allowed" {{if $pattern.IsAllowed}}selected{{end}}>Allowed</option>
</select>
</div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_pattern_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
{{else}}
<div class="row form_field_patterns_outer_row">
<div class="form-group col-md-4">
<input type="text" class="form-control" id="idPatternPath0" name="pattern_path0" placeholder="directory path, i.e. /dir" value="" maxlength="255">
</div>
<div class="form-group col-md-5">
<input type="text" class="form-control" id="idPatterns0" name="patterns0" placeholder="*.zip,?.txt" value="" maxlength="255">
</div>
<div class="form-group col-md-2">
<select class="form-control" id="idPatternType0" name="pattern_type0">
<option value="denied">Denied</option>
<option value="allowed">Allowed</option>
</select>
</div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_pattern_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
{{end}}
</div>
</div>
<div class="form-group"> <div class="row mx-1">
<div class="form-check"> <button type="button" class="btn btn-secondary add_new_pattern_field_btn">
<input type="checkbox" class="form-check-input" id="idDisableFsChecks" name="disable_fs_checks" <i class="fas fa-plus"></i> Add new file pattern
{{if .User.Filters.DisableFsChecks}}checked{{end}} aria-describedby="disableFsChecksHelpBlock"> </button>
<label for="idDisableFsChecks" class="form-check-label">Disable filesystem checks</label> </div>
<small id="disableFsChecksHelpBlock" class="form-text text-muted"> </div>
Disable checks for existence and automatic creation of home directory and virtual folders </div>
</small>
</div>
</div>
<div class="form-group row"> <div class="form-group row">
<label for="idUID" class="col-sm-2 col-form-label">UID</label> <label for="idMaxSessions" class="col-sm-2 col-form-label">Max sessions</label>
<div class="col-sm-3"> <div class="col-sm-10">
<input type="number" class="form-control" id="idUID" name="uid" placeholder="" value="{{.User.UID}}" <input type="number" class="form-control" id="idMaxSessions" name="max_sessions" placeholder=""
min="0" max="2147483647"> value="{{.User.MaxSessions}}" min="0" aria-describedby="sessionsHelpBlock">
</div> <small id="sessionsHelpBlock" class="form-text text-muted">
<div class="col-sm-2"></div> Maximun number of concurrent sessions. 0 means no limit
<label for="idGID" class="col-sm-2 col-form-label">GID</label> </small>
<div class="col-sm-3"> </div>
<input type="number" class="form-control" id="idGID" name="gid" placeholder="" value="{{.User.GID}}" </div>
min="0" max="2147483647">
</div>
</div>
<div class="form-group row"> <div class="form-group row">
<label for="idQuotaSize" class="col-sm-2 col-form-label">Quota size (bytes)</label> <label for="idProtocols" class="col-sm-2 col-form-label">Denied protocols</label>
<div class="col-sm-3"> <div class="col-sm-10">
<input type="number" class="form-control" id="idQuotaSize" name="quota_size" placeholder="" <select class="form-control" id="idProtocols" name="denied_protocols" multiple>
value="{{.User.QuotaSize}}" min="0" aria-describedby="qsHelpBlock"> {{range $protocol := .ValidProtocols}}
<small id="qsHelpBlock" class="form-text text-muted"> <option value="{{$protocol}}" {{range $p :=$.User.Filters.DeniedProtocols }}{{if eq $p $protocol}}selected{{end}}{{end}}>{{$protocol}}
0 means no limit </option>
</small> {{end}}
</div> </select>
<div class="col-sm-2"></div> </div>
<label for="idQuotaFiles" class="col-sm-2 col-form-label">Quota files</label> </div>
<div class="col-sm-3">
<input type="number" class="form-control" id="idQuotaFiles" name="quota_files" placeholder=""
value="{{.User.QuotaFiles}}" min="0" aria-describedby="qfHelpBlock">
<small id="qfHelpBlock" class="form-text text-muted">
0 means no limit
</small>
</div>
</div>
<div class="form-group row"> <div class="form-group row">
<label for="idMaxUploadSize" class="col-sm-2 col-form-label">Max file upload size (bytes)</label> <label for="idLoginMethods" class="col-sm-2 col-form-label">Denied login methods</label>
<div class="col-sm-3"> <div class="col-sm-10">
<input type="number" class="form-control" id="idMaxUploadSize" name="max_upload_file_size" <select class="form-control" id="idLoginMethods" name="ssh_login_methods" multiple>
placeholder="" value="{{.User.Filters.MaxUploadFileSize}}" min="0" {{range $method := .ValidLoginMethods}}
aria-describedby="fqsHelpBlock"> <option value="{{$method}}" {{range $m :=$.User.Filters.DeniedLoginMethods }}{{if eq $m $method}}selected{{end}}{{end}}>{{$method}}
<small id="fqsHelpBlock" class="form-text text-muted"> </option>
0 means no limit {{end}}
</small> </select>
</div> </div>
<div class="col-sm-2"></div> </div>
<label for="idMaxSessions" class="col-sm-2 col-form-label">Max sessions</label>
<div class="col-sm-3">
<input type="number" class="form-control" id="idMaxSessions" name="max_sessions" placeholder=""
value="{{.User.MaxSessions}}" min="0" aria-describedby="sessionsHelpBlock">
<small id="sessionsHelpBlock" class="form-text text-muted">
0 means no limit
</small>
</div>
</div>
<div class="form-group row"> <div class="form-group row">
<label for="idUploadBandwidth" class="col-sm-2 col-form-label">Bandwidth UL (KB/s)</label> <label for="idWebClient" class="col-sm-2 col-form-label">Web client/REST API</label>
<div class="col-sm-3"> <div class="col-sm-10">
<input type="number" class="form-control" id="idUploadBandwidth" name="upload_bandwidth" <select class="form-control" id="idWebClient" name="web_client_options" multiple>
placeholder="" value="{{.User.UploadBandwidth}}" min="0" aria-describedby="ulHelpBlock"> {{range $option := .WebClientOptions}}
<small id="ulHelpBlock" class="form-text text-muted"> <option value="{{$option}}" {{range $p :=$.User.Filters.WebClient }}{{if eq $p $option}}selected{{end}}{{end}}>{{$option}}
0 means no limit </option>
</small> {{end}}
</div> </select>
<div class="col-sm-2"></div> </div>
<label for="idDownloadBandwidth" class="col-sm-2 col-form-label">Bandwidth DL (KB/s)</label> </div>
<div class="col-sm-3">
<input type="number" class="form-control" id="idDownloadBandwidth" name="download_bandwidth"
placeholder="" value="{{.User.DownloadBandwidth}}" min="0" aria-describedby="dlHelpBlock">
<small id="dlHelpBlock" class="form-text text-muted">
0 means no limit
</small>
</div>
</div>
<div class="card bg-light mb-3"> <div class="form-group row">
<div class="card-header"> <label for="idDeniedIP" class="col-sm-2 col-form-label">Denied IP/Mask</label>
Per-source bandwidth limits <div class="col-sm-10">
</div> <textarea class="form-control" id="idDeniedIP" name="denied_ip" rows="3" placeholder=""
<div class="card-body"> aria-describedby="deniedIPHelpBlock">{{.User.GetDeniedIPAsString}}</textarea>
<div class="form-group row"> <small id="deniedIPHelpBlock" class="form-text text-muted">
<div class="col-md-12 form_field_bwlimits_outer">
{{range $idx, $bwLimit := .User.Filters.BandwidthLimits -}}
<div class="row form_field_bwlimits_outer_row">
<div class="form-group col-md-8">
<textarea class="form-control" id="idBandwidthLimitSources{{$idx}}" name="bandwidth_limit_sources{{$idx}}" rows="4" placeholder=""
aria-describedby="bwLimitSourcesHelpBlock{{$idx}}">{{$bwLimit.GetSourcesAsString}}</textarea>
<small id="bwLimitSourcesHelpBlock{{$idx}}" class="form-text text-muted">
Comma separated IP/Mask in CIDR format, for example "192.168.1.0/24,10.8.0.100/32" Comma separated IP/Mask in CIDR format, for example "192.168.1.0/24,10.8.0.100/32"
</small> </small>
</div> </div>
<div class="col-md-3">
<div class="form-group">
<input type="number" class="form-control" id="idUploadBandwidthSource{{$idx}}" name="upload_bandwidth_source{{$idx}}"
placeholder="" value="{{$bwLimit.UploadBandwidth}}" min="0" aria-describedby="ulHelpBlock{{$idx}}">
<small id="ulHelpBlock{{$idx}}" class="form-text text-muted">
UL (KB/s). 0 means no limit
</small>
</div>
<div class="form-group">
<input type="number" class="form-control" id="idDownloadBandwidthSource{{$idx}}" name="download_bandwidth_source{{$idx}}"
placeholder="" value="{{$bwLimit.DownloadBandwidth}}" min="0" aria-describedby="dlHelpBlock{{$idx}}">
<small id="dlHelpBlock{{$idx}}" class="form-text text-muted">
DL (KB/s). 0 means no limit
</small>
</div>
</div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_bwlimit_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
</div>
</div> </div>
{{else}}
<div class="row form_field_bwlimits_outer_row"> <div class="form-group row">
<div class="form-group col-md-8"> <label for="idAllowedIP" class="col-sm-2 col-form-label">Allowed IP/Mask</label>
<textarea class="form-control" id="idBandwidthLimitSources0" name="bandwidth_limit_sources0" rows="4" placeholder="" <div class="col-sm-10">
aria-describedby="bwLimitSourcesHelpBlock0"></textarea> <textarea class="form-control" id="idAllowedIP" name="allowed_ip" rows="3" placeholder=""
<small id="bwLimitSourcesHelpBlock0" class="form-text text-muted"> aria-describedby="allowedIPHelpBlock">{{.User.GetAllowedIPAsString}}</textarea>
<small id="allowedIPHelpBlock" class="form-text text-muted">
Comma separated IP/Mask in CIDR format, for example "192.168.1.0/24,10.8.0.100/32" Comma separated IP/Mask in CIDR format, for example "192.168.1.0/24,10.8.0.100/32"
</small> </small>
</div> </div>
<div class="col-md-3">
<div class="form-group">
<input type="number" class="form-control" id="idUploadBandwidthSource0" name="upload_bandwidth_source0"
placeholder="" value="" min="0" aria-describedby="ulHelpBlock0">
<small id="ulHelpBlock0" class="form-text text-muted">
UL (KB/s). 0 means no limit
</small>
</div>
<div class="form-group">
<input type="number" class="form-control" id="idDownloadBandwidthSource0" name="download_bandwidth_source0"
placeholder="" value="" min="0" aria-describedby="dlHelpBlock0">
<small id="dlHelpBlock0" class="form-text text-muted">
DL (KB/s). 0 means no limit
</small>
</div>
</div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_bwlimit_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
</div>
</div> </div>
{{end}}
</div> </div>
</div> </div>
</div>
<div class="row mx-1"> <div class="card">
<button type="button" class="btn btn-secondary add_new_bwlimit_field_btn"> <div class="card-header" id="headingQuota">
<i class="fas fa-plus"></i> Add new bandwidth limit <h2 class="mb-0">
</button> <button class="btn btn-link btn-block text-left collapsed" type="button" data-toggle="collapse"
data-target="#collapseQuota" aria-expanded="false" aria-controls="collapseQuota">
<h6 class="m-0 font-weight-bold text-primary">Disk quota and bandwidth limits</h6>
</button>
</h2>
</div> </div>
</div> <div id="collapseQuota" class="collapse" aria-labelledby="headingTwo" data-parent="#accordionUser">
</div> <div class="card-body">
<div class="form-group row"> <div class="form-group row">
<label for="idProtocols" class="col-sm-2 col-form-label">Denied protocols</label> <label for="idQuotaSize" class="col-sm-2 col-form-label">Quota size (bytes)</label>
<div class="col-sm-10"> <div class="col-sm-3">
<select class="form-control" id="idProtocols" name="denied_protocols" multiple> <input type="number" class="form-control" id="idQuotaSize" name="quota_size" placeholder=""
{{range $protocol := .ValidProtocols}} value="{{.User.QuotaSize}}" min="0" aria-describedby="qsHelpBlock">
<option value="{{$protocol}}" {{range $p :=$.User.Filters.DeniedProtocols }}{{if eq $p $protocol}}selected{{end}}{{end}}>{{$protocol}} <small id="qsHelpBlock" class="form-text text-muted">
</option> 0 means no limit
{{end}} </small>
</select>
</div>
</div>
<div class="form-group row">
<label for="idLoginMethods" class="col-sm-2 col-form-label">Denied login methods</label>
<div class="col-sm-10">
<select class="form-control" id="idLoginMethods" name="ssh_login_methods" multiple>
{{range $method := .ValidLoginMethods}}
<option value="{{$method}}" {{range $m :=$.User.Filters.DeniedLoginMethods }}{{if eq $m $method}}selected{{end}}{{end}}>{{$method}}
</option>
{{end}}
</select>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="idAllowAPIKeyAuth" name="allow_api_key_auth"
{{if .User.Filters.AllowAPIKeyAuth}}checked{{end}} aria-describedby="allowAPIKeyAuthHelpBlock">
<label for="idAllowAPIKeyAuth" class="form-check-label">Allow API key authentication</label>
<small id="allowAPIKeyAuthHelpBlock" class="form-text text-muted">
Allow to impersonate this user, in REST API, with an API key
</small>
</div>
</div>
<div class="form-group row">
<label for="idDeniedIP" class="col-sm-2 col-form-label">Denied IP/Mask</label>
<div class="col-sm-10">
<textarea class="form-control" id="idDeniedIP" name="denied_ip" rows="3" placeholder=""
aria-describedby="deniedIPHelpBlock">{{.User.GetDeniedIPAsString}}</textarea>
<small id="deniedIPHelpBlock" class="form-text text-muted">
Comma separated IP/Mask in CIDR format, for example "192.168.1.0/24,10.8.0.100/32"
</small>
</div>
</div>
<div class="form-group row">
<label for="idAllowedIP" class="col-sm-2 col-form-label">Allowed IP/Mask</label>
<div class="col-sm-10">
<textarea class="form-control" id="idAllowedIP" name="allowed_ip" rows="3" placeholder=""
aria-describedby="allowedIPHelpBlock">{{.User.GetAllowedIPAsString}}</textarea>
<small id="allowedIPHelpBlock" class="form-text text-muted">
Comma separated IP/Mask in CIDR format, for example "192.168.1.0/24,10.8.0.100/32"
</small>
</div>
</div>
<div class="card bg-light mb-3">
<div class="card-header">
Per-directory file patterns
</div>
<div class="card-body">
<h6 class="card-title mb-4">Comma separated denied or allowed files, based on shell patterns</h6>
<div class="form-group row">
<div class="col-md-12 form_field_patterns_outer">
{{range $idx, $pattern := .User.GetFlatFilePatterns -}}
<div class="row form_field_patterns_outer_row">
<div class="form-group col-md-4">
<input type="text" class="form-control" id="idPatternPath{{$idx}}" name="pattern_path{{$idx}}" placeholder="directory path, i.e. /dir" value="{{$pattern.Path}}" maxlength="255">
</div> </div>
<div class="form-group col-md-5"> <div class="col-sm-2"></div>
<input type="text" class="form-control" id="idPatterns{{$idx}}" name="patterns{{$idx}}" placeholder="*.zip,?.txt" value="{{$pattern.GetCommaSeparatedPatterns}}" maxlength="255"> <label for="idQuotaFiles" class="col-sm-2 col-form-label">Quota files</label>
</div> <div class="col-sm-3">
<div class="form-group col-md-2"> <input type="number" class="form-control" id="idQuotaFiles" name="quota_files" placeholder=""
<select class="form-control" id="idPatternType{{$idx}}" name="pattern_type{{$idx}}"> value="{{.User.QuotaFiles}}" min="0" aria-describedby="qfHelpBlock">
<option value="denied" {{if $pattern.IsDenied}}selected{{end}}>Denied</option> <small id="qfHelpBlock" class="form-text text-muted">
<option value="allowed" {{if $pattern.IsAllowed}}selected{{end}}>Allowed</option> 0 means no limit
</select> </small>
</div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_pattern_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
</div> </div>
</div> </div>
{{else}}
<div class="row form_field_patterns_outer_row"> <div class="form-group row">
<div class="form-group col-md-4"> <label for="idMaxUploadSize" class="col-sm-2 col-form-label">Max file upload size (bytes)</label>
<input type="text" class="form-control" id="idPatternPath0" name="pattern_path0" placeholder="directory path, i.e. /dir" value="" maxlength="255"> <div class="col-sm-10">
</div> <input type="number" class="form-control" id="idMaxUploadSize" name="max_upload_file_size"
<div class="form-group col-md-5"> placeholder="" value="{{.User.Filters.MaxUploadFileSize}}" min="0"
<input type="text" class="form-control" id="idPatterns0" name="patterns0" placeholder="*.zip,?.txt" value="" maxlength="255"> aria-describedby="fqsHelpBlock">
</div> <small id="fqsHelpBlock" class="form-text text-muted">
<div class="form-group col-md-2"> Maximum upload size for a single file. 0 means no limit
<select class="form-control" id="idPatternType0" name="pattern_type0"> </small>
<option value="denied">Denied</option>
<option value="allowed">Allowed</option>
</select>
</div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_pattern_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
</div> </div>
</div> </div>
{{end}}
<div class="form-group row">
<label for="idUploadBandwidth" class="col-sm-2 col-form-label">Bandwidth UL (KB/s)</label>
<div class="col-sm-3">
<input type="number" class="form-control" id="idUploadBandwidth" name="upload_bandwidth"
placeholder="" value="{{.User.UploadBandwidth}}" min="0" aria-describedby="ulHelpBlock">
<small id="ulHelpBlock" class="form-text text-muted">
0 means no limit
</small>
</div>
<div class="col-sm-2"></div>
<label for="idDownloadBandwidth" class="col-sm-2 col-form-label">Bandwidth DL (KB/s)</label>
<div class="col-sm-3">
<input type="number" class="form-control" id="idDownloadBandwidth" name="download_bandwidth"
placeholder="" value="{{.User.DownloadBandwidth}}" min="0" aria-describedby="dlHelpBlock">
<small id="dlHelpBlock" class="form-text text-muted">
0 means no limit
</small>
</div>
</div>
<div class="card bg-light mb-3">
<div class="card-header">
<b>Per-source bandwidth limits</b>
</div>
<div class="card-body">
<div class="form-group row">
<div class="col-md-12 form_field_bwlimits_outer">
{{range $idx, $bwLimit := .User.Filters.BandwidthLimits -}}
<div class="row form_field_bwlimits_outer_row">
<div class="form-group col-md-8">
<textarea class="form-control" id="idBandwidthLimitSources{{$idx}}" name="bandwidth_limit_sources{{$idx}}" rows="4" placeholder=""
aria-describedby="bwLimitSourcesHelpBlock{{$idx}}">{{$bwLimit.GetSourcesAsString}}</textarea>
<small id="bwLimitSourcesHelpBlock{{$idx}}" class="form-text text-muted">
Comma separated IP/Mask in CIDR format, for example "192.168.1.0/24,10.8.0.100/32"
</small>
</div>
<div class="col-md-3">
<div class="form-group">
<input type="number" class="form-control" id="idUploadBandwidthSource{{$idx}}" name="upload_bandwidth_source{{$idx}}"
placeholder="" value="{{$bwLimit.UploadBandwidth}}" min="0" aria-describedby="ulHelpBlock{{$idx}}">
<small id="ulHelpBlock{{$idx}}" class="form-text text-muted">
UL (KB/s). 0 means no limit
</small>
</div>
<div class="form-group">
<input type="number" class="form-control" id="idDownloadBandwidthSource{{$idx}}" name="download_bandwidth_source{{$idx}}"
placeholder="" value="{{$bwLimit.DownloadBandwidth}}" min="0" aria-describedby="dlHelpBlock{{$idx}}">
<small id="dlHelpBlock{{$idx}}" class="form-text text-muted">
DL (KB/s). 0 means no limit
</small>
</div>
</div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_bwlimit_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
{{else}}
<div class="row form_field_bwlimits_outer_row">
<div class="form-group col-md-8">
<textarea class="form-control" id="idBandwidthLimitSources0" name="bandwidth_limit_sources0" rows="4" placeholder=""
aria-describedby="bwLimitSourcesHelpBlock0"></textarea>
<small id="bwLimitSourcesHelpBlock0" class="form-text text-muted">
Comma separated IP/Mask in CIDR format, for example "192.168.1.0/24,10.8.0.100/32"
</small>
</div>
<div class="col-md-3">
<div class="form-group">
<input type="number" class="form-control" id="idUploadBandwidthSource0" name="upload_bandwidth_source0"
placeholder="" value="" min="0" aria-describedby="ulHelpBlock0">
<small id="ulHelpBlock0" class="form-text text-muted">
UL (KB/s). 0 means no limit
</small>
</div>
<div class="form-group">
<input type="number" class="form-control" id="idDownloadBandwidthSource0" name="download_bandwidth_source0"
placeholder="" value="" min="0" aria-describedby="dlHelpBlock0">
<small id="dlHelpBlock0" class="form-text text-muted">
DL (KB/s). 0 means no limit
</small>
</div>
</div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_bwlimit_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
{{end}}
</div>
</div>
<div class="row mx-1">
<button type="button" class="btn btn-secondary add_new_bwlimit_field_btn">
<i class="fas fa-plus"></i> Add new bandwidth limit
</button>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div>
<div class="row mx-1"> <div class="card">
<button type="button" class="btn btn-secondary add_new_pattern_field_btn"> <div class="card-header" id="headingAdvanced">
<i class="fas fa-plus"></i> Add new file pattern <h2 class="mb-0">
</button> <button class="btn btn-link btn-block text-left collapsed" type="button" data-toggle="collapse"
data-target="#collapseAdvanced" aria-expanded="false" aria-controls="collapseAdvanced">
<h6 class="m-0 font-weight-bold text-primary">More</h6>
</button>
</h2>
</div>
<div id="collapseAdvanced" class="collapse" aria-labelledby="headingTwo" data-parent="#accordionUser">
<div class="card-body">
<div class="form-group row">
<label for="idTLSUsername" class="col-sm-2 col-form-label">TLS username</label>
<div class="col-sm-10">
<select class="form-control" id="idTLSUsername" name="tls_username" aria-describedby="tlsUsernameHelpBlock">
<option value="None" {{if eq .User.Filters.TLSUsername "None" }}selected{{end}}>None</option>
<option value="CommonName" {{if eq .User.Filters.TLSUsername "CommonName" }}selected{{end}}>Common Name</option>
</select>
<small id="tlsUsernameHelpBlock" class="form-text text-muted">
Defines the TLS certificate field to use as username. Ignored if mutual TLS is disabled
</small>
</div>
</div>
<div class="form-group row {{if not .CanImpersonate}}d-none{{end}}">
<label for="idUID" class="col-sm-2 col-form-label">UID</label>
<div class="col-sm-3">
<input type="number" class="form-control" id="idUID" name="uid" placeholder="" value="{{.User.UID}}"
min="0" max="2147483647">
</div>
<div class="col-sm-2"></div>
<label for="idGID" class="col-sm-2 col-form-label">GID</label>
<div class="col-sm-3">
<input type="number" class="form-control" id="idGID" name="gid" placeholder="" value="{{.User.GID}}"
min="0" max="2147483647">
</div>
</div>
<div class="form-group">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="idDisableFsChecks" name="disable_fs_checks"
{{if .User.Filters.DisableFsChecks}}checked{{end}} aria-describedby="disableFsChecksHelpBlock">
<label for="idDisableFsChecks" class="form-check-label">Disable filesystem checks</label>
<small id="disableFsChecksHelpBlock" class="form-text text-muted">
Disable checks for existence and automatic creation of home directory and virtual folders
</small>
</div>
</div>
<div class="form-group row">
<label for="idHooks" class="col-sm-2 col-form-label">Hooks</label>
<div class="col-sm-10">
<select class="form-control" id="idHooks" name="hooks" multiple>
<option value="external_auth_disabled" {{if .User.Filters.Hooks.ExternalAuthDisabled}}selected{{end}}>
External auth disabled
</option>
<option value="pre_login_disabled" {{if .User.Filters.Hooks.PreLoginDisabled}}selected{{end}}>
Pre-login disabled
</option>
<option value="check_password_disabled" {{if .User.Filters.Hooks.CheckPasswordDisabled}}selected{{end}}>
Check password disabled
</option>
</select>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
<br>
<div class="form-group row">
<label for="idWebClient" class="col-sm-2 col-form-label">Web client/REST API</label>
<div class="col-sm-10">
<select class="form-control" id="idWebClient" name="web_client_options" multiple>
{{range $option := .WebClientOptions}}
<option value="{{$option}}" {{range $p :=$.User.Filters.WebClient }}{{if eq $p $option}}selected{{end}}{{end}}>{{$option}}
</option>
{{end}}
</select>
</div>
</div>
<div class="form-group row">
<label for="idHooks" class="col-sm-2 col-form-label">Hooks</label>
<div class="col-sm-10">
<select class="form-control" id="idHooks" name="hooks" multiple>
<option value="external_auth_disabled" {{if .User.Filters.Hooks.ExternalAuthDisabled}}selected{{end}}>
External auth disabled
</option>
<option value="pre_login_disabled" {{if .User.Filters.Hooks.PreLoginDisabled}}selected{{end}}>
Pre-login disabled
</option>
<option value="check_password_disabled" {{if .User.Filters.Hooks.CheckPasswordDisabled}}selected{{end}}>
Check password disabled
</option>
</select>
</div>
</div>
<div class="form-group row">
<label for="idAdditionalInfo" class="col-sm-2 col-form-label">Additional info</label>
<div class="col-sm-10">
<textarea class="form-control" id="idAdditionalInfo" name="additional_info" rows="3"
aria-describedby="additionalInfoHelpBlock">{{.User.AdditionalInfo}}</textarea>
<small id="additionalInfoHelpBlock" class="form-text text-muted">
Free form text field
</small>
</div>
</div>
{{if eq .Mode 2}} {{if eq .Mode 2}}
<div class="form-group"> <div class="form-group">
@ -704,7 +764,9 @@
<script src="{{.StaticURL}}/vendor/tempusdominus/js/tempusdominus-bootstrap-4.min.js"></script> <script src="{{.StaticURL}}/vendor/tempusdominus/js/tempusdominus-bootstrap-4.min.js"></script>
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function () { $(document).ready(function () {
{{if .Error}}
$('#accordionUser .collapse').removeAttr("data-parent").collapse('show');
{{end}}
$('#expirationDatePicker').datetimepicker({ $('#expirationDatePicker').datetimepicker({
format: 'YYYY-MM-DD', format: 'YYYY-MM-DD',
buttons: { buttons: {