feat(gateways): decouple gateways from projects

This commit is contained in:
Daniel Hougaard
2025-05-13 14:59:58 +04:00
parent fb2b64cb19
commit daf0731580
29 changed files with 361 additions and 405 deletions
-8
View File
@@ -215,9 +215,6 @@ import {
TProjectEnvironments, TProjectEnvironments,
TProjectEnvironmentsInsert, TProjectEnvironmentsInsert,
TProjectEnvironmentsUpdate, TProjectEnvironmentsUpdate,
TProjectGateways,
TProjectGatewaysInsert,
TProjectGatewaysUpdate,
TProjectKeys, TProjectKeys,
TProjectKeysInsert, TProjectKeysInsert,
TProjectKeysUpdate, TProjectKeysUpdate,
@@ -1018,11 +1015,6 @@ declare module "knex/types/tables" {
TKmipClientCertificatesUpdate TKmipClientCertificatesUpdate
>; >;
[TableName.Gateway]: KnexOriginal.CompositeTableType<TGateways, TGatewaysInsert, TGatewaysUpdate>; [TableName.Gateway]: KnexOriginal.CompositeTableType<TGateways, TGatewaysInsert, TGatewaysUpdate>;
[TableName.ProjectGateway]: KnexOriginal.CompositeTableType<
TProjectGateways,
TProjectGatewaysInsert,
TProjectGatewaysUpdate
>;
[TableName.OrgGatewayConfig]: KnexOriginal.CompositeTableType< [TableName.OrgGatewayConfig]: KnexOriginal.CompositeTableType<
TOrgGatewayConfig, TOrgGatewayConfig,
TOrgGatewayConfigInsert, TOrgGatewayConfigInsert,
@@ -68,8 +68,8 @@ export async function up(knex: Knex): Promise<void> {
await createOnUpdateTrigger(knex, TableName.Gateway); await createOnUpdateTrigger(knex, TableName.Gateway);
} }
if (!(await knex.schema.hasTable(TableName.ProjectGateway))) { if (!(await knex.schema.hasTable("project_gateways"))) {
await knex.schema.createTable(TableName.ProjectGateway, (t) => { await knex.schema.createTable("project_gateways", (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("projectId").notNullable(); t.string("projectId").notNullable();
@@ -81,7 +81,7 @@ export async function up(knex: Knex): Promise<void> {
t.timestamps(true, true, true); t.timestamps(true, true, true);
}); });
await createOnUpdateTrigger(knex, TableName.ProjectGateway); await createOnUpdateTrigger(knex, "project_gateways");
} }
if (await knex.schema.hasTable(TableName.DynamicSecret)) { if (await knex.schema.hasTable(TableName.DynamicSecret)) {
@@ -90,7 +90,7 @@ export async function up(knex: Knex): Promise<void> {
// not setting a foreign constraint so that cascade effects are not triggered // not setting a foreign constraint so that cascade effects are not triggered
if (!doesGatewayColExist) { if (!doesGatewayColExist) {
t.uuid("projectGatewayId"); t.uuid("projectGatewayId");
t.foreign("projectGatewayId").references("id").inTable(TableName.ProjectGateway); t.foreign("projectGatewayId").references("id").inTable("project_gateways");
} }
}); });
} }
@@ -104,8 +104,8 @@ export async function down(knex: Knex): Promise<void> {
}); });
} }
await knex.schema.dropTableIfExists(TableName.ProjectGateway); await knex.schema.dropTableIfExists("project_gateways");
await dropOnUpdateTrigger(knex, TableName.ProjectGateway); await dropOnUpdateTrigger(knex, "project_gateways");
await knex.schema.dropTableIfExists(TableName.Gateway); await knex.schema.dropTableIfExists(TableName.Gateway);
await dropOnUpdateTrigger(knex, TableName.Gateway); await dropOnUpdateTrigger(knex, TableName.Gateway);
@@ -0,0 +1,20 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
// Note(daniel): We aren't dropping tables or columns in this migrations so we can easily rollback if needed.
// In the future we need to drop the projectGatewayId on the dynamic secrets table, and drop the project_gateways table entirely.
export async function up(knex: Knex): Promise<void> {
await knex.schema.alterTable(TableName.DynamicSecret, (table) => {
table.uuid("gatewayId").nullable();
table.foreign("gatewayId").references("id").inTable(TableName.Gateway).onDelete("SET NULL");
});
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.alterTable(TableName.DynamicSecret, (table) => {
table.dropForeign("gatewayId");
table.dropColumn("gatewayId");
});
}
+2 -1
View File
@@ -27,7 +27,8 @@ export const DynamicSecretsSchema = z.object({
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
encryptedInput: zodBuffer, encryptedInput: zodBuffer,
projectGatewayId: z.string().uuid().nullable().optional() projectGatewayId: z.string().uuid().nullable().optional(),
gatewayId: z.string().uuid().nullable().optional()
}); });
export type TDynamicSecrets = z.infer<typeof DynamicSecretsSchema>; export type TDynamicSecrets = z.infer<typeof DynamicSecretsSchema>;
-1
View File
@@ -71,7 +71,6 @@ export * from "./pki-collection-items";
export * from "./pki-collections"; export * from "./pki-collections";
export * from "./project-bots"; export * from "./project-bots";
export * from "./project-environments"; export * from "./project-environments";
export * from "./project-gateways";
export * from "./project-keys"; export * from "./project-keys";
export * from "./project-memberships"; export * from "./project-memberships";
export * from "./project-roles"; export * from "./project-roles";
-1
View File
@@ -123,7 +123,6 @@ export enum TableName {
// Gateway // Gateway
OrgGatewayConfig = "org_gateway_config", OrgGatewayConfig = "org_gateway_config",
Gateway = "gateways", Gateway = "gateways",
ProjectGateway = "project_gateways",
// junction tables with tags // junction tables with tags
SecretV2JnTag = "secret_v2_tag_junction", SecretV2JnTag = "secret_v2_tag_junction",
JnSecretTag = "secret_tag_junction", JnSecretTag = "secret_tag_junction",
+6 -17
View File
@@ -121,14 +121,7 @@ export const registerGatewayRouter = async (server: FastifyZodProvider) => {
identity: z.object({ identity: z.object({
name: z.string(), name: z.string(),
id: z.string() id: z.string()
}), })
projects: z
.object({
name: z.string(),
id: z.string(),
slug: z.string()
})
.array()
}).array() }).array()
}) })
} }
@@ -158,17 +151,15 @@ export const registerGatewayRouter = async (server: FastifyZodProvider) => {
identity: z.object({ identity: z.object({
name: z.string(), name: z.string(),
id: z.string() id: z.string()
}), })
projectGatewayId: z.string()
}).array() }).array()
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]), onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]),
handler: async (req) => { handler: async (req) => {
const gateways = await server.services.gateway.getProjectGateways({ const gateways = await server.services.gateway.listGateways({
projectId: req.params.projectId, orgPermission: req.permission
projectPermission: req.permission
}); });
return { gateways }; return { gateways };
} }
@@ -216,8 +207,7 @@ export const registerGatewayRouter = async (server: FastifyZodProvider) => {
id: z.string() id: z.string()
}), }),
body: z.object({ body: z.object({
name: slugSchema({ field: "name" }).optional(), name: slugSchema({ field: "name" }).optional()
projectIds: z.string().array().optional()
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -230,8 +220,7 @@ export const registerGatewayRouter = async (server: FastifyZodProvider) => {
const gateway = await server.services.gateway.updateGatewayById({ const gateway = await server.services.gateway.updateGatewayById({
orgPermission: req.permission, orgPermission: req.permission,
id: req.params.id, id: req.params.id,
name: req.body.name, name: req.body.name
projectIds: req.body.projectIds
}); });
return { gateway }; return { gateway };
} }
@@ -17,7 +17,8 @@ import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-fold
import { TDynamicSecretLeaseDALFactory } from "../dynamic-secret-lease/dynamic-secret-lease-dal"; import { TDynamicSecretLeaseDALFactory } from "../dynamic-secret-lease/dynamic-secret-lease-dal";
import { TDynamicSecretLeaseQueueServiceFactory } from "../dynamic-secret-lease/dynamic-secret-lease-queue"; import { TDynamicSecretLeaseQueueServiceFactory } from "../dynamic-secret-lease/dynamic-secret-lease-queue";
import { TProjectGatewayDALFactory } from "../gateway/project-gateway-dal"; import { TGatewayDALFactory } from "../gateway/gateway-dal";
import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TDynamicSecretDALFactory } from "./dynamic-secret-dal"; import { TDynamicSecretDALFactory } from "./dynamic-secret-dal";
import { import {
DynamicSecretStatus, DynamicSecretStatus,
@@ -44,9 +45,9 @@ type TDynamicSecretServiceFactoryDep = {
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "findBySecretPathMultiEnv">; folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "findBySecretPathMultiEnv">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">; projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
projectGatewayDAL: Pick<TProjectGatewayDALFactory, "findOne">; gatewayDAL: Pick<TGatewayDALFactory, "findOne" | "find">;
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">; resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
}; };
@@ -62,7 +63,7 @@ export const dynamicSecretServiceFactory = ({
dynamicSecretQueueService, dynamicSecretQueueService,
projectDAL, projectDAL,
kmsService, kmsService,
projectGatewayDAL, gatewayDAL,
resourceMetadataDAL resourceMetadataDAL
}: TDynamicSecretServiceFactoryDep) => { }: TDynamicSecretServiceFactoryDep) => {
const create = async ({ const create = async ({
@@ -117,15 +118,31 @@ export const dynamicSecretServiceFactory = ({
const inputs = await selectedProvider.validateProviderInputs(provider.inputs); const inputs = await selectedProvider.validateProviderInputs(provider.inputs);
let selectedGatewayId: string | null = null; let selectedGatewayId: string | null = null;
if (inputs && typeof inputs === "object" && "projectGatewayId" in inputs && inputs.projectGatewayId) { if (inputs && typeof inputs === "object" && "gatewayId" in inputs && inputs.gatewayId) {
const projectGatewayId = inputs.projectGatewayId as string; const gatewayId = inputs.gatewayId as string;
const projectGateway = await projectGatewayDAL.findOne({ id: projectGatewayId, projectId }); const [gateway] = await gatewayDAL.find({ id: gatewayId });
if (!projectGateway)
if (!gateway) {
throw new NotFoundError({ throw new NotFoundError({
message: `Project gateway with ${projectGatewayId} not found` message: `Gateway with ID ${gatewayId} not found`
}); });
selectedGatewayId = projectGateway.id; }
const { permission: orgPermission } = await permissionService.getOrgPermission(
actor,
actorId,
gateway.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(orgPermission).throwUnlessCan(
OrgPermissionGatewayActions.AttachGateways,
OrgPermissionSubjects.Gateway
);
selectedGatewayId = gateway.id;
} }
const isConnected = await selectedProvider.validateConnection(provider.inputs); const isConnected = await selectedProvider.validateConnection(provider.inputs);
@@ -146,7 +163,7 @@ export const dynamicSecretServiceFactory = ({
defaultTTL, defaultTTL,
folderId: folder.id, folderId: folder.id,
name, name,
projectGatewayId: selectedGatewayId gatewayId: selectedGatewayId
}, },
tx tx
); );
@@ -255,20 +272,30 @@ export const dynamicSecretServiceFactory = ({
const updatedInput = await selectedProvider.validateProviderInputs(newInput); const updatedInput = await selectedProvider.validateProviderInputs(newInput);
let selectedGatewayId: string | null = null; let selectedGatewayId: string | null = null;
if ( if (updatedInput && typeof updatedInput === "object" && "gatewayId" in updatedInput && updatedInput?.gatewayId) {
updatedInput && const gatewayId = updatedInput.gatewayId as string;
typeof updatedInput === "object" &&
"projectGatewayId" in updatedInput &&
updatedInput?.projectGatewayId
) {
const projectGatewayId = updatedInput.projectGatewayId as string;
const projectGateway = await projectGatewayDAL.findOne({ id: projectGatewayId, projectId }); const [gateway] = await gatewayDAL.find({ id: gatewayId });
if (!projectGateway) if (!gateway) {
throw new NotFoundError({ throw new NotFoundError({
message: `Project gateway with ${projectGatewayId} not found` message: `Gateway with ID ${gatewayId} not found`
}); });
selectedGatewayId = projectGateway.id; }
const { permission: orgPermission } = await permissionService.getOrgPermission(
actor,
actorId,
gateway.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(orgPermission).throwUnlessCan(
OrgPermissionGatewayActions.AttachGateways,
OrgPermissionSubjects.Gateway
);
selectedGatewayId = gateway.id;
} }
const isConnected = await selectedProvider.validateConnection(newInput); const isConnected = await selectedProvider.validateConnection(newInput);
@@ -284,7 +311,7 @@ export const dynamicSecretServiceFactory = ({
defaultTTL, defaultTTL,
name: newName ?? name, name: newName ?? name,
status: null, status: null,
projectGatewayId: selectedGatewayId gatewayId: selectedGatewayId
}, },
tx tx
); );
@@ -18,7 +18,7 @@ import { SqlDatabaseProvider } from "./sql-database";
import { TotpProvider } from "./totp"; import { TotpProvider } from "./totp";
type TBuildDynamicSecretProviderDTO = { type TBuildDynamicSecretProviderDTO = {
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTls">; gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
}; };
export const buildDynamicSecretProviders = ({ export const buildDynamicSecretProviders = ({
@@ -137,7 +137,7 @@ export const DynamicSecretSqlDBSchema = z.object({
revocationStatement: z.string().trim(), revocationStatement: z.string().trim(),
renewStatement: z.string().trim().optional(), renewStatement: z.string().trim().optional(),
ca: z.string().optional(), ca: z.string().optional(),
projectGatewayId: z.string().nullable().optional() gatewayId: z.string().nullable().optional()
}); });
export const DynamicSecretCassandraSchema = z.object({ export const DynamicSecretCassandraSchema = z.object({
@@ -112,14 +112,14 @@ const generateUsername = (provider: SqlProviders) => {
}; };
type TSqlDatabaseProviderDTO = { type TSqlDatabaseProviderDTO = {
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTls">; gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
}; };
export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO): TDynamicProviderFns => { export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => { const validateProviderInputs = async (inputs: unknown) => {
const providerInputs = await DynamicSecretSqlDBSchema.parseAsync(inputs); const providerInputs = await DynamicSecretSqlDBSchema.parseAsync(inputs);
const [hostIp] = await verifyHostInputValidity(providerInputs.host, Boolean(providerInputs.projectGatewayId)); const [hostIp] = await verifyHostInputValidity(providerInputs.host, Boolean(providerInputs.gatewayId));
validateHandlebarTemplate("SQL creation", providerInputs.creationStatement, { validateHandlebarTemplate("SQL creation", providerInputs.creationStatement, {
allowedExpressions: (val) => ["username", "password", "expiration", "database"].includes(val) allowedExpressions: (val) => ["username", "password", "expiration", "database"].includes(val)
}); });
@@ -168,7 +168,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
providerInputs: z.infer<typeof DynamicSecretSqlDBSchema>, providerInputs: z.infer<typeof DynamicSecretSqlDBSchema>,
gatewayCallback: (host: string, port: number) => Promise<void> gatewayCallback: (host: string, port: number) => Promise<void>
) => { ) => {
const relayDetails = await gatewayService.fnGetGatewayClientTls(providerInputs.projectGatewayId as string); const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(providerInputs.gatewayId as string);
const [relayHost, relayPort] = relayDetails.relayAddress.split(":"); const [relayHost, relayPort] = relayDetails.relayAddress.split(":");
await withGatewayProxy( await withGatewayProxy(
async (port) => { async (port) => {
@@ -202,7 +202,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
await db.destroy(); await db.destroy();
}; };
if (providerInputs.projectGatewayId) { if (providerInputs.gatewayId) {
await gatewayProxyWrapper(providerInputs, gatewayCallback); await gatewayProxyWrapper(providerInputs, gatewayCallback);
} else { } else {
await gatewayCallback(); await gatewayCallback();
@@ -238,7 +238,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
await db.destroy(); await db.destroy();
} }
}; };
if (providerInputs.projectGatewayId) { if (providerInputs.gatewayId) {
await gatewayProxyWrapper(providerInputs, gatewayCallback); await gatewayProxyWrapper(providerInputs, gatewayCallback);
} else { } else {
await gatewayCallback(); await gatewayCallback();
@@ -265,7 +265,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
await db.destroy(); await db.destroy();
} }
}; };
if (providerInputs.projectGatewayId) { if (providerInputs.gatewayId) {
await gatewayProxyWrapper(providerInputs, gatewayCallback); await gatewayProxyWrapper(providerInputs, gatewayCallback);
} else { } else {
await gatewayCallback(); await gatewayCallback();
@@ -301,7 +301,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
await db.destroy(); await db.destroy();
} }
}; };
if (providerInputs.projectGatewayId) { if (providerInputs.gatewayId) {
await gatewayProxyWrapper(providerInputs, gatewayCallback); await gatewayProxyWrapper(providerInputs, gatewayCallback);
} else { } else {
await gatewayCallback(); await gatewayCallback();
+16 -58
View File
@@ -1,16 +1,7 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { GatewaysSchema, TableName, TGateways } from "@app/db/schemas"; import { GatewaysSchema, TableName, TGateways } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { import { buildFindFilter, ormify, selectAllTableCols, TFindFilter, TFindOpt } from "@app/lib/knex";
buildFindFilter,
ormify,
selectAllTableCols,
sqlNestRelationships,
TFindFilter,
TFindOpt
} from "@app/lib/knex";
export type TGatewayDALFactory = ReturnType<typeof gatewayDALFactory>; export type TGatewayDALFactory = ReturnType<typeof gatewayDALFactory>;
@@ -21,17 +12,16 @@ export const gatewayDALFactory = (db: TDbClient) => {
try { try {
const query = (tx || db)(TableName.Gateway) const query = (tx || db)(TableName.Gateway)
// eslint-disable-next-line @typescript-eslint/no-misused-promises // eslint-disable-next-line @typescript-eslint/no-misused-promises
.where(buildFindFilter(filter)) .where(buildFindFilter(filter, TableName.Gateway))
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Gateway}.identityId`) .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Gateway}.identityId`)
.leftJoin(TableName.ProjectGateway, `${TableName.ProjectGateway}.gatewayId`, `${TableName.Gateway}.id`) .join(
.leftJoin(TableName.Project, `${TableName.Project}.id`, `${TableName.ProjectGateway}.projectId`) TableName.IdentityOrgMembership,
`${TableName.IdentityOrgMembership}.identityId`,
`${TableName.Gateway}.identityId`
)
.select(selectAllTableCols(TableName.Gateway)) .select(selectAllTableCols(TableName.Gateway))
.select( .select(db.ref("orgId").withSchema(TableName.IdentityOrgMembership).as("identityOrgId"))
db.ref("name").withSchema(TableName.Identity).as("identityName"), .select(db.ref("name").withSchema(TableName.Identity).as("identityName"));
db.ref("name").withSchema(TableName.Project).as("projectName"),
db.ref("slug").withSchema(TableName.Project).as("projectSlug"),
db.ref("id").withSchema(TableName.Project).as("projectId")
);
if (limit) void query.limit(limit); if (limit) void query.limit(limit);
if (offset) void query.offset(offset); if (offset) void query.offset(offset);
if (sort) { if (sort) {
@@ -39,48 +29,16 @@ export const gatewayDALFactory = (db: TDbClient) => {
} }
const docs = await query; const docs = await query;
return sqlNestRelationships({
data: docs, return docs.map((el) => ({
key: "id", ...GatewaysSchema.parse(el),
parentMapper: (data) => ({ orgId: el.identityOrgId as string, // todo(daniel): figure out why typescript is not inferring this as a string
...GatewaysSchema.parse(data), identity: { id: el.identityId, name: el.identityName }
identity: { id: data.identityId, name: data.identityName } }));
}),
childrenMapper: [
{
key: "projectId",
label: "projects" as const,
mapper: ({ projectId, projectName, projectSlug }) => ({
id: projectId,
name: projectName,
slug: projectSlug
})
}
]
});
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: `${TableName.Gateway}: Find` }); throw new DatabaseError({ error, name: `${TableName.Gateway}: Find` });
} }
}; };
const findByProjectId = async (projectId: string, tx?: Knex) => { return { ...orm, find };
try {
const query = (tx || db)(TableName.Gateway)
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Gateway}.identityId`)
.join(TableName.ProjectGateway, `${TableName.ProjectGateway}.gatewayId`, `${TableName.Gateway}.id`)
.select(selectAllTableCols(TableName.Gateway))
.select(
db.ref("name").withSchema(TableName.Identity).as("identityName"),
db.ref("id").withSchema(TableName.ProjectGateway).as("projectGatewayId")
)
.where({ [`${TableName.ProjectGateway}.projectId` as "projectId"]: projectId });
const docs = await query;
return docs.map((el) => ({ ...el, identity: { id: el.identityId, name: el.identityName } }));
} catch (error) {
throw new DatabaseError({ error, name: `${TableName.Gateway}: Find by project id` });
}
};
return { ...orm, find, findByProjectId };
}; };
@@ -4,7 +4,6 @@ import { ForbiddenError } from "@casl/ability";
import * as x509 from "@peculiar/x509"; import * as x509 from "@peculiar/x509";
import { z } from "zod"; import { z } from "zod";
import { ActionProjectType } from "@app/db/schemas";
import { KeyStorePrefixes, PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore"; import { KeyStorePrefixes, PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
@@ -27,17 +26,14 @@ import { TGatewayDALFactory } from "./gateway-dal";
import { import {
TExchangeAllocatedRelayAddressDTO, TExchangeAllocatedRelayAddressDTO,
TGetGatewayByIdDTO, TGetGatewayByIdDTO,
TGetProjectGatewayByIdDTO,
THeartBeatDTO, THeartBeatDTO,
TListGatewaysDTO, TListGatewaysDTO,
TUpdateGatewayByIdDTO TUpdateGatewayByIdDTO
} from "./gateway-types"; } from "./gateway-types";
import { TOrgGatewayConfigDALFactory } from "./org-gateway-config-dal"; import { TOrgGatewayConfigDALFactory } from "./org-gateway-config-dal";
import { TProjectGatewayDALFactory } from "./project-gateway-dal";
type TGatewayServiceFactoryDep = { type TGatewayServiceFactoryDep = {
gatewayDAL: TGatewayDALFactory; gatewayDAL: TGatewayDALFactory;
projectGatewayDAL: TProjectGatewayDALFactory;
orgGatewayConfigDAL: Pick<TOrgGatewayConfigDALFactory, "findOne" | "create" | "transaction" | "findById">; orgGatewayConfigDAL: Pick<TOrgGatewayConfigDALFactory, "findOne" | "create" | "transaction" | "findById">;
licenseService: Pick<TLicenseServiceFactory, "onPremFeatures" | "getPlan">; licenseService: Pick<TLicenseServiceFactory, "onPremFeatures" | "getPlan">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "decryptWithRootKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "decryptWithRootKey">;
@@ -57,8 +53,7 @@ export const gatewayServiceFactory = ({
kmsService, kmsService,
permissionService, permissionService,
orgGatewayConfigDAL, orgGatewayConfigDAL,
keyStore, keyStore
projectGatewayDAL
}: TGatewayServiceFactoryDep) => { }: TGatewayServiceFactoryDep) => {
const $validateOrgAccessToGateway = async (orgId: string, actorId: string, actorAuthMethod: ActorAuthMethod) => { const $validateOrgAccessToGateway = async (orgId: string, actorId: string, actorAuthMethod: ActorAuthMethod) => {
// if (!licenseService.onPremFeatures.gateway) { // if (!licenseService.onPremFeatures.gateway) {
@@ -526,7 +521,7 @@ export const gatewayServiceFactory = ({
return gateway; return gateway;
}; };
const updateGatewayById = async ({ orgPermission, id, name, projectIds }: TUpdateGatewayByIdDTO) => { const updateGatewayById = async ({ orgPermission, id, name }: TUpdateGatewayByIdDTO) => {
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
orgPermission.type, orgPermission.type,
orgPermission.id, orgPermission.id,
@@ -543,15 +538,6 @@ export const gatewayServiceFactory = ({
const [gateway] = await gatewayDAL.update({ id, orgGatewayRootCaId: orgGatewayConfig.id }, { name }); const [gateway] = await gatewayDAL.update({ id, orgGatewayRootCaId: orgGatewayConfig.id }, { name });
if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` }); if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` });
if (projectIds) {
await projectGatewayDAL.transaction(async (tx) => {
await projectGatewayDAL.delete({ gatewayId: gateway.id }, tx);
await projectGatewayDAL.insertMany(
projectIds.map((el) => ({ gatewayId: gateway.id, projectId: el })),
tx
);
});
}
return gateway; return gateway;
}; };
@@ -576,20 +562,6 @@ export const gatewayServiceFactory = ({
return gateway; return gateway;
}; };
const getProjectGateways = async ({ projectId, projectPermission }: TGetProjectGatewayByIdDTO) => {
await permissionService.getProjectPermission({
projectId,
actor: projectPermission.type,
actorId: projectPermission.id,
actorOrgId: projectPermission.orgId,
actorAuthMethod: projectPermission.authMethod,
actionProjectType: ActionProjectType.Any
});
const gateways = await gatewayDAL.findByProjectId(projectId);
return gateways;
};
const fnGetGatewayClientTlsByGatewayId = async (gatewayId: string) => { const fnGetGatewayClientTlsByGatewayId = async (gatewayId: string) => {
const gateway = await gatewayDAL.findById(gatewayId); const gateway = await gatewayDAL.findById(gatewayId);
if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${gatewayId} not found.` }); if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${gatewayId} not found.` });
@@ -632,17 +604,6 @@ export const gatewayServiceFactory = ({
}; };
}; };
// this has no permission check and used for dynamic secrets directly
// assumes permission check is already done
const fnGetGatewayClientTls = async (projectGatewayId: string) => {
const projectGateway = await projectGatewayDAL.findById(projectGatewayId);
if (!projectGateway) throw new NotFoundError({ message: `Project gateway with ID ${projectGatewayId} not found.` });
const gatewayDetails = await fnGetGatewayClientTlsByGatewayId(projectGateway.gatewayId);
return gatewayDetails;
};
return { return {
getGatewayRelayDetails, getGatewayRelayDetails,
exchangeAllocatedRelayAddress, exchangeAllocatedRelayAddress,
@@ -650,8 +611,6 @@ export const gatewayServiceFactory = ({
getGatewayById, getGatewayById,
updateGatewayById, updateGatewayById,
deleteGatewayById, deleteGatewayById,
getProjectGateways,
fnGetGatewayClientTls,
fnGetGatewayClientTlsByGatewayId, fnGetGatewayClientTlsByGatewayId,
heartbeat heartbeat
}; };
@@ -20,7 +20,6 @@ export type TGetGatewayByIdDTO = {
export type TUpdateGatewayByIdDTO = { export type TUpdateGatewayByIdDTO = {
id: string; id: string;
name?: string; name?: string;
projectIds?: string[];
orgPermission: OrgServiceActor; orgPermission: OrgServiceActor;
}; };
@@ -1,10 +0,0 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TProjectGatewayDALFactory = ReturnType<typeof projectGatewayDALFactory>;
export const projectGatewayDALFactory = (db: TDbClient) => {
const orm = ormify(db, TableName.ProjectGateway);
return orm;
};
@@ -41,7 +41,8 @@ export enum OrgPermissionGatewayActions {
CreateGateways = "create-gateways", CreateGateways = "create-gateways",
ListGateways = "list-gateways", ListGateways = "list-gateways",
EditGateways = "edit-gateways", EditGateways = "edit-gateways",
DeleteGateways = "delete-gateways" DeleteGateways = "delete-gateways",
AttachGateways = "attach-gateways"
} }
export enum OrgPermissionIdentityActions { export enum OrgPermissionIdentityActions {
@@ -337,6 +338,7 @@ const buildAdminPermission = () => {
can(OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway); can(OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionGatewayActions.EditGateways, OrgPermissionSubjects.Gateway); can(OrgPermissionGatewayActions.EditGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionGatewayActions.DeleteGateways, OrgPermissionSubjects.Gateway); can(OrgPermissionGatewayActions.DeleteGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionGatewayActions.AttachGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole); can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
@@ -378,6 +380,7 @@ const buildMemberPermission = () => {
can(OrgPermissionAppConnectionActions.Connect, OrgPermissionSubjects.AppConnections); can(OrgPermissionAppConnectionActions.Connect, OrgPermissionSubjects.AppConnections);
can(OrgPermissionGatewayActions.ListGateways, OrgPermissionSubjects.Gateway); can(OrgPermissionGatewayActions.ListGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway); can(OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionGatewayActions.AttachGateways, OrgPermissionSubjects.Gateway);
return rules; return rules;
}; };
+3 -5
View File
@@ -32,7 +32,6 @@ import { externalKmsServiceFactory } from "@app/ee/services/external-kms/externa
import { gatewayDALFactory } from "@app/ee/services/gateway/gateway-dal"; import { gatewayDALFactory } from "@app/ee/services/gateway/gateway-dal";
import { gatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; import { gatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { orgGatewayConfigDALFactory } from "@app/ee/services/gateway/org-gateway-config-dal"; import { orgGatewayConfigDALFactory } from "@app/ee/services/gateway/org-gateway-config-dal";
import { projectGatewayDALFactory } from "@app/ee/services/gateway/project-gateway-dal";
import { githubOrgSyncDALFactory } from "@app/ee/services/github-org-sync/github-org-sync-dal"; import { githubOrgSyncDALFactory } from "@app/ee/services/github-org-sync/github-org-sync-dal";
import { githubOrgSyncServiceFactory } from "@app/ee/services/github-org-sync/github-org-sync-service"; import { githubOrgSyncServiceFactory } from "@app/ee/services/github-org-sync/github-org-sync-service";
import { groupDALFactory } from "@app/ee/services/group/group-dal"; import { groupDALFactory } from "@app/ee/services/group/group-dal";
@@ -434,7 +433,6 @@ export const registerRoutes = async (
const orgGatewayConfigDAL = orgGatewayConfigDALFactory(db); const orgGatewayConfigDAL = orgGatewayConfigDALFactory(db);
const gatewayDAL = gatewayDALFactory(db); const gatewayDAL = gatewayDALFactory(db);
const projectGatewayDAL = projectGatewayDALFactory(db);
const secretReminderRecipientsDAL = secretReminderRecipientsDALFactory(db); const secretReminderRecipientsDAL = secretReminderRecipientsDALFactory(db);
const githubOrgSyncDAL = githubOrgSyncDALFactory(db); const githubOrgSyncDAL = githubOrgSyncDALFactory(db);
@@ -1408,8 +1406,7 @@ export const registerRoutes = async (
kmsService, kmsService,
licenseService, licenseService,
orgGatewayConfigDAL, orgGatewayConfigDAL,
keyStore, keyStore
projectGatewayDAL
}); });
const identityKubernetesAuthService = identityKubernetesAuthServiceFactory({ const identityKubernetesAuthService = identityKubernetesAuthServiceFactory({
@@ -1419,6 +1416,7 @@ export const registerRoutes = async (
permissionService, permissionService,
licenseService, licenseService,
gatewayService, gatewayService,
gatewayDAL,
kmsService kmsService
}); });
const identityGcpAuthService = identityGcpAuthServiceFactory({ const identityGcpAuthService = identityGcpAuthServiceFactory({
@@ -1494,7 +1492,7 @@ export const registerRoutes = async (
permissionService, permissionService,
licenseService, licenseService,
kmsService, kmsService,
projectGatewayDAL, gatewayDAL,
resourceMetadataDAL resourceMetadataDAL
}); });
@@ -4,9 +4,14 @@ import https from "https";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { IdentityAuthMethod, TIdentityKubernetesAuthsUpdate } from "@app/db/schemas"; import { IdentityAuthMethod, TIdentityKubernetesAuthsUpdate } from "@app/db/schemas";
import { TGatewayDALFactory } from "@app/ee/services/gateway/gateway-dal";
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import {
OrgPermissionGatewayActions,
OrgPermissionIdentityActions,
OrgPermissionSubjects
} from "@app/ee/services/permission/org-permission";
import { import {
constructPermissionErrorMessage, constructPermissionErrorMessage,
validatePrivilegeChangeOperation validatePrivilegeChangeOperation
@@ -46,6 +51,7 @@ type TIdentityKubernetesAuthServiceFactoryDep = {
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
gatewayService: TGatewayServiceFactory; gatewayService: TGatewayServiceFactory;
gatewayDAL: Pick<TGatewayDALFactory, "find">;
}; };
export type TIdentityKubernetesAuthServiceFactory = ReturnType<typeof identityKubernetesAuthServiceFactory>; export type TIdentityKubernetesAuthServiceFactory = ReturnType<typeof identityKubernetesAuthServiceFactory>;
@@ -57,6 +63,7 @@ export const identityKubernetesAuthServiceFactory = ({
permissionService, permissionService,
licenseService, licenseService,
gatewayService, gatewayService,
gatewayDAL,
kmsService kmsService
}: TIdentityKubernetesAuthServiceFactoryDep) => { }: TIdentityKubernetesAuthServiceFactoryDep) => {
const $gatewayProxyWrapper = async <T>( const $gatewayProxyWrapper = async <T>(
@@ -343,6 +350,27 @@ export const identityKubernetesAuthServiceFactory = ({
return extractIPDetails(accessTokenTrustedIp.ipAddress); return extractIPDetails(accessTokenTrustedIp.ipAddress);
}); });
if (gatewayId) {
const [gateway] = await gatewayDAL.find({ id: gatewayId });
if (!gateway) {
throw new NotFoundError({
message: `Gateway with ID ${gatewayId} not found`
});
}
const { permission: orgPermission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(orgPermission).throwUnlessCan(
OrgPermissionGatewayActions.AttachGateways,
OrgPermissionSubjects.Gateway
);
}
const { encryptor } = await kmsService.createCipherPairWithDataKey({ const { encryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization, type: KmsDataKey.Organization,
orgId: identityMembershipOrg.orgId orgId: identityMembershipOrg.orgId
@@ -438,6 +466,27 @@ export const identityKubernetesAuthServiceFactory = ({
return extractIPDetails(accessTokenTrustedIp.ipAddress); return extractIPDetails(accessTokenTrustedIp.ipAddress);
}); });
if (gatewayId) {
const [gateway] = await gatewayDAL.find({ id: gatewayId });
if (!gateway) {
throw new NotFoundError({
message: `Gateway with ID ${gatewayId} not found`
});
}
const { permission: orgPermission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(orgPermission).throwUnlessCan(
OrgPermissionGatewayActions.AttachGateways,
OrgPermissionSubjects.Gateway
);
}
const updateQuery: TIdentityKubernetesAuthsUpdate = { const updateQuery: TIdentityKubernetesAuthsUpdate = {
kubernetesHost, kubernetesHost,
allowedNamespaces, allowedNamespaces,
@@ -12,7 +12,8 @@ export enum OrgGatewayPermissionActions {
CreateGateways = "create-gateways", CreateGateways = "create-gateways",
ListGateways = "list-gateways", ListGateways = "list-gateways",
EditGateways = "edit-gateways", EditGateways = "edit-gateways",
DeleteGateways = "delete-gateways" DeleteGateways = "delete-gateways",
AttachGateways = "attach-gateways"
} }
export enum OrgPermissionSubjects { export enum OrgPermissionSubjects {
+2 -2
View File
@@ -20,8 +20,8 @@ export const useDeleteGatewayById = () => {
export const useUpdateGatewayById = () => { export const useUpdateGatewayById = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: ({ id, name, projectIds }: TUpdateGatewayDTO) => { mutationFn: ({ id, name }: TUpdateGatewayDTO) => {
return apiRequest.patch(`/api/v1/gateways/${id}`, { name, projectIds }); return apiRequest.patch(`/api/v1/gateways/${id}`, { name });
}, },
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries(gatewaysQueryKeys.list()); queryClient.invalidateQueries(gatewaysQueryKeys.list());
+1 -16
View File
@@ -2,7 +2,7 @@ import { queryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { TGateway, TListProjectGatewayDTO, TProjectGateway } from "./types"; import { TGateway } from "./types";
export const gatewaysQueryKeys = { export const gatewaysQueryKeys = {
allKey: () => ["gateways"], allKey: () => ["gateways"],
@@ -14,20 +14,5 @@ export const gatewaysQueryKeys = {
const { data } = await apiRequest.get<{ gateways: TGateway[] }>("/api/v1/gateways"); const { data } = await apiRequest.get<{ gateways: TGateway[] }>("/api/v1/gateways");
return data.gateways; return data.gateways;
} }
}),
listProjectGatewayKey: ({ projectId }: TListProjectGatewayDTO) => [
...gatewaysQueryKeys.allKey(),
"list",
{ projectId }
],
listProjectGateways: ({ projectId }: TListProjectGatewayDTO) =>
queryOptions({
queryKey: gatewaysQueryKeys.listProjectGatewayKey({ projectId }),
queryFn: async () => {
const { data } = await apiRequest.get<{ gateways: TProjectGateway[] }>(
`/api/v1/gateways/projects/${projectId}`
);
return data.gateways;
}
}) })
}; };
-26
View File
@@ -11,39 +11,13 @@ export type TGateway = {
name: string; name: string;
id: string; id: string;
}; };
projects: {
name: string;
id: string;
slug: string;
}[];
};
export type TProjectGateway = {
id: string;
identityId: string;
name: string;
createdAt: string;
updatedAt: string;
issuedAt: string;
serialNumber: string;
heartbeat: string;
projectGatewayId: string;
identity: {
name: string;
id: string;
};
}; };
export type TUpdateGatewayDTO = { export type TUpdateGatewayDTO = {
id: string; id: string;
name?: string; name?: string;
projectIds?: string[];
}; };
export type TDeleteGatewayDTO = { export type TDeleteGatewayDTO = {
id: string; id: string;
}; };
export type TListProjectGatewayDTO = {
projectId: string;
};
@@ -7,6 +7,7 @@ import { useQuery } from "@tanstack/react-query";
import { z } from "zod"; import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import { import {
Button, Button,
FormControl, FormControl,
@@ -18,9 +19,14 @@ import {
TabList, TabList,
TabPanel, TabPanel,
Tabs, Tabs,
TextArea TextArea,
Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context"; import { useOrganization, useSubscription } from "@app/context";
import {
OrgGatewayPermissionActions,
OrgPermissionSubjects
} from "@app/context/OrgPermissionContext/types";
import { import {
gatewaysQueryKeys, gatewaysQueryKeys,
useAddIdentityKubernetesAuth, useAddIdentityKubernetesAuth,
@@ -103,7 +109,7 @@ export const IdentityKubernetesAuthForm = ({
tokenReviewerJwt: "", tokenReviewerJwt: "",
allowedNames: "", allowedNames: "",
allowedNamespaces: "", allowedNamespaces: "",
gatewayId: null, gatewayId: "",
allowedAudience: "", allowedAudience: "",
caCert: "", caCert: "",
accessTokenTTL: "2592000", accessTokenTTL: "2592000",
@@ -294,38 +300,60 @@ export const IdentityKubernetesAuthForm = ({
)} )}
/> />
<Controller <OrgPermissionCan
control={control} I={OrgGatewayPermissionActions.AttachGateways}
name="gatewayId" a={OrgPermissionSubjects.Gateway}
defaultValue="" >
render={({ field: { value, onChange }, fieldState: { error } }) => ( {(isAllowed) => (
<FormControl <Controller
isError={Boolean(error?.message)} control={control}
errorText={error?.message} name="gatewayId"
label="Gateway" defaultValue=""
isOptional render={({ field: { value, onChange }, fieldState: { error } }) => (
> <FormControl
<Select isError={Boolean(error?.message)}
value={value as string} errorText={error?.message}
onValueChange={onChange} label="Gateway"
className="w-full border border-mineshaft-500" isOptional
dropdownContainerClassName="max-w-none" >
isLoading={isGatewayLoading} <Tooltip
placeholder="Select Gateway" isDisabled={isAllowed}
position="popper" content="Restricted access. You don't have permission to attach gateways to resources."
> >
<SelectItem value={null as unknown as string} onClick={() => onChange(undefined)}> <div>
Internet Gateway <Select
</SelectItem> isDisabled={!isAllowed}
{gateways?.map((el) => ( value={value as string}
<SelectItem value={el.id} key={el.id}> onValueChange={(v) => {
{el.name} if (v !== "") {
</SelectItem> onChange(v);
))} }
</Select> }}
</FormControl> className="w-full border border-mineshaft-500"
dropdownContainerClassName="max-w-none"
isLoading={isGatewayLoading}
placeholder="Select Gateway"
position="popper"
>
<SelectItem
value={null as unknown as string}
onClick={() => onChange(null)}
>
Internet Gateway
</SelectItem>
{gateways?.map((el) => (
<SelectItem value={el.id} key={el.id}>
{el.name}
</SelectItem>
))}
</Select>
</div>
</Tooltip>
</FormControl>
)}
/>
)} )}
/> </OrgPermissionCan>
<Controller <Controller
control={control} control={control}
@@ -32,7 +32,6 @@ import {
Table, Table,
TableContainer, TableContainer,
TableSkeleton, TableSkeleton,
Tag,
TBody, TBody,
Td, Td,
Th, Th,
@@ -128,7 +127,6 @@ export const GatewayListPage = withPermission(
<Tr> <Tr>
<Th className="w-1/3">Name</Th> <Th className="w-1/3">Name</Th>
<Th>Cert Issued At</Th> <Th>Cert Issued At</Th>
<Th>Projects</Th>
<Th>Identity</Th> <Th>Identity</Th>
<Th> <Th>
Health Check Health Check
@@ -151,13 +149,6 @@ export const GatewayListPage = withPermission(
<Tr key={el.id}> <Tr key={el.id}>
<Td>{el.name}</Td> <Td>{el.name}</Td>
<Td>{format(new Date(el.issuedAt), "yyyy-MM-dd hh:mm:ss aaa")}</Td> <Td>{format(new Date(el.issuedAt), "yyyy-MM-dd hh:mm:ss aaa")}</Td>
<Td>
{el.projects.map((projectDetails) => (
<Tag key={projectDetails.id} size="xs">
{projectDetails.name}
</Tag>
))}
</Td>
<Td>{el.identity.name}</Td> <Td>{el.identity.name}</Td>
<Td> <Td>
{el.heartbeat {el.heartbeat
@@ -3,10 +3,9 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod"; import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button, FilterableSelect, FormControl, Input } from "@app/components/v2"; import { Button, FormControl, Input } from "@app/components/v2";
import { useGetUserWorkspaces, useUpdateGatewayById } from "@app/hooks/api"; import { useUpdateGatewayById } from "@app/hooks/api";
import { TGateway } from "@app/hooks/api/gateways/types"; import { TGateway } from "@app/hooks/api/gateways/types";
import { ProjectType } from "@app/hooks/api/workspace/types";
type Props = { type Props = {
gatewayDetails: TGateway; gatewayDetails: TGateway;
@@ -14,13 +13,7 @@ type Props = {
}; };
const schema = z.object({ const schema = z.object({
name: z.string(), name: z.string()
projects: z
.object({
id: z.string(),
name: z.string()
})
.array()
}); });
export type FormData = z.infer<typeof schema>; export type FormData = z.infer<typeof schema>;
@@ -38,20 +31,13 @@ export const EditGatewayDetailsModal = ({ gatewayDetails, onClose }: Props) => {
}); });
const updateGatewayById = useUpdateGatewayById(); const updateGatewayById = useUpdateGatewayById();
// when gateway goes to other products switch to all
const { data: secretManagerWorkspaces, isLoading: isSecretManagerLoading } = useGetUserWorkspaces(
{
type: ProjectType.SecretManager
}
);
const onFormSubmit = ({ name, projects }: FormData) => { const onFormSubmit = ({ name }: FormData) => {
if (isSubmitting) return; if (isSubmitting) return;
updateGatewayById.mutate( updateGatewayById.mutate(
{ {
id: gatewayDetails.id, id: gatewayDetails.id,
name, name
projectIds: projects.map((el) => el.id)
}, },
{ {
onSuccess: () => { onSuccess: () => {
@@ -76,30 +62,6 @@ export const EditGatewayDetailsModal = ({ gatewayDetails, onClose }: Props) => {
</FormControl> </FormControl>
)} )}
/> />
<Controller
control={control}
name="projects"
render={({ field: { onChange, value }, fieldState: { error } }) => (
<FormControl
className="w-full"
label="Projects"
tooltipText="Select the project(s) that you'd like to add this gateway to"
errorText={error?.message}
isError={Boolean(error)}
>
<FilterableSelect
options={secretManagerWorkspaces}
placeholder="Select projects..."
value={value}
onChange={onChange}
isMulti
isLoading={isSecretManagerLoading}
getOptionValue={(option) => option.id}
getOptionLabel={(option) => option.name}
/>
</FormControl>
)}
/>
<div className="mt-4 flex items-center"> <div className="mt-4 flex items-center">
<Button className="mr-4" size="sm" type="submit" isLoading={isSubmitting}> <Button className="mr-4" size="sm" type="submit" isLoading={isSubmitting}>
Update Update
@@ -69,7 +69,8 @@ const orgGatewayPermissionSchema = z
[OrgGatewayPermissionActions.ListGateways]: z.boolean().optional(), [OrgGatewayPermissionActions.ListGateways]: z.boolean().optional(),
[OrgGatewayPermissionActions.EditGateways]: z.boolean().optional(), [OrgGatewayPermissionActions.EditGateways]: z.boolean().optional(),
[OrgGatewayPermissionActions.DeleteGateways]: z.boolean().optional(), [OrgGatewayPermissionActions.DeleteGateways]: z.boolean().optional(),
[OrgGatewayPermissionActions.CreateGateways]: z.boolean().optional() [OrgGatewayPermissionActions.CreateGateways]: z.boolean().optional(),
[OrgGatewayPermissionActions.AttachGateways]: z.boolean().optional()
}) })
.optional(); .optional();
@@ -27,7 +27,8 @@ const PERMISSION_ACTIONS = [
{ action: OrgGatewayPermissionActions.ListGateways, label: "List Gateways" }, { action: OrgGatewayPermissionActions.ListGateways, label: "List Gateways" },
{ action: OrgGatewayPermissionActions.CreateGateways, label: "Create Gateways" }, { action: OrgGatewayPermissionActions.CreateGateways, label: "Create Gateways" },
{ action: OrgGatewayPermissionActions.EditGateways, label: "Edit Gateways" }, { action: OrgGatewayPermissionActions.EditGateways, label: "Edit Gateways" },
{ action: OrgGatewayPermissionActions.DeleteGateways, label: "Delete Gateways" } { action: OrgGatewayPermissionActions.DeleteGateways, label: "Delete Gateways" },
{ action: OrgGatewayPermissionActions.AttachGateways, label: "Attach Gateways" }
] as const; ] as const;
export const OrgGatewayPermissionRow = ({ isEditable, control, setValue }: Props) => { export const OrgGatewayPermissionRow = ({ isEditable, control, setValue }: Props) => {
@@ -6,6 +6,7 @@ import { z } from "zod";
import { TtlFormLabel } from "@app/components/features"; import { TtlFormLabel } from "@app/components/features";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import { import {
Accordion, Accordion,
AccordionContent, AccordionContent,
@@ -18,9 +19,13 @@ import {
SecretInput, SecretInput,
Select, Select,
SelectItem, SelectItem,
TextArea TextArea,
Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
import { useWorkspace } from "@app/context"; import {
OrgGatewayPermissionActions,
OrgPermissionSubjects
} from "@app/context/OrgPermissionContext/types";
import { gatewaysQueryKeys, useCreateDynamicSecret } from "@app/hooks/api"; import { gatewaysQueryKeys, useCreateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretProviders, SqlProviders } from "@app/hooks/api/dynamicSecret/types"; import { DynamicSecretProviders, SqlProviders } from "@app/hooks/api/dynamicSecret/types";
import { WorkspaceEnv } from "@app/hooks/api/types"; import { WorkspaceEnv } from "@app/hooks/api/types";
@@ -61,7 +66,7 @@ const formSchema = z.object({
revocationStatement: z.string().min(1), revocationStatement: z.string().min(1),
renewStatement: z.string().optional(), renewStatement: z.string().optional(),
ca: z.string().optional(), ca: z.string().optional(),
projectGatewayId: z.string().optional() gatewayId: z.string().optional()
}), }),
defaultTTL: z.string().superRefine((val, ctx) => { defaultTTL: z.string().superRefine((val, ctx) => {
const valMs = ms(val); const valMs = ms(val);
@@ -164,8 +169,6 @@ export const SqlDatabaseInputForm = ({
projectSlug, projectSlug,
isSingleEnvironmentMode isSingleEnvironmentMode
}: Props) => { }: Props) => {
const { currentWorkspace } = useWorkspace();
const { const {
control, control,
setValue, setValue,
@@ -193,9 +196,7 @@ export const SqlDatabaseInputForm = ({
}); });
const createDynamicSecret = useCreateDynamicSecret(); const createDynamicSecret = useCreateDynamicSecret();
const { data: projectGateways, isPending: isProjectGatewaysLoading } = useQuery( const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
gatewaysQueryKeys.listProjectGateways({ projectId: currentWorkspace.id })
);
const handleCreateDynamicSecret = async ({ const handleCreateDynamicSecret = async ({
name, name,
@@ -301,40 +302,55 @@ export const SqlDatabaseInputForm = ({
Configuration Configuration
</div> </div>
<div> <div>
<Controller <OrgPermissionCan
control={control} I={OrgGatewayPermissionActions.AttachGateways}
name="provider.projectGatewayId" a={OrgPermissionSubjects.Gateway}
defaultValue="" >
render={({ field: { value, onChange }, fieldState: { error } }) => ( {(isAllowed) => (
<FormControl <Controller
isError={Boolean(error?.message)} control={control}
errorText={error?.message} name="provider.gatewayId"
label="Gateway" defaultValue=""
> render={({ field: { value, onChange }, fieldState: { error } }) => (
<Select <FormControl
value={value} isError={Boolean(error?.message)}
onValueChange={onChange} errorText={error?.message}
className="w-full border border-mineshaft-500" label="Gateway"
dropdownContainerClassName="max-w-none"
isLoading={isProjectGatewaysLoading}
placeholder="Internet gateway"
position="popper"
>
<SelectItem
value={null as unknown as string}
onClick={() => onChange(undefined)}
> >
Internet Gateway <Tooltip
</SelectItem> isDisabled={isAllowed}
{projectGateways?.map((el) => ( content="Restricted access. You don't have permission to attach gateways to resources."
<SelectItem value={el.projectGatewayId} key={el.projectGatewayId}> >
{el.name} <div>
</SelectItem> <Select
))} isDisabled={!isAllowed}
</Select> value={value}
</FormControl> onValueChange={onChange}
className="w-full border border-mineshaft-500"
dropdownContainerClassName="max-w-none"
isLoading={isGatewaysLoading}
placeholder="Internet gateway"
position="popper"
>
<SelectItem
value={null as unknown as string}
onClick={() => onChange(undefined)}
>
Internet Gateway
</SelectItem>
{gateways?.map((el) => (
<SelectItem value={el.id} key={el.id}>
{el.name}
</SelectItem>
))}
</Select>
</div>
</Tooltip>
</FormControl>
)}
/>
)} )}
/> </OrgPermissionCan>
</div> </div>
<div className="flex flex-col"> <div className="flex flex-col">
<div className="pb-0.5 pl-1 text-sm text-mineshaft-400">Service</div> <div className="pb-0.5 pl-1 text-sm text-mineshaft-400">Service</div>
@@ -6,6 +6,7 @@ import { z } from "zod";
import { TtlFormLabel } from "@app/components/features"; import { TtlFormLabel } from "@app/components/features";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import { import {
Accordion, Accordion,
AccordionContent, AccordionContent,
@@ -17,9 +18,11 @@ import {
SecretInput, SecretInput,
Select, Select,
SelectItem, SelectItem,
TextArea TextArea,
Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
import { useWorkspace } from "@app/context"; import { OrgPermissionSubjects } from "@app/context";
import { OrgGatewayPermissionActions } from "@app/context/OrgPermissionContext/types";
import { gatewaysQueryKeys, useUpdateDynamicSecret } from "@app/hooks/api"; import { gatewaysQueryKeys, useUpdateDynamicSecret } from "@app/hooks/api";
import { SqlProviders, TDynamicSecret } from "@app/hooks/api/dynamicSecret/types"; import { SqlProviders, TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
@@ -60,7 +63,7 @@ const formSchema = z.object({
revocationStatement: z.string().min(1), revocationStatement: z.string().min(1),
renewStatement: z.string().optional(), renewStatement: z.string().optional(),
ca: z.string().optional(), ca: z.string().optional(),
projectGatewayId: z.string().optional().nullable() gatewayId: z.string().optional().nullable()
}) })
.partial(), .partial(),
defaultTTL: z.string().superRefine((val, ctx) => { defaultTTL: z.string().superRefine((val, ctx) => {
@@ -147,15 +150,11 @@ export const EditDynamicSecretSqlProviderForm = ({
} }
}); });
const { currentWorkspace } = useWorkspace(); const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
const { data: projectGateways, isPending: isProjectGatewaysLoading } = useQuery(
gatewaysQueryKeys.listProjectGateways({ projectId: currentWorkspace.id })
);
const updateDynamicSecret = useUpdateDynamicSecret(); const updateDynamicSecret = useUpdateDynamicSecret();
const selectedProjectGatewayId = watch("inputs.projectGatewayId"); const selectedGatewayId = watch("inputs.gatewayId");
const isGatewayInActive = const isGatewayInActive = gateways?.findIndex((el) => el.id === selectedGatewayId) === -1;
projectGateways?.findIndex((el) => el.projectGatewayId === selectedProjectGatewayId) === -1;
const handleUpdateDynamicSecret = async ({ const handleUpdateDynamicSecret = async ({
inputs, inputs,
@@ -177,7 +176,7 @@ export const EditDynamicSecretSqlProviderForm = ({
defaultTTL, defaultTTL,
inputs: { inputs: {
...inputs, ...inputs,
projectGatewayId: isGatewayInActive ? null : inputs.projectGatewayId gatewayId: isGatewayInActive ? null : inputs.gatewayId
}, },
newName: newName === dynamicSecret.name ? undefined : newName, newName: newName === dynamicSecret.name ? undefined : newName,
metadata metadata
@@ -250,45 +249,60 @@ export const EditDynamicSecretSqlProviderForm = ({
<div> <div>
<div className="mb-4 border-b border-b-mineshaft-600 pb-2">Configuration</div> <div className="mb-4 border-b border-b-mineshaft-600 pb-2">Configuration</div>
<div> <div>
<Controller <OrgPermissionCan
control={control} I={OrgGatewayPermissionActions.AttachGateways}
name="inputs.projectGatewayId" a={OrgPermissionSubjects.Gateway}
defaultValue="" >
render={({ field: { value, onChange }, fieldState: { error } }) => ( {(isAllowed) => (
<FormControl <Controller
isError={Boolean(error?.message) || isGatewayInActive} control={control}
errorText={ name="inputs.gatewayId"
isGatewayInActive && selectedProjectGatewayId defaultValue=""
? `Project Gateway ${selectedProjectGatewayId} is removed` render={({ field: { value, onChange }, fieldState: { error } }) => (
: error?.message <FormControl
} isError={Boolean(error?.message) || isGatewayInActive}
label="Gateway" errorText={
helperText="" isGatewayInActive && selectedGatewayId
> ? `Project Gateway ${selectedGatewayId} is removed`
<Select : error?.message
value={value || undefined} }
onValueChange={onChange} label="Gateway"
className="w-full border border-mineshaft-500" helperText=""
dropdownContainerClassName="max-w-none"
isLoading={isProjectGatewaysLoading}
placeholder="Internet Gateway"
position="popper"
>
<SelectItem
value={null as unknown as string}
onClick={() => onChange(undefined)}
> >
Internet Gateway <Tooltip
</SelectItem> isDisabled={isAllowed}
{projectGateways?.map((el) => ( content="Restricted access. You don't have permission to attach gateways to resources."
<SelectItem value={el.projectGatewayId} key={el.id}> >
{el.name} <div>
</SelectItem> <Select
))} isDisabled={!isAllowed}
</Select> value={value || undefined}
</FormControl> onValueChange={onChange}
className="w-full border border-mineshaft-500"
dropdownContainerClassName="max-w-none"
isLoading={isGatewaysLoading}
placeholder="Internet Gateway"
position="popper"
>
<SelectItem
value={null as unknown as string}
onClick={() => onChange(undefined)}
>
Internet Gateway
</SelectItem>
{gateways?.map((el) => (
<SelectItem value={el.id} key={el.id}>
{el.name}
</SelectItem>
))}
</Select>
</div>
</Tooltip>
</FormControl>
)}
/>
)} )}
/> </OrgPermissionCan>
</div> </div>
<div className="flex flex-col"> <div className="flex flex-col">
<Controller <Controller