nordabiz/database/migrations/035_add_role_system.sql
Maciej Pienczyn ae70ad326e feat: Add hierarchical role system with delegated permissions
Implements 6-tier role hierarchy:
- ADMIN (100): Full system access
- OFFICE_MANAGER (50): Admin panel without user management
- MANAGER (40): Full company control + user management
- EMPLOYEE (30): Edit company data (with delegated permissions)
- MEMBER (20): Full content access (forum, contacts, chat)
- UNAFFILIATED (10): Public profiles only

Features:
- SystemRole and CompanyRole enums in database.py
- UserCompanyPermissions model for delegation
- New decorators: @role_required(), @company_permission()
- Auto-detection of MANAGER role from KRS data
- Backward compatible with is_admin flag

Migration: 035_add_role_system.sql

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 06:42:39 +01:00

184 lines
7.1 KiB
SQL

-- Migration: 035_add_role_system.sql
-- Description: Add hierarchical role system for granular access control
-- Author: Claude Code
-- Date: 2026-02-01
--
-- Role hierarchy:
-- UNAFFILIATED (10) - Firma spoza Izby - tylko publiczne profile
-- MEMBER (20) - Członek Norda bez firmy - pełny dostęp do treści
-- EMPLOYEE (30) - Pracownik firmy członkowskiej - edycja danych firmy
-- MANAGER (40) - Kadra zarządzająca - pełna kontrola firmy + użytkownicy
-- OFFICE_MANAGER (50) - Kierownik biura Norda - panel admina
-- ADMIN (100) - Administrator portalu - pełne prawa
--
-- Company roles:
-- NONE (0) - Brak powiązania z firmą
-- VIEWER (10) - Może przeglądać dashboard firmy
-- EMPLOYEE (20) - Może edytować dane firmy
-- MANAGER (30) - Pełna kontrola + zarządzanie użytkownikami
-- ============================================================
-- STEP 1: Add new columns
-- ============================================================
-- Add role column with default UNAFFILIATED
ALTER TABLE users ADD COLUMN IF NOT EXISTS role VARCHAR(20) DEFAULT 'UNAFFILIATED' NOT NULL;
-- Add company_role column with default NONE
ALTER TABLE users ADD COLUMN IF NOT EXISTS company_role VARCHAR(20) DEFAULT 'NONE' NOT NULL;
-- Add index for role lookups
CREATE INDEX IF NOT EXISTS idx_users_role ON users(role);
-- ============================================================
-- STEP 2: Migrate existing admins
-- ============================================================
UPDATE users
SET role = 'ADMIN'
WHERE is_admin = TRUE AND (role IS NULL OR role = 'UNAFFILIATED');
-- ============================================================
-- STEP 3: Auto-detect MANAGER role from KRS data
-- Users linked to company management (zarząd) get MANAGER role
-- ============================================================
UPDATE users u
SET
role = 'MANAGER',
company_role = 'MANAGER'
WHERE
u.company_id IS NOT NULL
AND u.role NOT IN ('ADMIN', 'OFFICE_MANAGER')
AND EXISTS (
SELECT 1 FROM company_people cp
WHERE cp.person_id = u.person_id
AND cp.company_id = u.company_id
AND cp.role_category = 'zarzad'
);
-- ============================================================
-- STEP 4: Set EMPLOYEE role for users with company but no KRS link
-- ============================================================
UPDATE users
SET
role = 'EMPLOYEE',
company_role = 'EMPLOYEE'
WHERE
company_id IS NOT NULL
AND role NOT IN ('ADMIN', 'OFFICE_MANAGER', 'MANAGER');
-- ============================================================
-- STEP 5: Set MEMBER role for Norda members without company
-- ============================================================
UPDATE users
SET role = 'MEMBER'
WHERE
is_norda_member = TRUE
AND company_id IS NULL
AND role = 'UNAFFILIATED';
-- ============================================================
-- STEP 6: Create user_company_permissions table for delegation
-- ============================================================
CREATE TABLE IF NOT EXISTS user_company_permissions (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
company_id INTEGER NOT NULL REFERENCES companies(id) ON DELETE CASCADE,
-- Content editing permissions
can_edit_description BOOLEAN DEFAULT TRUE, -- Opis firmy, historia, wartości
can_edit_services BOOLEAN DEFAULT TRUE, -- Usługi, kompetencje, technologie
can_edit_contacts BOOLEAN DEFAULT FALSE, -- Email, telefon, adres
can_edit_social BOOLEAN DEFAULT FALSE, -- Social media, strona www
-- Feature permissions
can_manage_classifieds BOOLEAN DEFAULT TRUE, -- B2B ogłoszenia w imieniu firmy
can_post_forum BOOLEAN DEFAULT TRUE, -- Posty na forum w imieniu firmy
can_view_analytics BOOLEAN DEFAULT FALSE, -- Statystyki firmy, wyświetlenia
-- Audit trail
granted_by_id INTEGER REFERENCES users(id),
granted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- Unique constraint: one permission record per user-company pair
CONSTRAINT uq_user_company_permissions UNIQUE (user_id, company_id)
);
-- Indexes for lookups
CREATE INDEX IF NOT EXISTS idx_user_company_permissions_user ON user_company_permissions(user_id);
CREATE INDEX IF NOT EXISTS idx_user_company_permissions_company ON user_company_permissions(company_id);
-- Grant permissions to application user
GRANT ALL ON TABLE user_company_permissions TO nordabiz_app;
GRANT USAGE, SELECT ON SEQUENCE user_company_permissions_id_seq TO nordabiz_app;
-- ============================================================
-- STEP 7: Create default permissions for existing EMPLOYEE users
-- ============================================================
INSERT INTO user_company_permissions (user_id, company_id, can_edit_description, can_edit_services)
SELECT id, company_id, TRUE, TRUE
FROM users
WHERE role = 'EMPLOYEE' AND company_id IS NOT NULL
ON CONFLICT (user_id, company_id) DO NOTHING;
-- ============================================================
-- STEP 8: Verify migration results
-- ============================================================
-- Log migration statistics (view in PostgreSQL logs)
DO $$
DECLARE
admin_count INTEGER;
office_mgr_count INTEGER;
manager_count INTEGER;
employee_count INTEGER;
member_count INTEGER;
unaffiliated_count INTEGER;
permissions_count INTEGER;
BEGIN
SELECT COUNT(*) INTO admin_count FROM users WHERE role = 'ADMIN';
SELECT COUNT(*) INTO office_mgr_count FROM users WHERE role = 'OFFICE_MANAGER';
SELECT COUNT(*) INTO manager_count FROM users WHERE role = 'MANAGER';
SELECT COUNT(*) INTO employee_count FROM users WHERE role = 'EMPLOYEE';
SELECT COUNT(*) INTO member_count FROM users WHERE role = 'MEMBER';
SELECT COUNT(*) INTO unaffiliated_count FROM users WHERE role = 'UNAFFILIATED';
SELECT COUNT(*) INTO permissions_count FROM user_company_permissions;
RAISE NOTICE 'Role migration complete:';
RAISE NOTICE ' ADMIN: %', admin_count;
RAISE NOTICE ' OFFICE_MANAGER: %', office_mgr_count;
RAISE NOTICE ' MANAGER: %', manager_count;
RAISE NOTICE ' EMPLOYEE: %', employee_count;
RAISE NOTICE ' MEMBER: %', member_count;
RAISE NOTICE ' UNAFFILIATED: %', unaffiliated_count;
RAISE NOTICE ' Delegated permissions records: %', permissions_count;
END $$;
-- ============================================================
-- VERIFICATION QUERIES (run manually to verify)
-- ============================================================
-- Check for any users with NULL role (should be 0)
-- SELECT COUNT(*) FROM users WHERE role IS NULL;
-- View role distribution
-- SELECT role, COUNT(*) as count FROM users GROUP BY role ORDER BY count DESC;
-- View users with company but no company_role
-- SELECT id, email, company_id, role, company_role
-- FROM users
-- WHERE company_id IS NOT NULL AND company_role = 'NONE';
-- Check KRS-linked managers
-- SELECT u.id, u.email, u.role, u.company_role, c.name as company_name
-- FROM users u
-- JOIN companies c ON u.company_id = c.id
-- WHERE u.role = 'MANAGER'
-- LIMIT 10;