erp-core-database/ddl/07-users-rbac.sql
rckrdmrd 5043a640e4 refactor: Restructure DDL with numbered schema files
- Replace old DDL structure with new numbered files (01-24)
- Update migrations and seeds for new schema
- Clean up deprecated files

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 00:40:32 -06:00

566 lines
21 KiB
PL/PgSQL

-- =============================================================
-- ARCHIVO: 07-users-rbac.sql
-- DESCRIPCION: Sistema RBAC (Roles, Permisos, Invitaciones)
-- VERSION: 1.0.0
-- PROYECTO: ERP-Core V2
-- FECHA: 2026-01-10
-- EPIC: SAAS-CORE-AUTH (EPIC-SAAS-001)
-- HISTORIAS: US-004, US-005, US-006, US-030
-- =============================================================
-- =====================
-- SCHEMA: users
-- =====================
CREATE SCHEMA IF NOT EXISTS users;
-- =====================
-- TABLA: users.roles
-- Roles del sistema con herencia (US-004)
-- =====================
CREATE TABLE IF NOT EXISTS users.roles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES auth.tenants(id) ON DELETE CASCADE,
-- Info basica
name VARCHAR(100) NOT NULL,
display_name VARCHAR(255),
description TEXT,
color VARCHAR(20),
icon VARCHAR(50),
-- Jerarquia
parent_role_id UUID REFERENCES users.roles(id) ON DELETE SET NULL,
hierarchy_level INTEGER DEFAULT 0,
-- Flags
is_system BOOLEAN DEFAULT FALSE, -- No editable por usuarios
is_default BOOLEAN DEFAULT FALSE, -- Asignado a nuevos usuarios
is_superadmin BOOLEAN DEFAULT FALSE, -- Acceso total
-- Metadata
metadata JSONB DEFAULT '{}',
-- Timestamps
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
created_by UUID REFERENCES auth.users(id),
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_by UUID REFERENCES auth.users(id),
deleted_at TIMESTAMPTZ,
-- Constraint: nombre unico por tenant (o global si tenant_id es NULL)
UNIQUE NULLS NOT DISTINCT (tenant_id, name)
);
-- Indices para roles
CREATE INDEX IF NOT EXISTS idx_roles_tenant ON users.roles(tenant_id);
CREATE INDEX IF NOT EXISTS idx_roles_parent ON users.roles(parent_role_id);
CREATE INDEX IF NOT EXISTS idx_roles_system ON users.roles(is_system) WHERE is_system = TRUE;
CREATE INDEX IF NOT EXISTS idx_roles_default ON users.roles(tenant_id, is_default) WHERE is_default = TRUE;
-- =====================
-- TABLA: users.permissions
-- Permisos granulares del sistema (US-004)
-- =====================
CREATE TABLE IF NOT EXISTS users.permissions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- Identificacion
resource VARCHAR(100) NOT NULL, -- users, tenants, branches, invoices, etc.
action VARCHAR(50) NOT NULL, -- create, read, update, delete, export, etc.
scope VARCHAR(50) DEFAULT 'own', -- own, tenant, global
-- Info
display_name VARCHAR(255),
description TEXT,
category VARCHAR(100), -- auth, billing, inventory, sales, etc.
-- Flags
is_dangerous BOOLEAN DEFAULT FALSE, -- Requiere confirmacion adicional
-- Timestamps
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
UNIQUE(resource, action, scope)
);
-- Indices para permissions
CREATE INDEX IF NOT EXISTS idx_permissions_resource ON users.permissions(resource);
CREATE INDEX IF NOT EXISTS idx_permissions_category ON users.permissions(category);
-- =====================
-- TABLA: users.role_permissions
-- Asignacion de permisos a roles (US-004)
-- =====================
CREATE TABLE IF NOT EXISTS users.role_permissions (
role_id UUID NOT NULL REFERENCES users.roles(id) ON DELETE CASCADE,
permission_id UUID NOT NULL REFERENCES users.permissions(id) ON DELETE CASCADE,
-- Condiciones opcionales
conditions JSONB DEFAULT '{}', -- Condiciones adicionales (ej: solo ciertos estados)
-- Metadata
granted_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
granted_by UUID REFERENCES auth.users(id),
PRIMARY KEY (role_id, permission_id)
);
-- Indices para role_permissions
CREATE INDEX IF NOT EXISTS idx_role_permissions_role ON users.role_permissions(role_id);
CREATE INDEX IF NOT EXISTS idx_role_permissions_permission ON users.role_permissions(permission_id);
-- =====================
-- TABLA: users.user_roles
-- Asignacion de roles a usuarios (US-004)
-- =====================
CREATE TABLE IF NOT EXISTS users.user_roles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
role_id UUID NOT NULL REFERENCES users.roles(id) ON DELETE CASCADE,
-- Contexto
tenant_id UUID REFERENCES auth.tenants(id) ON DELETE CASCADE,
branch_id UUID REFERENCES core.branches(id) ON DELETE CASCADE, -- Opcional: rol por sucursal
-- Flags
is_primary BOOLEAN DEFAULT FALSE,
-- Vigencia
valid_from TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
valid_until TIMESTAMPTZ,
-- Metadata
assigned_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
assigned_by UUID REFERENCES auth.users(id),
revoked_at TIMESTAMPTZ,
revoked_by UUID REFERENCES auth.users(id),
-- Constraint: un usuario solo puede tener un rol una vez por branch (o global si branch es NULL)
UNIQUE (user_id, role_id, branch_id)
);
-- Indices para user_roles
CREATE INDEX IF NOT EXISTS idx_user_roles_user ON users.user_roles(user_id);
CREATE INDEX IF NOT EXISTS idx_user_roles_role ON users.user_roles(role_id);
CREATE INDEX IF NOT EXISTS idx_user_roles_tenant ON users.user_roles(tenant_id);
CREATE INDEX IF NOT EXISTS idx_user_roles_branch ON users.user_roles(branch_id) WHERE branch_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_user_roles_active ON users.user_roles(user_id, revoked_at) WHERE revoked_at IS NULL;
-- =====================
-- TABLA: users.invitations
-- Invitaciones de usuario (US-006)
-- =====================
CREATE TABLE IF NOT EXISTS users.invitations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
-- Destinatario
email VARCHAR(255) NOT NULL,
first_name VARCHAR(100),
last_name VARCHAR(100),
-- Token de invitacion
token_hash VARCHAR(255) NOT NULL UNIQUE,
token_expires_at TIMESTAMPTZ NOT NULL,
-- Rol a asignar
role_id UUID REFERENCES users.roles(id) ON DELETE SET NULL,
branch_id UUID REFERENCES core.branches(id) ON DELETE SET NULL,
-- Estado
status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'accepted', 'expired', 'revoked')),
-- Mensaje personalizado
message TEXT,
-- Metadata
metadata JSONB DEFAULT '{}',
-- Timestamps
invited_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
invited_by UUID NOT NULL REFERENCES auth.users(id),
accepted_at TIMESTAMPTZ,
accepted_user_id UUID REFERENCES auth.users(id),
resent_at TIMESTAMPTZ,
resent_count INTEGER DEFAULT 0,
-- Constraint: email unico por tenant mientras este pendiente
CONSTRAINT unique_pending_invitation UNIQUE (tenant_id, email, status)
);
-- Indices para invitations
CREATE INDEX IF NOT EXISTS idx_invitations_tenant ON users.invitations(tenant_id);
CREATE INDEX IF NOT EXISTS idx_invitations_email ON users.invitations(email);
CREATE INDEX IF NOT EXISTS idx_invitations_token ON users.invitations(token_hash);
CREATE INDEX IF NOT EXISTS idx_invitations_status ON users.invitations(status) WHERE status = 'pending';
CREATE INDEX IF NOT EXISTS idx_invitations_expires ON users.invitations(token_expires_at) WHERE status = 'pending';
-- =====================
-- TABLA: users.tenant_settings
-- Configuraciones por tenant (US-005)
-- =====================
CREATE TABLE IF NOT EXISTS users.tenant_settings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE UNIQUE,
-- Limites
max_users INTEGER DEFAULT 10,
max_branches INTEGER DEFAULT 5,
max_storage_mb INTEGER DEFAULT 1024,
-- Features habilitadas
features_enabled TEXT[] DEFAULT '{}',
-- Configuracion de branding
branding JSONB DEFAULT '{
"logo_url": null,
"primary_color": "#2563eb",
"secondary_color": "#64748b"
}',
-- Configuracion regional
locale VARCHAR(10) DEFAULT 'es-MX',
timezone VARCHAR(50) DEFAULT 'America/Mexico_City',
currency VARCHAR(3) DEFAULT 'MXN',
date_format VARCHAR(20) DEFAULT 'DD/MM/YYYY',
-- Configuracion de seguridad
security_settings JSONB DEFAULT '{
"require_mfa": false,
"session_timeout_minutes": 480,
"password_min_length": 8,
"password_require_special": true
}',
-- Configuracion de notificaciones
notification_settings JSONB DEFAULT '{
"email_enabled": true,
"push_enabled": true,
"sms_enabled": false
}',
-- Metadata
metadata JSONB DEFAULT '{}',
-- Timestamps
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- =====================
-- TABLA: users.profile_role_mapping
-- Mapeo de perfiles ERP a roles RBAC (US-030)
-- =====================
CREATE TABLE IF NOT EXISTS users.profile_role_mapping (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
profile_code VARCHAR(10) NOT NULL, -- ADM, CNT, VNT, ALM, etc.
role_id UUID NOT NULL REFERENCES users.roles(id) ON DELETE CASCADE,
-- Flags
is_default BOOLEAN DEFAULT TRUE,
-- Timestamps
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
UNIQUE(profile_code, role_id)
);
-- Indice para profile_role_mapping
CREATE INDEX IF NOT EXISTS idx_profile_role_mapping_profile ON users.profile_role_mapping(profile_code);
-- =====================
-- RLS POLICIES
-- =====================
ALTER TABLE users.roles ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_roles ON users.roles
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid OR tenant_id IS NULL);
ALTER TABLE users.role_permissions ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_role_permissions ON users.role_permissions
USING (role_id IN (
SELECT id FROM users.roles
WHERE tenant_id = current_setting('app.current_tenant_id', true)::uuid OR tenant_id IS NULL
));
ALTER TABLE users.user_roles ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_user_roles ON users.user_roles
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid OR tenant_id IS NULL);
ALTER TABLE users.invitations ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_invitations ON users.invitations
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
ALTER TABLE users.tenant_settings ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_settings ON users.tenant_settings
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
-- =====================
-- FUNCIONES
-- =====================
-- Funcion para verificar si un usuario tiene un permiso especifico
CREATE OR REPLACE FUNCTION users.has_permission(
p_user_id UUID,
p_resource VARCHAR(100),
p_action VARCHAR(50),
p_scope VARCHAR(50) DEFAULT 'own'
)
RETURNS BOOLEAN AS $$
DECLARE
has_perm BOOLEAN;
BEGIN
-- Verificar si es superadmin
IF EXISTS (
SELECT 1 FROM auth.users
WHERE id = p_user_id AND is_superadmin = TRUE
) THEN
RETURN TRUE;
END IF;
-- Verificar permisos via roles
SELECT EXISTS (
SELECT 1
FROM users.user_roles ur
JOIN users.role_permissions rp ON rp.role_id = ur.role_id
JOIN users.permissions p ON p.id = rp.permission_id
WHERE ur.user_id = p_user_id
AND ur.revoked_at IS NULL
AND (ur.valid_until IS NULL OR ur.valid_until > CURRENT_TIMESTAMP)
AND p.resource = p_resource
AND p.action = p_action
AND (p.scope = p_scope OR p.scope = 'global')
) INTO has_perm;
RETURN has_perm;
END;
$$ LANGUAGE plpgsql STABLE;
-- Funcion para obtener todos los permisos de un usuario
CREATE OR REPLACE FUNCTION users.get_user_permissions(p_user_id UUID)
RETURNS TABLE (
resource VARCHAR(100),
action VARCHAR(50),
scope VARCHAR(50),
conditions JSONB
) AS $$
BEGIN
-- Si es superadmin, devolver wildcard
IF EXISTS (
SELECT 1 FROM auth.users
WHERE id = p_user_id AND is_superadmin = TRUE
) THEN
RETURN QUERY
SELECT '*'::VARCHAR(100), '*'::VARCHAR(50), 'global'::VARCHAR(50), '{}'::JSONB;
RETURN;
END IF;
RETURN QUERY
SELECT DISTINCT
p.resource,
p.action,
p.scope,
rp.conditions
FROM users.user_roles ur
JOIN users.role_permissions rp ON rp.role_id = ur.role_id
JOIN users.permissions p ON p.id = rp.permission_id
WHERE ur.user_id = p_user_id
AND ur.revoked_at IS NULL
AND (ur.valid_until IS NULL OR ur.valid_until > CURRENT_TIMESTAMP);
END;
$$ LANGUAGE plpgsql STABLE;
-- Funcion para obtener permisos heredados de un rol
CREATE OR REPLACE FUNCTION users.get_role_permissions_with_inheritance(p_role_id UUID)
RETURNS TABLE (
permission_id UUID,
resource VARCHAR(100),
action VARCHAR(50),
scope VARCHAR(50),
inherited_from UUID
) AS $$
WITH RECURSIVE role_hierarchy AS (
-- Rol base
SELECT id, parent_role_id, 0 as level
FROM users.roles
WHERE id = p_role_id
UNION ALL
-- Roles padre (herencia)
SELECT r.id, r.parent_role_id, rh.level + 1
FROM users.roles r
JOIN role_hierarchy rh ON r.id = rh.parent_role_id
WHERE rh.level < 10 -- Limite de profundidad
)
SELECT
p.id as permission_id,
p.resource,
p.action,
p.scope,
rh.id as inherited_from
FROM role_hierarchy rh
JOIN users.role_permissions rp ON rp.role_id = rh.id
JOIN users.permissions p ON p.id = rp.permission_id;
$$ LANGUAGE sql STABLE;
-- Funcion para limpiar invitaciones expiradas
CREATE OR REPLACE FUNCTION users.cleanup_expired_invitations()
RETURNS INTEGER AS $$
DECLARE
updated_count INTEGER;
BEGIN
UPDATE users.invitations
SET status = 'expired'
WHERE status = 'pending'
AND token_expires_at < CURRENT_TIMESTAMP;
GET DIAGNOSTICS updated_count = ROW_COUNT;
RETURN updated_count;
END;
$$ LANGUAGE plpgsql;
-- =====================
-- TRIGGERS
-- =====================
-- Trigger para actualizar updated_at en roles
CREATE OR REPLACE FUNCTION users.update_role_timestamp()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_roles_updated_at
BEFORE UPDATE ON users.roles
FOR EACH ROW
EXECUTE FUNCTION users.update_role_timestamp();
-- Trigger para actualizar updated_at en tenant_settings
CREATE TRIGGER trg_tenant_settings_updated_at
BEFORE UPDATE ON users.tenant_settings
FOR EACH ROW
EXECUTE FUNCTION users.update_role_timestamp();
-- =====================
-- SEED DATA: Permisos Base
-- =====================
INSERT INTO users.permissions (resource, action, scope, display_name, description, category) VALUES
-- Auth
('users', 'create', 'tenant', 'Crear usuarios', 'Crear nuevos usuarios en el tenant', 'auth'),
('users', 'read', 'tenant', 'Ver usuarios', 'Ver lista de usuarios del tenant', 'auth'),
('users', 'read', 'own', 'Ver perfil propio', 'Ver su propio perfil', 'auth'),
('users', 'update', 'tenant', 'Editar usuarios', 'Editar cualquier usuario del tenant', 'auth'),
('users', 'update', 'own', 'Editar perfil propio', 'Editar su propio perfil', 'auth'),
('users', 'delete', 'tenant', 'Eliminar usuarios', 'Eliminar usuarios del tenant', 'auth'),
('roles', 'create', 'tenant', 'Crear roles', 'Crear nuevos roles', 'auth'),
('roles', 'read', 'tenant', 'Ver roles', 'Ver roles del tenant', 'auth'),
('roles', 'update', 'tenant', 'Editar roles', 'Editar roles existentes', 'auth'),
('roles', 'delete', 'tenant', 'Eliminar roles', 'Eliminar roles', 'auth'),
('invitations', 'create', 'tenant', 'Invitar usuarios', 'Enviar invitaciones', 'auth'),
('invitations', 'read', 'tenant', 'Ver invitaciones', 'Ver invitaciones pendientes', 'auth'),
('invitations', 'delete', 'tenant', 'Cancelar invitaciones', 'Revocar invitaciones', 'auth'),
-- Tenants
('tenants', 'read', 'own', 'Ver configuracion', 'Ver configuracion del tenant', 'tenants'),
('tenants', 'update', 'own', 'Editar configuracion', 'Editar configuracion del tenant', 'tenants'),
('tenant_settings', 'read', 'own', 'Ver ajustes', 'Ver ajustes del tenant', 'tenants'),
('tenant_settings', 'update', 'own', 'Editar ajustes', 'Editar ajustes del tenant', 'tenants'),
-- Branches
('branches', 'create', 'tenant', 'Crear sucursales', 'Crear nuevas sucursales', 'branches'),
('branches', 'read', 'tenant', 'Ver sucursales', 'Ver todas las sucursales', 'branches'),
('branches', 'read', 'own', 'Ver sucursal asignada', 'Ver solo su sucursal', 'branches'),
('branches', 'update', 'tenant', 'Editar sucursales', 'Editar cualquier sucursal', 'branches'),
('branches', 'delete', 'tenant', 'Eliminar sucursales', 'Eliminar sucursales', 'branches'),
-- Billing
('billing', 'read', 'tenant', 'Ver facturacion', 'Ver informacion de facturacion', 'billing'),
('billing', 'update', 'tenant', 'Gestionar facturacion', 'Cambiar plan, metodo de pago', 'billing'),
('invoices', 'read', 'tenant', 'Ver facturas', 'Ver historial de facturas', 'billing'),
('invoices', 'export', 'tenant', 'Exportar facturas', 'Descargar facturas', 'billing'),
-- Audit
('audit_logs', 'read', 'tenant', 'Ver auditoria', 'Ver logs de auditoria', 'audit'),
('audit_logs', 'export', 'tenant', 'Exportar auditoria', 'Exportar logs', 'audit'),
('activity', 'read', 'own', 'Ver mi actividad', 'Ver actividad propia', 'audit'),
-- Notifications
('notifications', 'read', 'own', 'Ver notificaciones', 'Ver notificaciones propias', 'notifications'),
('notifications', 'update', 'own', 'Gestionar notificaciones', 'Marcar como leidas', 'notifications'),
('notification_settings', 'read', 'own', 'Ver preferencias', 'Ver preferencias de notificacion', 'notifications'),
('notification_settings', 'update', 'own', 'Editar preferencias', 'Editar preferencias', 'notifications')
ON CONFLICT DO NOTHING;
-- =====================
-- SEED DATA: Roles Base del Sistema
-- =====================
INSERT INTO users.roles (id, tenant_id, name, display_name, description, is_system, is_superadmin, hierarchy_level, icon, color) VALUES
('10000000-0000-0000-0000-000000000001', NULL, 'superadmin', 'Super Administrador', 'Acceso total a la plataforma', TRUE, TRUE, 0, 'shield-check', '#dc2626'),
('10000000-0000-0000-0000-000000000002', NULL, 'admin', 'Administrador', 'Administrador del tenant', TRUE, FALSE, 1, 'shield', '#ea580c'),
('10000000-0000-0000-0000-000000000003', NULL, 'manager', 'Gerente', 'Gerente con acceso a reportes', TRUE, FALSE, 2, 'briefcase', '#0891b2'),
('10000000-0000-0000-0000-000000000004', NULL, 'user', 'Usuario', 'Usuario estandar', TRUE, FALSE, 3, 'user', '#64748b'),
('10000000-0000-0000-0000-000000000005', NULL, 'viewer', 'Visor', 'Solo lectura', TRUE, FALSE, 4, 'eye', '#94a3b8')
ON CONFLICT DO NOTHING;
-- Asignar permisos al rol Admin
INSERT INTO users.role_permissions (role_id, permission_id)
SELECT '10000000-0000-0000-0000-000000000002', id
FROM users.permissions
WHERE resource NOT IN ('audit_logs') -- Admin no tiene acceso a audit
ON CONFLICT DO NOTHING;
-- Asignar permisos al rol Manager
INSERT INTO users.role_permissions (role_id, permission_id)
SELECT '10000000-0000-0000-0000-000000000003', id
FROM users.permissions
WHERE scope = 'own'
OR (resource IN ('branches', 'users', 'invoices') AND action = 'read')
ON CONFLICT DO NOTHING;
-- Asignar permisos al rol User
INSERT INTO users.role_permissions (role_id, permission_id)
SELECT '10000000-0000-0000-0000-000000000004', id
FROM users.permissions
WHERE scope = 'own'
ON CONFLICT DO NOTHING;
-- Asignar permisos al rol Viewer
INSERT INTO users.role_permissions (role_id, permission_id)
SELECT '10000000-0000-0000-0000-000000000005', id
FROM users.permissions
WHERE action = 'read' AND scope = 'own'
ON CONFLICT DO NOTHING;
-- =====================
-- SEED DATA: Mapeo de Perfiles ERP a Roles (US-030)
-- =====================
INSERT INTO users.profile_role_mapping (profile_code, role_id) VALUES
('ADM', '10000000-0000-0000-0000-000000000002'), -- Admin
('CNT', '10000000-0000-0000-0000-000000000003'), -- Manager
('VNT', '10000000-0000-0000-0000-000000000004'), -- User
('CMP', '10000000-0000-0000-0000-000000000004'), -- User
('ALM', '10000000-0000-0000-0000-000000000004'), -- User
('HRH', '10000000-0000-0000-0000-000000000003'), -- Manager
('PRD', '10000000-0000-0000-0000-000000000004'), -- User
('EMP', '10000000-0000-0000-0000-000000000005'), -- Viewer
('GER', '10000000-0000-0000-0000-000000000003'), -- Manager
('AUD', '10000000-0000-0000-0000-000000000005') -- Viewer (read-only)
ON CONFLICT DO NOTHING;
-- =====================
-- COMENTARIOS
-- =====================
COMMENT ON TABLE users.roles IS 'Roles del sistema con soporte para herencia';
COMMENT ON TABLE users.permissions IS 'Permisos granulares (resource.action.scope)';
COMMENT ON TABLE users.role_permissions IS 'Asignacion de permisos a roles';
COMMENT ON TABLE users.user_roles IS 'Asignacion de roles a usuarios';
COMMENT ON TABLE users.invitations IS 'Invitaciones para nuevos usuarios';
COMMENT ON TABLE users.tenant_settings IS 'Configuraciones personalizadas por tenant';
COMMENT ON TABLE users.profile_role_mapping IS 'Mapeo de perfiles ERP a roles RBAC';
COMMENT ON FUNCTION users.has_permission IS 'Verifica si un usuario tiene un permiso especifico';
COMMENT ON FUNCTION users.get_user_permissions IS 'Obtiene todos los permisos de un usuario';
COMMENT ON FUNCTION users.get_role_permissions_with_inheritance IS 'Obtiene permisos de un rol incluyendo herencia';