Browse Source

wip: add abac

pull/1660/head
Bernd Storath 6 months ago
parent
commit
40368b5b06
  1. 10
      src/server/api/client/[clientId]/configuration.get.ts
  2. 9
      src/server/api/client/[clientId]/disable.post.ts
  3. 9
      src/server/api/client/[clientId]/enable.post.ts
  4. 9
      src/server/api/client/[clientId]/generateOneTimeLink.post.ts
  5. 9
      src/server/api/client/[clientId]/index.delete.ts
  6. 10
      src/server/api/client/[clientId]/index.get.ts
  7. 8
      src/server/api/client/[clientId]/index.post.ts
  8. 9
      src/server/api/client/[clientId]/qrcode.svg.get.ts
  9. 7
      src/server/api/client/index.get.ts
  10. 4
      src/server/api/client/index.post.ts
  11. 16
      src/server/database/migrations/0000_short_skin.sql
  12. 2
      src/server/database/migrations/0001_classy_the_stranger.sql
  13. 51
      src/server/database/migrations/meta/0000_snapshot.json
  14. 53
      src/server/database/migrations/meta/0001_snapshot.json
  15. 4
      src/server/database/migrations/meta/_journal.json
  16. 12
      src/server/database/repositories/client/schema.ts
  17. 14
      src/server/database/repositories/client/service.ts
  18. 2
      src/server/database/repositories/client/types.ts
  19. 8
      src/server/database/repositories/general/schema.ts
  20. 2
      src/server/database/repositories/oneTimeLink/schema.ts
  21. 8
      src/server/database/repositories/user/schema.ts
  22. 34
      src/server/utils/WireGuard.ts
  23. 51
      src/server/utils/handler.ts
  24. 113
      src/shared/utils/permissions.ts

10
src/server/api/client/[clientId]/configuration.get.ts

@ -1,30 +1,36 @@
import { ClientGetSchema } from '#db/repositories/client/types'; import { ClientGetSchema } from '#db/repositories/client/types';
export default definePermissionEventHandler( export default definePermissionEventHandler(
actions.CLIENT, 'clients',
async ({ event }) => { 'view',
async ({ event, checkPermissions }) => {
const { clientId } = await getValidatedRouterParams( const { clientId } = await getValidatedRouterParams(
event, event,
validateZod(ClientGetSchema) validateZod(ClientGetSchema)
); );
const client = await Database.clients.get(clientId); const client = await Database.clients.get(clientId);
checkPermissions(client);
if (!client) { if (!client) {
throw createError({ throw createError({
statusCode: 404, statusCode: 404,
statusMessage: 'Client not found', statusMessage: 'Client not found',
}); });
} }
const config = await WireGuard.getClientConfiguration({ clientId }); const config = await WireGuard.getClientConfiguration({ clientId });
const configName = client.name const configName = client.name
.replace(/[^a-zA-Z0-9_=+.-]/g, '-') .replace(/[^a-zA-Z0-9_=+.-]/g, '-')
.replace(/(-{2,}|-$)/g, '-') .replace(/(-{2,}|-$)/g, '-')
.replace(/-$/, '') .replace(/-$/, '')
.substring(0, 32); .substring(0, 32);
setHeader( setHeader(
event, event,
'Content-Disposition', 'Content-Disposition',
`attachment; filename="${configName || clientId}.conf"` `attachment; filename="${configName || clientId}.conf"`
); );
setHeader(event, 'Content-Type', 'text/plain'); setHeader(event, 'Content-Type', 'text/plain');
return config; return config;
} }

9
src/server/api/client/[clientId]/disable.post.ts

@ -1,12 +1,17 @@
import { ClientGetSchema } from '#db/repositories/client/types'; import { ClientGetSchema } from '#db/repositories/client/types';
export default definePermissionEventHandler( export default definePermissionEventHandler(
actions.CLIENT, 'clients',
async ({ event }) => { 'update',
async ({ event, checkPermissions }) => {
const { clientId } = await getValidatedRouterParams( const { clientId } = await getValidatedRouterParams(
event, event,
validateZod(ClientGetSchema) validateZod(ClientGetSchema)
); );
const client = await Database.clients.get(clientId);
checkPermissions(client);
await Database.clients.toggle(clientId, false); await Database.clients.toggle(clientId, false);
await WireGuard.saveConfig(); await WireGuard.saveConfig();
return { success: true }; return { success: true };

9
src/server/api/client/[clientId]/enable.post.ts

@ -1,12 +1,17 @@
import { ClientGetSchema } from '#db/repositories/client/types'; import { ClientGetSchema } from '#db/repositories/client/types';
export default definePermissionEventHandler( export default definePermissionEventHandler(
actions.CLIENT, 'clients',
async ({ event }) => { 'update',
async ({ event, checkPermissions }) => {
const { clientId } = await getValidatedRouterParams( const { clientId } = await getValidatedRouterParams(
event, event,
validateZod(ClientGetSchema) validateZod(ClientGetSchema)
); );
const client = await Database.clients.get(clientId);
checkPermissions(client);
await Database.clients.toggle(clientId, false); await Database.clients.toggle(clientId, false);
await WireGuard.saveConfig(); await WireGuard.saveConfig();
return { success: true }; return { success: true };

9
src/server/api/client/[clientId]/generateOneTimeLink.post.ts

@ -1,12 +1,17 @@
import { ClientGetSchema } from '#db/repositories/client/types'; import { ClientGetSchema } from '#db/repositories/client/types';
export default definePermissionEventHandler( export default definePermissionEventHandler(
actions.CLIENT, 'clients',
async ({ event }) => { 'update',
async ({ event, checkPermissions }) => {
const { clientId } = await getValidatedRouterParams( const { clientId } = await getValidatedRouterParams(
event, event,
validateZod(ClientGetSchema) validateZod(ClientGetSchema)
); );
const client = await Database.clients.get(clientId);
checkPermissions(client);
await Database.oneTimeLinks.generate(clientId); await Database.oneTimeLinks.generate(clientId);
return { success: true }; return { success: true };
} }

9
src/server/api/client/[clientId]/index.delete.ts

@ -1,12 +1,17 @@
import { ClientGetSchema } from '#db/repositories/client/types'; import { ClientGetSchema } from '#db/repositories/client/types';
export default definePermissionEventHandler( export default definePermissionEventHandler(
actions.CLIENT, 'clients',
async ({ event }) => { 'delete',
async ({ event, checkPermissions }) => {
const { clientId } = await getValidatedRouterParams( const { clientId } = await getValidatedRouterParams(
event, event,
validateZod(ClientGetSchema) validateZod(ClientGetSchema)
); );
const client = await Database.clients.get(clientId);
checkPermissions(client);
await Database.clients.delete(clientId); await Database.clients.delete(clientId);
await WireGuard.saveConfig(); await WireGuard.saveConfig();
return { success: true }; return { success: true };

10
src/server/api/client/[clientId]/index.get.ts

@ -1,13 +1,17 @@
import { ClientGetSchema } from '~~/server/database/repositories/client/types'; import { ClientGetSchema } from '#db/repositories/client/types';
export default definePermissionEventHandler( export default definePermissionEventHandler(
actions.CLIENT, 'clients',
async ({ event }) => { 'view',
async ({ event, checkPermissions }) => {
const { clientId } = await getValidatedRouterParams( const { clientId } = await getValidatedRouterParams(
event, event,
validateZod(ClientGetSchema, event) validateZod(ClientGetSchema, event)
); );
const result = await Database.clients.get(clientId); const result = await Database.clients.get(clientId);
checkPermissions(result);
if (!result) { if (!result) {
throw createError({ throw createError({
statusCode: 404, statusCode: 404,

8
src/server/api/client/[clientId]/index.post.ts

@ -4,8 +4,9 @@ import {
} from '#db/repositories/client/types'; } from '#db/repositories/client/types';
export default definePermissionEventHandler( export default definePermissionEventHandler(
actions.CLIENT, 'clients',
async ({ event }) => { 'update',
async ({ event, checkPermissions }) => {
const { clientId } = await getValidatedRouterParams( const { clientId } = await getValidatedRouterParams(
event, event,
validateZod(ClientGetSchema) validateZod(ClientGetSchema)
@ -16,6 +17,9 @@ export default definePermissionEventHandler(
validateZod(ClientUpdateSchema, event) validateZod(ClientUpdateSchema, event)
); );
const client = await Database.clients.get(clientId);
checkPermissions(client);
await Database.clients.update(clientId, data); await Database.clients.update(clientId, data);
await WireGuard.saveConfig(); await WireGuard.saveConfig();

9
src/server/api/client/[clientId]/qrcode.svg.get.ts

@ -1,12 +1,17 @@
import { ClientGetSchema } from '#db/repositories/client/types'; import { ClientGetSchema } from '#db/repositories/client/types';
export default definePermissionEventHandler( export default definePermissionEventHandler(
actions.CLIENT, 'clients',
async ({ event }) => { 'view',
async ({ event, checkPermissions }) => {
const { clientId } = await getValidatedRouterParams( const { clientId } = await getValidatedRouterParams(
event, event,
validateZod(ClientGetSchema) validateZod(ClientGetSchema)
); );
const client = await Database.clients.get(clientId);
checkPermissions(client);
const svg = await WireGuard.getClientQRCodeSVG({ clientId }); const svg = await WireGuard.getClientQRCodeSVG({ clientId });
setHeader(event, 'Content-Type', 'image/svg+xml'); setHeader(event, 'Content-Type', 'image/svg+xml');
return svg; return svg;

7
src/server/api/client/index.get.ts

@ -1,3 +1,6 @@
export default definePermissionEventHandler(actions.CLIENT, () => { export default definePermissionEventHandler('clients', 'custom', ({ user }) => {
return WireGuard.getClients(); if (user.role === roles.ADMIN) {
return WireGuard.getAllClients();
}
return WireGuard.getClientsForUser(user.id);
}); });

4
src/server/api/client/index.post.ts

@ -1,12 +1,14 @@
import { ClientCreateSchema } from '#db/repositories/client/types'; import { ClientCreateSchema } from '#db/repositories/client/types';
export default definePermissionEventHandler( export default definePermissionEventHandler(
actions.CLIENT, 'clients',
'create',
async ({ event }) => { async ({ event }) => {
const { name, expiresAt } = await readValidatedBody( const { name, expiresAt } = await readValidatedBody(
event, event,
validateZod(ClientCreateSchema) validateZod(ClientCreateSchema)
); );
await Database.clients.create({ name, expiresAt }); await Database.clients.create({ name, expiresAt });
await WireGuard.saveConfig(); await WireGuard.saveConfig();
return { success: true }; return { success: true };

16
src/server/database/migrations/0000_short_skin.sql

@ -1,5 +1,6 @@
CREATE TABLE `clients_table` ( CREATE TABLE `clients_table` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`user_id` integer NOT NULL,
`name` text NOT NULL, `name` text NOT NULL,
`ipv4_address` text NOT NULL, `ipv4_address` text NOT NULL,
`ipv6_address` text NOT NULL, `ipv6_address` text NOT NULL,
@ -14,19 +15,20 @@ CREATE TABLE `clients_table` (
`dns` text NOT NULL, `dns` text NOT NULL,
`enabled` integer NOT NULL, `enabled` integer NOT NULL,
`created_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL, `created_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL,
`updated_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL `updated_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `users_table`(`id`) ON UPDATE cascade ON DELETE restrict
); );
--> statement-breakpoint --> statement-breakpoint
CREATE UNIQUE INDEX `clients_table_ipv4_address_unique` ON `clients_table` (`ipv4_address`);--> statement-breakpoint CREATE UNIQUE INDEX `clients_table_ipv4_address_unique` ON `clients_table` (`ipv4_address`);--> statement-breakpoint
CREATE UNIQUE INDEX `clients_table_ipv6_address_unique` ON `clients_table` (`ipv6_address`);--> statement-breakpoint CREATE UNIQUE INDEX `clients_table_ipv6_address_unique` ON `clients_table` (`ipv6_address`);--> statement-breakpoint
CREATE TABLE `general_table` ( CREATE TABLE `general_table` (
`id` integer PRIMARY KEY DEFAULT 1 NOT NULL, `id` integer PRIMARY KEY DEFAULT 1 NOT NULL,
`setupStep` integer NOT NULL, `setup_step` integer NOT NULL,
`session_password` text NOT NULL, `session_password` text NOT NULL,
`session_timeout` integer NOT NULL, `session_timeout` integer NOT NULL,
`metricsPrometheus` integer NOT NULL, `metrics_prometheus` integer NOT NULL,
`metricsJson` integer NOT NULL, `metrics_json` integer NOT NULL,
`metricsPassword` text, `metrics_password` text,
`created_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL, `created_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL,
`updated_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL `updated_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL
); );
@ -61,10 +63,10 @@ CREATE TABLE `one_time_links_table` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`one_time_link` text NOT NULL, `one_time_link` text NOT NULL,
`expires_at` text NOT NULL, `expires_at` text NOT NULL,
`clientId` integer NOT NULL, `client_id` integer NOT NULL,
`created_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL, `created_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL,
`updated_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL, `updated_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL,
FOREIGN KEY (`clientId`) REFERENCES `clients_table`(`id`) ON UPDATE cascade ON DELETE cascade FOREIGN KEY (`client_id`) REFERENCES `clients_table`(`id`) ON UPDATE cascade ON DELETE cascade
); );
--> statement-breakpoint --> statement-breakpoint
CREATE UNIQUE INDEX `one_time_links_table_one_time_link_unique` ON `one_time_links_table` (`one_time_link`);--> statement-breakpoint CREATE UNIQUE INDEX `one_time_links_table_one_time_link_unique` ON `one_time_links_table` (`one_time_link`);--> statement-breakpoint

2
src/server/database/migrations/0001_classy_the_stranger.sql

@ -1,5 +1,5 @@
PRAGMA journal_mode=WAL;--> statement-breakpoint PRAGMA journal_mode=WAL;--> statement-breakpoint
INSERT INTO `general_table` (`setupStep`, `session_password`, `session_timeout`, `metricsPrometheus`, `metricsJson`) INSERT INTO `general_table` (`setup_step`, `session_password`, `session_timeout`, `metrics_prometheus`, `metrics_json`)
VALUES (1, hex(randomblob(256)), 3600, 0, 0); VALUES (1, hex(randomblob(256)), 3600, 0, 0);
--> statement-breakpoint --> statement-breakpoint
INSERT INTO `interfaces_table` (`name`, `device`, `port`, `private_key`, `public_key`, `ipv4_cidr`, `ipv6_cidr`, `mtu`, `enabled`) INSERT INTO `interfaces_table` (`name`, `device`, `port`, `private_key`, `public_key`, `ipv4_cidr`, `ipv6_cidr`, `mtu`, `enabled`)

51
src/server/database/migrations/meta/0000_snapshot.json

@ -1,7 +1,7 @@
{ {
"version": "6", "version": "6",
"dialect": "sqlite", "dialect": "sqlite",
"id": "2c4694af-5916-430f-96d3-55aac2653e7e", "id": "b1dde023-d141-4eab-9226-89a832b2ed2b",
"prevId": "00000000-0000-0000-0000-000000000000", "prevId": "00000000-0000-0000-0000-000000000000",
"tables": { "tables": {
"clients_table": { "clients_table": {
@ -14,6 +14,13 @@
"notNull": true, "notNull": true,
"autoincrement": true "autoincrement": true
}, },
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"name": { "name": {
"name": "name", "name": "name",
"type": "text", "type": "text",
@ -138,7 +145,21 @@
"isUnique": true "isUnique": true
} }
}, },
"foreignKeys": {}, "foreignKeys": {
"clients_table_user_id_users_table_id_fk": {
"name": "clients_table_user_id_users_table_id_fk",
"tableFrom": "clients_table",
"tableTo": "users_table",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "restrict",
"onUpdate": "cascade"
}
},
"compositePrimaryKeys": {}, "compositePrimaryKeys": {},
"uniqueConstraints": {}, "uniqueConstraints": {},
"checkConstraints": {} "checkConstraints": {}
@ -154,8 +175,8 @@
"autoincrement": false, "autoincrement": false,
"default": 1 "default": 1
}, },
"setupStep": { "setup_step": {
"name": "setupStep", "name": "setup_step",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
@ -175,22 +196,22 @@
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"metricsPrometheus": { "metrics_prometheus": {
"name": "metricsPrometheus", "name": "metrics_prometheus",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"metricsJson": { "metrics_json": {
"name": "metricsJson", "name": "metrics_json",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"metricsPassword": { "metrics_password": {
"name": "metricsPassword", "name": "metrics_password",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
@ -415,8 +436,8 @@
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"clientId": { "client_id": {
"name": "clientId", "name": "client_id",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
@ -449,12 +470,12 @@
} }
}, },
"foreignKeys": { "foreignKeys": {
"one_time_links_table_clientId_clients_table_id_fk": { "one_time_links_table_client_id_clients_table_id_fk": {
"name": "one_time_links_table_clientId_clients_table_id_fk", "name": "one_time_links_table_client_id_clients_table_id_fk",
"tableFrom": "one_time_links_table", "tableFrom": "one_time_links_table",
"tableTo": "clients_table", "tableTo": "clients_table",
"columnsFrom": [ "columnsFrom": [
"clientId" "client_id"
], ],
"columnsTo": [ "columnsTo": [
"id" "id"

53
src/server/database/migrations/meta/0001_snapshot.json

@ -1,6 +1,6 @@
{ {
"id": "91d39ed5-2c45-4af6-ba39-4cd72ba71f6a", "id": "720d420c-361f-4427-a45b-db0ca613934d",
"prevId": "2c4694af-5916-430f-96d3-55aac2653e7e", "prevId": "b1dde023-d141-4eab-9226-89a832b2ed2b",
"version": "6", "version": "6",
"dialect": "sqlite", "dialect": "sqlite",
"tables": { "tables": {
@ -14,6 +14,13 @@
"notNull": true, "notNull": true,
"autoincrement": true "autoincrement": true
}, },
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"name": { "name": {
"name": "name", "name": "name",
"type": "text", "type": "text",
@ -138,7 +145,21 @@
"isUnique": true "isUnique": true
} }
}, },
"foreignKeys": {}, "foreignKeys": {
"clients_table_user_id_users_table_id_fk": {
"name": "clients_table_user_id_users_table_id_fk",
"tableFrom": "clients_table",
"columnsFrom": [
"user_id"
],
"tableTo": "users_table",
"columnsTo": [
"id"
],
"onUpdate": "cascade",
"onDelete": "restrict"
}
},
"compositePrimaryKeys": {}, "compositePrimaryKeys": {},
"uniqueConstraints": {}, "uniqueConstraints": {},
"checkConstraints": {} "checkConstraints": {}
@ -154,8 +175,8 @@
"autoincrement": false, "autoincrement": false,
"default": 1 "default": 1
}, },
"setupStep": { "setup_step": {
"name": "setupStep", "name": "setup_step",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
@ -175,22 +196,22 @@
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"metricsPrometheus": { "metrics_prometheus": {
"name": "metricsPrometheus", "name": "metrics_prometheus",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"metricsJson": { "metrics_json": {
"name": "metricsJson", "name": "metrics_json",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"metricsPassword": { "metrics_password": {
"name": "metricsPassword", "name": "metrics_password",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
@ -415,8 +436,8 @@
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"clientId": { "client_id": {
"name": "clientId", "name": "client_id",
"type": "integer", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
@ -449,11 +470,11 @@
} }
}, },
"foreignKeys": { "foreignKeys": {
"one_time_links_table_clientId_clients_table_id_fk": { "one_time_links_table_client_id_clients_table_id_fk": {
"name": "one_time_links_table_clientId_clients_table_id_fk", "name": "one_time_links_table_client_id_clients_table_id_fk",
"tableFrom": "one_time_links_table", "tableFrom": "one_time_links_table",
"columnsFrom": [ "columnsFrom": [
"clientId" "client_id"
], ],
"tableTo": "clients_table", "tableTo": "clients_table",
"columnsTo": [ "columnsTo": [

4
src/server/database/migrations/meta/_journal.json

@ -5,14 +5,14 @@
{ {
"idx": 0, "idx": 0,
"version": "6", "version": "6",
"when": 1739191645161, "when": 1739266828300,
"tag": "0000_short_skin", "tag": "0000_short_skin",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 1, "idx": 1,
"version": "6", "version": "6",
"when": 1739191678456, "when": 1739266837347,
"tag": "0001_classy_the_stranger", "tag": "0001_classy_the_stranger",
"breakpoints": true "breakpoints": true
} }

12
src/server/database/repositories/client/schema.ts

@ -1,10 +1,16 @@
import { sql, relations } from 'drizzle-orm'; import { sql, relations } from 'drizzle-orm';
import { int, sqliteTable, text } from 'drizzle-orm/sqlite-core'; import { int, sqliteTable, text } from 'drizzle-orm/sqlite-core';
import { oneTimeLink } from '../../schema'; import { oneTimeLink, user } from '../../schema';
export const client = sqliteTable('clients_table', { export const client = sqliteTable('clients_table', {
id: int().primaryKey({ autoIncrement: true }), id: int().primaryKey({ autoIncrement: true }),
userId: int('user_id')
.notNull()
.references(() => user.id, {
onDelete: 'restrict',
onUpdate: 'cascade',
}),
name: text().notNull(), name: text().notNull(),
ipv4Address: text('ipv4_address').notNull().unique(), ipv4Address: text('ipv4_address').notNull().unique(),
ipv6Address: text('ipv6_address').notNull().unique(), ipv6Address: text('ipv6_address').notNull().unique(),
@ -34,4 +40,8 @@ export const clientsRelations = relations(client, ({ one }) => ({
fields: [client.id], fields: [client.id],
references: [oneTimeLink.clientId], references: [oneTimeLink.clientId],
}), }),
user: one(user, {
fields: [client.userId],
references: [user.id],
}),
})); }));

14
src/server/database/repositories/client/service.ts

@ -18,6 +18,9 @@ function createPreparedStatement(db: DBType) {
findById: db.query.client findById: db.query.client
.findFirst({ where: eq(client.id, sql.placeholder('id')) }) .findFirst({ where: eq(client.id, sql.placeholder('id')) })
.prepare(), .prepare(),
findByUserId: db.query.client
.findMany({ where: eq(client.userId, sql.placeholder('userId')) })
.prepare(),
toggle: db toggle: db
.update(client) .update(client)
.set({ enabled: sql.placeholder('enabled') as never as boolean }) .set({ enabled: sql.placeholder('enabled') as never as boolean })
@ -39,6 +42,15 @@ export class ClientService {
this.#statements = createPreparedStatement(db); this.#statements = createPreparedStatement(db);
} }
async getForUser(userId: ID) {
const result = await this.#statements.findByUserId.execute({ userId });
return result.map((row) => ({
...row,
createdAt: new Date(row.createdAt),
updatedAt: new Date(row.updatedAt),
}));
}
async getAll() { async getAll() {
const result = await this.#statements.findAll.execute(); const result = await this.#statements.findAll.execute();
return result.map((row) => ({ return result.map((row) => ({
@ -97,6 +109,8 @@ export class ClientService {
.insert(client) .insert(client)
.values({ .values({
name, name,
// TODO: fix
userId: 1,
expiresAt: parsedExpiresAt, expiresAt: parsedExpiresAt,
privateKey, privateKey,
publicKey, publicKey,

2
src/server/database/repositories/client/types.ts

@ -14,7 +14,7 @@ export type CreateClientType = Omit<
export type UpdateClientType = Omit< export type UpdateClientType = Omit<
CreateClientType, CreateClientType,
'privateKey' | 'publicKey' | 'preSharedKey' 'privateKey' | 'publicKey' | 'preSharedKey' | 'userId'
>; >;
const name = z const name = z

8
src/server/database/repositories/general/schema.ts

@ -4,14 +4,14 @@ import { sqliteTable, text, int } from 'drizzle-orm/sqlite-core';
export const general = sqliteTable('general_table', { export const general = sqliteTable('general_table', {
id: int().primaryKey({ autoIncrement: false }).default(1), id: int().primaryKey({ autoIncrement: false }).default(1),
setupStep: int().notNull(), setupStep: int('setup_step').notNull(),
sessionPassword: text('session_password').notNull(), sessionPassword: text('session_password').notNull(),
sessionTimeout: int('session_timeout').notNull(), sessionTimeout: int('session_timeout').notNull(),
metricsPrometheus: int({ mode: 'boolean' }).notNull(), metricsPrometheus: int('metrics_prometheus', { mode: 'boolean' }).notNull(),
metricsJson: int({ mode: 'boolean' }).notNull(), metricsJson: int('metrics_json', { mode: 'boolean' }).notNull(),
metricsPassword: text(), metricsPassword: text('metrics_password'),
createdAt: text('created_at') createdAt: text('created_at')
.notNull() .notNull()

2
src/server/database/repositories/oneTimeLink/schema.ts

@ -7,7 +7,7 @@ export const oneTimeLink = sqliteTable('one_time_links_table', {
id: int().primaryKey({ autoIncrement: true }), id: int().primaryKey({ autoIncrement: true }),
oneTimeLink: text('one_time_link').notNull().unique(), oneTimeLink: text('one_time_link').notNull().unique(),
expiresAt: text('expires_at').notNull(), expiresAt: text('expires_at').notNull(),
clientId: int() clientId: int('client_id')
.notNull() .notNull()
.references(() => client.id, { onDelete: 'cascade', onUpdate: 'cascade' }), .references(() => client.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
createdAt: text('created_at') createdAt: text('created_at')

8
src/server/database/repositories/user/schema.ts

@ -1,6 +1,8 @@
import { sql } from 'drizzle-orm'; import { sql, relations } from 'drizzle-orm';
import { int, sqliteTable, text } from 'drizzle-orm/sqlite-core'; import { int, sqliteTable, text } from 'drizzle-orm/sqlite-core';
import { client } from '../../schema';
export const user = sqliteTable('users_table', { export const user = sqliteTable('users_table', {
id: int().primaryKey({ autoIncrement: true }), id: int().primaryKey({ autoIncrement: true }),
username: text().notNull().unique(), username: text().notNull().unique(),
@ -17,3 +19,7 @@ export const user = sqliteTable('users_table', {
.default(sql`(CURRENT_TIMESTAMP)`) .default(sql`(CURRENT_TIMESTAMP)`)
.$onUpdate(() => sql`(CURRENT_TIMESTAMP)`), .$onUpdate(() => sql`(CURRENT_TIMESTAMP)`),
}); });
export const usersRelations = relations(user, ({ many }) => ({
clients: many(client),
}));

34
src/server/utils/WireGuard.ts

@ -52,7 +52,39 @@ class WireGuard {
WG_DEBUG('Config synced successfully.'); WG_DEBUG('Config synced successfully.');
} }
async getClients() { async getClientsForUser(userId: ID) {
const wgInterface = await Database.interfaces.get();
const dbClients = await Database.clients.getForUser(userId);
const clients = dbClients.map((client) => ({
...client,
latestHandshakeAt: null as Date | null,
endpoint: null as string | null,
transferRx: null as number | null,
transferTx: null as number | null,
}));
// Loop WireGuard status
const dump = await wg.dump(wgInterface.name);
dump.forEach(
({ publicKey, latestHandshakeAt, endpoint, transferRx, transferTx }) => {
const client = clients.find((client) => client.publicKey === publicKey);
if (!client) {
return;
}
client.latestHandshakeAt = latestHandshakeAt;
client.endpoint = endpoint;
client.transferRx = transferRx;
client.transferTx = transferTx;
}
);
return clients;
}
async getAllClients() {
const wgInterface = await Database.interfaces.get(); const wgInterface = await Database.interfaces.get();
const dbClients = await Database.clients.getAll(); const dbClients = await Database.clients.getAll();
const clients = dbClients.map((client) => ({ const clients = dbClients.map((client) => ({

51
src/server/utils/handler.ts

@ -1,32 +1,65 @@
import type { EventHandlerRequest, EventHandlerResponse, H3Event } from 'h3'; import type { EventHandlerRequest, EventHandlerResponse, H3Event } from 'h3';
import type { UserType } from '#db/repositories/user/types'; import type { UserType } from '#db/repositories/user/types';
import type { SetupStepType } from '../database/repositories/general/types'; import type { SetupStepType } from '#db/repositories/general/types';
import {
type Permissions,
hasPermissionsWithData,
} from '#shared/utils/permissions';
type PermissionHandler< type PermissionHandler<
TReq extends EventHandlerRequest, TReq extends EventHandlerRequest,
TRes extends EventHandlerResponse, TRes extends EventHandlerResponse,
> = { (params: { event: H3Event<TReq>; user: UserType }): TRes }; Resource extends keyof Permissions,
> = {
(params: {
event: H3Event<TReq>;
user: UserType;
/**
* check if user has permissions to access the resource
*
* see: {@link hasPermissionsWithData}
*/
checkPermissions: (data?: Permissions[Resource]['dataType']) => true;
}): TRes;
};
/** /**
* check if the user has the permission to perform the action * get current user
*/ */
export const definePermissionEventHandler = < export const definePermissionEventHandler = <
TReq extends EventHandlerRequest, TReq extends EventHandlerRequest,
TRes extends EventHandlerResponse, TRes extends EventHandlerResponse,
Resource extends keyof Permissions,
>( >(
action: Action, resource: Resource,
handler: PermissionHandler<TReq, TRes> action: Permissions[Resource]['action'],
handler: PermissionHandler<TReq, TRes, Resource>
) => { ) => {
return defineEventHandler(async (event) => { return defineEventHandler(async (event) => {
const user = await getCurrentUser(event); const user = await getCurrentUser(event);
if (!checkPermissions(action, user)) {
const permissions = hasPermissionsWithData(user, resource, action);
// if no data is required, check permissions
if (permissions.isBoolean()) {
permissions.check();
}
const response = await handler({
event,
user,
checkPermissions: permissions.check,
});
// if data is required, make sure permissions were checked
if (!permissions.checked) {
throw createError({ throw createError({
statusCode: 403, statusCode: 500,
statusMessage: 'Forbidden', statusMessage: 'Permission was not checked',
}); });
} }
return await handler({ event, user }); return response;
}); });
}; };

113
src/shared/utils/permissions.ts

@ -1,9 +1,5 @@
// TODO: implement ABAC import type { ClientType } from '#db/repositories/client/types';
import type { UserType } from '#db/repositories/user/types';
export const actions = {
ADMIN: 'ADMIN',
CLIENT: 'CLIENT',
} as const;
export type Role = number & { readonly __role: unique symbol }; export type Role = number & { readonly __role: unique symbol };
@ -12,20 +8,105 @@ export const roles = {
CLIENT: 2 as Role, CLIENT: 2 as Role,
} as const; } as const;
export type Action = keyof typeof actions; type Roles = keyof typeof roles;
/**
* convert role to key
* @example roleToKey(roles.ADMIN) => 'ADMIN'
*/
function roleToKey(role: Role) {
const roleKey = Object.keys(roles).find(
(key) => roles[key as Roles] === role
);
if (roleKey === undefined) {
throw new Error('Invalid role');
}
return roleKey as Roles;
}
type PermissionCheck<Key extends keyof Permissions> =
| boolean
| ((user: UserType, data: Permissions[Key]['dataType']) => boolean);
type MATRIX = { type RolesWithPermissions = {
readonly [key in keyof typeof actions]: readonly (typeof roles)[keyof typeof roles][]; [R in Roles]: {
[Key in keyof Permissions]: {
[Action in Permissions[Key]['action']]: PermissionCheck<Key>;
};
};
}; };
export const MATRIX: MATRIX = { export type Permissions = {
[actions.ADMIN]: [roles.ADMIN] as const, clients: {
[actions.CLIENT]: [roles.CLIENT, roles.ADMIN] as const, dataType: ClientType;
} as const; action: 'view' | 'create' | 'update' | 'delete' | 'custom';
};
};
export const ROLES = {
ADMIN: {
clients: {
view: true,
create: true,
update: true,
delete: true,
custom: true,
},
},
CLIENT: {
clients: {
view: (user, client) => user.id === client.userId,
create: false,
update: (user, client) => user.id === client.userId,
delete: (user, client) => user.id === client.userId,
custom: true,
},
},
} as const satisfies RolesWithPermissions;
export function hasPermissions<Resource extends keyof Permissions>(
user: UserType,
resource: Resource,
action: Permissions[Resource]['action'],
data?: Permissions[Resource]['dataType']
) {
const permission = ROLES[roleToKey(user.role)][resource][action];
export const checkPermissions = (action: Action, user: { role: Role }) => { if (typeof permission === 'boolean') {
if (!MATRIX[action].includes(user.role)) { return permission;
}
if (data === undefined) {
return false; return false;
} }
return true;
return permission(user, data);
}
export function hasPermissionsWithData<Resource extends keyof Permissions>(
user: UserType,
resource: Resource,
action: Permissions[Resource]['action']
) {
let checked = false;
return {
check(data?: Permissions[Resource]['dataType']) {
checked = true;
const isAllowed = hasPermissions(user, resource, action, data);
if (!isAllowed) {
throw new Error('Permission denied');
}
return isAllowed;
},
isBoolean() {
return typeof ROLES[roleToKey(user.role)][resource][action] === 'boolean';
},
get checked() {
return checked;
},
}; };
}

Loading…
Cancel
Save