How RAGFlow Handles User Invitations and Multi-Tenant Access Control
How RAGFlow Handles User Invitations and Multi-Tenant Access Control
RAGFlow implements a sophisticated multi-tenant architecture that goes far beyond simply adding a join table. Understanding the full permission model is essential for building team workflows on top of RAGFlow. This post breaks down the invitation flow, dataset access control, and model sharing mechanics discovered through source-level analysis.
The UserTenant Table: More Than a Join Table
At the core of the permission system is the UserTenant model, defined in api/db/db_models.py:
class UserTenant(DataBaseModel):
id = CharField(max_length=32, primary_key=True)
user_id = CharField(max_length=32, null=False, index=True)
tenant_id = CharField(max_length=32, null=False, index=True)
role = CharField(max_length=32, null=False, help_text="UserTenantRole", index=True)
invited_by = CharField(max_length=32, null=False, index=True)
status = CharField(max_length=1, null=True, default="1", index=True)
class Meta:
db_table = "user_tenant"
Key fields to notice:
rolenot a binary flag but an enum:owner,admin,normal, andinvite. Theinviterole is a pending state.invited_bytracks the chain of trust back to the inviting user.statussoft-delete flag (0= wasted,1= valid).
Every user who registers also automatically gets their own tenant entry. The user’s
user_iddoubles as theirtenant_id, and they are assigned theownerrole from the start.
The Invitation Flow: A Two-Step Acceptance Protocol
Inviting a user is not a single atomic operation. It is a deliberate two-step protocol:
Step 1 The Owner Sends an Invitation
The tenant owner calls the invite API. This creates a UserTenant record with role = UserTenantRole.INVITE:
UserTenantService.save(
id=get_uuid(),
user_id=user_id_to_invite,
tenant_id=tenant_id,
invited_by=current_user.id,
role=UserTenantRole.INVITE,
status=StatusEnum.VALID.value
)
At this point the invited user is visible to the system but has no meaningful access.
Step 2 The Invitee Accepts
When the invited user accepts, their role is promoted from INVITE to NORMAL:
UserTenantService.filter_update(
[UserTenant.tenant_id == tenant_id, UserTenant.user_id == current_user.id],
{"role": UserTenantRole.NORMAL}
)
Only after this step can the user consume team resources.
Important restriction: Only the tenant owner can send invitations. Regular members (
normalrole) do not have the ability to invite others.
Dataset Access Control: The Permission Field Matters
Joining a team does not automatically grant access to all of the owner’s datasets. Dataset visibility is governed by a separate permission field on the Knowledgebase model.
The query that lists accessible knowledge bases filters on two conditions simultaneously:
(cls.model.tenant_id.in_(joined_tenant_ids)
& (cls.model.permission == TenantPermission.TEAM.value))
| (cls.model.tenant_id == user_id)
This means:
- A dataset is visible to team members only when its
permissionis set to"Team". - Datasets left at the default
"Me"(private) remain invisible to everyone except the owner.
The accessible() helper enforces this at the row level:
def accessible(cls, kb_id, user_id):
docs = cls.model.select(
cls.model.id
).join(
UserTenant, on=(UserTenant.tenant_id == Knowledgebase.tenant_id)
).where(
cls.model.id == kb_id,
UserTenant.user_id == user_id
).paginate(0, 1)
return bool(docs.dicts())
The JOIN against UserTenant ensures only users with an active membership record can pass the check.
Model Access Control: Community vs. Enterprise
Model sharing follows a tiered policy:
| Feature | Community Edition | Enterprise Edition |
|---|---|---|
| Use tenant’s default model config | Yes | Yes |
| Share custom/fine-tuned models with team | No | Yes |
Team members in the community edition can use the owner’s selected base models but cannot access any custom models the owner may have configured. Custom model sharing is an enterprise-only capability.
What Team Members Can Do After Joining
Once a user accepts an invitation and their role becomes normal, they can:
- Upload documents to the team owner’s shared datasets (permission = Team).
- Trigger document parsing jobs inside those datasets.
- Use the team owner’s shared Agents.
They cannot:
- Invite other users to the team.
- Access datasets with private permission.
- Share custom models (community edition).
Key Takeaways
- The
UserTenanttable is a state machine, not just a mapping. Theinviterole represents a pending state, and acceptance transitions the record tonormal. - Dataset sharing is opt-in per dataset. Joining a team does not open up all resources automatically owners must explicitly set each dataset’s permission to
"Team". - Access is enforced via SQL JOIN, not application-layer checks alone, making it harder to accidentally bypass.
- Invitation authority is strictly centralized at the owner level, which simplifies audit trails at the cost of flexibility.
Understanding these mechanics is critical before designing any automation or integration that involves multi-user RAGFlow deployments.