Organizations, Groups, and Virtual Folders
The identity model introduces first-class organizations, groups, users to groups, and folder-level access control lists. This chapter is for operators who run a multi-tenant deployment - for example, a publishing team sharing files with multiple customer organizations.
The model
Four concepts:
- Organization - a tenant. There is always one operator organization (the company running the server). Customer organizations are additional tenants.
- Group - a capability container scoped to one org.
Carries flags like
read_only,can_checksum,can_sign, andmfa_required. - User ↔ group membership - many-to-many. A user can belong to multiple groups in the same or different organizations. This matches the convention used by OS, LDAP, and AD groups.
- Folder ACL - a (folder, group, permission) tuple
that grants a group access to a shared folder. Permissions:
READ,WRITE,READ_WRITE.
Quick start (CLI)
Create an org, a group, a folder, and grant the group access to the folder. Then put a user in the group.
admin org-add force5 --display-name "Force 5" --operator
admin group-add force5 publishers --can-sign --can-checksum
admin folder-add /releases/ --real-path /var/sss/releases
admin folder-perm-add --folder 1 --group force5/publishers --permission READ_WRITE
admin user-join-group alice force5/publishers
After these commands, user alice can read and write into
/releases/ via the Web Portal (the path resolves to
/var/sss/releases on disk). Her personal home folder still
works exactly as before.
Capability matrix
A user's effective capabilities are the union across all their groups, applying these rules:
| Field | Rule | Why |
|---|---|---|
read_only | AND across groups | Most permissive wins. Joining any non-read-only group lets the user write. |
can_checksum, can_sign | OR | Any grant of the capability is enough. |
mfa_required | OR | One group requiring MFA forces it. |
quota_bytes | MIN ignoring NULL | Most restrictive wins; NULL inherits from org. |
Folder ACLs
A folder is a (logical path, real path) pair stored in
sftp_folders. The same logical path can map to different
real paths across folder rows - this enables per-group root mounts where
two distinct groups see / as different physical roots.
Grants are rows in sftp_folder_permissions with
folder_id, group_id, and
permission. A user reaches a folder through any group they
belong to.
Permission rollup for the same folder:
READ ∪ WRITE = READ_WRITE.
The effective permission is the union across all the user's groups that
grant the folder.
Mount conflict detection
A mount conflict exists when a single user is granted two
different folders (different folder_id) with the same
folder_path but different folder_real_path
values. The Web Portal cannot show two different physical locations at
the same logical path, so this configuration is rejected.
The check runs at five mutation sites:
- Admin adds a user to a group (Web, Swing, CLI).
- Admin attaches a folder grant to a group.
- Admin edits a folder's
folder_path. - LDAP refresh on login: the conflict-introducing groups are skipped, login proceeds with the safe pre-existing memberships, and an
AUTH_MOUNT_CONFLICT_SUPPRESSEDaudit event is emitted. - Login backstop: rejected with
AUTH_MOUNT_CONFLICTif a conflict somehow exists at session bootstrap.
Diagnose a user's conflicts with
admin mount-conflicts <username>.
SFTP mount routing
By default, an SFTP user's session is anchored at their personal home
directory: the SFTP root / IS their home. Web Portal and
WebDAV clients see any ACL'd folders they have access to; SFTP clients do
not — folder ACLs affect the Portal and WebDAV without changing what SFTP
sees.
Enable mount routing to make ACL'd folders visible over SFTP as
well. Each user's mount table is computed at session bootstrap
(v_user_effective_folder_permissions) and folders appear as
top-level directories at the SFTP root, alongside the user's home files.
Turn it on in conf/sftp-server.xml:
<sftp-mount-routing enabled="true"/>
Default is false — existing installs upgrade with SFTP
semantics bit-identical to the pre-feature behavior. Turning it on is a
one-way opt-in per install; users see the change on their next SFTP
connection.
Semantics
- Root listing (
ls /) shows home contents unioned with mount roots. If a mount root name collides with a home entry, the mount wins. - Deepest-prefix wins on nested mounts. A grant on
/releases/and a nested grant on/releases/acme/resolve independently —ls /releases/shows the parent's real contents,ls /releases/acme/shows the nested mount's real contents. - Read-only mounts refuse writes at the storage layer — the SFTP
client sees an
AccessDeniederror. - Cross-mount rename and copy are refused. Move within a single mount or a single home is fine; crossing mount boundaries requires download + re-upload.
- Path-traversal attempts inside a mount resolve against the mount's
real base — a request for
/shared/../../etc/passwdfails withAccessDenied, same as home-anchored SFTP.
The mount table is a snapshot taken at session bootstrap — admin edits mid-session don't affect running SFTP sessions. The user's next connection picks up the fresh state.
Caveats
- Requires Standard edition or above (the identity model is not available on Free).
- Requires an identity-model database (H2 default is fine; MySQL / PostgreSQL / SQL Server also supported).
- Supported only with the local filesystem storage backend. The database storage backend (Enterprise) does not yet participate in mount routing; the flag is a no-op there.
- Existing SFTP integrations that hardcode paths inside the home directory keep working. New scripts that rely on the mount roots should target the paths configured in the Virtual Folders admin tab.
LDAP membership refresh
When LDAP authenticates a user, the server reads the entry's
memberOf, extracts each group's CN
(e.g. CN=Force5-Publishers,OU=Groups,... →
Force5-Publishers), and matches those CN strings
case-insensitively against enabled
sftp_groups.name rows in the provider's org. Every matching
group becomes an active membership for that user in that org.
This is the entire LDAP-to-SSS-group mapping mechanism. No XML mapping table. Operators name SSS groups to match their AD group names; mapping is automatic.
What the refresh replaces (and what it doesn't)
The refresh is an atomic replace of the user's memberships within the provider's org, not just an add:
- Memberships in the same org that no longer match the current
memberOfset are removed on this login. If the user was inAcme:writersandAcme:auditors, and this login'smemberOfmatches onlywriters, thenauditorsis dropped. - Disabled groups (
enabled = false) never match, even if a CN would otherwise line up. The user loses that membership on the next login. - Cross-org memberships are untouched. Groups in other orgs (typically operator-org groups a customer user was manually added to) survive the refresh.
- Additions that would introduce a mount
conflict are dropped iteratively and audited as
AUTH_MOUNT_CONFLICT_SUPPRESSED; login proceeds with the safe subset. - If no CNs match any SSS group, the legacy
<access-groups>/<read-only-groups>fallback insftp-server.xmlruns instead — so upgrading installs keep working with their pre-existing group-list configuration until they populatesftp_groups.
Folder Health (reconciler)
The folder reconciler iterates every row in sftp_folders
on a periodic tick (default 30 min) and checks whether
folder_real_path exists as a directory on disk. It writes
the verdict to sftp_folder_status.
When a folder is marked MISSING (volume not mounted,
directory deleted out-of-band), file operations on it return
"folder is currently unavailable" instead of leaking ENOENT or showing
a confusing empty listing.
List all currently-MISSING folders with
admin folder-health, or the REST endpoint
GET /admin/api/folder-health.
Authentication providers (probe-all-parallel)
Each authentication source (LDAP, JDBC, flat file, public key) is now a
row in sftp_authentication_providers scoped to an
organization. Login probes every enabled provider in
parallel on virtual threads and reduces:
- 0 successes → reject with generic message.
- 1 success → admit.
- >1 successes → reject as
AUTH_AMBIGUOUS- cross-provider username collision; no identity leak to the client.
Per-provider timeouts (default 3s) prevent a stuck provider from
extending the global wait (default 10s). Wall-clock cost of login is
max(latencies), not sum.
Free vs paid
| Feature | Free | Paid |
|---|---|---|
| One default org + one default group | Yes | Yes |
| Add additional orgs / groups | No | Yes |
| Folder ACLs | Single default group only | Any number of groups |
Per-user read_only flag | Yes | Yes |
| Group capability flags (can_checksum, can_sign, mfa_required) | No | Yes |
| LDAP / probe-all-parallel | No | Professional+ |
| Folder reconciler | Yes | Yes |
CLI reference
# Organizations
admin org-list
admin org-add <name> [--display-name S] [--contact-email E] [--quota BYTES] [--operator]
admin org-show <name>
admin org-edit <name> [--display-name S] [--contact-email E] [--quota BYTES] [--enable | --disable]
# Groups
admin group-list <org-name>
admin group-add <org-name> <group-name> [--display-name S] [--readonly] [--can-checksum] [--can-sign] [--mfa-required]
admin group-show <org-name>/<group-name>
# User ↔ group membership
admin user-join-group <username> <org-name>/<group-name>
admin user-leave-group <username> <org-name>/<group-name>
admin user-memberships <username>
admin user-folders <username>
# Folders
admin folder-list
admin folder-add <folder-path> --real-path <p>
admin folder-edit <folder-id> [--folder-path <p>] [--real-path <p>]
admin folder-delete <folder-id>
# Folder grants
admin folder-perm-list [--folder <id>] [--group <org/name>]
admin folder-perm-add --folder <id> --group <org/name> --permission <READ|WRITE|READ_WRITE>
admin folder-perm-remove <permission-id>
admin folder-perm-set <permission-id> <READ|WRITE|READ_WRITE>
# Mount routing (SFTP / FTP / Portal channels)
admin get-mount-routing
admin set-mount-routing [--sftp true|false] [--ftp true|false] [--portal true|false]
# Diagnostics
admin folder-health
admin mount-conflicts <username>
# Auth providers
admin provider-list
admin provider-prop-list <provider-id>
admin provider-prop-set <provider-id> <key> <value> [--secret]
Folder path normalization
Folder paths are auto-normalized on save: a missing leading slash is prepended
(shared becomes /shared), and only single-segment
paths are accepted (the root / alone, or one segment like
/shared/). Nested paths such as /a/b/c/ are rejected
with a clear error — the model doesn't support nested mounts. This is
enforced in the DAO, so all three admin surfaces (Web, Swing, CLI) behave
identically, and manual DB edits still surface the error at the next admin
write. Grouping conventions like /team-shared/ handle
"nested" naming without needing nested paths.
Editing a folder
Renaming or repointing a folder runs the same mount-conflict check as adding a user to a group or attaching a new grant. If the edit would introduce a conflict for any user who currently reaches the folder, the write is rolled back and the admin surface returns the affected paths and groups. There is no partial-save state — either the edit is clean or the folder keeps its prior values.
Mount routing per channel
Mount routing is a per-channel toggle. Enabling it makes ACL'd folders visible to that channel's clients; leaving it off preserves the pre-identity- model behavior where clients see only the user's home.
| Channel | Config element | File |
|---|---|---|
| SFTP | <sftp-mount-routing enabled="true"/> | sftp-server.xml |
| FTP / FTPS | <ftp-mount-routing enabled="true"/> | sftp-server.xml |
| Web File Portal | <portal-mount-routing enabled="true"/> | admin-console.xml |
The Portal toggle is auto-flipped the first time a folder is granted to any group. Operators upgrading from a pre-identity-model install see Portal mount routing turn on as soon as they create the first grant; SFTP and FTP stay off unless explicitly enabled. Toggling a channel while the server is running takes effect on new sessions immediately — no restart required.
The $shared$ reserved name
$shared$ is reserved as a folder path segment and cannot be
used as either a logical or physical name. The admin surfaces reject it at
input time and the runtime throws if it appears through any other route.