Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# compilation output
sftpgo
sftpgo.exe
.idea
5 changes: 4 additions & 1 deletion internal/common/connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ func NewBaseConnection(id, protocol, localAddr, remoteAddr string, user dataprov
if slices.Contains(supportedProtocols, protocol) {
connID = fmt.Sprintf("%s_%s", protocol, id)
}
user.UploadBandwidth, user.DownloadBandwidth = user.GetBandwidthForIP(util.GetIPFromRemoteAddress(remoteAddr), connID)
user.SessionRemoteIP = util.GetIPFromRemoteAddress(remoteAddr)
user.SessionProtocol = protocol
user.UploadBandwidth, user.DownloadBandwidth = user.GetBandwidthForIP(user.SessionRemoteIP, connID)
c := &BaseConnection{
ID: connID,
User: user,
Expand Down Expand Up @@ -132,6 +134,7 @@ func (c *BaseConnection) GetRemoteIP() string {
// SetProtocol sets the protocol for this connection
func (c *BaseConnection) SetProtocol(protocol string) {
c.protocol = protocol
c.User.SessionProtocol = protocol
if slices.Contains(supportedProtocols, c.protocol) {
c.ID = fmt.Sprintf("%v_%v", c.protocol, c.ID)
}
Expand Down
112 changes: 112 additions & 0 deletions internal/dataprovider/ip_permissions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package dataprovider

import (
"os"
"strings"
)

const (
envIPFilterEnabled = "SFTPGO_IP_FILTER_ENABLED"
envIPFilterMode = "SFTPGO_IP_FILTER_MODE"
envIPFilterScope = "SFTPGO_IP_FILTER_SCOPE"

ipFilterModeAllowUnmatched = "allow_unmatched"
ipFilterModeDenyUnmatched = "deny_unmatched"
ipFilterScopeDataOnly = "data_only"
ipFilterScopeAllRequests = "all_requests"
)

func isWritePermission(permission string) bool {
switch permission {
case PermUpload, PermOverwrite, PermCreateDirs, PermRename, PermRenameFiles, PermRenameDirs,
PermDelete, PermDeleteFiles, PermDeleteDirs, PermCreateSymlinks, PermChmod, PermChown,
PermChtimes, PermCopy:
return true
default:
return false
}
}

func isReadPermission(permission string) bool {
switch permission {
case PermDownload, PermListItems, PermCopy:
return true
default:
return false
}
}

func getIPFilterMode() string {
mode := strings.ToLower(strings.TrimSpace(os.Getenv(envIPFilterMode)))
switch mode {
case "", ipFilterModeAllowUnmatched:
return ipFilterModeAllowUnmatched
case ipFilterModeDenyUnmatched:
return ipFilterModeDenyUnmatched
default:
return ipFilterModeAllowUnmatched
}
}

func getIPFilterScope() string {
scope := strings.ToLower(strings.TrimSpace(os.Getenv(envIPFilterScope)))
switch scope {
case "", ipFilterScopeDataOnly:
return ipFilterScopeDataOnly
case ipFilterScopeAllRequests:
return ipFilterScopeAllRequests
default:
return ipFilterScopeDataOnly
}
}

func normalizeIPListProtocol(protocol string) string {
switch strings.ToUpper(strings.TrimSpace(protocol)) {
case "SFTP", "SCP", protocolSSH:
return protocolSSH
case protocolFTP:
return protocolFTP
case protocolWebDAV:
return protocolWebDAV
case protocolHTTP, "HTTPSHARE", "OIDC":
return protocolHTTP
default:
return protocol
}
}

func isIPFilterEnabled() bool {
val := strings.TrimSpace(os.Getenv(envIPFilterEnabled))
switch strings.ToLower(val) {
case "1", "true", "yes", "on":
return true
default:
return false
}
}

func isPermissionAllowedForIP(permission, remoteIP, protocol string) bool {
if !isIPFilterEnabled() {
return true
}
if getIPFilterScope() != ipFilterScopeDataOnly && getIPFilterScope() != ipFilterScopeAllRequests {
return true
}
if remoteIP == "" {
return false
}
entry, ok, err := GetIPListEntryForIP(remoteIP, normalizeIPListProtocol(protocol), IPListTypeAllowList)
if err != nil {
return false
}
if !ok {
return getIPFilterMode() != ipFilterModeDenyUnmatched
}
if isWritePermission(permission) && !entry.AllowsUpload() {
return false
}
if isReadPermission(permission) && !entry.AllowsDownload() {
return false
}
return true
}
93 changes: 85 additions & 8 deletions internal/dataprovider/iplist.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,12 @@ const (

// Supported IP list modes
const (
// for defender entries
ListModeAllow = iota + 1
ListModeDeny
// for allow list entries only
ListModeAllowUploadOnly
ListModeAllowDownloadOnly
)

const (
Expand Down Expand Up @@ -127,7 +131,7 @@ func (e *IPListEntry) PrepareForRendering() {

// HasProtocol returns true if the specified protocol is defined
func (e *IPListEntry) HasProtocol(proto string) bool {
switch proto {
switch normalizeIPListProtocol(proto) {
case protocolSSH:
return e.Protocols&1 != 0
case protocolFTP:
Expand Down Expand Up @@ -186,6 +190,62 @@ func (e *IPListEntry) getLast() netip.Addr {
return netip.AddrFrom16(a16)
}

func (e *IPListEntry) GetAccessMode() int {
if e.Type != IPListTypeAllowList {
return ListModeAllow
}
switch e.Mode {
case ListModeAllowUploadOnly, ListModeAllowDownloadOnly:
return e.Mode
default:
return ListModeAllow
}
}

func (e *IPListEntry) AllowsUpload() bool {
return e.GetAccessMode() != ListModeAllowDownloadOnly
}

func (e *IPListEntry) AllowsDownload() bool {
return e.GetAccessMode() != ListModeAllowUploadOnly
}

func getMaskBits(ipOrNet string) int {
if strings.Contains(ipOrNet, "/") {
_, n, err := net.ParseCIDR(ipOrNet)
if err == nil {
ones, _ := n.Mask.Size()
return ones
}
}
if parsed := net.ParseIP(ipOrNet); parsed != nil {
if parsed.To4() != nil {
return 32
}
return 128
}
return -1
}

func selectMostSpecificIPListEntry(entries []IPListEntry, protocol string) (IPListEntry, bool) {
bestBits := -1
var best IPListEntry
for _, e := range entries {
if e.Protocols != 0 && !e.HasProtocol(protocol) {
continue
}
bits := getMaskBits(e.IPOrNet)
if bits > bestBits {
bestBits = bits
best = e
}
}
if bestBits < 0 {
return IPListEntry{}, false
}
return best, true
}

func (e *IPListEntry) checkProtocols() {
for _, proto := range ValidProtocols {
if !e.HasProtocol(proto) {
Expand All @@ -205,6 +265,10 @@ func (e *IPListEntry) validate() error {
if e.Mode < ListModeAllow || e.Mode > ListModeDeny {
return util.NewValidationError(fmt.Sprintf("invalid list mode: %d", e.Mode))
}
case IPListTypeAllowList:
if e.Mode != ListModeAllow && e.Mode != ListModeAllowUploadOnly && e.Mode != ListModeAllowDownloadOnly {
return util.NewValidationError("invalid list mode")
}
default:
if e.Mode != ListModeAllow {
return util.NewValidationError("invalid list mode")
Expand Down Expand Up @@ -431,14 +495,16 @@ func (l *IPList) IsListed(ip, protocol string) (bool, int, error) {
if err != nil {
return false, 0, fmt.Errorf("unable to find containing networks for ip %q: %w", ip, err)
}
matches := make([]IPListEntry, 0, len(entries))
for _, e := range entries {
entry, ok := e.(*rangerEntry)
if ok {
if entry.entry.Protocols == 0 || entry.entry.HasProtocol(protocol) {
return true, entry.entry.Mode, nil
}
matches = append(matches, *entry.entry)
}
}
if match, ok := selectMostSpecificIPListEntry(matches, protocol); ok {
return true, match.Mode, nil
}

return false, 0, nil
}
Expand All @@ -447,15 +513,26 @@ func (l *IPList) IsListed(ip, protocol string) (bool, int, error) {
if err != nil {
return false, 0, err
}
for _, e := range entries {
if e.Protocols == 0 || e.HasProtocol(protocol) {
return true, e.Mode, nil
}
if match, ok := selectMostSpecificIPListEntry(entries, protocol); ok {
return true, match.Mode, nil
}

return false, 0, nil
}

// GetIPListEntryForIP returns the most specific matching entry for the given IP/protocol.
func GetIPListEntryForIP(ip, protocol string, listType IPListType) (IPListEntry, bool, error) {
entries, err := provider.getListEntriesForIP(ip, listType)
if err != nil {
return IPListEntry{}, false, err
}
entry, ok := selectMostSpecificIPListEntry(entries, protocol)
if !ok {
return IPListEntry{}, false, nil
}
return entry, true, nil
}

// NewIPList returns a new IP list for the specified type
func NewIPList(listType IPListType) (*IPList, error) {
delete(inMemoryLists, listType)
Expand Down
55 changes: 21 additions & 34 deletions internal/dataprovider/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,12 @@ type UserFilters struct {
// User defines a SFTPGo user
type User struct {
sdk.BaseUser
// SessionRemoteIP is the remote IP associated with the current live connection.
// It is not persisted and is used for connection-scoped permission enforcement.
SessionRemoteIP string `json:"-"`
// SessionProtocol is the protocol associated with the current live connection.
// It is not persisted and is used for connection-scoped permission enforcement.
SessionProtocol string `json:"-"`
// Additional restrictions
Filters UserFilters `json:"filters"`
// Mapping between virtual paths and virtual folders
Expand Down Expand Up @@ -857,6 +863,13 @@ func (u *User) HasPermissionsInside(virtualPath string) bool {
// HasPerm returns true if the user has the given permission or any permission
func (u *User) HasPerm(permission, path string) bool {
perms := u.GetPermissionsForPath(path)
return u.hasPermissionInList(permission, perms)
}

func (u *User) hasPermissionInList(permission string, perms []string) bool {
if !isPermissionAllowedForIP(permission, u.SessionRemoteIP, u.SessionProtocol) {
return false
}
if slices.Contains(perms, PermAny) {
return true
}
Expand All @@ -866,11 +879,8 @@ func (u *User) HasPerm(permission, path string) bool {
// HasAnyPerm returns true if the user has at least one of the given permissions
func (u *User) HasAnyPerm(permissions []string, path string) bool {
perms := u.GetPermissionsForPath(path)
if slices.Contains(perms, PermAny) {
return true
}
for _, permission := range permissions {
if slices.Contains(perms, permission) {
if u.hasPermissionInList(permission, perms) {
return true
}
}
Expand All @@ -880,11 +890,8 @@ func (u *User) HasAnyPerm(permissions []string, path string) bool {
// HasPerms returns true if the user has all the given permissions
func (u *User) HasPerms(permissions []string, path string) bool {
perms := u.GetPermissionsForPath(path)
if slices.Contains(perms, PermAny) {
return true
}
for _, permission := range permissions {
if !slices.Contains(perms, permission) {
if !u.hasPermissionInList(permission, perms) {
return false
}
}
Expand All @@ -895,40 +902,20 @@ func (u *User) HasPerms(permissions []string, path string) bool {
// for the given path
func (u *User) HasPermsDeleteAll(path string) bool {
perms := u.GetPermissionsForPath(path)
canDeleteFiles := false
canDeleteDirs := false
for _, permission := range perms {
if permission == PermAny || permission == PermDelete {
return true
}
if permission == PermDeleteFiles {
canDeleteFiles = true
}
if permission == PermDeleteDirs {
canDeleteDirs = true
}
if u.hasPermissionInList(PermDelete, perms) {
return true
}
return canDeleteFiles && canDeleteDirs
return u.hasPermissionInList(PermDeleteFiles, perms) && u.hasPermissionInList(PermDeleteDirs, perms)
}

// HasPermsRenameAll returns true if the user can rename both files and directories
// for the given path
func (u *User) HasPermsRenameAll(path string) bool {
perms := u.GetPermissionsForPath(path)
canRenameFiles := false
canRenameDirs := false
for _, permission := range perms {
if permission == PermAny || permission == PermRename {
return true
}
if permission == PermRenameFiles {
canRenameFiles = true
}
if permission == PermRenameDirs {
canRenameDirs = true
}
if u.hasPermissionInList(PermRename, perms) {
return true
}
return canRenameFiles && canRenameDirs
return u.hasPermissionInList(PermRenameFiles, perms) && u.hasPermissionInList(PermRenameDirs, perms)
}

// HasNoQuotaRestrictions returns true if no quota restrictions need to be applyed
Expand Down
Loading