⚠️ Not suited to beginner contributors!
Is your feature request related to a problem? Please describe.
Yes, this is a security issue related to identity verification across different organizations.
The Security Problem:
Consider two organizations:
- Org A: Requires identity verification for all users
- Org B: Does not require identity verification
Current Behavior:
- User signs up to Org B (no identity verification required)
- User attempts to join Org A (which requires identity verification)
- The system allows the user to join Org A without completing identity verification
- This is a security vulnerability as users can bypass Org A's identity verification requirements
Why This Happens:
The current system assumes all organizations are managed by the same administrators who enforce consistent rules. However, this assumption doesn't hold when:
- Different organizations have different security requirements
- Organizations belong to different entities with varying compliance needs
- Some organizations require strong identity verification (e.g., government, healthcare) while others don't
Why the Currently Proposed Alternative is Insufficient:
The patch in #674 introduces a cross_organization_login_enabled
setting that prevents users from joining organizations with different identity verification requirements.
While this prevents the security issue, it creates a new problem:
- Users cannot join Org A at all if they already have an account in Org B
- Users would need to create a new account with a different email/phone number, which may not always be feasible
- This is not a practical solution for real-world scenarios
Describe the solution you'd like
Proposed Architecture: Per-Organization RegisteredUser Records
Core Concept:
Change the RegisteredUser model to support multiple records per user, with one record per organization. This allows each organization to have its own verification status, method, and requirements for the same user.
Technical Changes:
-
Database Schema Changes:
class AbstractRegisteredUser(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="registered_users",
)
organization = models.ForeignKey(
"openwisp_users.Organization",
on_delete=models.CASCADE,
related_name="registered_users",
null=True,
blank=True,
help_text="The organization this registration info belongs to. "
"If null, applies to all orgs without specific requirements."
)
method = models.CharField(max_length=64) # Registration method
is_verified = models.BooleanField(default=False) # Verification status
modified = AutoLastModifiedField()
class Meta:
unique_together = ("user", "organization")
-
Key Changes:
- Change
OneToOneField to ForeignKey for user relationship
- Add
organization field (nullable for backward compatibility)
- Add
unique_together constraint on (user, organization)
- When
organization=None, the record applies globally (backward compatible)
-
API Changes:
- When a user tries to join a new organization with different requirements:
- Check if they have a
RegisteredUser record for that org
- If not, allow them to initiate the verification process
- Create a new
RegisteredUser record specific to that organization
- Update
IDVerificationHelper to check org-specific verification status:
def is_identity_verified_strong(self, user, organization=None):
try:
# Try org-specific record first
if organization:
reg_user = RegisteredUser.objects.get(
user=user, organization=organization
)
else:
# Fall back to global record
reg_user = user.registered_users.get(organization=None)
return reg_user.is_identity_verified_strong
except RegisteredUser.DoesNotExist:
return False
-
Registration Flow:
- When registering to a new organization:
- Check if user already exists globally
- If yes, check if a
RegisteredUser record exists for this org
- If not, initiate verification process (SMS, email, etc.)
- Create org-specific
RegisteredUser record upon completion
-
Monitoring Integration Updates:
- Update
write_user_registration_metrics to handle multiple RegisteredUser records per user
- Ensure metrics correctly count unique users per organization
- Update
post_save_radiusaccounting to get the correct registration method for the org
-
Backward Compatibility:
- Existing installations with
organization=None continue working as before
- New organizations can opt into per-org verification requirements
- Global settings can define default behavior when
organization=None
Benefits:
- Security: Users must complete identity verification for each organization that requires it
- Flexibility: Organizations can have independent onboarding rules (verification, limits, pricing)
- User Experience: Users don't need multiple accounts - they can use the same email across organizations
- Backward Compatibility: Existing single-org setups continue working without changes
- Extensibility: Supports future features like organization-specific pricing (openwisp-subscription)
Describe alternatives you've considered
Alternative 1: Current Branch Approach (Block Cross-Org Login)
Description: Use the cross_organization_login_enabled setting to prevent users from joining organizations with different requirements.
Problems:
- Users cannot join organizations with different requirements at all
- Forces users to create new accounts with different credentials
- Not feasible for users who need access to multiple organizations
- Poor user experience
Verdict: Rejected - too restrictive and impractical
Alternative 2: Global Identity Verification
Description: Require all users to complete the highest level of verification required by any organization they might join.
Problems:
- Over-verification for users who only need low-security organizations
- Privacy concerns - collecting more data than necessary
- Increased friction for user registration
- Not scalable across diverse organization types
Verdict: Rejected - too invasive and inflexible
Alternative 3: Organization-Specific User Accounts
Description: Create separate user accounts for each organization (like multi-tenant SaaS).
Problems:
- Users must remember multiple sets of credentials
- Difficult to manage account consolidation
- Complex password reset flows
- Poor user experience
Verdict: Rejected - duplicates data and creates management overhead
Alternative 4: Verification Requirements Inheritance
Description: Organizations can inherit verification from other organizations.
Problems:
- Complex inheritance rules
- Difficult to track which org verified the user
- Potential for verification gaps
- Harder to implement and maintain
Verdict: Rejected - adds unnecessary complexity
Additional context
Current Implementation Analysis
The current branch adds these files/changes:
- New migration adding
cross_organization_login_enabled field to OrganizationRadiusSettings
- API changes in
ObtainAuthTokenView.validate_membership() to check the new setting
- Documentation updates explaining the setting
- Tests for the new behavior
Files Modified in Current Branch:
.github/workflows/backport.yml (removed)
docs/images/organization_cross_login.png (new)
docs/user/rest-api.rst (documentation)
docs/user/settings.rst (documentation)
openwisp_radius/admin.py (admin UI)
openwisp_radius/api/views.py (API logic)
openwisp_radius/base/models.py (model changes)
openwisp_radius/migrations/0038_clean_fallbackfields.py (migration fix)
openwisp_radius/migrations/0043_organizationradiussettings_cross_organization_registration_enabled.py (new migration)
openwisp_radius/settings.py (new setting)
openwisp_radius/tests/test_api/test_rest_token.py (tests)
openwisp_radius/tests/test_api/test_utils.py (tests)
setup.py (dependencies)
Implementation Plan for Proposed Solution
-
Phase 1: Database Schema
- Modify
AbstractRegisteredUser model
- Create migration to:
- Add
id field as primary key
- Change
user from OneToOneField to ForeignKey
- Add
organization field
- Add unique constraint on
(user, organization)
- Migrate existing data (set
organization=None for all current records)
-
Phase 2: API Updates
- Update
IDVerificationHelper.is_identity_verified_strong() to accept organization
- Update
ObtainAuthTokenView.validate_membership() to check org-specific verification
- Update registration serializers to create org-specific records
- Add helper method
RegisteredUser.get_for_user_and_org()
-
Phase 3: Monitoring Integration
- Update
write_user_registration_metrics() in monitoring/tasks.py
- Update
post_save_radiusaccounting() to get correct method for org
- Ensure metrics handle multiple records per user correctly
-
Phase 4: Admin Updates
- Update
RegisteredUserInline to show org-specific records
- Update
RegisteredUserFilter to filter by organization
- Add organization column to user admin
-
Phase 5: Testing
- Unit tests for new model methods
- Integration tests for cross-org registration flows
- Tests for monitoring metrics with multi-org users
- Backward compatibility tests
-
Phase 6: Documentation
- Update API documentation
- Update admin documentation
- Migration guide for existing installations
- Best practices for multi-org setups
Use Cases Supported
-
Single Organization (Current Behavior)
- One
RegisteredUser record with organization=None
- All existing functionality unchanged
- Zero migration friction
-
Multiple Organizations, Same Requirements
- One
RegisteredUser record with organization=None
- Applies to all organizations
- Efficient and simple
-
Multiple Organizations, Different Requirements
- Global record for common orgs
- Org-specific records for special orgs
- User completes verification per org as needed
-
Premium/Paid Organizations
- Integration with openwisp-subscription
- Org-specific registration metadata
- Different pricing tiers per organization
Security Considerations
- Verification Isolation: Each organization's verification status is independent
- No Bypass: Users cannot use verification from Org B to access Org A
- Audit Trail: Each
RegisteredUser record tracks when/where verification occurred
- Granular Control: Organizations control their own verification requirements
- Privacy: Users choose what data to share with each organization
Migration Path
For existing installations:
- All current
RegisteredUser records will have organization=None
- Existing behavior continues unchanged
- Organizations can gradually adopt org-specific verification
- No breaking changes to existing APIs
Is your feature request related to a problem? Please describe.
Yes, this is a security issue related to identity verification across different organizations.
The Security Problem:
Consider two organizations:
Current Behavior:
Why This Happens:
The current system assumes all organizations are managed by the same administrators who enforce consistent rules. However, this assumption doesn't hold when:
Why the Currently Proposed Alternative is Insufficient:
The patch in #674 introduces a
cross_organization_login_enabledsetting that prevents users from joining organizations with different identity verification requirements.
While this prevents the security issue, it creates a new problem:
Describe the solution you'd like
Proposed Architecture: Per-Organization RegisteredUser Records
Core Concept:
Change the
RegisteredUsermodel to support multiple records per user, with one record per organization. This allows each organization to have its own verification status, method, and requirements for the same user.Technical Changes:
Database Schema Changes:
Key Changes:
OneToOneFieldtoForeignKeyfor user relationshiporganizationfield (nullable for backward compatibility)unique_togetherconstraint on(user, organization)organization=None, the record applies globally (backward compatible)API Changes:
RegisteredUserrecord for that orgRegisteredUserrecord specific to that organizationIDVerificationHelperto check org-specific verification status:Registration Flow:
RegisteredUserrecord exists for this orgRegisteredUserrecord upon completionMonitoring Integration Updates:
write_user_registration_metricsto handle multipleRegisteredUserrecords per userpost_save_radiusaccountingto get the correct registration method for the orgBackward Compatibility:
organization=Nonecontinue working as beforeorganization=NoneBenefits:
Describe alternatives you've considered
Alternative 1: Current Branch Approach (Block Cross-Org Login)
Description: Use the
cross_organization_login_enabledsetting to prevent users from joining organizations with different requirements.Problems:
Verdict: Rejected - too restrictive and impractical
Alternative 2: Global Identity Verification
Description: Require all users to complete the highest level of verification required by any organization they might join.
Problems:
Verdict: Rejected - too invasive and inflexible
Alternative 3: Organization-Specific User Accounts
Description: Create separate user accounts for each organization (like multi-tenant SaaS).
Problems:
Verdict: Rejected - duplicates data and creates management overhead
Alternative 4: Verification Requirements Inheritance
Description: Organizations can inherit verification from other organizations.
Problems:
Verdict: Rejected - adds unnecessary complexity
Additional context
Current Implementation Analysis
The current branch adds these files/changes:
cross_organization_login_enabledfield toOrganizationRadiusSettingsObtainAuthTokenView.validate_membership()to check the new settingFiles Modified in Current Branch:
.github/workflows/backport.yml(removed)docs/images/organization_cross_login.png(new)docs/user/rest-api.rst(documentation)docs/user/settings.rst(documentation)openwisp_radius/admin.py(admin UI)openwisp_radius/api/views.py(API logic)openwisp_radius/base/models.py(model changes)openwisp_radius/migrations/0038_clean_fallbackfields.py(migration fix)openwisp_radius/migrations/0043_organizationradiussettings_cross_organization_registration_enabled.py(new migration)openwisp_radius/settings.py(new setting)openwisp_radius/tests/test_api/test_rest_token.py(tests)openwisp_radius/tests/test_api/test_utils.py(tests)setup.py(dependencies)Implementation Plan for Proposed Solution
Phase 1: Database Schema
AbstractRegisteredUsermodelidfield as primary keyuserfrom OneToOneField to ForeignKeyorganizationfield(user, organization)organization=Nonefor all current records)Phase 2: API Updates
IDVerificationHelper.is_identity_verified_strong()to accept organizationObtainAuthTokenView.validate_membership()to check org-specific verificationRegisteredUser.get_for_user_and_org()Phase 3: Monitoring Integration
write_user_registration_metrics()in monitoring/tasks.pypost_save_radiusaccounting()to get correct method for orgPhase 4: Admin Updates
RegisteredUserInlineto show org-specific recordsRegisteredUserFilterto filter by organizationPhase 5: Testing
Phase 6: Documentation
Use Cases Supported
Single Organization (Current Behavior)
RegisteredUserrecord withorganization=NoneMultiple Organizations, Same Requirements
RegisteredUserrecord withorganization=NoneMultiple Organizations, Different Requirements
Premium/Paid Organizations
Security Considerations
RegisteredUserrecord tracks when/where verification occurredMigration Path
For existing installations:
RegisteredUserrecords will haveorganization=None