Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
1a4b5b5
rbac resolver;
bakhterets Feb 2, 2026
3ea42be
RequireRole mw;
bakhterets Feb 2, 2026
d6a460a
comments removed;
bakhterets Feb 3, 2026
ea3e607
rbac: add maintenance status workflow with role-based permissions
bakhterets Feb 3, 2026
17b1a8a
checker: transition reviewed -> planned
bakhterets Feb 3, 2026
d3aa143
mw and v2 func refactored, golangci changed
bakhterets Feb 4, 2026
fd17157
database migration;
bakhterets Feb 4, 2026
ae010d8
GET filter for unauthorized
bakhterets Feb 4, 2026
da5feb6
validateMaintenanceCreation added;
bakhterets Feb 4, 2026
7727a78
filter for pending review, contact email, created_by.
bakhterets Feb 4, 2026
aafa6aa
comments removed from v2_auth_test
bakhterets Feb 4, 2026
ec08d92
versioning for PATCH (implementation)
bakhterets Feb 5, 2026
21e1512
negative test 409 added
bakhterets Feb 5, 2026
4f1a116
middleware auth refactoring
bakhterets Feb 6, 2026
737d915
optimistic lock bug fixed
bakhterets Feb 7, 2026
9b7d3e0
tests refactored
bakhterets Feb 10, 2026
bfc19ef
fix events handler
sgmv Feb 10, 2026
3a43b21
middleware, v2.go, db.go changed.
bakhterets Feb 10, 2026
3e7e147
update
sgmv Feb 11, 2026
05956fa
refactor: inject RBAC service into handlers, simplify toAPIEvent, ren…
bakhterets Feb 12, 2026
2e37c6b
TestCalculateCurrentMntStatus added
bakhterets Feb 12, 2026
357ebce
fix FR-003/FR-029 bugs, refactored auth middleware, add unit and int…
bakhterets Feb 13, 2026
133b697
golangci-lint version updated
bakhterets Feb 13, 2026
e3ebc31
AuthenticationMW fixed; rbac tests added;
bakhterets Feb 16, 2026
5cbc720
add RBAC documentation, update OpenAPI security, hide reviewed status…
bakhterets Feb 17, 2026
43897c3
version requirement change
bakhterets Feb 18, 2026
42e02b0
fix: PR #152 review - RBAC config, status codes, tests
bakhterets Feb 23, 2026
1c0e9bd
fix: PR #152 review - tests
bakhterets Feb 23, 2026
88146d5
SD_RBAC_ADMINS -> SD_RBAC_GROUP_ADMINS renamed
bakhterets Feb 25, 2026
453babb
in_progress; pending_review renaming
bakhterets Feb 25, 2026
e432224
RBAC_ENABLED: true -> RBAC_DISABLED: false
bakhterets Feb 25, 2026
f22a8d6
docs updated
bakhterets Feb 25, 2026
c039cff
sd_rolename renamed to rolename;
bakhterets Feb 26, 2026
ed3ec39
leaking data (v1, rss) fixed;
bakhterets Feb 27, 2026
de10618
openapi.yaml fixed; typo fixed
bakhterets Feb 27, 2026
3d4620f
v2.go, conf.go review's fixes
bakhterets Mar 2, 2026
0920162
RBAC and auth refactored
bakhterets Mar 2, 2026
8e52ab9
RBAC and auth refactored
bakhterets Mar 2, 2026
a1ab8e3
pending_review checker fixed
bakhterets Mar 2, 2026
b117722
keycloak audience mapper (doc)
bakhterets Mar 3, 2026
4cc8272
Audience validation removed;
bakhterets Mar 3, 2026
ac85590
small fix
sgmv Mar 5, 2026
6648626
Update docker-build-push-on-draft.yaml env section
bakhterets Mar 16, 2026
65e6101
wrong env fixed (#157)
bakhterets Mar 18, 2026
cf213ba
inconsistencies between doc and code have been fixed
bakhterets Mar 18, 2026
d9261fe
fix(rbac): support comma-separated groups in SD_RBAC_GROUPS_* vars
bakhterets Mar 23, 2026
555bb6c
feat: implement state machine validation for maintenance event status…
bakhterets May 5, 2026
f5ca666
Merge branch 'main' into feature/rbac
bakhterets May 5, 2026
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
15 changes: 12 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
# Exampe for environment variables
# Example for environment variables
SD_DB=postgresql://pg:pass@localhost:5432/status_dashboard
SD_CACHE=internal
SD_LOG_LEVEL=devel
SD_WEB_URL=http://localhost:9000
SD_HOSTNAME=http://localhost:8000
SD_PORT=8000
SD_AUTHENTICATION_DISABLED=false
SD_AUTH_GROUP=my-auth-group

# At least one authentication provider must be configured.
# Local HMAC provider (for dev, tests, service-to-service)
SD_SECRET_KEY=change-me-to-a-strong-secret

# Keycloak RSA provider (for production SSO)
SD_KEYCLOAK_URL=http://localhost:8080
SD_KEYCLOAK_REALM=myapp
SD_KEYCLOAK_CLIENT_ID=myclient
SD_KEYCLOAK_CLIENT_SECRET=secret

# RBAC groups. SD_RBAC_GROUPS_ADMINS is mandatory.
SD_RBAC_GROUPS_CREATORS=status-dashboard-creators
SD_RBAC_GROUPS_OPERATORS=status-dashboard-operators
SD_RBAC_GROUPS_ADMINS=status-dashboard-admins,status-dashboard
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: stable
go-version: ${{ env.GO_VERSION }}
- name: golangci-lint
uses: golangci/golangci-lint-action@v7
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/docker-build-push-on-draft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,4 @@ jobs:
push: true
build-args: |
BASE_URL=${{ secrets.BASE_URL }}
AUTH_TOKEN=${{ secrets.AUTH_TOKEN }}
AUTH_TOKEN=${{ secrets.AUTH_TOKEN }}
6 changes: 6 additions & 0 deletions .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,12 @@ linters:
- testpackage
- wrapcheck
path: _test\.go
- linters:
- revive
text: "var-naming: avoid meaningless package names"
- linters:
- revive
text: "var-naming: avoid package names that conflict with Go standard library package names"
paths:
- third_party$
- builtin$
Expand Down
10 changes: 10 additions & 0 deletions db/migrations/000006_add_audit_user_fields.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
-- Rollback: remove audit user fields and contact_email

ALTER TABLE incident
DROP COLUMN IF EXISTS created_by,
DROP COLUMN IF EXISTS contact_email,
DROP COLUMN IF EXISTS version;

ALTER TABLE incident_status
DROP COLUMN IF EXISTS created_by,
DROP COLUMN IF EXISTS modified_by;
10 changes: 10 additions & 0 deletions db/migrations/000006_add_audit_user_fields.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
ALTER TABLE incident
ADD COLUMN created_by VARCHAR(255),
ADD COLUMN contact_email VARCHAR(255),
ADD COLUMN version INTEGER NOT NULL DEFAULT 1;

UPDATE incident SET version = 1 WHERE version IS NULL;

ALTER TABLE incident_status
ADD COLUMN created_by VARCHAR(255),
ADD COLUMN modified_by VARCHAR(255);
4 changes: 2 additions & 2 deletions docs/SD2_time_limits.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
### Valid Statuses
- completed
- modified
- in progress
- in_progress

### General Rules
- start_date is mandatory
Expand All @@ -60,7 +60,7 @@
- Updates status to completed
- Sets end_date to current time

#### "in progress":
#### "in_progress":
- update_date must be <= current time
- Only one status update allowed
- update_date must be < maintenance end_date
Expand Down
34 changes: 32 additions & 2 deletions docs/auth/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,38 @@ The backend calculates the SHA256 for `code_verifier` and extract saved data fro

# Authentication middleware

On the backend side we check all incoming requests and try to extract `Bearer` header with access token.
After successful extraction we get public keys from keycloak realm. And check the `access_token` by these keys.
The backend validates all incoming requests via a dual-IdP middleware chain:

1. **Token extraction**: `Authorization: Bearer <token>` header is parsed.
2. **Key selection**: Based on the JWT `alg` header:
- `RS256` → Keycloak RSA public key (fetched from JWKS endpoint, cached in memory)
- `HS256` / `HS384` / `HS512` → `SD_SECRET_KEY` (local HMAC)
3. **Signature verification**: `jwt-go/v5` validates the signature.
4. **Claims extraction**: `preferred_username` → user ID, `groups` → RBAC role resolution.
5. **Audit logging**: Every auth attempt is logged with `idp_type`, `username`, `result`, and `reason`.

### Two middleware variants

| Middleware | Behavior on missing/invalid token | Used for |
|-----------|----------------------------------|----------|
| `AuthenticationMW` (hard-auth) | Returns `401 Unauthorized` | Write endpoints (POST, PATCH) |
| `SetJWTClaims` (soft-auth) | Continues without user context | Read endpoints (GET) |

Both share a common `validateAndSetClaims()` helper (DRY).

### Configuration

At least one provider must be configured — otherwise the application fails to start.

| Variable | Provider | Required |
|----------|----------|----------|
| `SD_SECRET_KEY` | Local HMAC | At least one of HMAC or Keycloak |
| `SD_KEYCLOAK_URL` | Keycloak RSA | At least one of HMAC or Keycloak |
| `SD_KEYCLOAK_REALM` | Keycloak RSA | When Keycloak configured |
| `SD_KEYCLOAK_CLIENT_ID` | Keycloak RSA | When Keycloak configured |
| `SD_KEYCLOAK_CLIENT_SECRET` | Keycloak RSA | When Keycloak configured |

`SD_SECRET_KEY` must be ≥ 32 characters. `SD_AUTHENTICATION_DISABLED` has been removed.

# How to get a token locally

Expand Down
238 changes: 238 additions & 0 deletions docs/auth/permissions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
# Event Permissions & Status Transition Matrix

This document defines the complete permission model for all event types (`maintenance`, `incident`, `info`),
covering creation rules, PATCH status transitions, and automatic checker transitions.

---

## Role Hierarchy

```
Admin (50) > Operator (30) > Creator (10) > NoRole (0)
```

Role names (`admin`, `operator`, `creator`) are abstract application roles resolved from IdP group
names configured via `SD_RBAC_GROUPS_ADMINS`, `SD_RBAC_GROUPS_OPERATORS`, and `SD_RBAC_GROUPS_CREATORS`
environment variables. See [rbac.md](rbac.md) for configuration details.

---

## Maintenance Events

### Creation

| Role | Initial Status | Start Date Constraint | Notes |
|------|---------------|----------------------|-------|
| `admin` | `planned` | No restriction — past or future allowed | Bypasses review workflow |
| `operator` | `planned` | No restriction — past or future allowed | Bypasses review workflow |
| `creator` | `pending_review` | No restriction — past or future allowed | Enters review workflow |
| Unauthenticated | — | — | 401 Unauthorized |

> **Retroactive maintenance**: All roles may create maintenance events with start/end dates in the past.
> This is intentional and supports the use case of documenting maintenance that was not recorded in time.
> See [Retroactive Maintenance Workflow](#retroactive-maintenance-workflow) below.

**Required fields at creation:**
- `title`
- `description`
- `contact_email` (RFC 5322 format)
- `start_date`
- `end_date` (must be after `start_date`)
- `components` (at least one)

---

### PATCH Status Transitions

The table below shows which target statuses are reachable from each stored status, per role.
**Rows = current (stored) status. Columns = requested (incoming) status.**

All roles are subject to the **state machine** — only valid transitions are permitted regardless
of role. The state machine defines the allowed status flow; roles determine **which events** a user
may modify, not which transitions are valid.

Legend: ✅ allowed · ❌ 400 Bad Request (invalid transition) · 🚫 403 Forbidden · ⛔ terminal state

#### State Machine (applies to all roles)

| Current status (FROM ↓) \ Target status (TO →) | `pending_review` | `reviewed` | `planned` | `in_progress` | `modified` | `completed` | `cancelled` |
|------------------------------------------------|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
| `pending_review` | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ |
| `reviewed` | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ |
| `planned` | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ✅ |
| `in_progress` | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ |
| `modified` | ❌ | ❌ | ❌ | ✅ | ❌ | ✅ | ✅ |
| `completed` | ⛔ | ⛔ | ⛔ | ⛔ | ⛔ | ⛔ | ⛔ |
| `cancelled` | ⛔ | ⛔ | ⛔ | ⛔ | ⛔ | ⛔ | ⛔ |

> **Terminal states**: `completed` and `cancelled` have no outgoing transitions.
> Setting the same status again is rejected (400) except `pending_review → pending_review`
> which is explicitly allowed so creators can update the event text.

#### `admin` / `operator` — state machine enforced

Admin and operator pass the RBAC check unconditionally (any event, any stored status).
However, the state machine still restricts which transitions are valid.
The distinction from `admin` is that `admin` will gain additional system-level privileges in future
releases; `operator` scope is limited to event management.

**Effective matrix** = the state machine table above.

#### `creator` — restricted to own events in `pending_review`

Creators may only PATCH their **own** events (ownership check: `user_id == created_by`).
If the event belongs to another user: **403 Forbidden**.
If the stored status is not `pending_review`: **409 Conflict**.

| Current status (FROM ↓) \ Target status (TO →) | `pending_review` | `reviewed` | `planned` | `in_progress` | `modified` | `completed` | `cancelled` |
|------------------------------------------------|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
| `pending_review` (own event) | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| `pending_review` (other's event) | 🚫 | 🚫 | 🚫 | 🚫 | 🚫 | 🚫 | 🚫 |
| any other status | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |

---

### Automatic Transitions (Checker Goroutine)

The `checker` goroutine runs periodically and transitions maintenance events based on wall-clock time.
These transitions require no user action and are not subject to RBAC.

```
reviewed ──────────────────────────────► planned
(checker, unconditional)

planned ──[StartDate reached]──────────► in_progress
(checker, time-based)

in_progress ──[EndDate reached]───────────► completed
(checker, time-based)
```

If a `cancelled` status entry ever exists in the event's status history, the checker preserves
`cancelled` as the final status regardless of dates.

The checker also backfills missing intermediate statuses (e.g., if an event jumps directly to
`completed`, the checker adds `planned` and `in_progress` entries with correct timestamps).

---

### Retroactive Maintenance Workflow

Maintenance events may be created with start and/or end dates in the past. This supports the
operational scenario of documenting maintenance that was performed but not registered in time.

#### Path A — `admin` or `operator` creates with past dates

```
POST /v2/events { start_date: <past>, end_date: <past> }
▼ initial status: planned
▼ next checker run
calculateCurrentMntStatus: now > EndDate → completed
▼ fixMntMissedStatuses(completed):
- adds planned entry (timestamp = StartDate)
- adds in_progress entry (timestamp = StartDate)
- adds completed entry (timestamp = EndDate)
```

Result: the event appears in history with correct timestamps, as if it had been recorded in real time.
No manual PATCH is required after creation.

#### Path B — `creator` creates with past dates

```
POST /v2/events → initial status: pending_review
│ (checker leaves pending_review unchanged)
▼ Operator approves via PATCH → reviewed
▼ checker: reviewed → planned → (EndDate in past) → completed
fixMntMissedStatuses fills history with correct timestamps
```

---

### Full Maintenance Status Flow Diagram

```
State Machine (enforced for ALL roles)
======================================

[creator POST]──► pending_review ──┬──► reviewed ──► planned ──► in_progress ──┬──► completed (terminal)
│ ▲ │ │ ▲ │
│ │ │ │ │ │
└─┘ │ ▼ │ │
(self-transition │ modified ────┘
allowed) │ │
│ │
▼ ▼
[operator/admin POST]──► planned └───────────────────────────────────────────► cancelled (terminal)
(from any non-terminal status)

Legend:
──► = valid transition (state machine)
All roles: admin, operator, creator are subject to the state machine
RBAC restricts WHO can trigger transitions, not WHICH transitions exist
Creator: can only operate on own events in pending_review status
Admin/Operator: can operate on any event in any non-terminal status
```

---

## Incident Events

### Creation

| Role | Constraint |
|------|-----------|
| Any authenticated | `start_date` must NOT be in the future (incidents are present/past events) |
| Any authenticated | `end_date` must be omitted at creation |

### PATCH Status Transitions

Incidents do not use the RBAC `allowMaintenancePatch` path. Any authenticated user with a valid
role may patch incidents. Key rules:

- If the incident has an `end_date` (closed), only closed statuses (`resolved`, `reopened`, `changed`)
are accepted, unless using `IncidentChanged` to update dates.
- `impact` changes require status `impact changed`.
- `start_date` cannot be changed on an open incident.
- Status `resolved` automatically sets `end_date` to `update_date`.
- Status `reopened` clears `end_date`.

---

## Informational Events

### Creation

No special role restrictions beyond authentication. `impact` must be `0`.

### PATCH Status Transitions

Allowed statuses: `planned`, `active`, `completed`, `cancelled`.
Any authenticated user may patch info events to any of these statuses.

---

## Visibility Rules

| Condition | `pending_review` / `reviewed` maintenance | `creator` field | `contact_email` field | `version` field |
|-----------|:-----------------------------------------:|:---------------:|:--------------------:|:---------------:|
| Unauthenticated | Hidden (404) | Hidden | Hidden | Hidden |
| Authenticated (any role) | Visible | Visible | Visible | Visible for maintenance events only |

---

## HTTP Error Reference

| Code | Trigger |
|------|---------|
| `400 Bad Request` | Invalid request body, missing required fields, invalid email format, `end_date` before `start_date`, invalid state transition (state machine violation), setting same status again |
| `401 Unauthorized` | Missing or invalid JWT token |
| `403 Forbidden` | Insufficient role, or creator attempting to modify another user's event |
| `409 Conflict` | Status transition not permitted for current role/state (creator outside `pending_review`), or version mismatch |
| `500 Internal Server Error` | Database or unexpected server error |
Loading
Loading