add time-based access restrictions

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2024-03-17 11:30:03 +01:00
parent 74dd2a3b9a
commit cc9a0d4dc2
No known key found for this signature in database
GPG key ID: 935D2952DEC4EECF
17 changed files with 417 additions and 15 deletions

2
go.mod
View file

@ -53,7 +53,7 @@ require (
github.com/rs/cors v1.10.1
github.com/rs/xid v1.5.0
github.com/rs/zerolog v1.32.0
github.com/sftpgo/sdk v0.1.6-0.20240216180841-c13afec62842
github.com/sftpgo/sdk v0.1.6-0.20240317102632-f6eb95ea55c3
github.com/shirou/gopsutil/v3 v3.24.2
github.com/spf13/afero v1.11.0
github.com/spf13/cobra v1.8.0

4
go.sum
View file

@ -355,8 +355,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.20240216180841-c13afec62842 h1:Rqh/TYkMX6UmUWvgXrsOBoG7ee2GH1AJXBFlszIzKT0=
github.com/sftpgo/sdk v0.1.6-0.20240216180841-c13afec62842/go.mod h1:AWoY2YYe/P1ymfTlRER/meERQjCcZZTbgVPGcPQgaqc=
github.com/sftpgo/sdk v0.1.6-0.20240317102632-f6eb95ea55c3 h1:svxTNm3r2kRlpuVSUKi0WKQlsAq8VI0EzDWPNqeNn/o=
github.com/sftpgo/sdk v0.1.6-0.20240317102632-f6eb95ea55c3/go.mod h1:AWoY2YYe/P1ymfTlRER/meERQjCcZZTbgVPGcPQgaqc=
github.com/shirou/gopsutil/v3 v3.24.2 h1:kcR0erMbLg5/3LcInpw0X/rrPSqq4CDPyI6A6ZRC18Y=
github.com/shirou/gopsutil/v3 v3.24.2/go.mod h1:tSg/594BcA+8UdQU2XcW803GWYgdtauFFPgJCJKZlVk=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=

View file

@ -449,6 +449,7 @@ type ActiveConnection interface {
GetTransfers() []ConnectionTransfer
SignalTransferClose(transferID int64, err error)
CloseFS() error
isAccessAllowed() bool
}
// StatAttributes defines the attributes for set stat commands
@ -1081,9 +1082,15 @@ func (conns *ActiveConnections) checkIdles() {
if idleTime > Config.idleTimeoutAsDuration || (isUnauthenticatedFTPUser && idleTime > Config.idleLoginTimeout) {
defer func(conn ActiveConnection) {
err := conn.Disconnect()
logger.Debug(conn.GetProtocol(), conn.GetID(), "close idle connection, idle time: %v, username: %q close err: %v",
logger.Debug(conn.GetProtocol(), conn.GetID(), "close idle connection, idle time: %s, username: %q close err: %v",
time.Since(conn.GetLastActivity()), conn.GetUsername(), err)
}(c)
} else if !c.isAccessAllowed() {
defer func(conn ActiveConnection) {
err := conn.Disconnect()
logger.Info(conn.GetProtocol(), conn.GetID(), "access conditions not met for user: %q close connection err: %v",
conn.GetUsername(), err)
}(c)
}
}

View file

@ -748,6 +748,7 @@ func TestIdleConnections(t *testing.T) {
user := dataprovider.User{
BaseUser: sdk.BaseUser{
Username: username,
Status: 1,
},
}
c := NewBaseConnection(sshConn1.id+"_1", ProtocolSFTP, "", "", user)
@ -772,15 +773,34 @@ func TestIdleConnections(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, Connections.GetActiveSessions(username), 2)
cFTP := NewBaseConnection("id2", ProtocolFTP, "", "", dataprovider.User{})
cFTP := NewBaseConnection("id2", ProtocolFTP, "", "", dataprovider.User{
BaseUser: sdk.BaseUser{
Status: 1,
},
})
cFTP.lastActivity.Store(time.Now().UnixNano())
fakeConn = &fakeConnection{
BaseConnection: cFTP,
}
err = Connections.Add(fakeConn)
assert.NoError(t, err)
assert.Equal(t, Connections.GetActiveSessions(username), 2)
assert.Len(t, Connections.GetStats(""), 3)
// the user is expired, this connection will be removed
cDAV := NewBaseConnection("id3", ProtocolWebDAV, "", "", dataprovider.User{
BaseUser: sdk.BaseUser{
Username: username + "_2",
Status: 1,
ExpirationDate: util.GetTimeAsMsSinceEpoch(time.Now().Add(-24 * time.Hour)),
},
})
cDAV.lastActivity.Store(time.Now().UnixNano())
fakeConn = &fakeConnection{
BaseConnection: cDAV,
}
err = Connections.Add(fakeConn)
assert.NoError(t, err)
assert.Equal(t, 2, Connections.GetActiveSessions(username))
assert.Len(t, Connections.GetStats(""), 4)
Connections.RLock()
assert.Len(t, Connections.sshConnections, 2)
Connections.RUnlock()

View file

@ -109,6 +109,14 @@ func (c *BaseConnection) GetMaxSessions() int {
return c.User.MaxSessions
}
// isAccessAllowed returns true if the user's access conditions are met
func (c *BaseConnection) isAccessAllowed() bool {
if err := c.User.CheckLoginConditions(); err != nil {
return false
}
return true
}
// GetProtocol returns the protocol for the connection
func (c *BaseConnection) GetProtocol() string {
return c.protocol

View file

@ -531,6 +531,45 @@ func TestCheckFsAfterUpdate(t *testing.T) {
assert.NoError(t, err)
}
func TestLoginAccessTime(t *testing.T) {
u := getTestUser()
u.Filters.AccessTime = []sdk.TimePeriod{
{
DayOfWeek: int(time.Now().Add(-25 * time.Hour).UTC().Weekday()),
From: "00:00",
To: "23:59",
},
}
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
_, _, err = getSftpClient(user)
assert.Error(t, err)
user.Filters.AccessTime = []sdk.TimePeriod{
{
DayOfWeek: int(time.Now().UTC().Weekday()),
From: "00:00",
To: "23:59",
},
}
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
conn, client, err := getSftpClient(user)
if assert.NoError(t, err) {
defer conn.Close()
defer client.Close()
err := checkBasicSFTP(client)
assert.NoError(t, err)
}
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
}
func TestSetStat(t *testing.T) {
u := getTestUser()
user, _, err := httpdtest.AddUser(u, http.StatusCreated)

View file

@ -2650,6 +2650,14 @@ func copyBaseUserFilters(in sdk.BaseUserFilters) sdk.BaseUserFilters {
copy(bwLimit.Sources, limit.Sources)
filters.BandwidthLimits = append(filters.BandwidthLimits, bwLimit)
}
filters.AccessTime = make([]sdk.TimePeriod, 0, len(in.AccessTime))
for _, period := range in.AccessTime {
filters.AccessTime = append(filters.AccessTime, sdk.TimePeriod{
DayOfWeek: period.DayOfWeek,
From: period.From,
To: period.To,
})
}
return filters
}
@ -3129,9 +3137,60 @@ func validateBaseFilters(filters *sdk.BaseUserFilters) error {
}
updateFiltersValues(filters)
if err := validateAccessTimeFilters(filters); err != nil {
return err
}
return validateFiltersPatternExtensions(filters)
}
func isTimeOfDayValid(value string) bool {
if len(value) != 5 {
return false
}
parts := strings.Split(value, ":")
if len(parts) != 2 {
return false
}
hour, err := strconv.Atoi(parts[0])
if err != nil {
return false
}
if hour < 0 || hour > 23 {
return false
}
minute, err := strconv.Atoi(parts[1])
if err != nil {
return false
}
if minute < 0 || minute > 59 {
return false
}
return true
}
func validateAccessTimeFilters(filters *sdk.BaseUserFilters) error {
for _, period := range filters.AccessTime {
if period.DayOfWeek < int(time.Sunday) || period.DayOfWeek > int(time.Saturday) {
return util.NewValidationError(fmt.Sprintf("invalid day of week: %d", period.DayOfWeek))
}
if !isTimeOfDayValid(period.From) || !isTimeOfDayValid(period.To) {
return util.NewI18nError(
util.NewValidationError("invalid time of day. Supported format: HH:MM"),
util.I18nErrorTimeOfDayInvalid,
)
}
if period.To <= period.From {
return util.NewI18nError(
util.NewValidationError("invalid time of day. The end time cannot be earlier than the start time"),
util.I18nErrorTimeOfDayConflict,
)
}
}
return nil
}
func validateCombinedUserFilters(user *User) error {
if user.Filters.TOTPConfig.Enabled && util.Contains(user.Filters.WebClient, sdk.WebClientMFADisabled) {
return util.NewI18nError(

View file

@ -335,7 +335,27 @@ func (u *User) isFsEqual(other *User) bool {
return true
}
// CheckLoginConditions checks if the user is active and not expired
func (u *User) isTimeBasedAccessAllowed(when time.Time) bool {
if len(u.Filters.AccessTime) == 0 {
return true
}
if when.IsZero() {
when = time.Now()
}
when = when.UTC()
weekDay := when.Weekday()
hhMM := when.Format("15:04")
for _, p := range u.Filters.AccessTime {
if p.DayOfWeek == int(weekDay) {
if hhMM >= p.From && hhMM <= p.To {
return true
}
}
}
return false
}
// CheckLoginConditions checks user access restrictions
func (u *User) CheckLoginConditions() error {
if u.Status < 1 {
return fmt.Errorf("user %q is disabled", u.Username)
@ -344,7 +364,10 @@ func (u *User) CheckLoginConditions() error {
return fmt.Errorf("user %q is expired, expiration timestamp: %v current timestamp: %v", u.Username,
u.ExpirationDate, util.GetTimeAsMsSinceEpoch(time.Now()))
}
return nil
if u.isTimeBasedAccessAllowed(time.Now()) {
return nil
}
return errors.New("access is not allowed at this time")
}
// hideConfidentialData hides user confidential data
@ -1834,6 +1857,7 @@ func (u *User) mergeAdditiveProperties(group *Group, groupType int, replacer *st
u.Filters.DeniedProtocols = append(u.Filters.DeniedProtocols, group.UserSettings.Filters.DeniedProtocols...)
u.Filters.WebClient = append(u.Filters.WebClient, group.UserSettings.Filters.WebClient...)
u.Filters.TwoFactorAuthProtocols = append(u.Filters.TwoFactorAuthProtocols, group.UserSettings.Filters.TwoFactorAuthProtocols...)
u.Filters.AccessTime = append(u.Filters.AccessTime, group.UserSettings.Filters.AccessTime...)
}
func (u *User) mergeVirtualFolders(group *Group, groupType int, replacer *strings.Replacer) {

View file

@ -1269,6 +1269,13 @@ func TestGroupSettingsOverride(t *testing.T) {
},
VirtualPath: "/vdir4",
})
g2.UserSettings.Filters.AccessTime = []sdk.TimePeriod{
{
DayOfWeek: int(time.Now().UTC().Weekday()),
From: "10:00",
To: "18:00",
},
}
f1 := vfs.BaseVirtualFolder{
Name: folderName1,
MappedPath: mappedPath1,
@ -1363,10 +1370,12 @@ func TestGroupSettingsOverride(t *testing.T) {
assert.Equal(t, g2.UserSettings.Permissions["/dir3"], user.Permissions["/dir3"])
assert.Equal(t, g1.UserSettings.FsConfig.OSConfig.ReadBufferSize, user.FsConfig.OSConfig.ReadBufferSize)
assert.Equal(t, g1.UserSettings.FsConfig.OSConfig.WriteBufferSize, user.FsConfig.OSConfig.WriteBufferSize)
assert.Len(t, user.Filters.AccessTime, 1)
user, err = dataprovider.GetUserAfterIDPAuth(defaultUsername, "", common.ProtocolOIDC, nil)
assert.NoError(t, err)
assert.Len(t, user.VirtualFolders, 4)
assert.Len(t, user.Filters.AccessTime, 1)
user1, user2, err := dataprovider.GetUserVariants(defaultUsername, "")
assert.NoError(t, err)
@ -1374,6 +1383,8 @@ func TestGroupSettingsOverride(t *testing.T) {
assert.Len(t, user2.VirtualFolders, 4)
assert.Equal(t, int64(0), user1.ExpirationDate)
assert.Equal(t, int64(0), user2.ExpirationDate)
assert.Len(t, user1.Filters.AccessTime, 0)
assert.Len(t, user2.Filters.AccessTime, 1)
group2.UserSettings.FsConfig = vfs.Filesystem{
Provider: sdk.SFTPFilesystemProvider,
@ -2846,6 +2857,40 @@ func TestUserBandwidthLimits(t *testing.T) {
assert.NoError(t, err)
}
func TestAccessTimeValidation(t *testing.T) {
u := getTestUser()
u.Filters.AccessTime = []sdk.TimePeriod{
{
DayOfWeek: 8,
From: "10:00",
To: "18:00",
},
}
_, resp, err := httpdtest.AddUser(u, http.StatusBadRequest)
assert.NoError(t, err, string(resp))
assert.Contains(t, string(resp), "invalid day of week")
u.Filters.AccessTime = []sdk.TimePeriod{
{
DayOfWeek: 6,
From: "10:00",
To: "18",
},
}
_, resp, err = httpdtest.AddUser(u, http.StatusBadRequest)
assert.NoError(t, err, string(resp))
assert.Contains(t, string(resp), "invalid time of day")
u.Filters.AccessTime = []sdk.TimePeriod{
{
DayOfWeek: 6,
From: "11:00",
To: "10:58",
},
}
_, resp, err = httpdtest.AddUser(u, http.StatusBadRequest)
assert.NoError(t, err, string(resp))
assert.Contains(t, string(resp), "The end time cannot be earlier than the start time")
}
func TestUserTimestamps(t *testing.T) {
user, resp, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
assert.NoError(t, err, string(resp))
@ -20648,6 +20693,11 @@ func TestWebUserAddMock(t *testing.T) {
form.Set("directory_patterns[4][pattern_path]", "/dir2")
form.Set("directory_patterns[4][patterns]", "*.mkv")
form.Set("directory_patterns[4][pattern_type]", "denied")
form.Set("access_time_restrictions[0][access_time_day_of_week]", "2")
form.Set("access_time_restrictions[0][access_time_start]", "10") // invalid and no end, ignored
form.Set("access_time_restrictions[1][access_time_day_of_week]", "3")
form.Set("access_time_restrictions[1][access_time_start]", "12:00")
form.Set("access_time_restrictions[1][access_time_end]", "14:09")
form.Set("additional_info", user.AdditionalInfo)
form.Set("description", user.Description)
form.Add("hooks", "external_auth_disabled")
@ -20997,6 +21047,11 @@ func TestWebUserAddMock(t *testing.T) {
}
}
}
if assert.Len(t, newUser.Filters.AccessTime, 1) {
assert.Equal(t, 3, newUser.Filters.AccessTime[0].DayOfWeek)
assert.Equal(t, "12:00", newUser.Filters.AccessTime[0].From)
assert.Equal(t, "14:09", newUser.Filters.AccessTime[0].To)
}
assert.Len(t, newUser.Groups, 3)
assert.Equal(t, sdk.TLSUsernameNone, newUser.Filters.TLSUsername)
req, _ = http.NewRequest(http.MethodDelete, path.Join(userPath, newUser.Username), nil)

View file

@ -1284,6 +1284,36 @@ func getUserPermissionsFromPostFields(r *http.Request) map[string][]string {
return permissions
}
func getAccessTimeRestrictionsFromPostFields(r *http.Request) []sdk.TimePeriod {
var result []sdk.TimePeriod
dayOfWeeks := r.Form["access_time_day_of_week"]
starts := r.Form["access_time_start"]
ends := r.Form["access_time_end"]
for idx, dayOfWeek := range dayOfWeeks {
dayOfWeek = strings.TrimSpace(dayOfWeek)
start := ""
if len(starts) > idx {
start = strings.TrimSpace(starts[idx])
}
end := ""
if len(ends) > idx {
end = strings.TrimSpace(ends[idx])
}
dayNumber, err := strconv.Atoi(dayOfWeek)
if err == nil && start != "" && end != "" {
result = append(result, sdk.TimePeriod{
DayOfWeek: dayNumber,
From: start,
To: end,
})
}
}
return result
}
func getBandwidthLimitsFromPostFields(r *http.Request) ([]sdk.BandwidthLimit, error) {
var result []sdk.BandwidthLimit
bwSources := r.Form["bandwidth_limit_sources"]
@ -1463,6 +1493,7 @@ func getFiltersFromUserPostFields(r *http.Request) (sdk.BaseUserFilters, error)
filters.MaxSharesExpiration = maxSharesExpiration
filters.PasswordExpiration = passwordExpiration
filters.PasswordStrength = passwordStrength
filters.AccessTime = getAccessTimeRestrictionsFromPostFields(r)
hooks := r.Form["hooks"]
if util.Contains(hooks, "external_auth_disabled") {
filters.Hooks.ExternalAuthDisabled = true
@ -1969,6 +2000,13 @@ func updateRepeaterFormFields(r *http.Request) {
r.Form.Add("pattern_policy", strings.TrimSpace(r.Form.Get(base+"[pattern_policy]")))
continue
}
if hasPrefixAndSuffix(k, "access_time_restrictions[", "][access_time_day_of_week]") {
base, _ := strings.CutSuffix(k, "[access_time_day_of_week]")
r.Form.Add("access_time_day_of_week", strings.TrimSpace(r.Form.Get(k)))
r.Form.Add("access_time_start", strings.TrimSpace(r.Form.Get(base+"[access_time_start]")))
r.Form.Add("access_time_end", strings.TrimSpace(r.Form.Get(base+"[access_time_end]")))
continue
}
if hasPrefixAndSuffix(k, "src_bandwidth_limits[", "][bandwidth_limit_sources]") {
base, _ := strings.CutSuffix(k, "[bandwidth_limit_sources]")
r.Form.Add("bandwidth_limit_sources", r.Form.Get(k))

View file

@ -2516,6 +2516,9 @@ func compareUserFilters(expected sdk.BaseUserFilters, actual sdk.BaseUserFilters
if err := compareUserBandwidthLimitFilters(expected, actual); err != nil {
return err
}
if err := compareAccessTimeFilters(expected, actual); err != nil {
return err
}
return compareUserFilePatternsFilters(expected, actual)
}
@ -2531,6 +2534,26 @@ func checkFilterMatch(expected []string, actual []string) bool {
return true
}
func compareAccessTimeFilters(expected sdk.BaseUserFilters, actual sdk.BaseUserFilters) error {
if len(expected.AccessTime) != len(actual.AccessTime) {
return errors.New("access time filters mismatch")
}
for idx, p := range expected.AccessTime {
if actual.AccessTime[idx].DayOfWeek != p.DayOfWeek {
return errors.New("access time day of week mismatch")
}
if actual.AccessTime[idx].From != p.From {
return errors.New("access time from mismatch")
}
if actual.AccessTime[idx].To != p.To {
return errors.New("access time to mismatch")
}
}
return nil
}
func compareUserBandwidthLimitFilters(expected sdk.BaseUserFilters, actual sdk.BaseUserFilters) error {
if len(expected.BandwidthLimits) != len(actual.BandwidthLimits) {
return errors.New("bandwidth limits filters mismatch")

View file

@ -191,6 +191,8 @@ const (
I18nStorageSFTP = "storage.sftp"
I18nStorageHTTP = "storage.http"
I18nErrorInvalidQuotaSize = "user.invalid_quota_size"
I18nErrorTimeOfDayInvalid = "user.time_of_day_invalid"
I18nErrorTimeOfDayConflict = "user.time_of_day_conflict"
I18nErrorInvalidMaxFilesize = "filters.max_upload_size_invalid"
I18nErrorInvalidHomeDir = "storage.home_dir_invalid"
I18nErrorBucketRequired = "storage.bucket_required"

View file

@ -263,7 +263,16 @@
"month": "Month",
"options": "Options",
"expired": "Expired",
"unsupported": "Feature no longer supported"
"unsupported": "Feature no longer supported",
"start": "Start (HH:MM)",
"end": "End (HH:MM)",
"monday": "Monday",
"tuesday": "Tuesday",
"wednesday": "Wednesday",
"thursday": "Thursday",
"friday": "Friday",
"saturday": "Saturday",
"sunday": "Sunday"
},
"fs": {
"view_file": "View file \"{{- path}}\"",
@ -537,7 +546,9 @@
"template_password_placeholder": "replaced with the specified password",
"template_help1": "Placeholders will be replaced in paths and credentials of the configured storage backend.",
"template_help2": "The generated users can be saved or exported. Exported users can be imported from the \"Maintenance\" section of this SFTPGo instance or another.",
"template_no_user": "No valid user defined, unable to complete the requested action"
"template_no_user": "No valid user defined, unable to complete the requested action",
"time_of_day_invalid": "Invalid time of day. Supported format HH:MM",
"time_of_day_conflict": "Invalid time of day. The end time cannot be earlier than the start time"
},
"group": {
"view_manage": "View and manage groups",
@ -715,7 +726,9 @@
"disable_fs_checks_help": "Disable checks for existence and automatic creation of home directory and virtual folders",
"api_key_auth_help": "Allow to impersonate the user, in REST API, with an API key",
"external_auth_cache_time": "External auth cache time",
"external_auth_cache_time_help": "Cache time, in seconds, for users authenticated using an external auth hook. 0 means no cache"
"external_auth_cache_time_help": "Cache time, in seconds, for users authenticated using an external auth hook. 0 means no cache",
"access_time": "Access time restrictions",
"access_time_help": "No restrictions means access is always allowed, the time must be set in the format HH:MM. Use UTC time"
},
"admin": {
"role_permissions": "A role admin cannot have the following permissions: {{val}}",

View file

@ -263,7 +263,16 @@
"month": "Mese",
"options": "Opzioni",
"expired": "Scaduto",
"unsupported": "Funzionalità non più supportata"
"unsupported": "Funzionalità non più supportata",
"start": "Inizio (HH:MM)",
"end": "Fine (HH:MM)",
"monday": "Lunedì",
"tuesday": "Martedì",
"wednesday": "Mercoledì",
"thursday": "Giovedì",
"friday": "Venerdì",
"saturday": "Sabato",
"sunday": "Domenica"
},
"fs": {
"view_file": "Visualizza file \"{{- path}}\"",
@ -537,7 +546,9 @@
"template_password_placeholder": "sostituito con la password specificata",
"template_help1": "I segnaposto verranno sostituiti nei percorsi e nelle credenziali del backend di archiviazione configurato.",
"template_help2": "Gli utenti generati possono essere salvati o esportati. Gli utenti esportati possono essere importati dalla sezione \"Manutenzione\" di questa istanza SFTPGo o di un'altra.",
"template_no_user": "Nessun utente valido definito. Impossibile completare l'azione richiesta"
"template_no_user": "Nessun utente valido definito. Impossibile completare l'azione richiesta",
"time_of_day_invalid": "Ora del giorno non valida. Formato supportato HH:MM",
"time_of_day_conflict": "Ora del giorno non valida. L'ora di fine non può essere precedente all'ora di inizio"
},
"group": {
"view_manage": "Visualizza e gestisci gruppi",
@ -715,7 +726,9 @@
"disable_fs_checks_help": "Disabilita i controlli sull'esistenza e la creazione automatica della directory home e delle cartelle virtuali",
"api_key_auth_help": "Permetti di impersonare l'utente nelle API REST utilizzando una chiave API",
"external_auth_cache_time": "Cache per autenticazione esterna",
"external_auth_cache_time_help": "Tempo di memorizzazione nella cache, in secondi, per gli utenti autenticati utilizzando un hook di autenticazione esterno. 0 significa nessuna cache"
"external_auth_cache_time_help": "Tempo di memorizzazione nella cache, in secondi, per gli utenti autenticati utilizzando un hook di autenticazione esterno. 0 significa nessuna cache",
"access_time": "Limitazioni temporali all'accesso",
"access_time_help": "Nessuna restrizione significa che l'accesso è sempre consentito, l'ora deve essere impostata nel formato HH:MM. Utilizzare l'ora UTC"
},
"admin": {
"role_permissions": "Un amministratore di ruolo non può avere le seguenti autorizzazioni: {{val}}",

View file

@ -605,6 +605,101 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</div>
{{- end}}
{{- define "user_group_access_time"}}
<div class="card mt-10">
<div class="card-header bg-light">
<h3 data-i18n="filters.access_time" class="card-title section-title-inner">Access time restrictions</h3>
</div>
<div class="card-body">
<div id="access_time_restrictions">
{{- template "infomsg-no-mb" "filters.access_time_help"}}
<div class="form-group">
<div data-repeater-list="access_time_restrictions">
{{- range $idx, $period := .AccessTime -}}
<div data-repeater-item>
<div data-repeater-item>
<div class="form-group row">
<div class="col-md-5 mt-3 mt-md-8">
<select name="access_time_day_of_week" class="form-select select-repetear select-first" data-hide-search="true">
<option value="0" data-i18n="general.sunday" {{- if eq $period.DayOfWeek 0}} selected{{- end}}>Sunday</option>
<option value="1" data-i18n="general.monday" {{- if eq $period.DayOfWeek 1}} selected{{- end}}>Monday</option>
<option value="2" data-i18n="general.tuesday" {{- if eq $period.DayOfWeek 2}} selected{{- end}}>Tuesday</option>
<option value="3" data-i18n="general.wednesday" {{- if eq $period.DayOfWeek 3}} selected{{- end}}>Wednesday</option>
<option value="4" data-i18n="general.thursday" {{- if eq $period.DayOfWeek 4}} selected{{- end}}>Thursday</option>
<option value="5" data-i18n="general.friday" {{- if eq $period.DayOfWeek 5}} selected{{- end}}>Friday</option>
<option value="6" data-i18n="general.saturday" {{- if eq $period.DayOfWeek 6}} selected{{- end}}>Saturday</option>
</select>
</div>
<div class="col-md-3 mt-3 mt-md-8">
<input data-i18n="[placeholder]general.start" type="text" class="form-control" name="access_time_start" value="{{$period.From}}" />
</div>
<div class="col-md-3 mt-3 mt-md-8">
<input data-i18n="[placeholder]general.end" type="text" class="form-control" name="access_time_end" value="{{$period.To}}" />
</div>
<div class="col-md-1 mt-3 mt-md-8">
<a href="#" data-repeater-delete
class="btn btn-light-danger ps-5 pe-4">
<i class="ki-duotone ki-trash fs-2">
<span class="path1"></span>
<span class="path2"></span>
<span class="path3"></span>
<span class="path4"></span>
<span class="path5"></span>
</i>
</a>
</div>
</div>
</div>
</div>
{{- else}}
<div data-repeater-item>
<div class="form-group row">
<div class="col-md-5 mt-3 mt-md-8">
<select name="access_time_day_of_week" class="form-select select-repetear select-first" data-hide-search="true">
<option value="0" data-i18n="general.sunday">Sunday</option>
<option value="1" data-i18n="general.monday">Monday</option>
<option value="2" data-i18n="general.tuesday">Tuesday</option>
<option value="3" data-i18n="general.wednesday">Wednesday</option>
<option value="4" data-i18n="general.thursday">Thursday</option>
<option value="5" data-i18n="general.friday">Friday</option>
<option value="6" data-i18n="general.saturday">Saturday</option>
</select>
</div>
<div class="col-md-3 mt-3 mt-md-8">
<input data-i18n="[placeholder]general.start" type="text" class="form-control" name="access_time_start" value="" />
</div>
<div class="col-md-3 mt-3 mt-md-8">
<input data-i18n="[placeholder]general.end" type="text" class="form-control" name="access_time_end" value="" />
</div>
<div class="col-md-1 mt-3 mt-md-8">
<a href="#" data-repeater-delete
class="btn btn-light-danger ps-5 pe-4">
<i class="ki-duotone ki-trash fs-2">
<span class="path1"></span>
<span class="path2"></span>
<span class="path3"></span>
<span class="path4"></span>
<span class="path5"></span>
</i>
</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}}
{{- define "user_group_quota"}}
<div class="form-group row mt-10">
<label for="idQuotaSize" data-i18n="virtual_folders.quota_size" class="col-md-3 col-form-label">Quota size</label>

View file

@ -230,6 +230,8 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
{{- template "user_group_perms" .Group.UserSettings.Filters}}
{{- template "user_group_access_time" .Group.UserSettings.Filters}}
<div class="form-group row mt-10">
<label for="idMaxSessions" data-i18n="filters.max_sessions" class="col-md-3 col-form-label">Max sessions</label>
<div class="col-md-9">
@ -392,6 +394,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
initRepeater('#directory_permissions');
initRepeater('#directory_patterns');
initRepeater('#src_bandwidth_limits');
initRepeater('#access_time_restrictions');
initRepeaterItems();
//{{- if .Error}}
$('#accordionUser .collapse').removeAttr("data-bs-parent").collapse('show');

View file

@ -516,6 +516,8 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
{{- template "user_group_perms" .User.Filters}}
{{- template "user_group_access_time" .User.Filters}}
<div class="form-group row mt-10">
<label for="idMaxSessions" data-i18n="filters.max_sessions" class="col-md-3 col-form-label">Max sessions</label>
<div class="col-md-9">
@ -788,6 +790,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
initRepeater('#directory_patterns');
initRepeater('#src_bandwidth_limits');
initRepeater('#tls_certs');
initRepeater('#access_time_restrictions');
initRepeaterItems();
//{{- if .Error}}
//{{- if ne .LoggedUser.Filters.Preferences.VisibleUserPageSections 0}}