refactor(rbac): Complete RBAC migration - 154/154 admin routes protected
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
- Add @role_required to 2 missing routes (krs_api PDF download, zopk milestones) - Add role-based menu visibility in admin bar (hide Users, Security, Benefits, Model Comparison, Debug from OFFICE_MANAGER users) - Inject SystemRole into Jinja2 context processor for template role checks - Replace is_admin checkbox with role select dropdown in user creation form - Migrate routes.py and routes_users_api.py from is_admin to SystemRole-based role assignment via set_role() - Add deprecation notice to is_admin database column - Add 23 RBAC unit tests (hierarchy, has_role, set_role, permissions) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c0d60481f0
commit
f2fc1b89ec
1
app.py
1
app.py
@ -340,6 +340,7 @@ def inject_globals():
|
||||
'COMPANY_COUNT': COMPANY_COUNT_MARKETING, # Liczba podmiotów (cel marketingowy)
|
||||
'is_staging': is_staging,
|
||||
'staging_features': STAGING_TEST_FEATURES if is_staging else {},
|
||||
'SystemRole': SystemRole,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -208,8 +208,11 @@ def admin_user_add():
|
||||
)
|
||||
|
||||
db.add(new_user)
|
||||
if data.get('is_admin', False):
|
||||
new_user.set_role(SystemRole.ADMIN)
|
||||
role_name = data.get('role', 'MEMBER')
|
||||
try:
|
||||
new_user.set_role(SystemRole[role_name])
|
||||
except KeyError:
|
||||
new_user.set_role(SystemRole.MEMBER)
|
||||
db.commit()
|
||||
db.refresh(new_user)
|
||||
|
||||
@ -255,7 +258,7 @@ def admin_user_toggle_admin(user_id):
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'is_admin': is_now_admin,
|
||||
'is_admin': is_now_admin, # Backward compat for frontend
|
||||
'role': user.role,
|
||||
'message': f"{'Nadano' if is_now_admin else 'Odebrano'} uprawnienia admina"
|
||||
})
|
||||
|
||||
@ -440,6 +440,7 @@ def api_krs_audit_batch():
|
||||
|
||||
@bp.route('/krs-api/pdf/<int:company_id>')
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def api_krs_pdf_download(company_id):
|
||||
"""
|
||||
API: Download/serve KRS PDF file for a company.
|
||||
|
||||
@ -271,10 +271,12 @@ def admin_users_bulk_create():
|
||||
is_active=True
|
||||
)
|
||||
db.add(new_user)
|
||||
# Set role based on AI parse result (supports both old is_admin and new role field)
|
||||
# Set role based on AI parse result
|
||||
ai_role = user_data.get('role', 'MEMBER')
|
||||
if ai_role == 'ADMIN' or user_data.get('is_admin', False):
|
||||
new_user.set_role(SystemRole.ADMIN)
|
||||
try:
|
||||
new_user.set_role(SystemRole[ai_role])
|
||||
except KeyError:
|
||||
new_user.set_role(SystemRole.MEMBER)
|
||||
db.flush() # Get the ID
|
||||
|
||||
created.append({
|
||||
@ -336,10 +338,7 @@ def admin_users_change_role():
|
||||
return jsonify({'success': False, 'error': 'Nie możesz odebrać sobie uprawnień administratora'}), 400
|
||||
|
||||
old_role = user.role
|
||||
user.role = new_role
|
||||
|
||||
# Sync is_admin flag
|
||||
user.is_admin = (new_role == 'ADMIN')
|
||||
user.set_role(SystemRole[new_role])
|
||||
|
||||
# Update company_role based on new role
|
||||
if new_role in ['MANAGER']:
|
||||
|
||||
@ -32,6 +32,7 @@ def admin_zopk_timeline():
|
||||
|
||||
@bp.route('/zopk-api/milestones')
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def api_zopk_milestones():
|
||||
"""API - lista kamieni milowych ZOPK."""
|
||||
db = SessionLocal()
|
||||
|
||||
@ -283,10 +283,10 @@ class User(Base, UserMixin):
|
||||
# Role within assigned company (if any)
|
||||
company_role = Column(String(20), default='NONE', nullable=False)
|
||||
|
||||
# Status (is_admin kept for backward compatibility, synced with role)
|
||||
# Status
|
||||
is_active = Column(Boolean, default=True)
|
||||
is_verified = Column(Boolean, default=False)
|
||||
is_admin = Column(Boolean, default=False) # Deprecated: use role == ADMIN instead
|
||||
is_admin = Column(Boolean, default=False) # DEPRECATED: synced by set_role() for backward compat. Use has_role(SystemRole.ADMIN) instead. Will be removed in future migration.
|
||||
is_norda_member = Column(Boolean, default=False)
|
||||
is_rada_member = Column(Boolean, default=False) # Member of Rada Izby (Board Council)
|
||||
|
||||
|
||||
@ -1274,11 +1274,17 @@
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" style="display: flex; gap: var(--spacing-lg);">
|
||||
<label style="display: flex; align-items: center; gap: var(--spacing-xs); cursor: pointer;">
|
||||
<input type="checkbox" id="addUserAdmin">
|
||||
Administrator
|
||||
</label>
|
||||
<div class="form-group" style="display: flex; gap: var(--spacing-lg); align-items: flex-end;">
|
||||
<div>
|
||||
<label>Rola</label>
|
||||
<select id="addUserRole" class="form-control" style="min-width: 160px;">
|
||||
<option value="MEMBER">Członek</option>
|
||||
<option value="EMPLOYEE">Pracownik</option>
|
||||
<option value="MANAGER">Kadra Zarządzająca</option>
|
||||
<option value="OFFICE_MANAGER">Kierownik Biura</option>
|
||||
<option value="ADMIN">Administrator</option>
|
||||
</select>
|
||||
</div>
|
||||
<label style="display: flex; align-items: center; gap: var(--spacing-xs); cursor: pointer;">
|
||||
<input type="checkbox" id="addUserVerified" checked>
|
||||
Zweryfikowany
|
||||
@ -1881,7 +1887,7 @@ Lub format CSV, Excel, lista emaili..."></textarea>
|
||||
document.getElementById('addUserEmail').value = '';
|
||||
document.getElementById('addUserName').value = '';
|
||||
document.getElementById('addUserCompany').value = '';
|
||||
document.getElementById('addUserAdmin').checked = false;
|
||||
document.getElementById('addUserRole').value = 'MEMBER';
|
||||
document.getElementById('addUserVerified').checked = true;
|
||||
document.getElementById('addUserModal').classList.add('active');
|
||||
}
|
||||
@ -1894,7 +1900,7 @@ Lub format CSV, Excel, lista emaili..."></textarea>
|
||||
const email = document.getElementById('addUserEmail').value.trim();
|
||||
const name = document.getElementById('addUserName').value.trim();
|
||||
const companyId = document.getElementById('addUserCompany').value || null;
|
||||
const isAdmin = document.getElementById('addUserAdmin').checked;
|
||||
const role = document.getElementById('addUserRole').value;
|
||||
const isVerified = document.getElementById('addUserVerified').checked;
|
||||
|
||||
if (!email) {
|
||||
@ -1919,7 +1925,7 @@ Lub format CSV, Excel, lista emaili..."></textarea>
|
||||
email: email,
|
||||
name: name || null,
|
||||
company_id: companyId ? parseInt(companyId) : null,
|
||||
is_admin: isAdmin,
|
||||
role: role,
|
||||
is_verified: isVerified
|
||||
})
|
||||
});
|
||||
|
||||
@ -1408,12 +1408,14 @@
|
||||
</svg>
|
||||
Firmy
|
||||
</a>
|
||||
{% if current_user.has_role(SystemRole.ADMIN) %}
|
||||
<a href="{{ url_for('admin.admin_users') }}">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"/>
|
||||
</svg>
|
||||
Użytkownicy
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('admin.admin_membership') }}">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z"/>
|
||||
@ -1426,12 +1428,14 @@
|
||||
</svg>
|
||||
Składki
|
||||
</a>
|
||||
{% if current_user.has_role(SystemRole.ADMIN) %}
|
||||
<a href="{{ url_for('admin.admin_benefits') }}">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v13m0-13V6a2 2 0 112 2h-2zm0 0V5.5A2.5 2.5 0 109.5 8H12zm-7 4h14M5 12a2 2 0 110-4h14a2 2 0 110 4M5 12v7a2 2 0 002 2h10a2 2 0 002-2v-7"/>
|
||||
</svg>
|
||||
Korzyści
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('admin.admin_recommendations') }}">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z"/>
|
||||
@ -1474,12 +1478,14 @@
|
||||
</svg>
|
||||
ZOP Kaszubia
|
||||
</a>
|
||||
{% if current_user.has_role(SystemRole.ADMIN) %}
|
||||
<a href="{{ url_for('admin.admin_security') }}">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
|
||||
</svg>
|
||||
Bezpieczeństwo
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('admin.admin_status') }}">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
|
||||
@ -1565,6 +1571,7 @@
|
||||
</svg>
|
||||
Monitoring AI
|
||||
</a>
|
||||
{% if current_user.has_role(SystemRole.ADMIN) %}
|
||||
<a href="{{ url_for('admin.admin_model_comparison') }}">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2"/>
|
||||
@ -1577,6 +1584,7 @@
|
||||
</svg>
|
||||
Debug Panel
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
226
tests/unit/test_rbac.py
Normal file
226
tests/unit/test_rbac.py
Normal file
@ -0,0 +1,226 @@
|
||||
"""
|
||||
RBAC Unit Tests
|
||||
===============
|
||||
|
||||
Tests for the Role-Based Access Control system:
|
||||
- SystemRole hierarchy
|
||||
- has_role() method
|
||||
- set_role() with is_admin sync
|
||||
- Permission helper methods (can_access_admin_panel, can_manage_users, etc.)
|
||||
- role_required decorator
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
# Add project root to path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
|
||||
|
||||
from database import SystemRole, CompanyRole, User
|
||||
|
||||
|
||||
# ============================================================
|
||||
# SystemRole Hierarchy Tests
|
||||
# ============================================================
|
||||
|
||||
class TestSystemRoleHierarchy:
|
||||
"""Test that SystemRole enum values enforce correct hierarchy."""
|
||||
|
||||
def test_role_ordering(self):
|
||||
assert SystemRole.UNAFFILIATED < SystemRole.MEMBER
|
||||
assert SystemRole.MEMBER < SystemRole.EMPLOYEE
|
||||
assert SystemRole.EMPLOYEE < SystemRole.MANAGER
|
||||
assert SystemRole.MANAGER < SystemRole.OFFICE_MANAGER
|
||||
assert SystemRole.OFFICE_MANAGER < SystemRole.ADMIN
|
||||
|
||||
def test_role_values(self):
|
||||
assert SystemRole.UNAFFILIATED == 10
|
||||
assert SystemRole.MEMBER == 20
|
||||
assert SystemRole.EMPLOYEE == 30
|
||||
assert SystemRole.MANAGER == 40
|
||||
assert SystemRole.OFFICE_MANAGER == 50
|
||||
assert SystemRole.ADMIN == 100
|
||||
|
||||
def test_from_string_valid(self):
|
||||
assert SystemRole.from_string('ADMIN') == SystemRole.ADMIN
|
||||
assert SystemRole.from_string('OFFICE_MANAGER') == SystemRole.OFFICE_MANAGER
|
||||
assert SystemRole.from_string('MEMBER') == SystemRole.MEMBER
|
||||
|
||||
def test_from_string_case_insensitive(self):
|
||||
assert SystemRole.from_string('admin') == SystemRole.ADMIN
|
||||
assert SystemRole.from_string('member') == SystemRole.MEMBER
|
||||
|
||||
def test_from_string_invalid_defaults_to_unaffiliated(self):
|
||||
assert SystemRole.from_string('INVALID') == SystemRole.UNAFFILIATED
|
||||
assert SystemRole.from_string('') == SystemRole.UNAFFILIATED
|
||||
|
||||
def test_enum_by_name(self):
|
||||
assert SystemRole['ADMIN'] == SystemRole.ADMIN
|
||||
assert SystemRole['MEMBER'] == SystemRole.MEMBER
|
||||
with pytest.raises(KeyError):
|
||||
SystemRole['INVALID']
|
||||
|
||||
|
||||
# ============================================================
|
||||
# User.has_role() Tests
|
||||
# ============================================================
|
||||
|
||||
def _make_user(role_name='MEMBER', is_admin=False):
|
||||
"""Create a fake user object with User methods for testing RBAC logic.
|
||||
|
||||
Cannot use User() directly since SA instrumented attributes require
|
||||
a mapped session. Instead, use a plain object with User's methods bound.
|
||||
"""
|
||||
class FakeUser:
|
||||
pass
|
||||
|
||||
user = FakeUser()
|
||||
user.role = role_name
|
||||
user.is_admin = is_admin
|
||||
user.company_role = 'NONE'
|
||||
user.company_id = None
|
||||
|
||||
# Bind User's property and methods
|
||||
user.system_role = User.system_role.fget(user)
|
||||
user.has_role = lambda required_role: User.has_role(user, required_role)
|
||||
user.can_access_admin_panel = lambda: User.can_access_admin_panel(user)
|
||||
user.can_manage_users = lambda: User.can_manage_users(user)
|
||||
user.can_moderate_forum = lambda: User.can_moderate_forum(user)
|
||||
return user
|
||||
|
||||
|
||||
class TestHasRole:
|
||||
"""Test User.has_role() hierarchical check."""
|
||||
|
||||
def test_admin_has_all_roles(self):
|
||||
user = _make_user('ADMIN')
|
||||
assert user.has_role(SystemRole.ADMIN)
|
||||
assert user.has_role(SystemRole.OFFICE_MANAGER)
|
||||
assert user.has_role(SystemRole.MANAGER)
|
||||
assert user.has_role(SystemRole.EMPLOYEE)
|
||||
assert user.has_role(SystemRole.MEMBER)
|
||||
assert user.has_role(SystemRole.UNAFFILIATED)
|
||||
|
||||
def test_office_manager_cannot_access_admin(self):
|
||||
user = _make_user('OFFICE_MANAGER')
|
||||
assert not user.has_role(SystemRole.ADMIN)
|
||||
assert user.has_role(SystemRole.OFFICE_MANAGER)
|
||||
assert user.has_role(SystemRole.MANAGER)
|
||||
assert user.has_role(SystemRole.MEMBER)
|
||||
|
||||
def test_member_minimal_access(self):
|
||||
user = _make_user('MEMBER')
|
||||
assert not user.has_role(SystemRole.ADMIN)
|
||||
assert not user.has_role(SystemRole.OFFICE_MANAGER)
|
||||
assert not user.has_role(SystemRole.MANAGER)
|
||||
assert not user.has_role(SystemRole.EMPLOYEE)
|
||||
assert user.has_role(SystemRole.MEMBER)
|
||||
assert user.has_role(SystemRole.UNAFFILIATED)
|
||||
|
||||
def test_unaffiliated_lowest_access(self):
|
||||
user = _make_user('UNAFFILIATED')
|
||||
assert not user.has_role(SystemRole.MEMBER)
|
||||
assert user.has_role(SystemRole.UNAFFILIATED)
|
||||
|
||||
def test_none_role_defaults_to_unaffiliated(self):
|
||||
user = _make_user(None)
|
||||
assert user.has_role(SystemRole.UNAFFILIATED)
|
||||
assert not user.has_role(SystemRole.MEMBER)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# User.set_role() Tests
|
||||
# ============================================================
|
||||
|
||||
class TestSetRole:
|
||||
"""Test User.set_role() with is_admin sync.
|
||||
|
||||
Uses a simple namespace object to test set_role logic directly,
|
||||
since SQLAlchemy instrumented attributes don't work outside a session.
|
||||
"""
|
||||
|
||||
def _make_settable_user(self, role_name='MEMBER', is_admin=False):
|
||||
"""Create an object that can receive set_role() assignments."""
|
||||
class FakeUser:
|
||||
pass
|
||||
user = FakeUser()
|
||||
user.role = role_name
|
||||
user.is_admin = is_admin
|
||||
# Bind set_role method from User class
|
||||
user.set_role = lambda new_role, sync_is_admin=True: User.set_role(user, new_role, sync_is_admin)
|
||||
return user
|
||||
|
||||
def test_set_admin_syncs_is_admin_true(self):
|
||||
user = self._make_settable_user('MEMBER', is_admin=False)
|
||||
user.set_role(SystemRole.ADMIN)
|
||||
assert user.role == 'ADMIN'
|
||||
assert user.is_admin is True
|
||||
|
||||
def test_set_member_syncs_is_admin_false(self):
|
||||
user = self._make_settable_user('ADMIN', is_admin=True)
|
||||
user.set_role(SystemRole.MEMBER)
|
||||
assert user.role == 'MEMBER'
|
||||
assert user.is_admin is False
|
||||
|
||||
def test_set_office_manager_is_admin_false(self):
|
||||
user = self._make_settable_user('ADMIN', is_admin=True)
|
||||
user.set_role(SystemRole.OFFICE_MANAGER)
|
||||
assert user.role == 'OFFICE_MANAGER'
|
||||
assert user.is_admin is False
|
||||
|
||||
def test_set_role_without_sync(self):
|
||||
user = self._make_settable_user('MEMBER', is_admin=False)
|
||||
user.set_role(SystemRole.ADMIN, sync_is_admin=False)
|
||||
assert user.role == 'ADMIN'
|
||||
assert user.is_admin is False # Not synced
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Permission Helper Methods Tests
|
||||
# ============================================================
|
||||
|
||||
class TestPermissionHelpers:
|
||||
"""Test can_access_admin_panel, can_manage_users, can_moderate_forum."""
|
||||
|
||||
def test_admin_can_access_admin_panel(self):
|
||||
assert _make_user('ADMIN').can_access_admin_panel()
|
||||
|
||||
def test_office_manager_can_access_admin_panel(self):
|
||||
assert _make_user('OFFICE_MANAGER').can_access_admin_panel()
|
||||
|
||||
def test_manager_cannot_access_admin_panel(self):
|
||||
assert not _make_user('MANAGER').can_access_admin_panel()
|
||||
|
||||
def test_member_cannot_access_admin_panel(self):
|
||||
assert not _make_user('MEMBER').can_access_admin_panel()
|
||||
|
||||
def test_only_admin_can_manage_users(self):
|
||||
assert _make_user('ADMIN').can_manage_users()
|
||||
assert not _make_user('OFFICE_MANAGER').can_manage_users()
|
||||
assert not _make_user('MANAGER').can_manage_users()
|
||||
|
||||
def test_office_manager_can_moderate_forum(self):
|
||||
assert _make_user('ADMIN').can_moderate_forum()
|
||||
assert _make_user('OFFICE_MANAGER').can_moderate_forum()
|
||||
assert not _make_user('MANAGER').can_moderate_forum()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# CompanyRole Tests
|
||||
# ============================================================
|
||||
|
||||
class TestCompanyRole:
|
||||
"""Test CompanyRole enum and hierarchy."""
|
||||
|
||||
def test_role_ordering(self):
|
||||
assert CompanyRole.NONE < CompanyRole.VIEWER
|
||||
assert CompanyRole.VIEWER < CompanyRole.EMPLOYEE
|
||||
assert CompanyRole.EMPLOYEE < CompanyRole.MANAGER
|
||||
|
||||
def test_from_string(self):
|
||||
assert CompanyRole.from_string('MANAGER') == CompanyRole.MANAGER
|
||||
assert CompanyRole.from_string('EMPLOYEE') == CompanyRole.EMPLOYEE
|
||||
assert CompanyRole.from_string('INVALID') == CompanyRole.NONE
|
||||
Loading…
Reference in New Issue
Block a user