diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index e9f3a1a..dd41002 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -31,7 +31,7 @@ Package manager: **Bun** (`bun.lock`). Usar `bun`, nunca `npm`/`yarn`. ### Módulos (DDD ligero) -La lógica de negocio vive en `src/modules//` (`auth`, `config`, `projects`, `storage`), cada uno separado en: +La lógica de negocio vive en `src/modules//` (`auth`, `config`, `organization`, `projects`, `storage`), cada uno separado en: - `domain/` — entidades (`entities.ts`), interfaces de repositorio (`repositories.ts`), clases de dominio (`*.domain.ts`) - `application/` — servicios que orquestan el dominio (`*.service.ts`) — esto es lo que llaman las rutas @@ -53,11 +53,22 @@ Env vars (`.env.example`): `GITDB_REPOSITORY_URL` (obligatoria), `GITDB_ENCRYPTI ### Puerta de acceso global -`src/hooks.server.ts`: inicializa gitdb, resuelve usuario desde la cookie `pos_session`, redirige a `/login` si no hay sesión, y restringe `/settings/*` + `/api/system/*` a admins. +`src/hooks.server.ts`: inicializa gitdb, resuelve usuario desde la cookie `pos_session`, redirige a `/login` si no hay sesión, valida permisos de proyecto/organización en rutas `/org/*/projects/*/settings` y `/org/*/settings`, y restringe `/settings/*` + `/api/system/*` a admins. ## RBAC / Permisos -Permisos = strings `section:action` (`vault|openreport|stateiac` : `read|create|update|delete`) o el atajo `section:all`, definidos en `src/lib/permissions/index.ts`. Roles en gitdb (`.gitdb/roles.json`), gestionados por `roleService`. +Permisos = strings con formato global (`vault:read`), organización (`organization:projects:all`) o proyecto (`project:vault:secrets:read`), definidos en `src/lib/permissions/index.ts`. Roles en gitdb (`.gitdb/roles.json`), gestionados por `roleService`. + +**Roles de organización** (`scope: 'organization'`) se crean automáticamente al crear organización (`createDefaultOrganizationRoles`): +- `org-admin`: permisos completos (`organization:projects:all`, `organization:users:all`, etc.) +- `org-developer`: lectura/creación/actualización de proyectos (`organization:projects:read|create|update`) + +**Roles de proyecto** (`scope: 'project'`) se crean automáticamente al crear proyecto (`createDefaultProjectRoles`): +- `project-admin`: permisos completos a nivel de proyecto +- `project-developer`: lectura/creación/actualización de vault/state/code report (sin delete ni gestión de roles) +- `project-viewer`: solo lectura en todos los recursos del proyecto + +**Control de acceso a organizaciones**: `cancanService.organizationIdsForUser(user)` devuelve los IDs de organizaciones a las que el usuario puede acceder (directo o a través de proyectos), o `null` para admins sin restricción. Se usa en rutas como `/org` para filtrar contenido. ```typescript // ✅ patrón correcto en un endpoint (api/roles, api/projects, api/backends) diff --git a/CLAUDE.md b/CLAUDE.md index 2cb1c85..e49ea5d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -50,7 +50,7 @@ La lógica de negocio vive en `src/modules//`, cada uno separado en: | `infrastructure/repositories/` | Implementaciones concretas de repositorio | | `index.ts` | **Composition root** del módulo: instancia repositorios + servicios y exporta singletons | -Módulos existentes: `auth`, `config`, `projects`, `storage`. +Módulos existentes: `auth`, `config`, `organization`, `projects`, `storage`. **Regla de acceso**: las rutas y otros módulos importan únicamente desde el `index.ts` de cada módulo (p. ej. `import { can, isAdmin, roleService } from '../../modules/auth'`), nunca de `application/`, `domain/` o `infrastructure/` directamente. @@ -79,19 +79,33 @@ Variables de entorno (`.env.example`): `GITDB_REPOSITORY_URL` (obligatoria, lanz 1. Inicializa gitdb (`getGitDb()` a nivel de módulo, una sola vez). 2. Deja pasar `/login` y `/api/auth/*` sin sesión. 3. Resuelve el usuario desde la cookie `pos_session` (`authService.resolveAuthenticatedUser`); si no hay usuario válido, redirige (302) a `/login`. -4. Restringe `/settings/*` y `/api/system/*` a admins (`canAccessAdminArea`). +4. Valida permisos de proyecto para rutas `/org/[org]/projects/[slug]/settings` (`canManageProject`); si no tiene acceso, redirige a `/`. +5. Restringe `/org/[org]/settings` a usuarios con permiso de organización (`canManageOrganization`); si no, redirige a `/`. +6. Restringe `/settings/*` y `/api/system/*` a admins (`canAccessAdminArea`). --- ## RBAC / Permisos -Los permisos son strings `section:action` (p. ej. `vault:read`) o el atajo `section:all`, definidos en `src/lib/permissions/index.ts`: +Los permisos son strings `section:action` (p. ej. `vault:read`) o el atajo `section:all`, definidos en `src/lib/permissions/index.ts`. El formato soporta dos niveles de scope: -- `section` ∈ `vault | openreport | stateiac` -- `action` ∈ `read | create | update | delete` +- **Global**: `section:action` donde `section` ∈ `vault | openreport | stateiac` +- **Organización**: `organization:action` (p. ej. `organization:projects:read`, `organization:users:all`) +- **Proyecto**: `project:action` (p. ej. `project:vault:secrets:read`, `project:stateiac:stacks:all`) + +Roles a nivel de organización (`scope: 'organization'`) se crean automáticamente al crear una organización (método `RoleService.createDefaultOrganizationRoles`): +- `org-admin`: permisos completos a nivel de organización (`organization:projects:all`, `organization:users:all`, `organization:roles:all`, etc.) +- `org-developer`: lectura, creación y actualización de proyectos (`organization:projects:read|create|update`) + +Roles a nivel de proyecto (`scope: 'project'`) se crean automáticamente al crear un proyecto (método `RoleService.createDefaultProjectRoles`): +- `project-admin`: permisos completos a nivel de proyecto (vault, state/IaC, code report, usuarios, roles, auditoría) +- `project-developer`: permisos de lectura, creación y actualización para vault/state/code report (sin permisos de eliminación ni gestión de roles) +- `project-viewer`: permisos de lectura exclusivamente en todos los recursos del proyecto Los roles (y su array de `permissions`) viven en gitdb (`.gitdb/roles.json`), gestionados por `roleService`. El rol `admin` es especial: no se puede borrar (`RoleService.deleteRole` lo bloquea explícitamente) y `isAdmin(user)` comprueba `user.role.slug === 'admin'`. +**Control de acceso a organizaciones**: `CanCanService.organizationIdsForUser(user)` devuelve los IDs de organizaciones a las que un usuario tiene acceso — directo (vía rol de organización) o indirecto (vía rol en un proyecto). Devuelve `null` para admins de cluster (sin restricción). Se usa en rutas como `/org` para filtrar qué organizaciones ve el usuario. + **Patrón correcto en un endpoint** (todas las rutas bajo `api/roles`, `api/projects`, `api/backends` lo siguen): ```typescript diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 3651215..e58e580 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,6 +1,7 @@ import type { Handle } from '@sveltejs/kit'; import { authService, cancanService, ensureAuthReady } from './modules/auth'; import { ensureOrganizationReady, organizationService } from './modules/organization'; +import { projectService } from './modules/projects'; import { getGitDb } from '$lib/server/gitdb'; getGitDb(); @@ -43,8 +44,20 @@ export const handle: Handle = async ({ event, resolve }) => { return new Response(null, { status: 302, headers: { location: '/login' } }); } + const projectSettingsMatch = event.url.pathname.match( + /^\/org\/[^/]+\/projects\/([^/]+)\/settings/, + ); const organizationSettingsMatch = event.url.pathname.match(/^\/org\/([^/]+)\/settings/); - if (organizationSettingsMatch) { + + if (projectSettingsMatch) { + const project = await projectService.tryFindBySlug(projectSettingsMatch[1]); + if ( + !project || + !(await cancanService.canManageProject(currentUser, project.id, project.organization?.id)) + ) { + return new Response(null, { status: 302, headers: { location: '/' } }); + } + } else if (organizationSettingsMatch) { const organization = await organizationService.tryFindBySlug(organizationSettingsMatch[1]); if ( !organization || diff --git a/src/lib/components/AppNavbar.svelte b/src/lib/components/AppNavbar.svelte index 1659b32..6afdd58 100644 --- a/src/lib/components/AppNavbar.svelte +++ b/src/lib/components/AppNavbar.svelte @@ -175,7 +175,7 @@ Change Organization -
+