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
32 changes: 27 additions & 5 deletions backend/internal/access-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import accessListModel from "../models/access_list.js";
import accessListAuthModel from "../models/access_list_auth.js";
import accessListClientModel from "../models/access_list_client.js";
import proxyHostModel from "../models/proxy_host.js";
import streamModel from "../models/stream.js";
import internalAuditLog from "./audit-log.js";
import internalNginx from "./nginx.js";

Expand Down Expand Up @@ -189,7 +190,7 @@ const internalAccessList = {
access,
{
id: data.id,
expand: ["owner", "items", "clients", "proxy_hosts.[certificate,access_list.[clients,items]]"],
expand: ["owner", "items", "clients", "proxy_hosts.[certificate,access_list.[clients,items]]", "streams"],
},
true // skip masking
);
Expand All @@ -198,6 +199,10 @@ const internalAccessList = {
if (Number.parseInt(freshRow.proxy_host_count, 10)) {
await internalNginx.bulkGenerateConfigs("proxy_host", freshRow.proxy_hosts);
}
// Also regenerate stream configs if any streams use this access list
if (freshRow.streams && freshRow.streams.length > 0) {
await internalNginx.bulkGenerateConfigs("stream", freshRow.streams);
}
await internalNginx.reload();
return internalAccessList.maskItems(freshRow);
},
Expand Down Expand Up @@ -228,7 +233,7 @@ const internalAccessList = {
.where("access_list.is_deleted", 0)
.andWhere("access_list.id", thisData.id)
.groupBy("access_list.id")
.allowGraph("[owner,items,clients,proxy_hosts.[certificate,access_list.[clients,items]]]")
.allowGraph("[owner,items,clients,proxy_hosts.[certificate,access_list.[clients,items]],streams]")
.first();

if (accessData.permission_visibility !== "all") {
Expand Down Expand Up @@ -265,16 +270,16 @@ const internalAccessList = {
await access.can("access_lists:delete", data.id);
const row = await internalAccessList.get(access, {
id: data.id,
expand: ["proxy_hosts", "items", "clients"],
expand: ["proxy_hosts", "streams", "items", "clients"],
});

if (!row || !row.id) {
throw new errs.ItemNotFoundError(data.id);
}

// 1. update row to be deleted
// 2. update any proxy hosts that were using it (ignoring permissions)
// 3. reconfigure those hosts
// 2. update any proxy hosts and streams that were using it (ignoring permissions)
// 3. reconfigure those hosts and streams
// 4. audit log

// 1. update row to be deleted
Expand Down Expand Up @@ -302,6 +307,23 @@ const internalAccessList = {
await internalNginx.bulkGenerateConfigs("proxy_host", row.proxy_hosts);
}

// 2b. update any streams that were using it (ignoring permissions)
if (row.streams) {
await streamModel
.query()
.where("access_list_id", "=", row.id)
.patch({ access_list_id: 0 });

// 3b. reconfigure those streams
// set the access_list_id to zero for these items
row.streams.map((_val, idx) => {
row.streams[idx].access_list_id = 0;
return true;
});

await internalNginx.bulkGenerateConfigs("stream", row.streams);
}

await internalNginx.reload();

// delete the htpasswd file
Expand Down
12 changes: 6 additions & 6 deletions backend/internal/stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import internalHost from "./host.js";
import internalNginx from "./nginx.js";

const omissions = () => {
return ["is_deleted", "owner.is_deleted", "certificate.is_deleted"];
return ["is_deleted", "owner.is_deleted", "certificate.is_deleted", "access_list.is_deleted"];
};

const internalStream = {
Expand Down Expand Up @@ -62,7 +62,7 @@ const internalStream = {
// re-fetch with cert
return internalStream.get(access, {
id: row.id,
expand: ["certificate", "owner"],
expand: ["certificate", "owner", "access_list.clients"],
});
})
.then((row) => {
Expand Down Expand Up @@ -159,7 +159,7 @@ const internalStream = {
});
})
.then(() => {
return internalStream.get(access, { id: thisData.id, expand: ["owner", "certificate"] }).then((row) => {
return internalStream.get(access, { id: thisData.id, expand: ["owner", "certificate", "access_list.clients"] }).then((row) => {
return internalNginx.configure(streamModel, "stream", row).then((new_meta) => {
row.meta = new_meta;
return _.omit(internalHost.cleanRowCertificateMeta(row), omissions());
Expand All @@ -186,7 +186,7 @@ const internalStream = {
.query()
.where("is_deleted", 0)
.andWhere("id", thisData.id)
.allowGraph("[owner,certificate]")
.allowGraph("[owner,certificate,access_list.clients]")
.first();

if (access_data.permission_visibility !== "all") {
Expand Down Expand Up @@ -271,7 +271,7 @@ const internalStream = {
.then(() => {
return internalStream.get(access, {
id: data.id,
expand: ["certificate", "owner"],
expand: ["certificate", "owner", "access_list.clients"],
});
})
.then((row) => {
Expand Down Expand Up @@ -375,7 +375,7 @@ const internalStream = {
.query()
.where("is_deleted", 0)
.groupBy("id")
.allowGraph("[owner,certificate]")
.allowGraph("[owner,certificate,access_list.clients]")
.orderBy("incoming_port", "ASC");

if (access_data.permission_visibility !== "all") {
Expand Down
43 changes: 43 additions & 0 deletions backend/migrations/20260123000000_stream_access.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { migrate as logger } from "../logger.js";

const migrateName = "stream_access";

/**
* Migrate
*
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @returns {Promise}
*/
const up = (knex) => {
logger.info(`[${migrateName}] Migrating Up...`);

return knex.schema
.table("stream", (table) => {
table.integer("access_list_id").notNull().unsigned().defaultTo(0);
})
.then(() => {
logger.info(`[${migrateName}] stream Table altered`);
});
};

/**
* Undo Migrate
*
* @param {Object} knex
* @returns {Promise}
*/
const down = (knex) => {
logger.info(`[${migrateName}] Migrating Down...`);

return knex.schema
.table("stream", (table) => {
table.dropColumn("access_list_id");
})
.then(() => {
logger.info(`[${migrateName}] stream Table altered`);
});
};

export { up, down };
12 changes: 12 additions & 0 deletions backend/models/access_list.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import AccessListAuth from "./access_list_auth.js";
import AccessListClient from "./access_list_client.js";
import now from "./now_helper.js";
import ProxyHostModel from "./proxy_host.js";
import StreamModel from "./stream.js";
import User from "./user.js";

Model.knex(db());
Expand Down Expand Up @@ -91,6 +92,17 @@ class AccessList extends Model {
qb.where("proxy_host.is_deleted", 0);
},
},
streams: {
relation: Model.HasManyRelation,
modelClass: StreamModel,
join: {
from: "access_list.id",
to: "stream.access_list_id",
},
modify: (qb) => {
qb.where("stream.is_deleted", 0);
},
},
};
}
}
Expand Down
12 changes: 12 additions & 0 deletions backend/models/stream.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Model } from "objection";
import db from "../db.js";
import { convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js";
import AccessList from "./access_list.js";
import Certificate from "./certificate.js";
import now from "./now_helper.js";
import User from "./user.js";
Expand Down Expand Up @@ -70,6 +71,17 @@ class Stream extends Model {
qb.where("certificate.is_deleted", 0);
},
},
access_list: {
relation: Model.HasOneRelation,
modelClass: AccessList,
join: {
from: "stream.access_list_id",
to: "access_list.id",
},
modify: (qb) => {
qb.where("access_list.is_deleted", 0);
},
},
};
}
}
Expand Down
15 changes: 15 additions & 0 deletions backend/schema/components/stream-object.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"tcp_forwarding",
"udp_forwarding",
"enabled",
"access_list_id",
"meta"
],
"additionalProperties": false,
Expand Down Expand Up @@ -73,6 +74,20 @@
"certificate_id": {
"$ref": "../common.json#/properties/certificate_id"
},
"access_list_id": {
"$ref": "../common.json#/properties/access_list_id"
},
"access_list": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "./access-list-object.json"
}
],
"example": null
},
"meta": {
"type": "object",
"example": {}
Expand Down
3 changes: 2 additions & 1 deletion backend/schema/paths/nginx/streams/get.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@
"nginx_err": null
},
"enabled": true,
"certificate_id": 0
"certificate_id": 0,
"access_list_id": 0
}
]
}
Expand Down
7 changes: 6 additions & 1 deletion backend/schema/paths/nginx/streams/post.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@
"certificate_id": {
"$ref": "../../../components/stream-object.json#/properties/certificate_id"
},
"access_list_id": {
"$ref": "../../../components/stream-object.json#/properties/access_list_id"
},
"meta": {
"$ref": "../../../components/stream-object.json#/properties/meta"
},
Expand All @@ -56,6 +59,7 @@
"tcp_forwarding": true,
"udp_forwarding": false,
"certificate_id": 0,
"access_list_id": 0,
"meta": {}
}
}
Expand Down Expand Up @@ -96,7 +100,8 @@
"admin"
]
},
"certificate_id": 0
"certificate_id": 0,
"access_list_id": 0
}
}
},
Expand Down
3 changes: 2 additions & 1 deletion backend/schema/paths/nginx/streams/streamID/get.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@
"nginx_err": null
},
"enabled": true,
"certificate_id": 0
"certificate_id": 0,
"access_list_id": 0
}
}
},
Expand Down
6 changes: 5 additions & 1 deletion backend/schema/paths/nginx/streams/streamID/put.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@
"certificate_id": {
"$ref": "../../../../components/stream-object.json#/properties/certificate_id"
},
"access_list_id": {
"$ref": "../../../../components/stream-object.json#/properties/access_list_id"
},
"meta": {
"$ref": "../../../../components/stream-object.json#/properties/meta"
}
Expand Down Expand Up @@ -89,7 +92,8 @@
"avatar": "",
"roles": ["admin"]
},
"certificate_id": 0
"certificate_id": 0,
"access_list_id": 0
}
}
},
Expand Down
10 changes: 10 additions & 0 deletions backend/templates/_access_stream.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{% if access_list_id > 0 %}

# Stream Access Control (IP-based only)
# Note: nginx stream module does not support basic auth or satisfy directives
# Access Rules: {{ access_list.clients | size }} total
{% for client in access_list.clients %}
{{client | nginxAccessRule}}
{% endfor %}
deny all;
{% endif %}
4 changes: 4 additions & 0 deletions backend/templates/stream.conf
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ server {

{%- include "_certificates_stream.conf" %}

{%- include "_access_stream.conf" %}

proxy_pass {{ forwarding_host }}:{{ forwarding_port }};

access_log /data/logs/stream-{{ id }}_access.log stream;
Expand All @@ -26,6 +28,8 @@ server {
listen {{ incoming_port }} udp;
{% unless ipv6 -%} # {%- endunless -%} listen [::]:{{ incoming_port }} udp;

{%- include "_access_stream.conf" %}

proxy_pass {{ forwarding_host }}:{{ forwarding_port }};

access_log /data/logs/stream-{{ id }}_access.log stream;
Expand Down
1 change: 1 addition & 0 deletions frontend/src/api/backend/expansions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ export type AuditLogExpansion = "user";
export type CertificateExpansion = "owner" | "proxy_hosts" | "redirection_hosts" | "dead_hosts" | "streams";
export type HostExpansion = "owner" | "certificate";
export type ProxyHostExpansion = "owner" | "access_list" | "certificate";
export type StreamExpansion = "owner" | "certificate" | "access_list";
export type UserExpansion = "permissions";
4 changes: 2 additions & 2 deletions frontend/src/api/backend/getStream.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import * as api from "./base";
import type { HostExpansion } from "./expansions";
import type { StreamExpansion } from "./expansions";
import type { Stream } from "./models";

export async function getStream(id: number, expand?: HostExpansion[], params = {}): Promise<Stream> {
export async function getStream(id: number, expand?: StreamExpansion[], params = {}): Promise<Stream> {
return await api.get({
url: `/nginx/streams/${id}`,
params: {
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/api/backend/getStreams.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import * as api from "./base";
import type { HostExpansion } from "./expansions";
import type { StreamExpansion } from "./expansions";
import type { Stream } from "./models";

export async function getStreams(expand?: HostExpansion[], params = {}): Promise<Stream[]> {
export async function getStreams(expand?: StreamExpansion[], params = {}): Promise<Stream[]> {
return await api.get({
url: "/nginx/streams",
params: {
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/api/backend/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,9 +189,11 @@ export interface Stream {
meta: Record<string, any>;
enabled: boolean;
certificateId: number;
accessListId: number;
// Expansions:
owner?: User;
certificate?: Certificate;
accessList?: AccessList;
}

export interface Setting {
Expand Down
Loading