feat(gateway-clients): add policy-based gateway client tokens and gateway client route and DNS management endpoints

This commit is contained in:
2026-05-09 11:53:45 +00:00
parent 7e3b89d9b4
commit 97505935bb
15 changed files with 604 additions and 92 deletions
+1 -1
View File
@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/dcrouter',
version: '13.25.0',
version: '13.26.0',
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
}
+7 -1
View File
@@ -2506,7 +2506,12 @@ export const fetchUsersAction = usersStatePart.createAction(async (statePartArg)
}
});
export async function createApiToken(name: string, scopes: interfaces.data.TApiTokenScope[], expiresInDays?: number | null) {
export async function createApiToken(
name: string,
scopes: interfaces.data.TApiTokenScope[],
expiresInDays?: number | null,
policy?: any,
) {
const context = getActionContext();
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_CreateApiToken
@@ -2516,6 +2521,7 @@ export async function createApiToken(name: string, scopes: interfaces.data.TApiT
identity: context.identity!,
name,
scopes,
policy,
expiresInDays,
});
}
+47 -2
View File
@@ -200,6 +200,7 @@ export class OpsViewApiTokens extends DeesElement {
const { DeesModal } = await import('@design.estate/dees-catalog');
const allScopes = [
'*',
'routes:read',
'routes:write',
'config:read',
@@ -213,6 +214,8 @@ export class OpsViewApiTokens extends DeesElement {
'dns-records:write',
'email-domains:read',
'email-domains:write',
'gateway-clients:read',
'gateway-clients:write',
'workhosters:read',
'workhosters:write',
];
@@ -228,10 +231,15 @@ export class OpsViewApiTokens extends DeesElement {
<dees-input-tags
.key=${'scopes'}
.label=${'Token Scopes'}
.value=${['routes:read', 'routes:write']}
.value=${['gateway-clients:read', 'gateway-clients:write']}
.suggestions=${allScopes}
.required=${true}
></dees-input-tags>
<dees-input-text .key=${'policyRole'} .label=${'Policy Role'} .description=${'admin, gatewayClient, or operator'}></dees-input-text>
<dees-input-text .key=${'gatewayClientType'} .label=${'Gateway Client Type'} .description=${'For gatewayClient tokens: onebox, cloudly, or custom'}></dees-input-text>
<dees-input-text .key=${'gatewayClientId'} .label=${'Gateway Client ID'} .description=${'Required for gatewayClient tokens'}></dees-input-text>
<dees-input-text .key=${'hostnamePatterns'} .label=${'Hostname Patterns'} .description=${'Comma separated, e.g. *.apps.example.com'}></dees-input-text>
<dees-input-text .key=${'allowedRouteTarget'} .label=${'Allowed Route Target'} .description=${'Optional host:ports, e.g. 203.0.113.10:80,443'}></dees-input-text>
<dees-input-text .key=${'expiresInDays'} .label=${'Expires in'} .description=${'Number of days; leave blank for no expiration'}></dees-input-text>
</dees-form>
`,
@@ -257,6 +265,7 @@ export class OpsViewApiTokens extends DeesElement {
const rawScopes: string[] = tagsInput?.getValue?.() || tagsInput?.value || formData.scopes || [];
const scopes = rawScopes
.filter((s: string) => allScopes.includes(s as any)) as TApiTokenScope[];
const policy = this.buildPolicy(formData, scopes);
const expiresInDays = formData.expiresInDays
? parseInt(formData.expiresInDays, 10)
@@ -265,7 +274,7 @@ export class OpsViewApiTokens extends DeesElement {
await modalArg.destroy();
try {
const response = await appstate.createApiToken(formData.name, scopes, expiresInDays);
const response = await appstate.createApiToken(formData.name, scopes, expiresInDays, policy);
if (response.success && response.tokenValue) {
// Refresh the list first so it's ready when user dismisses the modal
await appstate.routeManagementStatePart.dispatchAction(appstate.fetchApiTokensAction, null);
@@ -299,6 +308,42 @@ export class OpsViewApiTokens extends DeesElement {
});
}
private buildPolicy(formData: any, scopes: TApiTokenScope[]): any | undefined {
const role = String(formData.policyRole || '').trim();
if (!role) return undefined;
const policy: any = {
role,
scopes,
};
if (role === 'gatewayClient') {
const type = String(formData.gatewayClientType || 'onebox').trim() as 'onebox' | 'cloudly' | 'custom';
const id = String(formData.gatewayClientId || '').trim();
if (id) {
policy.gatewayClient = { type, id };
}
policy.hostnamePatterns = String(formData.hostnamePatterns || '')
.split(',')
.map((pattern) => pattern.trim())
.filter(Boolean);
const target = String(formData.allowedRouteTarget || '').trim();
if (target.includes(':')) {
const [host, portsValue] = target.split(':');
policy.allowedRouteTargets = [{
host: host.trim(),
ports: portsValue.split(',').map((port) => Number(port.trim())).filter((port) => Number.isInteger(port)),
}];
}
policy.capabilities = {
readDomains: true,
readDnsRecords: true,
syncRoutes: true,
syncDnsRecords: false,
requestCertificates: false,
};
}
return policy;
}
private async showRollTokenDialog(token: interfaces.data.IApiTokenInfo) {
const { DeesModal } = await import('@design.estate/dees-catalog');