add time-based access restrictions
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
74dd2a3b9a
commit
cc9a0d4dc2
17 changed files with 417 additions and 15 deletions
2
go.mod
2
go.mod
|
@ -53,7 +53,7 @@ require (
|
||||||
github.com/rs/cors v1.10.1
|
github.com/rs/cors v1.10.1
|
||||||
github.com/rs/xid v1.5.0
|
github.com/rs/xid v1.5.0
|
||||||
github.com/rs/zerolog v1.32.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/shirou/gopsutil/v3 v3.24.2
|
||||||
github.com/spf13/afero v1.11.0
|
github.com/spf13/afero v1.11.0
|
||||||
github.com/spf13/cobra v1.8.0
|
github.com/spf13/cobra v1.8.0
|
||||||
|
|
4
go.sum
4
go.sum
|
@ -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/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 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
||||||
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
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.20240317102632-f6eb95ea55c3 h1:svxTNm3r2kRlpuVSUKi0WKQlsAq8VI0EzDWPNqeNn/o=
|
||||||
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/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 h1:kcR0erMbLg5/3LcInpw0X/rrPSqq4CDPyI6A6ZRC18Y=
|
||||||
github.com/shirou/gopsutil/v3 v3.24.2/go.mod h1:tSg/594BcA+8UdQU2XcW803GWYgdtauFFPgJCJKZlVk=
|
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=
|
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
||||||
|
|
|
@ -449,6 +449,7 @@ type ActiveConnection interface {
|
||||||
GetTransfers() []ConnectionTransfer
|
GetTransfers() []ConnectionTransfer
|
||||||
SignalTransferClose(transferID int64, err error)
|
SignalTransferClose(transferID int64, err error)
|
||||||
CloseFS() error
|
CloseFS() error
|
||||||
|
isAccessAllowed() bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// StatAttributes defines the attributes for set stat commands
|
// StatAttributes defines the attributes for set stat commands
|
||||||
|
@ -1081,9 +1082,15 @@ func (conns *ActiveConnections) checkIdles() {
|
||||||
if idleTime > Config.idleTimeoutAsDuration || (isUnauthenticatedFTPUser && idleTime > Config.idleLoginTimeout) {
|
if idleTime > Config.idleTimeoutAsDuration || (isUnauthenticatedFTPUser && idleTime > Config.idleLoginTimeout) {
|
||||||
defer func(conn ActiveConnection) {
|
defer func(conn ActiveConnection) {
|
||||||
err := conn.Disconnect()
|
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)
|
time.Since(conn.GetLastActivity()), conn.GetUsername(), err)
|
||||||
}(c)
|
}(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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -748,6 +748,7 @@ func TestIdleConnections(t *testing.T) {
|
||||||
user := dataprovider.User{
|
user := dataprovider.User{
|
||||||
BaseUser: sdk.BaseUser{
|
BaseUser: sdk.BaseUser{
|
||||||
Username: username,
|
Username: username,
|
||||||
|
Status: 1,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
c := NewBaseConnection(sshConn1.id+"_1", ProtocolSFTP, "", "", user)
|
c := NewBaseConnection(sshConn1.id+"_1", ProtocolSFTP, "", "", user)
|
||||||
|
@ -772,15 +773,34 @@ func TestIdleConnections(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, Connections.GetActiveSessions(username), 2)
|
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())
|
cFTP.lastActivity.Store(time.Now().UnixNano())
|
||||||
fakeConn = &fakeConnection{
|
fakeConn = &fakeConnection{
|
||||||
BaseConnection: cFTP,
|
BaseConnection: cFTP,
|
||||||
}
|
}
|
||||||
err = Connections.Add(fakeConn)
|
err = Connections.Add(fakeConn)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, Connections.GetActiveSessions(username), 2)
|
// the user is expired, this connection will be removed
|
||||||
assert.Len(t, Connections.GetStats(""), 3)
|
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()
|
Connections.RLock()
|
||||||
assert.Len(t, Connections.sshConnections, 2)
|
assert.Len(t, Connections.sshConnections, 2)
|
||||||
Connections.RUnlock()
|
Connections.RUnlock()
|
||||||
|
|
|
@ -109,6 +109,14 @@ func (c *BaseConnection) GetMaxSessions() int {
|
||||||
return c.User.MaxSessions
|
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
|
// GetProtocol returns the protocol for the connection
|
||||||
func (c *BaseConnection) GetProtocol() string {
|
func (c *BaseConnection) GetProtocol() string {
|
||||||
return c.protocol
|
return c.protocol
|
||||||
|
|
|
@ -531,6 +531,45 @@ func TestCheckFsAfterUpdate(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
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) {
|
func TestSetStat(t *testing.T) {
|
||||||
u := getTestUser()
|
u := getTestUser()
|
||||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||||
|
|
|
@ -2650,6 +2650,14 @@ func copyBaseUserFilters(in sdk.BaseUserFilters) sdk.BaseUserFilters {
|
||||||
copy(bwLimit.Sources, limit.Sources)
|
copy(bwLimit.Sources, limit.Sources)
|
||||||
filters.BandwidthLimits = append(filters.BandwidthLimits, bwLimit)
|
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
|
return filters
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3129,9 +3137,60 @@ func validateBaseFilters(filters *sdk.BaseUserFilters) error {
|
||||||
}
|
}
|
||||||
updateFiltersValues(filters)
|
updateFiltersValues(filters)
|
||||||
|
|
||||||
|
if err := validateAccessTimeFilters(filters); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return validateFiltersPatternExtensions(filters)
|
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 {
|
func validateCombinedUserFilters(user *User) error {
|
||||||
if user.Filters.TOTPConfig.Enabled && util.Contains(user.Filters.WebClient, sdk.WebClientMFADisabled) {
|
if user.Filters.TOTPConfig.Enabled && util.Contains(user.Filters.WebClient, sdk.WebClientMFADisabled) {
|
||||||
return util.NewI18nError(
|
return util.NewI18nError(
|
||||||
|
|
|
@ -335,7 +335,27 @@ func (u *User) isFsEqual(other *User) bool {
|
||||||
return true
|
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 {
|
func (u *User) CheckLoginConditions() error {
|
||||||
if u.Status < 1 {
|
if u.Status < 1 {
|
||||||
return fmt.Errorf("user %q is disabled", u.Username)
|
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,
|
return fmt.Errorf("user %q is expired, expiration timestamp: %v current timestamp: %v", u.Username,
|
||||||
u.ExpirationDate, util.GetTimeAsMsSinceEpoch(time.Now()))
|
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
|
// 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.DeniedProtocols = append(u.Filters.DeniedProtocols, group.UserSettings.Filters.DeniedProtocols...)
|
||||||
u.Filters.WebClient = append(u.Filters.WebClient, group.UserSettings.Filters.WebClient...)
|
u.Filters.WebClient = append(u.Filters.WebClient, group.UserSettings.Filters.WebClient...)
|
||||||
u.Filters.TwoFactorAuthProtocols = append(u.Filters.TwoFactorAuthProtocols, group.UserSettings.Filters.TwoFactorAuthProtocols...)
|
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) {
|
func (u *User) mergeVirtualFolders(group *Group, groupType int, replacer *strings.Replacer) {
|
||||||
|
|
|
@ -1269,6 +1269,13 @@ func TestGroupSettingsOverride(t *testing.T) {
|
||||||
},
|
},
|
||||||
VirtualPath: "/vdir4",
|
VirtualPath: "/vdir4",
|
||||||
})
|
})
|
||||||
|
g2.UserSettings.Filters.AccessTime = []sdk.TimePeriod{
|
||||||
|
{
|
||||||
|
DayOfWeek: int(time.Now().UTC().Weekday()),
|
||||||
|
From: "10:00",
|
||||||
|
To: "18:00",
|
||||||
|
},
|
||||||
|
}
|
||||||
f1 := vfs.BaseVirtualFolder{
|
f1 := vfs.BaseVirtualFolder{
|
||||||
Name: folderName1,
|
Name: folderName1,
|
||||||
MappedPath: mappedPath1,
|
MappedPath: mappedPath1,
|
||||||
|
@ -1363,10 +1370,12 @@ func TestGroupSettingsOverride(t *testing.T) {
|
||||||
assert.Equal(t, g2.UserSettings.Permissions["/dir3"], user.Permissions["/dir3"])
|
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.ReadBufferSize, user.FsConfig.OSConfig.ReadBufferSize)
|
||||||
assert.Equal(t, g1.UserSettings.FsConfig.OSConfig.WriteBufferSize, user.FsConfig.OSConfig.WriteBufferSize)
|
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)
|
user, err = dataprovider.GetUserAfterIDPAuth(defaultUsername, "", common.ProtocolOIDC, nil)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, user.VirtualFolders, 4)
|
assert.Len(t, user.VirtualFolders, 4)
|
||||||
|
assert.Len(t, user.Filters.AccessTime, 1)
|
||||||
|
|
||||||
user1, user2, err := dataprovider.GetUserVariants(defaultUsername, "")
|
user1, user2, err := dataprovider.GetUserVariants(defaultUsername, "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -1374,6 +1383,8 @@ func TestGroupSettingsOverride(t *testing.T) {
|
||||||
assert.Len(t, user2.VirtualFolders, 4)
|
assert.Len(t, user2.VirtualFolders, 4)
|
||||||
assert.Equal(t, int64(0), user1.ExpirationDate)
|
assert.Equal(t, int64(0), user1.ExpirationDate)
|
||||||
assert.Equal(t, int64(0), user2.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{
|
group2.UserSettings.FsConfig = vfs.Filesystem{
|
||||||
Provider: sdk.SFTPFilesystemProvider,
|
Provider: sdk.SFTPFilesystemProvider,
|
||||||
|
@ -2846,6 +2857,40 @@ func TestUserBandwidthLimits(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
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) {
|
func TestUserTimestamps(t *testing.T) {
|
||||||
user, resp, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
|
user, resp, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
|
||||||
assert.NoError(t, err, string(resp))
|
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][pattern_path]", "/dir2")
|
||||||
form.Set("directory_patterns[4][patterns]", "*.mkv")
|
form.Set("directory_patterns[4][patterns]", "*.mkv")
|
||||||
form.Set("directory_patterns[4][pattern_type]", "denied")
|
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("additional_info", user.AdditionalInfo)
|
||||||
form.Set("description", user.Description)
|
form.Set("description", user.Description)
|
||||||
form.Add("hooks", "external_auth_disabled")
|
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.Len(t, newUser.Groups, 3)
|
||||||
assert.Equal(t, sdk.TLSUsernameNone, newUser.Filters.TLSUsername)
|
assert.Equal(t, sdk.TLSUsernameNone, newUser.Filters.TLSUsername)
|
||||||
req, _ = http.NewRequest(http.MethodDelete, path.Join(userPath, newUser.Username), nil)
|
req, _ = http.NewRequest(http.MethodDelete, path.Join(userPath, newUser.Username), nil)
|
||||||
|
|
|
@ -1284,6 +1284,36 @@ func getUserPermissionsFromPostFields(r *http.Request) map[string][]string {
|
||||||
return permissions
|
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) {
|
func getBandwidthLimitsFromPostFields(r *http.Request) ([]sdk.BandwidthLimit, error) {
|
||||||
var result []sdk.BandwidthLimit
|
var result []sdk.BandwidthLimit
|
||||||
bwSources := r.Form["bandwidth_limit_sources"]
|
bwSources := r.Form["bandwidth_limit_sources"]
|
||||||
|
@ -1463,6 +1493,7 @@ func getFiltersFromUserPostFields(r *http.Request) (sdk.BaseUserFilters, error)
|
||||||
filters.MaxSharesExpiration = maxSharesExpiration
|
filters.MaxSharesExpiration = maxSharesExpiration
|
||||||
filters.PasswordExpiration = passwordExpiration
|
filters.PasswordExpiration = passwordExpiration
|
||||||
filters.PasswordStrength = passwordStrength
|
filters.PasswordStrength = passwordStrength
|
||||||
|
filters.AccessTime = getAccessTimeRestrictionsFromPostFields(r)
|
||||||
hooks := r.Form["hooks"]
|
hooks := r.Form["hooks"]
|
||||||
if util.Contains(hooks, "external_auth_disabled") {
|
if util.Contains(hooks, "external_auth_disabled") {
|
||||||
filters.Hooks.ExternalAuthDisabled = true
|
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]")))
|
r.Form.Add("pattern_policy", strings.TrimSpace(r.Form.Get(base+"[pattern_policy]")))
|
||||||
continue
|
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]") {
|
if hasPrefixAndSuffix(k, "src_bandwidth_limits[", "][bandwidth_limit_sources]") {
|
||||||
base, _ := strings.CutSuffix(k, "[bandwidth_limit_sources]")
|
base, _ := strings.CutSuffix(k, "[bandwidth_limit_sources]")
|
||||||
r.Form.Add("bandwidth_limit_sources", r.Form.Get(k))
|
r.Form.Add("bandwidth_limit_sources", r.Form.Get(k))
|
||||||
|
|
|
@ -2516,6 +2516,9 @@ func compareUserFilters(expected sdk.BaseUserFilters, actual sdk.BaseUserFilters
|
||||||
if err := compareUserBandwidthLimitFilters(expected, actual); err != nil {
|
if err := compareUserBandwidthLimitFilters(expected, actual); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := compareAccessTimeFilters(expected, actual); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return compareUserFilePatternsFilters(expected, actual)
|
return compareUserFilePatternsFilters(expected, actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2531,6 +2534,26 @@ func checkFilterMatch(expected []string, actual []string) bool {
|
||||||
return true
|
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 {
|
func compareUserBandwidthLimitFilters(expected sdk.BaseUserFilters, actual sdk.BaseUserFilters) error {
|
||||||
if len(expected.BandwidthLimits) != len(actual.BandwidthLimits) {
|
if len(expected.BandwidthLimits) != len(actual.BandwidthLimits) {
|
||||||
return errors.New("bandwidth limits filters mismatch")
|
return errors.New("bandwidth limits filters mismatch")
|
||||||
|
|
|
@ -191,6 +191,8 @@ const (
|
||||||
I18nStorageSFTP = "storage.sftp"
|
I18nStorageSFTP = "storage.sftp"
|
||||||
I18nStorageHTTP = "storage.http"
|
I18nStorageHTTP = "storage.http"
|
||||||
I18nErrorInvalidQuotaSize = "user.invalid_quota_size"
|
I18nErrorInvalidQuotaSize = "user.invalid_quota_size"
|
||||||
|
I18nErrorTimeOfDayInvalid = "user.time_of_day_invalid"
|
||||||
|
I18nErrorTimeOfDayConflict = "user.time_of_day_conflict"
|
||||||
I18nErrorInvalidMaxFilesize = "filters.max_upload_size_invalid"
|
I18nErrorInvalidMaxFilesize = "filters.max_upload_size_invalid"
|
||||||
I18nErrorInvalidHomeDir = "storage.home_dir_invalid"
|
I18nErrorInvalidHomeDir = "storage.home_dir_invalid"
|
||||||
I18nErrorBucketRequired = "storage.bucket_required"
|
I18nErrorBucketRequired = "storage.bucket_required"
|
||||||
|
|
|
@ -263,7 +263,16 @@
|
||||||
"month": "Month",
|
"month": "Month",
|
||||||
"options": "Options",
|
"options": "Options",
|
||||||
"expired": "Expired",
|
"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": {
|
"fs": {
|
||||||
"view_file": "View file \"{{- path}}\"",
|
"view_file": "View file \"{{- path}}\"",
|
||||||
|
@ -537,7 +546,9 @@
|
||||||
"template_password_placeholder": "replaced with the specified password",
|
"template_password_placeholder": "replaced with the specified password",
|
||||||
"template_help1": "Placeholders will be replaced in paths and credentials of the configured storage backend.",
|
"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_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": {
|
"group": {
|
||||||
"view_manage": "View and manage groups",
|
"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",
|
"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",
|
"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": "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": {
|
"admin": {
|
||||||
"role_permissions": "A role admin cannot have the following permissions: {{val}}",
|
"role_permissions": "A role admin cannot have the following permissions: {{val}}",
|
||||||
|
|
|
@ -263,7 +263,16 @@
|
||||||
"month": "Mese",
|
"month": "Mese",
|
||||||
"options": "Opzioni",
|
"options": "Opzioni",
|
||||||
"expired": "Scaduto",
|
"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": {
|
"fs": {
|
||||||
"view_file": "Visualizza file \"{{- path}}\"",
|
"view_file": "Visualizza file \"{{- path}}\"",
|
||||||
|
@ -537,7 +546,9 @@
|
||||||
"template_password_placeholder": "sostituito con la password specificata",
|
"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_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_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": {
|
"group": {
|
||||||
"view_manage": "Visualizza e gestisci gruppi",
|
"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",
|
"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",
|
"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": "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": {
|
"admin": {
|
||||||
"role_permissions": "Un amministratore di ruolo non può avere le seguenti autorizzazioni: {{val}}",
|
"role_permissions": "Un amministratore di ruolo non può avere le seguenti autorizzazioni: {{val}}",
|
||||||
|
|
|
@ -605,6 +605,101 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
||||||
</div>
|
</div>
|
||||||
{{- end}}
|
{{- 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"}}
|
{{- define "user_group_quota"}}
|
||||||
<div class="form-group row mt-10">
|
<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>
|
<label for="idQuotaSize" data-i18n="virtual_folders.quota_size" class="col-md-3 col-form-label">Quota size</label>
|
||||||
|
|
|
@ -230,6 +230,8 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
||||||
|
|
||||||
{{- template "user_group_perms" .Group.UserSettings.Filters}}
|
{{- template "user_group_perms" .Group.UserSettings.Filters}}
|
||||||
|
|
||||||
|
{{- template "user_group_access_time" .Group.UserSettings.Filters}}
|
||||||
|
|
||||||
<div class="form-group row mt-10">
|
<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>
|
<label for="idMaxSessions" data-i18n="filters.max_sessions" class="col-md-3 col-form-label">Max sessions</label>
|
||||||
<div class="col-md-9">
|
<div class="col-md-9">
|
||||||
|
@ -392,6 +394,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
||||||
initRepeater('#directory_permissions');
|
initRepeater('#directory_permissions');
|
||||||
initRepeater('#directory_patterns');
|
initRepeater('#directory_patterns');
|
||||||
initRepeater('#src_bandwidth_limits');
|
initRepeater('#src_bandwidth_limits');
|
||||||
|
initRepeater('#access_time_restrictions');
|
||||||
initRepeaterItems();
|
initRepeaterItems();
|
||||||
//{{- if .Error}}
|
//{{- if .Error}}
|
||||||
$('#accordionUser .collapse').removeAttr("data-bs-parent").collapse('show');
|
$('#accordionUser .collapse').removeAttr("data-bs-parent").collapse('show');
|
||||||
|
|
|
@ -516,6 +516,8 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
||||||
|
|
||||||
{{- template "user_group_perms" .User.Filters}}
|
{{- template "user_group_perms" .User.Filters}}
|
||||||
|
|
||||||
|
{{- template "user_group_access_time" .User.Filters}}
|
||||||
|
|
||||||
<div class="form-group row mt-10">
|
<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>
|
<label for="idMaxSessions" data-i18n="filters.max_sessions" class="col-md-3 col-form-label">Max sessions</label>
|
||||||
<div class="col-md-9">
|
<div class="col-md-9">
|
||||||
|
@ -788,6 +790,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
||||||
initRepeater('#directory_patterns');
|
initRepeater('#directory_patterns');
|
||||||
initRepeater('#src_bandwidth_limits');
|
initRepeater('#src_bandwidth_limits');
|
||||||
initRepeater('#tls_certs');
|
initRepeater('#tls_certs');
|
||||||
|
initRepeater('#access_time_restrictions');
|
||||||
initRepeaterItems();
|
initRepeaterItems();
|
||||||
//{{- if .Error}}
|
//{{- if .Error}}
|
||||||
//{{- if ne .LoggedUser.Filters.Preferences.VisibleUserPageSections 0}}
|
//{{- if ne .LoggedUser.Filters.Preferences.VisibleUserPageSections 0}}
|
||||||
|
|
Loading…
Reference in a new issue