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
9 changes: 7 additions & 2 deletions client/src/components/accounts/roles.vue
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@
class="ma-2"
color="secondary"
@click="openEditRoleDialog(item)"
:disabled="item.name === 'admin' || !writeUserPermission"
:disabled="isBuiltInRole(item.name) || !writeUserPermission"
>
<v-icon color="primary">
mdi-pencil
Expand All @@ -149,7 +149,7 @@
class="ma-2"
color="secondary"
@click="deleteRole(item)"
:disabled="item.name === 'admin' || item.name === 'guest' || item.name === 'member' || !writeUserPermission"
:disabled="isBuiltInRole(item.name) || !writeUserPermission"
>
<v-icon color="primary">
mdi-delete
Expand Down Expand Up @@ -519,6 +519,10 @@ export default defineComponent({
}
}
*/
const builtInRoleNames = ['admin', 'member', 'developer', 'viewer', 'guest']

const isBuiltInRole = (name: string) => builtInRoleNames.includes(name)

const getResourcePermissions = (permissions: any, resource: string) => {
for (const permission of permissions) {
if (permission.resource === resource) {
Expand Down Expand Up @@ -560,6 +564,7 @@ export default defineComponent({
openCreateDialog,
saveCreate,
getResourcePermissions,
isBuiltInRole,
writeUserPermission,
}
},
Expand Down
7 changes: 7 additions & 0 deletions client/src/locale/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,13 @@ const messages = {
name: 'Roles',
search: 'Search for a Role',
permission: 'Permission',
builtIn: {
admin: 'Administrator — full access to apps, pipelines, accounts, and settings',
member: 'Member — manage apps and pipelines; read account info',
developer: 'Developer — deploy and operate apps; no account or settings access',
viewer: 'Viewer — read-only access to apps, pipelines, and logs',
guest: 'Guest — legacy read-only role (prefer Viewer for new users)',
},
actions: {
create: 'Create Role',
edit: 'Edit Role',
Expand Down
49 changes: 49 additions & 0 deletions docs/rbac.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Role-based access control (RBAC)

Kubero uses **roles**, **permissions**, and **teams** to control who can do what in the UI and API.

## Concepts

| Concept | Purpose |
|---------|---------|
| **Role** | Named set of permissions (stored in the Kubero database). |
| **Permission** | `resource:action` pair checked on API routes and in the UI (e.g. `app:write`). |
| **Team (user group)** | Groups users for **pipeline access** (`spec.access.teams` on a pipeline). Users in the `admin` team see all pipelines. |

Permissions are loaded at login and embedded in the JWT. The API enforces them via `PermissionsGuard`; the Vue UI uses `authStore.hasPermission(...)`.

## Built-in roles

These roles are created on first startup (see `server/src/database/database.service.ts`).

| Role | Intended use | Apps / pipelines | Accounts & settings | Logs / console / reboot | Security scans |
|------|----------------|------------------|---------------------|-------------------------|----------------|
| **admin** | Full administrators | write | write (users, config) | yes | write |
| **member** | Team members with broad access | write | read users; no config write | yes | write |
| **developer** | Build and operate workloads | write | no account or config access | yes | write |
| **viewer** | Read-only observers | read | no | logs only (no console/reboot) | read |
| **guest** | Legacy minimal role (prefer **viewer** for new users) | read | no | no | read |

### Permission actions

- **read** / **write** — used for `app`, `pipeline`, `user`, `config`, `security`.
- **ok** — used for `console`, `logs`, `reboot`, `token` (and audit read via `read`).
- **none** — stored in the database but does **not** satisfy API guards (guards require a positive permission such as `app:read`).

## Pipeline scoping (teams)

RBAC roles control *what operations* a user may perform. **Teams** control *which pipelines* they see:

- Assign users to teams under **Accounts → Users**.
- On each pipeline, set **access teams** (or rely on the `admin` team for global access).
- The `admin` **user group** bypasses pipeline filters.

## Managing roles

Administrators can review and customize roles under **Accounts → Roles** (requires `user:read` / `user:write`).

Built-in roles (`admin`, `member`, `developer`, `viewer`, `guest`) cannot be deleted from the UI.

## Related issue

This model implements the roles described in [kubero#545](https://github.com/kubero-dev/kubero/issues/545).
8 changes: 6 additions & 2 deletions server/src/addons/addons.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
} from '@nestjs/swagger';
import { OKDTO } from '../common/dto/ok.dto';
import { JwtAuthGuard } from '../auth/strategies/jwt.guard';
import { PermissionsGuard } from '../auth/permissions.guard';
import { Permissions } from '../auth/permissions.decorator';

@Controller({ path: 'api/addons', version: '1' })
export class AddonsController {
Expand All @@ -19,7 +21,8 @@ export class AddonsController {
type: OKDTO,
isArray: false,
})
@UseGuards(JwtAuthGuard)
@UseGuards(JwtAuthGuard, PermissionsGuard)
@Permissions('pipeline:read', 'pipeline:write')
@ApiBearerAuth('bearerAuth')
async getAddons() {
return this.addonsService.getAddonsList();
Expand All @@ -32,7 +35,8 @@ export class AddonsController {
type: OKDTO,
isArray: false,
})
@UseGuards(JwtAuthGuard)
@UseGuards(JwtAuthGuard, PermissionsGuard)
@Permissions('pipeline:read', 'pipeline:write')
@ApiBearerAuth('bearerAuth')
async getOperators() {
return this.addonsService.getOperatorsList();
Expand Down
58 changes: 58 additions & 0 deletions server/src/database/database.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,64 @@ export class DatabaseService {
Logger.log('Role "guest" seeded successfully.', 'DatabaseService');
});

// Developer: deploy and operate apps, no account or cluster settings access
await prisma.role
.upsert({
where: { name: 'developer' },
update: {},
create: {
name: 'developer',
description:
'Developer role — manage apps and pipelines, logs, console, and builds',
permissions: {
create: [
{ action: 'write', resource: 'app' },
{ action: 'write', resource: 'pipeline' },
{ action: 'none', resource: 'user' },
{ action: 'none', resource: 'config' },
{ action: 'ok', resource: 'console' },
{ action: 'ok', resource: 'logs' },
{ action: 'ok', resource: 'reboot' },
{ action: 'read', resource: 'audit' },
{ action: 'ok', resource: 'token' },
{ action: 'write', resource: 'security' },
],
},
},
})
.then(() => {
Logger.log('Role "developer" seeded successfully.', 'DatabaseService');
});

// Viewer: read-only access to apps, pipelines, metrics, and audit
await prisma.role
.upsert({
where: { name: 'viewer' },
update: {},
create: {
name: 'viewer',
description:
'Viewer role — read-only access to apps, pipelines, logs, and audit',
permissions: {
create: [
{ action: 'read', resource: 'app' },
{ action: 'read', resource: 'pipeline' },
{ action: 'none', resource: 'user' },
{ action: 'none', resource: 'config' },
{ action: 'none', resource: 'console' },
{ action: 'ok', resource: 'logs' },
{ action: 'none', resource: 'reboot' },
{ action: 'read', resource: 'audit' },
{ action: 'none', resource: 'token' },
{ action: 'read', resource: 'security' },
],
},
},
})
.then(() => {
Logger.log('Role "viewer" seeded successfully.', 'DatabaseService');
});

// Ensure the 'everyone' user group exists
prisma.userGroup
.upsert({
Expand Down
20 changes: 12 additions & 8 deletions server/src/deployments/deployments.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,17 @@ import { IUser } from '../auth/auth.interface';
import { CreateBuild } from './dto/CreateBuild.dto';
import { OKDTO } from '../common/dto/ok.dto';
import { JwtAuthGuard } from '../auth/strategies/jwt.guard';
import { PermissionsGuard } from '../auth/permissions.guard';
import { Permissions } from '../auth/permissions.decorator';
import { ReadonlyGuard } from '../common/guards/readonly.guard';

@Controller({ path: 'api/deployments', version: '1' })
export class DeploymentsController {
constructor(private readonly deploymentsService: DeploymentsService) {}

@Get('/:pipeline/:phase/:app')
@UseGuards(JwtAuthGuard)
@UseGuards(JwtAuthGuard, PermissionsGuard)
@Permissions('app:read', 'app:write')
@ApiForbiddenResponse({
description: 'Error: Unauthorized',
type: OKDTO,
Expand All @@ -49,8 +52,8 @@ export class DeploymentsController {
}

@Post('/build/:pipeline/:phase/:app')
@UseGuards(JwtAuthGuard)
@UseGuards(ReadonlyGuard)
@UseGuards(JwtAuthGuard, PermissionsGuard, ReadonlyGuard)
@Permissions('app:write')
@ApiForbiddenResponse({
description: 'Error: Unauthorized',
type: OKDTO,
Expand Down Expand Up @@ -91,8 +94,8 @@ export class DeploymentsController {
}

@Delete('/:pipeline/:phase/:app/:buildName')
@UseGuards(JwtAuthGuard)
@UseGuards(ReadonlyGuard)
@UseGuards(JwtAuthGuard, PermissionsGuard, ReadonlyGuard)
@Permissions('app:write')
@ApiBearerAuth('bearerAuth')
@ApiForbiddenResponse({
description: 'Error: Unauthorized',
Expand Down Expand Up @@ -126,7 +129,8 @@ export class DeploymentsController {
}

@Get('/:pipeline/:phase/:app/:build/:container/history')
@UseGuards(JwtAuthGuard)
@UseGuards(JwtAuthGuard, PermissionsGuard)
@Permissions('logs:ok', 'app:read', 'app:write')
@ApiBearerAuth('bearerAuth')
@ApiForbiddenResponse({
description: 'Error: Unauthorized',
Expand Down Expand Up @@ -158,8 +162,8 @@ export class DeploymentsController {
}

@Put('/:pipeline/:phase/:app/:tag')
@UseGuards(JwtAuthGuard)
@UseGuards(ReadonlyGuard)
@UseGuards(JwtAuthGuard, PermissionsGuard, ReadonlyGuard)
@Permissions('app:write')
@ApiBearerAuth('bearerAuth')
@ApiForbiddenResponse({
description: 'Error: Unauthorized',
Expand Down
14 changes: 10 additions & 4 deletions server/src/kubernetes/kubernetes.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@ import {
} from './dto/kubernetes.dto';
import { OKDTO } from '../common/dto/ok.dto';
import { JwtAuthGuard } from '../auth/strategies/jwt.guard';
import { PermissionsGuard } from '../auth/permissions.guard';
import { Permissions } from '../auth/permissions.decorator';

@Controller({ path: 'api/kubernetes', version: '1' })
export class KubernetesController {
constructor(private readonly kubernetesService: KubernetesService) {}

@Get('events')
@UseGuards(JwtAuthGuard)
@UseGuards(JwtAuthGuard, PermissionsGuard)
@Permissions('app:read', 'app:write')
@ApiBearerAuth('bearerAuth')
@ApiForbiddenResponse({
description: 'Error: Unauthorized',
Expand All @@ -41,7 +44,8 @@ export class KubernetesController {
}

@Get('storageclasses')
@UseGuards(JwtAuthGuard)
@UseGuards(JwtAuthGuard, PermissionsGuard)
@Permissions('app:read', 'app:write')
@ApiBearerAuth('bearerAuth')
@ApiForbiddenResponse({
description: 'Error: Unauthorized',
Expand All @@ -59,7 +63,8 @@ export class KubernetesController {
}

@Get('domains')
@UseGuards(JwtAuthGuard)
@UseGuards(JwtAuthGuard, PermissionsGuard)
@Permissions('app:read', 'app:write')
@ApiBearerAuth('bearerAuth')
@ApiForbiddenResponse({
description: 'Error: Unauthorized',
Expand All @@ -79,7 +84,8 @@ export class KubernetesController {
}

@Get('/contexts')
@UseGuards(JwtAuthGuard)
@UseGuards(JwtAuthGuard, PermissionsGuard)
@Permissions('pipeline:read', 'pipeline:write')
@ApiForbiddenResponse({
description: 'Error: Unauthorized',
type: OKDTO,
Expand Down
17 changes: 12 additions & 5 deletions server/src/metrics/metrics.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@ import {
} from '@nestjs/swagger';
import { MetricsService } from './metrics.service';
import { JwtAuthGuard } from '../auth/strategies/jwt.guard';
import { PermissionsGuard } from '../auth/permissions.guard';
import { Permissions } from '../auth/permissions.decorator';
import { OKDTO } from '../common/dto/ok.dto';

@Controller({ path: 'api/metrics', version: '1' })
export class MetricsController {
constructor(private metricsService: MetricsService) {}

@Get('/resources/:pipeline/:phase/:app')
@UseGuards(JwtAuthGuard)
@UseGuards(JwtAuthGuard, PermissionsGuard)
@Permissions('app:read', 'app:write')
@ApiBearerAuth('bearerAuth')
@ApiForbiddenResponse({
description: 'Error: Unauthorized',
Expand All @@ -34,7 +37,8 @@ export class MetricsController {
}

@Get('/uptimes/:pipeline/:phase')
@UseGuards(JwtAuthGuard)
@UseGuards(JwtAuthGuard, PermissionsGuard)
@Permissions('app:read', 'app:write')
@ApiBearerAuth('bearerAuth')
@ApiForbiddenResponse({
description: 'Error: Unauthorized',
Expand All @@ -52,7 +56,8 @@ export class MetricsController {
}

@Get('/timeseries')
@UseGuards(JwtAuthGuard)
@UseGuards(JwtAuthGuard, PermissionsGuard)
@Permissions('app:read', 'app:write')
@ApiBearerAuth('bearerAuth')
@ApiForbiddenResponse({
description: 'Error: Unauthorized',
Expand All @@ -65,7 +70,8 @@ export class MetricsController {
}

@Get('/timeseries/:type/:pipeline/:phase/:app')
@UseGuards(JwtAuthGuard)
@UseGuards(JwtAuthGuard, PermissionsGuard)
@Permissions('app:read', 'app:write')
@ApiBearerAuth('bearerAuth')
@ApiForbiddenResponse({
description: 'Error: Unauthorized',
Expand Down Expand Up @@ -169,7 +175,8 @@ export class MetricsController {
}

@Get('/rules/:pipeline/:phase/:app')
@UseGuards(JwtAuthGuard)
@UseGuards(JwtAuthGuard, PermissionsGuard)
@Permissions('app:read', 'app:write')
@ApiBearerAuth('bearerAuth')
@ApiForbiddenResponse({
description: 'Error: Unauthorized',
Expand Down
Loading