Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@ con SvelteKit. Mantener este archivo y `CLAUDE.md` equivalentes en arquitectura

GitOps combina autenticacion y RBAC, organizaciones y proyectos, Open Report de seguridad y
visibilidad de Pulumi State. La ruta Vault es actualmente una base visual sin backend completo.
Google SSO y SAML son configuracion, no estrategias de autenticacion implementadas.

Google SSO es por organización: `/org/<org>/settings/global` almacena el OAuth client
(entidad `OrganizationAuthProviderEntity`, client secret AES-256-GCM cifrado vía `SecretCipherService`
con clave derivada de `GITDB_ENCRYPTION_KEY`, nunca devuelto a UI) y opcionalmente dominios
Workspace permitidos. El servicio `ssoService` en `$modules/auth` ejecuta Authorization Code + PKCE
(rutas `/auth/sso/google/start` y `/auth/sso/google/callback`, ambas sin sesión), verifica el ID token contra JWKS
de Google via `GoogleOidcClient` en la capa OIDC, y por defecto solo autentica usuarios que ya tienen acceso a la organización o sus proyectos. Para habilitar auto-registro (`autoProvision`), se requiere al menos un dominio Workspace permitido (configurado en settings) y permiso `organization:users:invite` para cambiar esta opción. Con auto-registro activo, cualquier cuenta de Workspace que coincida con los dominios permitidos se registra automáticamente: se crea la cuenta `cluster-user` si falta y se asigna el rol configurado (por defecto `org-developer`) vía `userAccessService.provisionOrganizationMember()`. Ese rol debe cumplir `RoleDomain.isAutoProvisionable` (solo lecturas y permisos de Org Developer, nunca de administración), comprobado al guardar y en cada alta. Las cuentas personales de Google nunca se auto-registran, porque la validación se hace contra el claim `hd` de Workspace, no contra el dominio del email. El servicio vincula el `sub` de Google al usuario en `user.authProviders[]` y acepta la invitación pendiente en el primer sign-in. Los usuarios pueden deshabilitarse via `user.disabled`. Templates de notificación (`src/notifications/`) se leen en tiempo de ejecución. SAML es configuración solo.

- SvelteKit 2, Svelte 5, Vite 8 y TypeScript 6 strict
- Tailwind CSS 4 y `@lucide/svelte`
Expand All @@ -17,6 +23,8 @@ Google SSO y SAML son configuracion, no estrategias de autenticacion implementad
- Vitest, ESLint y Prettier
- Playwright para tests e2e de RBAC (`e2e/`)

Variables de entorno: `GITDB_REPOSITORY_URL`, `GITDB_ENCRYPTION_KEY`, `GITDB_SYNC_POLL_SECONDS`. Las credenciales de Google OAuth se configuran por organización en la UI (almacenadas cifradas en la base de datos), no como variables de entorno globales.

Comandos:

```bash
Expand Down
10 changes: 10 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
version: 2
updates:
# Mantiene al día los SHA de las actions fijadas en los workflows.
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
# No se adoptan releases de menos de 7 días.
cooldown:
default-days: 7
18 changes: 9 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0

- name: Setup Bun
uses: oven-sh/setup-bun@v2
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: latest
bun-version: 1.4.3

- name: Install dependencies
run: bun install
Expand All @@ -30,12 +30,12 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0

- name: Setup Bun
uses: oven-sh/setup-bun@v2
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: latest
bun-version: 1.4.3

- name: Install dependencies
run: bun install
Expand All @@ -48,12 +48,12 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0

- name: Setup Bun
uses: oven-sh/setup-bun@v2
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: latest
bun-version: 1.4.3

- name: Install dependencies
run: bun install
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/code-report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,18 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0

- name: Setup Bun
uses: oven-sh/setup-bun@v2
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: latest
bun-version: 1.4.3

- name: Install dependencies
run: bun install

- name: Install GitOps CLI
run: bun install --global @getgitops/cli
run: bun install --global @getgitops/cli@1.2.1

- name: Run Code Report scan
env:
Expand Down
10 changes: 5 additions & 5 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,18 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0

- name: Log in to the Container Registry
uses: docker/login-action@v3
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
Expand All @@ -40,10 +40,10 @@ jobs:
type=semver,pattern={{major}}

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0

- name: Build and push Docker image
uses: docker/build-push-action@v6
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
with:
context: .
push: true
Expand Down
25 changes: 23 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,25 @@ Guidance for working on GitOps, an open source internal developer platform built

GitOps combines identity and access management, organizations and projects, Open Report security
analysis, and Pulumi State visibility. The Vault route is currently a UI foundation and does not
yet provide a complete secrets backend. Google SSO and SAML settings are configuration only; no
external authentication strategy is implemented.
yet provide a complete secrets backend. SAML settings are configuration only.

Google SSO is per organization: `/org/<org>/settings/global` stores the OAuth client
(`OrganizationAuthProviderEntity` in schemas, client secret AES-256-GCM encrypted via
`SecretCipherService` with a key derived from `GITDB_ENCRYPTION_KEY`, never returned to the UI)
and an optional list of allowed Workspace domains (checked against the ID token `hd` claim).
`/auth/login` shows "Continue with Google" next to the always-available password form for the
organization named by `?org=<slug>` (the auth guard adds it when bouncing an `/org/<slug>/...`
request), else the `last_org` cookie, else the only organization with Google enabled, if exactly one. `ssoService` in `$modules/auth` runs Authorization Code +
PKCE through `/auth/sso/google/start` and `/auth/sso/google/callback` (both routes allow unauthenticated access in `hooks.server.ts`), verifies the ID token
against Google's JWKS via `GoogleOidcClient` in the OIDC infrastructure layer, and by default only signs in people who already have access to that organization
(or one of its projects). To enable automatic sign-up (`autoProvision`), the organization must configure at least one allowed Workspace domain and enable it in
`/org/<org>/settings/global` (requires `organization:users:invite` permission to change). With automatic sign-up enabled, a verified Workspace account whose
`hd` matches an allowed domain gets an active `cluster-user` account (if missing) plus organization access with the configured organization role (default
`org-developer`) through `userAccessService.provisionOrganizationMember()`. That role must pass `RoleDomain.isAutoProvisionable`
(allow-list: read grants plus `AUTO_PROVISION_ALLOWED_ORGANIZATION_GRANTS`, never admin rights), checked on save and again on every sign-up, so a role
edited later to gain admin rights blocks automatic sign-up instead of handing them out. Personal Google accounts are never auto-signed-up, as the domain is checked against
the Workspace `hd` claim, not the email domain. It links the Google `sub` to the user found by verified email (stored in `user.authProviders[]`), and a pending
invitation is accepted on first Google sign-in. Users can be disabled via `user.disabled` to block access without deletion.

Self-service registration (`/auth/registration`) lets a visitor create a plain `cluster-user`
account with no organization membership. It is gated by a cluster-wide toggle managed at
Expand Down Expand Up @@ -36,6 +53,10 @@ The roadmap is in `IDEAS.md`. Do not describe roadmap items as implemented featu
- Vitest, ESLint, and Prettier
- Playwright for RBAC end-to-end tests (`e2e/`)

Environment variables: `GITDB_REPOSITORY_URL`, `GITDB_ENCRYPTION_KEY`, `GITDB_SYNC_POLL_SECONDS`. Google OAuth credentials are configured per organization in the UI (stored encrypted in the database), not as global environment variables.

Email notification templates (`src/notifications/`) are read from disk at runtime; the Docker image copies them so emails can be sent from the container.

```bash
bun install
bun run dev
Expand Down
2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ RUN apk add --no-cache git openssh-client && \
COPY --from=builder /app/build ./build
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
# email templates are read from disk at runtime (src/lib/server/infra/notifications/template.ts)
COPY --from=builder /app/src/notifications ./src/notifications

# Persistent data directory (SQLite DB + gitdb clone)
# VOLUME ["/app/data"]
Expand Down
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ operational data versioned and auditable in Git.

- Identity and access management for users, organizations, projects, roles, permissions,
invitations, sessions, and API keys.
- Per-organization Google sign-in (OpenID Connect) alongside local email and password login.
- Security reporting for vulnerabilities, SBOMs, secrets, licenses, and services.
- Pulumi state, history, and lock visibility for S3 and Google Cloud Storage backends.
- Git-backed, auditable application data through GitDB.
Expand Down Expand Up @@ -40,6 +41,37 @@ docker compose up --build
```

The container is available at `http://localhost:3000` and stores persistent data in `./data`.
The image ships `git` without an SSH client, so point `GITDB_REPOSITORY_URL` at an HTTPS remote
and authenticate with `GITDB_TOKEN` (or `GITDB_USERNAME`/`GITDB_PASSWORD`); with an SSH
(`git@...`) remote the container can commit locally but cannot push.

### Google sign-in

Each organization configures its own Google OAuth client:

1. In Google Cloud Console, open **APIs & Services → Credentials** and create an **OAuth client ID**
of type **Web application** (configure the consent screen as *Internal* for Workspace-only
access).
2. Under **Authorized redirect URIs** (not *JavaScript origins*), add
`https://<your-host>/auth/sso/google/callback`. For local use add the URI of the port you run
on, which must match exactly: `http://localhost:5173/auth/sso/google/callback` for
`bun run dev`, `http://localhost:3000/auth/sso/google/callback` for Docker.
3. In GitOps, open **Organization settings → Global**, enable Google SSO, paste the client ID and
secret, and optionally restrict sign-in to your Workspace domains. Leave the domain list empty
for personal `@gmail.com` accounts, which carry no Workspace domain. With an *External*
consent screen in testing mode, only the accounts listed as test users can sign in.
4. Share `https://<your-host>/auth/login?org=<organization-slug>`.

By default only users already invited to the organization can sign in with Google. To let anyone
from your company in without an invitation, set at least one allowed domain and enable
**Automatically sign up accounts from the allowed domains**: the first Google sign-in of a
`@yourdomain.com` Workspace account creates the user and grants organization access with the role
you pick (Org Developer by default). Only roles without administrative permissions can be picked
(read access and, at most, creating and editing projects); if the role is later edited to gain
admin rights, automatic sign-up stops until another role is chosen. Changing this setting also requires permission to invite
organization users. Personal Google accounts are never signed up automatically, because the domain
is checked against the Workspace `hd` claim, not the email address. The client secret is stored encrypted with a key derived from
`GITDB_ENCRYPTION_KEY`, so rotating that key requires re-entering the secret.

## Development

Expand Down
11 changes: 8 additions & 3 deletions src/hooks.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,10 @@ const authGuard: Handle = async ({ event, resolve }) => {
return new Response(null, { status: 302, headers: { location: '/bootstrap' } });
}

// sign-in, sign-out, self-registration, password recovery and invitation acceptance must work
// without (or with a broken) session; each route re-checks whatever it needs server-side
if (
pathname === '/auth/login' ||
pathname === '/auth/sso/google/start' ||
pathname === '/auth/sso/google/callback' ||
pathname === '/auth/logout' ||
pathname === '/auth/invitation' ||
pathname === '/auth/registration' ||
Expand Down Expand Up @@ -130,7 +130,12 @@ const authGuard: Handle = async ({ event, resolve }) => {
if (isApiRequest) {
return unauthorized('Authentication required');
}
return new Response(null, { status: 302, headers: { location: '/auth/login' } });

const organizationSlug = event.params.org ?? pathname.match(/^\/org\/([^/]+)/)?.[1];
const location = organizationSlug
? `/auth/login?${new URLSearchParams({ org: organizationSlug })}`
: '/auth/login';
return new Response(null, { status: 302, headers: { location } });
}

const projectSettingsMatch = event.url.pathname.match(
Expand Down
20 changes: 20 additions & 0 deletions src/lib/database/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,26 @@ export const ClusterSettingsEntity = entity('cluster_settings', {
.$defaultFn(() => new Date().toISOString()),
});

export const OrganizationAuthProviderEntity = entity('organization_auth_providers', {
id: uuid().primaryKey(),
organizationId: uuid().notNull(),
provider: text().notNull(),
enabled: bool().notNull().default(false),
clientId: text(),
clientSecretEncrypted: text(),
allowedDomains: json()
.notNull()
.$defaultFn(() => []),
autoProvision: bool().notNull().default(false),
autoProvisionRoleId: uuid().$defaultFn(() => null),
createdAt: timestamp()
.notNull()
.$defaultFn(() => new Date().toISOString()),
updatedAt: timestamp()
.notNull()
.$defaultFn(() => new Date().toISOString()),
});

export const relations = defineRelations();

relations.for(UserEntity, ({ one, many }) => ({
Expand Down
26 changes: 24 additions & 2 deletions src/lib/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,17 @@
"newPassword": "New password",
"resetPassword": "Reset password",
"resetting": "Resetting...",
"passwordResetSuccess": "Your password has been reset. Sign in with your new password."
"passwordResetSuccess": "Your password has been reset. Sign in with your new password.",
"ssoContinueWithGoogle": "Continue with Google",
"ssoOrganizationHint": "Single sign-on for {organization}",
"ssoDivider": "or",
"ssoErrorCancelled": "Google sign-in was cancelled.",
"ssoErrorNotConfigured": "Google sign-in is not enabled for this organization.",
"ssoErrorInvalidState": "The sign-in request expired. Please try again.",
"ssoErrorEmailNotVerified": "Your Google account email is not verified.",
"ssoErrorDomainNotAllowed": "This Google account domain is not allowed for this organization.",
"ssoErrorNotInvited": "This Google account has no access to this organization. Ask an administrator for an invitation.",
"ssoErrorGeneric": "Google sign-in failed. Please try again or use your password."
},
"bootstrap": {
"title": "Cluster setup",
Expand Down Expand Up @@ -755,7 +765,19 @@
"comingSoon": "Coming soon",
"comingSoonDescription": "Key rotation, encryption policy, and scoped secret protection settings are planned for a future release.",
"saveNotWired": "Global authentication settings are not wired to a server action yet.",
"localAuthTitle": "Local authentication: always ON"
"localAuthTitle": "Local authentication: always ON",
"googleSecretConfigured": "A client secret is stored (encrypted). Leave empty to keep it, or type a new one to replace it.",
"googleAllowedDomains": "Allowed Google Workspace domains",
"googleAllowedDomainsHelp": "Comma-separated. Only accounts of these Workspace domains can sign in. Leave empty to accept any verified Google account. Without automatic sign-up, users still need an invitation to this organization.",
"googleAutoProvision": "Automatically sign up accounts from the allowed domains",
"googleAutoProvisionHelp": "Any Google Workspace account from the allowed domains can sign in without an invitation: its user is created and given access to this organization with the chosen role. Requires at least one allowed domain.",
"googleAutoProvisionNoPermission": "You need permission to invite organization users to change automatic sign-up.",
"googleAutoProvisionRole": "Role granted to automatically signed-up accounts",
"googleAutoProvisionRoleHelp": "Only roles without administrative permissions are listed: read access and, at most, creating and editing projects (like Org Developer). Administrators can grant more afterwards, per user.",
"googleAutoProvisionRoleInvalid": "The saved role no longer exists or now grants administrative permissions, so automatic sign-up is blocked. Pick another role and save.",
"googleRedirectUri": "Authorized redirect URI (GCP):",
"googleLoginUrl": "Organization login URL:",
"saveSaml": "Save SAML settings"
},
"projects": {
"title": "Projects",
Expand Down
Loading
Loading