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
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ Shashank Singh <shashanksgh3@gmail.com>
Jesper Pedersen <jesperpedersen.db@gmail.com>
Adnan Khan <adnan.kriaz@gmail.com>
Mohamed Hamed <alkmohamed40@gmail.com>
Yousuf Mahmoud <yusufmahmoud.dev@gmail.com>
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

- Keycloak Single Sign-On (SSO) authentication with 3 roles (Member, Lead, Admin)
- Persistent project registration with public and private projects
- Per-project membership with OWNER and MEMBER roles, to share a project with a team
- Manual Linux builds from a chosen branch, run in a container with Docker or Podman
- Build history with status, commit ID, timing and output
- Private Git access through environment-variable credential references
Expand Down
4 changes: 2 additions & 2 deletions docs/DEVELOPERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ See [BUILDING.md](BUILDING.md) for the current development, build, test and fron
Cisys uses **Keycloak** for sign-in and bearer-token authentication.

* The frontend is a public client (`cisys-ui`) using the authorization-code flow with PKCE.
* The backend is a Quarkus OIDC `service` application. It reads roles from the access token claim `realm_access/roles` and uses the immutable `sub` claim as the principal and project owner.
* The backend is a Quarkus OIDC `service` application. It reads roles from the access token claim `realm_access/roles` and uses the immutable `sub` claim as the principal, as the project owner (`owner_subject`), and for the `project_membership` rows.
* The realm roles are `member`, `lead` and `admin`.

The development realm is `contrib/keycloak/realm.json`. It is imported on the first Keycloak start only.
Expand Down Expand Up @@ -212,7 +212,7 @@ But they are not actually lost - using `git reflog`, you can find every commit t
## Verification status

* Backend tests (`mvn test`) and the frontend lint and production build pass in the development workspace.
* Keycloak 26.8.2 tokens for Member, Lead and Admin, and private-project visibility, were tested through the REST API against a real local Keycloak and PostgreSQL 18 on 2026-11-07.
* Keycloak 26.8.2 tokens for Member, Lead and Admin, and private-project visibility, were tested through the REST API against a real local Keycloak and PostgreSQL 18 on 2026-11-07. Project membership, member management and the global-role × visibility × membership matrix were tested in the backend suite on 2026-11-14.
* An opt-in smoke test fetches the public `octocat/Hello-World` repository via JGit and runs an Alpine container through Docker: `CISYS_RUNNER_SMOKE=true mvn -Dtest=RunnerSmokeTest test`.
* Not yet verified: a real private Git provider, Podman, Windows/WSL, and the full Compose stack end to end.

Expand Down
2 changes: 1 addition & 1 deletion docs/ROADMAP.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Next milestones

1. Finish the authentication slice. Done: Keycloak login, `member`/`lead`/`admin` realm roles, subject-based project ownership, role and private-project authorization tests, and configurable branding, and a Flyway baseline migration with Hibernate schema validation. Remaining: integration tests against a real Keycloak and server configuration.
2. Repository access. Done: JGit fetch of public HTTPS repositories on `github.com` and `gitlab.com`, private access through environment-variable credential references (admins only), and the commit SHA recorded per build, including after a force push. Remaining: private credentials stored using a secrets provider, and project membership and repository permissions.
2. Repository access. Done: JGit fetch of public HTTPS repositories on `github.com` and `gitlab.com`, private access through environment-variable credential references (admins only), the commit SHA recorded per build, including after a force push, and project membership with per-project roles (`OWNER` and `MEMBER`). Remaining: private credentials stored using a secrets provider, and per-repository permissions.
3. Manual builds. Done: a single in-process runner using Docker or Podman with no network, CPU/memory/PID limits, a timeout and stored logs; builds left running by a restart are marked failed. Persistent FIFO queue with stored request details, atomic claims, configurable concurrency and queue limits, and restart recovery. Remaining: cancellation, container cleanup after a crash, and a separately isolated Linux worker using disposable rootless containers with a restricted network and no container-engine socket or host credentials exposed. Define these before enabling untrusted builds.
4. Multi-user deployment: TLS, secure Keycloak settings and secrets, database backups, audit records, access control, rate limits, concurrency controls and tested upgrade/migration procedures.
5. CI configuration, webhooks, artifacts and additional worker platforms.
Expand Down
21 changes: 16 additions & 5 deletions docs/SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,16 @@ The Keycloak admin console (<http://localhost:8180/admin>) uses `admin` / `admin
| Endpoint | Member | Lead | Admin |
| :---------------------------------------- | :------------------------ | :------------------- | :---- |
| `GET /api/session` | yes | yes | yes |
| `GET /api/projects` | public projects | public and own | all |
| `GET /api/projects` | public projects and private projects they belong to | public projects and private projects they belong to | all |
| `POST /api/projects` | no | yes | yes |
| `GET /api/projects/{id}/builds[/{id}]` | visible projects | visible projects | all |
| `GET /api/projects/{id}/branches` | visible projects | visible projects | all |
| `POST /api/projects/{id}/builds` | no | yes | yes |
| `POST /api/projects/{id}/builds` | no | project members | yes |
| `POST` build with a `credentialRef` | no | no | yes |
| `PUT`/`DELETE .../builds/{id}/pin` | no | own projects | yes |
| `PUT`/`DELETE .../builds/{id}/pin` | no | leads who own the project | yes |
| `GET /api/projects/{id}/members` | project members | project members | yes |
| `POST /api/projects/{id}/members` | project owners | project owners | yes |
| `DELETE /api/projects/{id}/members/{username}` | project owners | project owners | yes |
| `GET /api/branding` | yes | yes | yes |
| `PUT /api/branding` | no | no | yes |
| `GET`/`PUT /api/settings/retention` | no | no | yes |
Expand All @@ -36,9 +39,17 @@ The Keycloak admin console (<http://localhost:8180/admin>) uses `admin` / `admin
| `PUT /api/images` | no | no | yes |
| `GET/POST/DELETE /api/images/local` | no | no | yes |

Without signing in, `GET /api/branding/public`, `GET /api/branding/logo`, `GET /api/stats/public`, the public project page (`GET /api/project-pages/{name}`, `GET /api/project-pages/{name}/builds/{buildId}` and its build logs) and `GET /api/project-pages/{name}/branches` for public projects are readable. The project page accepts an optional `branch` query parameter, which filters the builds but applies the same public/private visibility rules. The statistics are aggregate counts computed from public projects only (public projects, their builds by status, success rate, last build time). Private projects and their builds are left out of every number, and no names, repository URLs, logs or users are exposed. The server caches the result for `STATS_CACHE_SECONDS` (default 30) seconds. The branch endpoint returns remote branch names for public projects. For private projects it requires the same owner/admin access as the page and returns only the branches that already have builds, so the private repository is never read; anonymous callers and users without access receive `404`.
Without signing in, `GET /api/branding/public`, `GET /api/branding/logo`, `GET /api/stats/public`, the public project page (`GET /api/project-pages/{name}`, `GET /api/project-pages/{name}/builds/{buildId}` and its build logs) and `GET /api/project-pages/{name}/branches` for public projects are readable. The project page accepts an optional `branch` query parameter, which filters the builds but applies the same public/private visibility rules. The statistics are aggregate counts computed from public projects only (public projects, their builds by status, success rate, last build time). Private projects and their builds are left out of every number, and no names, repository URLs, logs or users are exposed. The server caches the result for `STATS_CACHE_SECONDS` (default 30) seconds. The branch endpoint returns remote branch names for public projects. For private projects it requires the same member/admin access as the page and returns only the branches that already have builds, so the private repository is never read; anonymous callers and users without access receive `404`.

Private projects (and their build history and logs) are visible only to their creator and admins. Ownership is recorded with the immutable Keycloak subject (`sub`), not the user name. A project that is not visible to the caller is reported as `404 Not Found`.
Private projects (and their build history and logs) are visible only to their members and to admins. Membership is recorded with the immutable Keycloak subject (`sub`), not the user name. A project always has exactly one `OWNER` member — the project's creator — and any number of `MEMBER` members. A project that is not visible to the caller is reported as `404 Not Found`.

### Project membership

* Every project has exactly one `OWNER` member, its creator, recorded atomically when the project is created. Other members hold the `MEMBER` role. Ownership cannot be transferred or removed through the API; removing the `OWNER` answers `409 Conflict`.
* Starting builds requires the Lead or Admin role, and a project membership; admins bypass membership. Editing a project and pinning its builds require the Lead role and being the project's `OWNER`, or the Admin role. Apart from member management by the `OWNER`, project membership never grants permissions the caller's global role does not allow — the global role decides what you can do, membership decides on which projects.
* Listing members (`GET /api/projects/{id}/members`) requires being a member of the project, or an admin. Adding (`POST`) and removing (`DELETE`) members requires being the project's `OWNER`, whatever the global role, or an admin. Only the `MEMBER` role is assignable; the `OWNER` role is never assignable through the membership API, and raw subject IDs are never exposed — the API accepts and returns usernames.
* Member-management endpoints resolve usernames to Keycloak through the `cisys-backend` service account; the membership endpoints only read users, but the account holds Keycloak's `manage-users` role, which signing users out on shutdown needs, so keep its secret protected.
* `ownerSubject` is kept as a denormalized pointer for the `owned` flag and for admins; project membership is the source of truth for authorization.

User subjects are returned to admins only:

Expand Down
15 changes: 13 additions & 2 deletions docs/entity-diagram.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,23 @@ erDiagram
INTEGER max_queued_builds
}

PROJECT_MEMBERSHIP {
BIGINT id PK
BIGINT project_id
STRING subject
STRING role
TIMESTAMP created_at
}

PROJECT ||--o{ BUILD : has
PROJECT ||--o{ PROJECT_MEMBERSHIP : has
```

* `PROJECT.owner_subject` and `BUILD.requested_by` hold the Keycloak subject (`sub`) of the user.
* `PROJECT.owner_subject` and `BUILD.requested_by` hold the Keycloak subject (`sub`) of the user. `PROJECT_MEMBERSHIP.subject` holds the same kind of subject; exactly one row per project has role `OWNER` — the creator, recorded atomically with project creation.
* `PROJECT_MEMBERSHIP.project_id` is a foreign key to `PROJECT` with `ON DELETE CASCADE`; a unique index over `(project_id, subject)` and a partial unique index on `project_id` for `OWNER` rows enforce one membership per user per project and one OWNER per project.
* `OWNER` rows are never assignable through the membership API and one must always exist; there is no ownership-transfer path.
* `BUILD.project_id` is a plain column, not a foreign key constraint.
* `BUILD.logoid` (type `lo`, from the `lo` extension) references the build log, stored as one PostgreSQL large object. It is not mapped in the `Build` entity. The `lo_manage` trigger unlinks the large object when the build is deleted, so no logs are orphaned.
* `BUILD.status` is `RUNNING`, `PASSED` or `FAILED`.
* `BUILD.status` is `QUEUED`, `RUNNING`, `PASSED` or `FAILED`.
* `BRANDING` and `SETTINGS` are singleton rows with `id = 1`. Until `SETTINGS` is first saved, its defaults apply.
* Users, roles and credentials live in Keycloak and are not stored in the cisys database.
7 changes: 4 additions & 3 deletions docs/manual/en/01-introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ It provides sign-in through Keycloak, three roles, project registration with pub

* Keycloak Single Sign-On (SSO) authentication with 3 roles (Member, Lead, Admin)
* Persistent project registration with public and private projects
* Per-project membership with OWNER and MEMBER roles; owners and admins manage project members
* Manual Linux builds from a chosen branch, run in a container through Docker or Podman
* Build history with status, commit ID, timing and output
* Private Git access through environment-variable credential references
Expand All @@ -22,16 +23,16 @@ It provides sign-in through Keycloak, three roles, project registration with pub
## Roles

* **Members** can view public projects and the build history of projects they can access.
* **Leads** can additionally create projects and start manual builds.
* **Admins** can see every project, change the branding, and use credential references for private Git.
* **Leads** can additionally create projects and start manual builds on projects they are members of.
* **Admins** can see every project, start and pin builds without membership, change the branding, and use credential references for private Git.

## Project handling

Each project keeps

* A name and an HTTPS repository URL
* A private flag, which controls project and build-log visibility
* The owner, recorded as the Keycloak subject
* The OWNER member (the creator) and any MEMBER members, who can see private projects and their builds

## Build handling

Expand Down
13 changes: 8 additions & 5 deletions docs/manual/en/03-roles.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,31 @@ The **Member** role is the standard read-only role. Members can view public proj

## Lead

The **Lead** role is for people who run projects. Leads can do everything a member can, create projects, start manual builds on projects they can see, and pin builds of the projects they created.
The **Lead** role is for people who run projects. Leads can do everything a member can, create projects, start manual builds on projects they are members of (a project owner adds them), and edit and pin builds of the projects they own.

## Admin

The **Admin** role manages the installation. Admins can see every project including private projects of other users, start and pin builds, use credential references for private Git repositories, manage the build images, change the branding, and change the settings such as build retention and the build queue.
The **Admin** role manages the installation. Admins can see every project, start and pin builds without membership, use credential references for private Git repositories, manage the build images, change the branding, manage project membership, and change the settings such as build retention and the build queue.

## Access summary

| Action | Member | Lead | Admin |
| :---------------------------------- | :----- | :--- | :---- |
| View public projects | yes | yes | yes |
| View own private projects | n/a | yes | yes |
| View private projects they belong to | yes | yes | yes |
| View other users' private projects | no | no | yes |
| Create a project | no | yes | yes |
| View build history and logs | yes | yes | yes |
| Start a manual build | no | yes | yes |
| Pin a build | no | own | yes |
| Start a manual build | no | member projects | yes |
| Pin a build | no | projects they own | yes |
| Add or remove project members | projects they own | projects they own | yes |
| Use a Git credential reference | no | no | yes |
| Manage build images | no | no | yes |
| Change branding | no | no | yes |
| Change settings (retention, queue) | no | no | yes |

"View" always means projects the user may access. The address of a project or build that does not exist or is not visible to the user opens the front page instead, so a private project is not revealed; the same applies to any unknown address.

Project membership scopes the projects a global role applies to; MEMBER membership does not grant powers on its own. Starting a build requires the Lead or Admin role **and** membership of the project — someone who is only a MEMBER of a project has no Lead powers. Every project has exactly one OWNER, its creator. The OWNER can add and remove members whatever their global role; admins can do the same on any project. Editing a project and pinning its builds need more: the Lead role **and** being the project's OWNER, or the Admin role. The OWNER role is never assignable through the membership API, and the project's OWNER cannot be removed.

The following chapters describe the application areas in more detail.
Loading
Loading