Merge remote-tracking branch 'origin' into ssh-certs
@@ -22,8 +22,10 @@ export const mockQueue = (): TQueueServiceFactory => {
|
|||||||
listen: (name, event) => {
|
listen: (name, event) => {
|
||||||
events[name] = event;
|
events[name] = event;
|
||||||
},
|
},
|
||||||
|
getRepeatableJobs: async () => [],
|
||||||
clearQueue: async () => {},
|
clearQueue: async () => {},
|
||||||
stopJobById: async () => {},
|
stopJobById: async () => {},
|
||||||
stopRepeatableJobByJobId: async () => true
|
stopRepeatableJobByJobId: async () => true,
|
||||||
|
stopRepeatableJobByKey: async () => true
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1137,6 +1137,7 @@ export const INTEGRATION = {
|
|||||||
shouldAutoRedeploy: "Used by Render to trigger auto deploy.",
|
shouldAutoRedeploy: "Used by Render to trigger auto deploy.",
|
||||||
secretGCPLabel: "The label for GCP secrets.",
|
secretGCPLabel: "The label for GCP secrets.",
|
||||||
secretAWSTag: "The tags for AWS secrets.",
|
secretAWSTag: "The tags for AWS secrets.",
|
||||||
|
azureLabel: "Define which label to assign to secrets created in Azure App Configuration.",
|
||||||
githubVisibility:
|
githubVisibility:
|
||||||
"Define where the secrets from the Github Integration should be visible. Option 'selected' lets you directly define which repositories to sync secrets to.",
|
"Define where the secrets from the Github Integration should be visible. Option 'selected' lets you directly define which repositories to sync secrets to.",
|
||||||
githubVisibilityRepoIds:
|
githubVisibilityRepoIds:
|
||||||
|
|||||||
@@ -20,11 +20,12 @@ export const withTransaction = <K extends object>(db: Knex, dal: K) => ({
|
|||||||
|
|
||||||
export type TFindFilter<R extends object = object> = Partial<R> & {
|
export type TFindFilter<R extends object = object> = Partial<R> & {
|
||||||
$in?: Partial<{ [k in keyof R]: R[k][] }>;
|
$in?: Partial<{ [k in keyof R]: R[k][] }>;
|
||||||
|
$notNull?: Array<keyof R>;
|
||||||
$search?: Partial<{ [k in keyof R]: R[k] }>;
|
$search?: Partial<{ [k in keyof R]: R[k] }>;
|
||||||
$complex?: TKnexDynamicOperator<R>;
|
$complex?: TKnexDynamicOperator<R>;
|
||||||
};
|
};
|
||||||
export const buildFindFilter =
|
export const buildFindFilter =
|
||||||
<R extends object = object>({ $in, $search, $complex, ...filter }: TFindFilter<R>) =>
|
<R extends object = object>({ $in, $notNull, $search, $complex, ...filter }: TFindFilter<R>) =>
|
||||||
(bd: Knex.QueryBuilder<R, R>) => {
|
(bd: Knex.QueryBuilder<R, R>) => {
|
||||||
void bd.where(filter);
|
void bd.where(filter);
|
||||||
if ($in) {
|
if ($in) {
|
||||||
@@ -34,6 +35,13 @@ export const buildFindFilter =
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($notNull?.length) {
|
||||||
|
$notNull.forEach((key) => {
|
||||||
|
void bd.whereNotNull(key as never);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if ($search) {
|
if ($search) {
|
||||||
Object.entries($search).forEach(([key, val]) => {
|
Object.entries($search).forEach(([key, val]) => {
|
||||||
if (val) {
|
if (val) {
|
||||||
|
|||||||
@@ -317,6 +317,13 @@ export const queueServiceFactory = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getRepeatableJobs = (name: QueueName, startOffset?: number, endOffset?: number) => {
|
||||||
|
const q = queueContainer[name];
|
||||||
|
if (!q) throw new Error(`Queue '${name}' not initialized`);
|
||||||
|
|
||||||
|
return q.getRepeatableJobs(startOffset, endOffset);
|
||||||
|
};
|
||||||
|
|
||||||
const stopRepeatableJobByJobId = async <T extends QueueName>(name: T, jobId: string) => {
|
const stopRepeatableJobByJobId = async <T extends QueueName>(name: T, jobId: string) => {
|
||||||
const q = queueContainer[name];
|
const q = queueContainer[name];
|
||||||
const job = await q.getJob(jobId);
|
const job = await q.getJob(jobId);
|
||||||
@@ -326,6 +333,11 @@ export const queueServiceFactory = (
|
|||||||
return q.removeRepeatableByKey(job.repeatJobKey);
|
return q.removeRepeatableByKey(job.repeatJobKey);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const stopRepeatableJobByKey = async <T extends QueueName>(name: T, repeatJobKey: string) => {
|
||||||
|
const q = queueContainer[name];
|
||||||
|
return q.removeRepeatableByKey(repeatJobKey);
|
||||||
|
};
|
||||||
|
|
||||||
const stopJobById = async <T extends QueueName>(name: T, jobId: string) => {
|
const stopJobById = async <T extends QueueName>(name: T, jobId: string) => {
|
||||||
const q = queueContainer[name];
|
const q = queueContainer[name];
|
||||||
const job = await q.getJob(jobId);
|
const job = await q.getJob(jobId);
|
||||||
@@ -349,8 +361,10 @@ export const queueServiceFactory = (
|
|||||||
shutdown,
|
shutdown,
|
||||||
stopRepeatableJob,
|
stopRepeatableJob,
|
||||||
stopRepeatableJobByJobId,
|
stopRepeatableJobByJobId,
|
||||||
|
stopRepeatableJobByKey,
|
||||||
clearQueue,
|
clearQueue,
|
||||||
stopJobById,
|
stopJobById,
|
||||||
|
getRepeatableJobs,
|
||||||
startPg,
|
startPg,
|
||||||
queuePg
|
queuePg
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -551,7 +551,11 @@ export const registerRoutes = async (
|
|||||||
|
|
||||||
const orgService = orgServiceFactory({
|
const orgService = orgServiceFactory({
|
||||||
userAliasDAL,
|
userAliasDAL,
|
||||||
|
queueService,
|
||||||
identityMetadataDAL,
|
identityMetadataDAL,
|
||||||
|
secretDAL,
|
||||||
|
secretV2BridgeDAL,
|
||||||
|
folderDAL,
|
||||||
licenseService,
|
licenseService,
|
||||||
samlConfigDAL,
|
samlConfigDAL,
|
||||||
orgRoleDAL,
|
orgRoleDAL,
|
||||||
@@ -572,6 +576,7 @@ export const registerRoutes = async (
|
|||||||
groupDAL,
|
groupDAL,
|
||||||
orgBotDAL,
|
orgBotDAL,
|
||||||
oidcConfigDAL,
|
oidcConfigDAL,
|
||||||
|
loginService,
|
||||||
projectBotService
|
projectBotService
|
||||||
});
|
});
|
||||||
const signupService = authSignupServiceFactory({
|
const signupService = authSignupServiceFactory({
|
||||||
@@ -805,10 +810,58 @@ export const registerRoutes = async (
|
|||||||
projectTemplateDAL
|
projectTemplateDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const integrationAuthService = integrationAuthServiceFactory({
|
||||||
|
integrationAuthDAL,
|
||||||
|
integrationDAL,
|
||||||
|
permissionService,
|
||||||
|
projectBotService,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const secretQueueService = secretQueueFactory({
|
||||||
|
keyStore,
|
||||||
|
queueService,
|
||||||
|
secretDAL,
|
||||||
|
folderDAL,
|
||||||
|
integrationAuthService,
|
||||||
|
projectBotService,
|
||||||
|
integrationDAL,
|
||||||
|
secretImportDAL,
|
||||||
|
projectEnvDAL,
|
||||||
|
webhookDAL,
|
||||||
|
orgDAL,
|
||||||
|
auditLogService,
|
||||||
|
userDAL,
|
||||||
|
projectMembershipDAL,
|
||||||
|
smtpService,
|
||||||
|
projectDAL,
|
||||||
|
projectBotDAL,
|
||||||
|
secretVersionDAL,
|
||||||
|
secretBlindIndexDAL,
|
||||||
|
secretTagDAL,
|
||||||
|
secretVersionTagDAL,
|
||||||
|
kmsService,
|
||||||
|
secretVersionV2BridgeDAL,
|
||||||
|
secretV2BridgeDAL,
|
||||||
|
secretVersionTagV2BridgeDAL,
|
||||||
|
secretRotationDAL,
|
||||||
|
integrationAuthDAL,
|
||||||
|
snapshotDAL,
|
||||||
|
snapshotSecretV2BridgeDAL,
|
||||||
|
secretApprovalRequestDAL,
|
||||||
|
projectKeyDAL,
|
||||||
|
projectUserMembershipRoleDAL,
|
||||||
|
orgService
|
||||||
|
});
|
||||||
|
|
||||||
const projectService = projectServiceFactory({
|
const projectService = projectServiceFactory({
|
||||||
permissionService,
|
permissionService,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
|
secretDAL,
|
||||||
|
secretV2BridgeDAL,
|
||||||
|
queueService,
|
||||||
projectQueue: projectQueueService,
|
projectQueue: projectQueueService,
|
||||||
|
projectBotService,
|
||||||
identityProjectDAL,
|
identityProjectDAL,
|
||||||
identityOrgMembershipDAL,
|
identityOrgMembershipDAL,
|
||||||
projectKeyDAL,
|
projectKeyDAL,
|
||||||
@@ -891,48 +944,6 @@ export const registerRoutes = async (
|
|||||||
projectDAL
|
projectDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const integrationAuthService = integrationAuthServiceFactory({
|
|
||||||
integrationAuthDAL,
|
|
||||||
integrationDAL,
|
|
||||||
permissionService,
|
|
||||||
projectBotService,
|
|
||||||
kmsService
|
|
||||||
});
|
|
||||||
const secretQueueService = secretQueueFactory({
|
|
||||||
keyStore,
|
|
||||||
queueService,
|
|
||||||
secretDAL,
|
|
||||||
folderDAL,
|
|
||||||
integrationAuthService,
|
|
||||||
projectBotService,
|
|
||||||
integrationDAL,
|
|
||||||
secretImportDAL,
|
|
||||||
projectEnvDAL,
|
|
||||||
webhookDAL,
|
|
||||||
orgDAL,
|
|
||||||
auditLogService,
|
|
||||||
userDAL,
|
|
||||||
projectMembershipDAL,
|
|
||||||
smtpService,
|
|
||||||
projectDAL,
|
|
||||||
projectBotDAL,
|
|
||||||
secretVersionDAL,
|
|
||||||
secretBlindIndexDAL,
|
|
||||||
secretTagDAL,
|
|
||||||
secretVersionTagDAL,
|
|
||||||
kmsService,
|
|
||||||
secretVersionV2BridgeDAL,
|
|
||||||
secretV2BridgeDAL,
|
|
||||||
secretVersionTagV2BridgeDAL,
|
|
||||||
secretRotationDAL,
|
|
||||||
integrationAuthDAL,
|
|
||||||
snapshotDAL,
|
|
||||||
snapshotSecretV2BridgeDAL,
|
|
||||||
secretApprovalRequestDAL,
|
|
||||||
projectKeyDAL,
|
|
||||||
projectUserMembershipRoleDAL,
|
|
||||||
orgService
|
|
||||||
});
|
|
||||||
const secretImportService = secretImportServiceFactory({
|
const secretImportService = secretImportServiceFactory({
|
||||||
licenseService,
|
licenseService,
|
||||||
projectBotService,
|
projectBotService,
|
||||||
@@ -1261,6 +1272,7 @@ export const registerRoutes = async (
|
|||||||
auditLogDAL,
|
auditLogDAL,
|
||||||
queueService,
|
queueService,
|
||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
|
secretDAL,
|
||||||
secretFolderVersionDAL: folderVersionDAL,
|
secretFolderVersionDAL: folderVersionDAL,
|
||||||
snapshotDAL,
|
snapshotDAL,
|
||||||
identityAccessTokenDAL,
|
identityAccessTokenDAL,
|
||||||
|
|||||||
@@ -1185,4 +1185,50 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
|
|||||||
return { spaces };
|
return { spaces };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:integrationAuthId/circleci/organizations",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
integrationAuthId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
organizations: z
|
||||||
|
.object({
|
||||||
|
name: z.string(),
|
||||||
|
slug: z.string(),
|
||||||
|
projects: z
|
||||||
|
.object({
|
||||||
|
name: z.string(),
|
||||||
|
id: z.string()
|
||||||
|
})
|
||||||
|
.array(),
|
||||||
|
contexts: z
|
||||||
|
.object({
|
||||||
|
name: z.string(),
|
||||||
|
id: z.string()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const organizations = await server.services.integrationAuth.getCircleCIOrganizations({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
id: req.params.integrationAuthId
|
||||||
|
});
|
||||||
|
return { organizations };
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
|||||||
import { slugSchema } from "@app/server/lib/schemas";
|
import { slugSchema } from "@app/server/lib/schemas";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { ActorType, AuthMode, MfaMethod } from "@app/services/auth/auth-type";
|
import { ActorType, AuthMode, MfaMethod } from "@app/services/auth/auth-type";
|
||||||
|
import { sanitizedOrganizationSchema } from "@app/services/org/org-schema";
|
||||||
|
|
||||||
import { integrationAuthPubSchema } from "../sanitizedSchemas";
|
import { integrationAuthPubSchema } from "../sanitizedSchemas";
|
||||||
|
|
||||||
@@ -29,9 +30,11 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
schema: {
|
schema: {
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
organizations: OrganizationsSchema.extend({
|
organizations: sanitizedOrganizationSchema
|
||||||
|
.extend({
|
||||||
orgAuthMethod: z.string()
|
orgAuthMethod: z.string()
|
||||||
}).array()
|
})
|
||||||
|
.array()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
UsersSchema
|
UsersSchema
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
import { ORGANIZATIONS } from "@app/lib/api-docs";
|
import { ORGANIZATIONS } from "@app/lib/api-docs";
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
|
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
|
||||||
@@ -363,21 +364,35 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
organization: OrganizationsSchema
|
organization: OrganizationsSchema,
|
||||||
|
accessToken: z.string()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
|
||||||
handler: async (req) => {
|
handler: async (req, res) => {
|
||||||
if (req.auth.actor !== ActorType.USER) return;
|
if (req.auth.actor !== ActorType.USER) return;
|
||||||
|
|
||||||
const organization = await server.services.org.deleteOrganizationById(
|
const cfg = getConfig();
|
||||||
req.permission.id,
|
|
||||||
req.params.organizationId,
|
const { organization, tokens } = await server.services.org.deleteOrganizationById({
|
||||||
req.permission.authMethod,
|
userId: req.permission.id,
|
||||||
req.permission.orgId
|
orgId: req.params.organizationId,
|
||||||
);
|
actorAuthMethod: req.permission.authMethod,
|
||||||
return { organization };
|
actorOrgId: req.permission.orgId,
|
||||||
|
authorizationHeader: req.headers.authorization,
|
||||||
|
userAgentHeader: req.headers["user-agent"],
|
||||||
|
ipAddress: req.realIp
|
||||||
|
});
|
||||||
|
|
||||||
|
void res.setCookie("jid", tokens.refreshToken, {
|
||||||
|
httpOnly: true,
|
||||||
|
path: "/",
|
||||||
|
sameSite: "strict",
|
||||||
|
secure: cfg.HTTPS_ENABLED
|
||||||
|
});
|
||||||
|
|
||||||
|
return { organization, accessToken: tokens.accessToken };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { AuthTokenSessionsSchema, OrganizationsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
|
import { AuthTokenSessionsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
|
||||||
import { ApiKeysSchema } from "@app/db/schemas/api-keys";
|
import { ApiKeysSchema } from "@app/db/schemas/api-keys";
|
||||||
import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMethod, AuthMode, MfaMethod } from "@app/services/auth/auth-type";
|
import { AuthMethod, AuthMode, MfaMethod } from "@app/services/auth/auth-type";
|
||||||
|
import { sanitizedOrganizationSchema } from "@app/services/org/org-schema";
|
||||||
|
|
||||||
export const registerUserRouter = async (server: FastifyZodProvider) => {
|
export const registerUserRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
@@ -134,7 +135,7 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
|
|||||||
description: "Return organizations that current user is part of",
|
description: "Return organizations that current user is part of",
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
organizations: OrganizationsSchema.array()
|
organizations: sanitizedOrganizationSchema.array()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,9 +12,12 @@ export type TTokenDALFactory = ReturnType<typeof tokenDALFactory>;
|
|||||||
export const tokenDALFactory = (db: TDbClient) => {
|
export const tokenDALFactory = (db: TDbClient) => {
|
||||||
const authOrm = ormify(db, TableName.AuthTokens);
|
const authOrm = ormify(db, TableName.AuthTokens);
|
||||||
|
|
||||||
const findOneTokenSession = async (filter: Partial<TAuthTokenSessions>): Promise<TAuthTokenSessions | undefined> => {
|
const findOneTokenSession = async (
|
||||||
|
filter: Partial<TAuthTokenSessions>,
|
||||||
|
tx?: Knex
|
||||||
|
): Promise<TAuthTokenSessions | undefined> => {
|
||||||
try {
|
try {
|
||||||
const doc = await db.replicaNode()(TableName.AuthTokenSession).where(filter).first();
|
const doc = await (tx || db.replicaNode())(TableName.AuthTokenSession).where(filter).first();
|
||||||
return doc;
|
return doc;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "FindOneTokenSession" });
|
throw new DatabaseError({ error, name: "FindOneTokenSession" });
|
||||||
@@ -54,10 +57,11 @@ export const tokenDALFactory = (db: TDbClient) => {
|
|||||||
const insertTokenSession = async (
|
const insertTokenSession = async (
|
||||||
userId: string,
|
userId: string,
|
||||||
ip: string,
|
ip: string,
|
||||||
userAgent: string
|
userAgent: string,
|
||||||
|
tx?: Knex
|
||||||
): Promise<TAuthTokenSessions | undefined> => {
|
): Promise<TAuthTokenSessions | undefined> => {
|
||||||
try {
|
try {
|
||||||
const [session] = await db(TableName.AuthTokenSession)
|
const [session] = await (tx || db)(TableName.AuthTokenSession)
|
||||||
.insert({
|
.insert({
|
||||||
userId,
|
userId,
|
||||||
ip,
|
ip,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
|
|
||||||
import bcrypt from "bcrypt";
|
import bcrypt from "bcrypt";
|
||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TAuthTokens, TAuthTokenSessions } from "@app/db/schemas";
|
import { TAuthTokens, TAuthTokenSessions } from "@app/db/schemas";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
@@ -123,14 +124,13 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAu
|
|||||||
return deletedToken?.[0];
|
return deletedToken?.[0];
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUserTokenSession = async ({
|
const getUserTokenSession = async (
|
||||||
userId,
|
{ userId, ip, userAgent }: TIssueAuthTokenDTO,
|
||||||
ip,
|
tx?: Knex
|
||||||
userAgent
|
): Promise<TAuthTokenSessions | undefined> => {
|
||||||
}: TIssueAuthTokenDTO): Promise<TAuthTokenSessions | undefined> => {
|
let session = await tokenDAL.findOneTokenSession({ userId, ip, userAgent }, tx);
|
||||||
let session = await tokenDAL.findOneTokenSession({ userId, ip, userAgent });
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
session = await tokenDAL.insertTokenSession(userId, ip, userAgent);
|
session = await tokenDAL.insertTokenSession(userId, ip, userAgent, tx);
|
||||||
}
|
}
|
||||||
return session;
|
return session;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import bcrypt from "bcrypt";
|
import bcrypt from "bcrypt";
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TUsers, UserDeviceSchema } from "@app/db/schemas";
|
import { TUsers, UserDeviceSchema } from "@app/db/schemas";
|
||||||
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
|
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
|
||||||
@@ -50,13 +51,13 @@ export const authLoginServiceFactory = ({
|
|||||||
* Not exported. This is to update user device list
|
* Not exported. This is to update user device list
|
||||||
* If new device is found. Will be saved and a mail will be send
|
* If new device is found. Will be saved and a mail will be send
|
||||||
*/
|
*/
|
||||||
const updateUserDeviceSession = async (user: TUsers, ip: string, userAgent: string) => {
|
const updateUserDeviceSession = async (user: TUsers, ip: string, userAgent: string, tx?: Knex) => {
|
||||||
const devices = await UserDeviceSchema.parseAsync(user.devices || []);
|
const devices = await UserDeviceSchema.parseAsync(user.devices || []);
|
||||||
const isDeviceSeen = devices.some((device) => device.ip === ip && device.userAgent === userAgent);
|
const isDeviceSeen = devices.some((device) => device.ip === ip && device.userAgent === userAgent);
|
||||||
|
|
||||||
if (!isDeviceSeen) {
|
if (!isDeviceSeen) {
|
||||||
const newDeviceList = devices.concat([{ ip, userAgent }]);
|
const newDeviceList = devices.concat([{ ip, userAgent }]);
|
||||||
await userDAL.updateById(user.id, { devices: JSON.stringify(newDeviceList) });
|
await userDAL.updateById(user.id, { devices: JSON.stringify(newDeviceList) }, tx);
|
||||||
if (user.email) {
|
if (user.email) {
|
||||||
await smtpService.sendMail({
|
await smtpService.sendMail({
|
||||||
template: SmtpTemplates.NewDeviceJoin,
|
template: SmtpTemplates.NewDeviceJoin,
|
||||||
@@ -97,7 +98,8 @@ export const authLoginServiceFactory = ({
|
|||||||
* Check user device and send mail if new device
|
* Check user device and send mail if new device
|
||||||
* generate the auth and refresh token. fn shared by mfa verification and login verification with mfa disabled
|
* generate the auth and refresh token. fn shared by mfa verification and login verification with mfa disabled
|
||||||
*/
|
*/
|
||||||
const generateUserTokens = async ({
|
const generateUserTokens = async (
|
||||||
|
{
|
||||||
user,
|
user,
|
||||||
ip,
|
ip,
|
||||||
userAgent,
|
userAgent,
|
||||||
@@ -113,14 +115,19 @@ export const authLoginServiceFactory = ({
|
|||||||
authMethod: AuthMethod;
|
authMethod: AuthMethod;
|
||||||
isMfaVerified?: boolean;
|
isMfaVerified?: boolean;
|
||||||
mfaMethod?: MfaMethod;
|
mfaMethod?: MfaMethod;
|
||||||
}) => {
|
},
|
||||||
|
tx?: Knex
|
||||||
|
) => {
|
||||||
const cfg = getConfig();
|
const cfg = getConfig();
|
||||||
await updateUserDeviceSession(user, ip, userAgent);
|
await updateUserDeviceSession(user, ip, userAgent, tx);
|
||||||
const tokenSession = await tokenService.getUserTokenSession({
|
const tokenSession = await tokenService.getUserTokenSession(
|
||||||
|
{
|
||||||
userAgent,
|
userAgent,
|
||||||
ip,
|
ip,
|
||||||
userId: user.id
|
userId: user.id
|
||||||
});
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
if (!tokenSession) throw new Error("Failed to create token");
|
if (!tokenSession) throw new Error("Failed to create token");
|
||||||
|
|
||||||
const accessToken = jwt.sign(
|
const accessToken = jwt.sign(
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export type TCircleCIContext = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
created_at: string;
|
||||||
|
};
|
||||||
@@ -17,6 +17,8 @@ import { getConfig } from "@app/lib/config/env";
|
|||||||
import { request } from "@app/lib/config/request";
|
import { request } from "@app/lib/config/request";
|
||||||
import { decryptSymmetric128BitHexKeyUTF8, encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
import { decryptSymmetric128BitHexKeyUTF8, encryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
||||||
import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { groupBy } from "@app/lib/fn";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
import { TGenericPermission, TProjectPermission } from "@app/lib/types";
|
import { TGenericPermission, TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
import { TIntegrationDALFactory } from "../integration/integration-dal";
|
import { TIntegrationDALFactory } from "../integration/integration-dal";
|
||||||
@@ -24,6 +26,7 @@ import { TKmsServiceFactory } from "../kms/kms-service";
|
|||||||
import { KmsDataKey } from "../kms/kms-types";
|
import { KmsDataKey } from "../kms/kms-types";
|
||||||
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
|
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
|
||||||
import { getApps } from "./integration-app-list";
|
import { getApps } from "./integration-app-list";
|
||||||
|
import { TCircleCIContext } from "./integration-app-types";
|
||||||
import { TIntegrationAuthDALFactory } from "./integration-auth-dal";
|
import { TIntegrationAuthDALFactory } from "./integration-auth-dal";
|
||||||
import { IntegrationAuthMetadataSchema, TIntegrationAuthMetadata } from "./integration-auth-schema";
|
import { IntegrationAuthMetadataSchema, TIntegrationAuthMetadata } from "./integration-auth-schema";
|
||||||
import {
|
import {
|
||||||
@@ -31,6 +34,7 @@ import {
|
|||||||
TBitbucketEnvironment,
|
TBitbucketEnvironment,
|
||||||
TBitbucketWorkspace,
|
TBitbucketWorkspace,
|
||||||
TChecklyGroups,
|
TChecklyGroups,
|
||||||
|
TCircleCIOrganization,
|
||||||
TDeleteIntegrationAuthByIdDTO,
|
TDeleteIntegrationAuthByIdDTO,
|
||||||
TDeleteIntegrationAuthsDTO,
|
TDeleteIntegrationAuthsDTO,
|
||||||
TDuplicateGithubIntegrationAuthDTO,
|
TDuplicateGithubIntegrationAuthDTO,
|
||||||
@@ -42,6 +46,7 @@ import {
|
|||||||
TIntegrationAuthBitbucketEnvironmentsDTO,
|
TIntegrationAuthBitbucketEnvironmentsDTO,
|
||||||
TIntegrationAuthBitbucketWorkspaceDTO,
|
TIntegrationAuthBitbucketWorkspaceDTO,
|
||||||
TIntegrationAuthChecklyGroupsDTO,
|
TIntegrationAuthChecklyGroupsDTO,
|
||||||
|
TIntegrationAuthCircleCIOrganizationDTO,
|
||||||
TIntegrationAuthGithubEnvsDTO,
|
TIntegrationAuthGithubEnvsDTO,
|
||||||
TIntegrationAuthGithubOrgsDTO,
|
TIntegrationAuthGithubOrgsDTO,
|
||||||
TIntegrationAuthHerokuPipelinesDTO,
|
TIntegrationAuthHerokuPipelinesDTO,
|
||||||
@@ -1578,6 +1583,120 @@ export const integrationAuthServiceFactory = ({
|
|||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getCircleCIOrganizations = async ({
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
id
|
||||||
|
}: TIntegrationAuthCircleCIOrganizationDTO) => {
|
||||||
|
const integrationAuth = await integrationAuthDAL.findById(id);
|
||||||
|
if (!integrationAuth) throw new NotFoundError({ message: `Integration auth with ID '${id}' not found` });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
integrationAuth.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
|
||||||
|
const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId);
|
||||||
|
const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey);
|
||||||
|
|
||||||
|
const { data: organizations }: { data: TCircleCIOrganization[] } = await request.get(
|
||||||
|
`${IntegrationUrls.CIRCLECI_API_URL}/v2/me/collaborations`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Circle-Token": `${accessToken}`,
|
||||||
|
"Accept-Encoding": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let projects: {
|
||||||
|
orgName: string;
|
||||||
|
projectName: string;
|
||||||
|
projectId?: string;
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const projectRes = (
|
||||||
|
await request.get<{ reponame: string; username: string; vcs_url: string }[]>(
|
||||||
|
`${IntegrationUrls.CIRCLECI_API_URL}/v1.1/projects`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Circle-Token": accessToken,
|
||||||
|
"Accept-Encoding": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).data;
|
||||||
|
|
||||||
|
projects = projectRes.map((a) => ({
|
||||||
|
orgName: a.username, // username maps to unique organization name in CircleCI
|
||||||
|
projectName: a.reponame, // reponame maps to project name within an organization in CircleCI
|
||||||
|
projectId: a.vcs_url.split("/").pop() // vcs_url maps to the project id in CircleCI
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectsByOrg = groupBy(
|
||||||
|
projects.map((p) => ({
|
||||||
|
orgName: p.orgName,
|
||||||
|
name: p.projectName,
|
||||||
|
id: p.projectId as string
|
||||||
|
})),
|
||||||
|
(p) => p.orgName
|
||||||
|
);
|
||||||
|
|
||||||
|
const getOrgContexts = async (orgSlug: string) => {
|
||||||
|
type NextPageToken = string | null | undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const contexts: TCircleCIContext[] = [];
|
||||||
|
let nextPageToken: NextPageToken;
|
||||||
|
|
||||||
|
while (nextPageToken !== null) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const { data } = await request.get<{
|
||||||
|
items: TCircleCIContext[];
|
||||||
|
next_page_token: NextPageToken;
|
||||||
|
}>(`${IntegrationUrls.CIRCLECI_API_URL}/v2/context`, {
|
||||||
|
headers: {
|
||||||
|
"Circle-Token": accessToken,
|
||||||
|
"Accept-Encoding": "application/json"
|
||||||
|
},
|
||||||
|
params: new URLSearchParams({
|
||||||
|
"owner-slug": orgSlug,
|
||||||
|
...(nextPageToken ? { "page-token": nextPageToken } : {})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
contexts.push(...data.items);
|
||||||
|
nextPageToken = data.next_page_token;
|
||||||
|
}
|
||||||
|
|
||||||
|
return contexts?.map((context) => ({
|
||||||
|
name: context.name,
|
||||||
|
id: context.id
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return Promise.all(
|
||||||
|
organizations.map(async (org) => ({
|
||||||
|
name: org.name,
|
||||||
|
slug: org.slug,
|
||||||
|
projects: projectsByOrg[org.name] ?? [],
|
||||||
|
contexts: (await getOrgContexts(org.slug)) ?? []
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const deleteIntegrationAuths = async ({
|
const deleteIntegrationAuths = async ({
|
||||||
projectId,
|
projectId,
|
||||||
integration,
|
integration,
|
||||||
@@ -1790,6 +1909,7 @@ export const integrationAuthServiceFactory = ({
|
|||||||
getTeamcityBuildConfigs,
|
getTeamcityBuildConfigs,
|
||||||
getBitbucketWorkspaces,
|
getBitbucketWorkspaces,
|
||||||
getBitbucketEnvironments,
|
getBitbucketEnvironments,
|
||||||
|
getCircleCIOrganizations,
|
||||||
getIntegrationAccessToken,
|
getIntegrationAccessToken,
|
||||||
duplicateIntegrationAuth,
|
duplicateIntegrationAuth,
|
||||||
getOctopusDeploySpaces,
|
getOctopusDeploySpaces,
|
||||||
|
|||||||
@@ -128,6 +128,10 @@ export type TGetIntegrationAuthTeamCityBuildConfigDTO = {
|
|||||||
appId: string;
|
appId: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TIntegrationAuthCircleCIOrganizationDTO = {
|
||||||
|
id: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TVercelBranches = {
|
export type TVercelBranches = {
|
||||||
ref: string;
|
ref: string;
|
||||||
lastCommit: string;
|
lastCommit: string;
|
||||||
@@ -189,6 +193,14 @@ export type TTeamCityBuildConfig = {
|
|||||||
webUrl: string;
|
webUrl: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TCircleCIOrganization = {
|
||||||
|
id: string;
|
||||||
|
vcsType: string;
|
||||||
|
name: string;
|
||||||
|
avatarUrl: string;
|
||||||
|
slug: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type TIntegrationsWithEnvironment = TIntegrations & {
|
export type TIntegrationsWithEnvironment = TIntegrations & {
|
||||||
environment?:
|
environment?:
|
||||||
| {
|
| {
|
||||||
@@ -215,6 +227,11 @@ export enum OctopusDeployScope {
|
|||||||
// add tenant, variable set, etc.
|
// add tenant, variable set, etc.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum CircleCiScope {
|
||||||
|
Project = "project",
|
||||||
|
Context = "context"
|
||||||
|
}
|
||||||
|
|
||||||
export type TOctopusDeployVariableSet = {
|
export type TOctopusDeployVariableSet = {
|
||||||
Id: string;
|
Id: string;
|
||||||
OwnerId: string;
|
OwnerId: string;
|
||||||
|
|||||||
@@ -76,7 +76,6 @@ export enum IntegrationUrls {
|
|||||||
RAILWAY_API_URL = "https://backboard.railway.app/graphql/v2",
|
RAILWAY_API_URL = "https://backboard.railway.app/graphql/v2",
|
||||||
FLYIO_API_URL = "https://api.fly.io/graphql",
|
FLYIO_API_URL = "https://api.fly.io/graphql",
|
||||||
CIRCLECI_API_URL = "https://circleci.com/api",
|
CIRCLECI_API_URL = "https://circleci.com/api",
|
||||||
DATABRICKS_API_URL = "https:/xxxx.com/api",
|
|
||||||
TRAVISCI_API_URL = "https://api.travis-ci.com",
|
TRAVISCI_API_URL = "https://api.travis-ci.com",
|
||||||
SUPABASE_API_URL = "https://api.supabase.com",
|
SUPABASE_API_URL = "https://api.supabase.com",
|
||||||
LARAVELFORGE_API_URL = "https://forge.laravel.com",
|
LARAVELFORGE_API_URL = "https://forge.laravel.com",
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
export const isAzureKeyVaultReference = (uri: string) => {
|
||||||
|
const tryJsonDecode = () => {
|
||||||
|
try {
|
||||||
|
return (JSON.parse(uri) as { uri: string }).uri || uri;
|
||||||
|
} catch {
|
||||||
|
return uri;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleanUri = tryJsonDecode();
|
||||||
|
|
||||||
|
if (!cleanUri.startsWith("https://")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cleanUri.includes(".vault.azure.net/secrets/")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Check for non-empty string between https:// and .vault.azure.net/secrets/
|
||||||
|
const parts = cleanUri.split(".vault.azure.net/secrets/");
|
||||||
|
const vaultName = parts[0].replace("https://", "");
|
||||||
|
if (!vaultName) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Check for non-empty secret name
|
||||||
|
const secretParts = parts[1].split("/");
|
||||||
|
const secretName = secretParts[0];
|
||||||
|
if (!secretName) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
@@ -39,13 +39,19 @@ import { TCreateManySecretsRawFn, TUpdateManySecretsRawFn } from "@app/services/
|
|||||||
import { TIntegrationDALFactory } from "../integration/integration-dal";
|
import { TIntegrationDALFactory } from "../integration/integration-dal";
|
||||||
import { IntegrationMetadataSchema } from "../integration/integration-schema";
|
import { IntegrationMetadataSchema } from "../integration/integration-schema";
|
||||||
import { IntegrationAuthMetadataSchema } from "./integration-auth-schema";
|
import { IntegrationAuthMetadataSchema } from "./integration-auth-schema";
|
||||||
import { OctopusDeployScope, TIntegrationsWithEnvironment, TOctopusDeployVariableSet } from "./integration-auth-types";
|
import {
|
||||||
|
CircleCiScope,
|
||||||
|
OctopusDeployScope,
|
||||||
|
TIntegrationsWithEnvironment,
|
||||||
|
TOctopusDeployVariableSet
|
||||||
|
} from "./integration-auth-types";
|
||||||
import {
|
import {
|
||||||
IntegrationInitialSyncBehavior,
|
IntegrationInitialSyncBehavior,
|
||||||
IntegrationMappingBehavior,
|
IntegrationMappingBehavior,
|
||||||
Integrations,
|
Integrations,
|
||||||
IntegrationUrls
|
IntegrationUrls
|
||||||
} from "./integration-list";
|
} from "./integration-list";
|
||||||
|
import { isAzureKeyVaultReference } from "./integration-sync-secret-fns";
|
||||||
|
|
||||||
const getSecretKeyValuePair = (secrets: Record<string, { value: string | null; comment?: string } | null>) =>
|
const getSecretKeyValuePair = (secrets: Record<string, { value: string | null; comment?: string } | null>) =>
|
||||||
Object.keys(secrets).reduce<Record<string, string | null | undefined>>((prev, key) => {
|
Object.keys(secrets).reduce<Record<string, string | null | undefined>>((prev, key) => {
|
||||||
@@ -320,11 +326,12 @@ const syncSecretsAzureAppConfig = async ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const metadata = IntegrationMetadataSchema.parse(integration.metadata);
|
const metadata = IntegrationMetadataSchema.parse(integration.metadata);
|
||||||
const azureAppConfigSecrets = (
|
|
||||||
await getCompleteAzureAppConfigValues(
|
const azureAppConfigValuesUrl = `${integration.app}/kv?api-version=2023-11-01&key=${metadata.secretPrefix}*${
|
||||||
`${integration.app}/kv?api-version=2023-11-01&key=${metadata.secretPrefix || ""}*`
|
metadata.azureLabel ? `&label=${metadata.azureLabel}` : ""
|
||||||
)
|
}`;
|
||||||
).reduce(
|
|
||||||
|
const azureAppConfigSecrets = (await getCompleteAzureAppConfigValues(azureAppConfigValuesUrl)).reduce(
|
||||||
(accum, entry) => {
|
(accum, entry) => {
|
||||||
accum[entry.key] = entry.value;
|
accum[entry.key] = entry.value;
|
||||||
|
|
||||||
@@ -405,14 +412,24 @@ const syncSecretsAzureAppConfig = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// create or update secrets on Azure App Config
|
// create or update secrets on Azure App Config
|
||||||
|
|
||||||
for await (const key of Object.keys(secrets)) {
|
for await (const key of Object.keys(secrets)) {
|
||||||
if (!(key in azureAppConfigSecrets) || secrets[key]?.value !== azureAppConfigSecrets[key]) {
|
if (!(key in azureAppConfigSecrets) || secrets[key]?.value !== azureAppConfigSecrets[key]) {
|
||||||
await request.put(
|
await request.put(
|
||||||
`${integration.app}/kv/${key}?api-version=2023-11-01`,
|
`${integration.app}/kv/${key}?api-version=2023-11-01`,
|
||||||
{
|
{
|
||||||
value: secrets[key]?.value
|
value: secrets[key]?.value,
|
||||||
|
...(isAzureKeyVaultReference(secrets[key]?.value || "") && {
|
||||||
|
content_type: "application/vnd.microsoft.appconfig.keyvaultref+json;charset=utf-8"
|
||||||
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
...(metadata.azureLabel && {
|
||||||
|
params: {
|
||||||
|
label: metadata.azureLabel
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${accessToken}`
|
Authorization: `Bearer ${accessToken}`
|
||||||
},
|
},
|
||||||
@@ -432,6 +449,11 @@ const syncSecretsAzureAppConfig = async ({
|
|||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${accessToken}`
|
Authorization: `Bearer ${accessToken}`
|
||||||
},
|
},
|
||||||
|
...(metadata.azureLabel && {
|
||||||
|
params: {
|
||||||
|
label: metadata.azureLabel
|
||||||
|
}
|
||||||
|
}),
|
||||||
// we force IPV4 because docker setup fails with ipv6
|
// we force IPV4 because docker setup fails with ipv6
|
||||||
httpsAgent: new https.Agent({
|
httpsAgent: new https.Agent({
|
||||||
family: 4
|
family: 4
|
||||||
@@ -2245,6 +2267,77 @@ const syncSecretsCircleCI = async ({
|
|||||||
secrets: Record<string, { value: string; comment?: string }>;
|
secrets: Record<string, { value: string; comment?: string }>;
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
}) => {
|
}) => {
|
||||||
|
if (integration.scope === CircleCiScope.Context) {
|
||||||
|
// sync secrets to CircleCI
|
||||||
|
await Promise.all(
|
||||||
|
Object.keys(secrets).map(async (key) =>
|
||||||
|
request.put(
|
||||||
|
`${IntegrationUrls.CIRCLECI_API_URL}/v2/context/${integration.appId}/environment-variable/${key}`,
|
||||||
|
{
|
||||||
|
value: secrets[key].value
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Circle-Token": accessToken,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// get secrets from CircleCI
|
||||||
|
const getSecretsRes = async () => {
|
||||||
|
type EnvVars = {
|
||||||
|
variable: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
context_id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
let nextPageToken: string | null | undefined;
|
||||||
|
const envVars: EnvVars[] = [];
|
||||||
|
|
||||||
|
while (nextPageToken !== null) {
|
||||||
|
const res = await request.get<{
|
||||||
|
items: EnvVars[];
|
||||||
|
next_page_token: string | null;
|
||||||
|
}>(`${IntegrationUrls.CIRCLECI_API_URL}/v2/context/${integration.appId}/environment-variable`, {
|
||||||
|
headers: {
|
||||||
|
"Circle-Token": accessToken,
|
||||||
|
"Accept-Encoding": "application/json"
|
||||||
|
},
|
||||||
|
params: nextPageToken
|
||||||
|
? new URLSearchParams({
|
||||||
|
"page-token": nextPageToken
|
||||||
|
})
|
||||||
|
: undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
envVars.push(...res.data.items);
|
||||||
|
nextPageToken = res.data.next_page_token;
|
||||||
|
}
|
||||||
|
|
||||||
|
return envVars;
|
||||||
|
};
|
||||||
|
|
||||||
|
// delete secrets from CircleCI
|
||||||
|
await Promise.all(
|
||||||
|
(await getSecretsRes()).map(async (sec) => {
|
||||||
|
if (!(sec.variable in secrets)) {
|
||||||
|
return request.delete(
|
||||||
|
`${IntegrationUrls.CIRCLECI_API_URL}/v2/context/${integration.appId}/environment-variable/${sec.variable}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Circle-Token": accessToken,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
const getProjectSlug = async () => {
|
const getProjectSlug = async () => {
|
||||||
const requestConfig = {
|
const requestConfig = {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -2341,6 +2434,7 @@ const syncSecretsCircleCI = async ({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ export const IntegrationMetadataSchema = z.object({
|
|||||||
.optional()
|
.optional()
|
||||||
.describe(INTEGRATION.CREATE.metadata.secretAWSTag),
|
.describe(INTEGRATION.CREATE.metadata.secretAWSTag),
|
||||||
|
|
||||||
|
azureLabel: z.string().optional().describe(INTEGRATION.CREATE.metadata.azureLabel),
|
||||||
|
|
||||||
githubVisibility: z
|
githubVisibility: z
|
||||||
.union([z.literal("selected"), z.literal("private"), z.literal("all")])
|
.union([z.literal("selected"), z.literal("private"), z.literal("all")])
|
||||||
.optional()
|
.optional()
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { OrganizationsSchema } from "@app/db/schemas";
|
||||||
|
|
||||||
|
export const sanitizedOrganizationSchema = OrganizationsSchema.pick({
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
customerId: true,
|
||||||
|
slug: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
authEnforced: true,
|
||||||
|
scimEnabled: true,
|
||||||
|
kmsDefaultKeyId: true,
|
||||||
|
defaultMembershipRole: true,
|
||||||
|
enforceMfa: true,
|
||||||
|
selectedMfaMethod: true
|
||||||
|
});
|
||||||
@@ -31,11 +31,13 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedErro
|
|||||||
import { groupBy } from "@app/lib/fn";
|
import { groupBy } from "@app/lib/fn";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { isDisposableEmail } from "@app/lib/validator";
|
import { isDisposableEmail } from "@app/lib/validator";
|
||||||
|
import { TQueueServiceFactory } from "@app/queue";
|
||||||
import { getDefaultOrgMembershipRoleForUpdateOrg } from "@app/services/org/org-role-fns";
|
import { getDefaultOrgMembershipRoleForUpdateOrg } from "@app/services/org/org-role-fns";
|
||||||
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
||||||
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
|
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
|
||||||
|
|
||||||
import { ActorAuthMethod, ActorType, AuthMethod, AuthTokenType } from "../auth/auth-type";
|
import { TAuthLoginFactory } from "../auth/auth-login-service";
|
||||||
|
import { ActorAuthMethod, ActorType, AuthMethod, AuthModeJwtTokenPayload, AuthTokenType } from "../auth/auth-type";
|
||||||
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
||||||
import { TokenType } from "../auth-token/auth-token-types";
|
import { TokenType } from "../auth-token/auth-token-types";
|
||||||
import { TIdentityMetadataDALFactory } from "../identity/identity-metadata-dal";
|
import { TIdentityMetadataDALFactory } from "../identity/identity-metadata-dal";
|
||||||
@@ -47,6 +49,10 @@ import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
|||||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||||
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
||||||
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
|
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
|
||||||
|
import { TSecretDALFactory } from "../secret/secret-dal";
|
||||||
|
import { fnDeleteProjectSecretReminders } from "../secret/secret-fns";
|
||||||
|
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
|
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
|
||||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||||
import { TUserDALFactory } from "../user/user-dal";
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
import { TIncidentContactsDALFactory } from "./incident-contacts-dal";
|
import { TIncidentContactsDALFactory } from "./incident-contacts-dal";
|
||||||
@@ -69,6 +75,9 @@ import {
|
|||||||
|
|
||||||
type TOrgServiceFactoryDep = {
|
type TOrgServiceFactoryDep = {
|
||||||
userAliasDAL: Pick<TUserAliasDALFactory, "delete">;
|
userAliasDAL: Pick<TUserAliasDALFactory, "delete">;
|
||||||
|
secretDAL: Pick<TSecretDALFactory, "find">;
|
||||||
|
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find">;
|
||||||
|
folderDAL: Pick<TSecretFolderDALFactory, "findByProjectId">;
|
||||||
orgDAL: TOrgDALFactory;
|
orgDAL: TOrgDALFactory;
|
||||||
orgBotDAL: TOrgBotDALFactory;
|
orgBotDAL: TOrgBotDALFactory;
|
||||||
orgRoleDAL: TOrgRoleDALFactory;
|
orgRoleDAL: TOrgRoleDALFactory;
|
||||||
@@ -97,6 +106,8 @@ type TOrgServiceFactoryDep = {
|
|||||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne" | "updateById">;
|
projectBotDAL: Pick<TProjectBotDALFactory, "findOne" | "updateById">;
|
||||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany" | "create">;
|
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany" | "create">;
|
||||||
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
||||||
|
queueService: Pick<TQueueServiceFactory, "stopRepeatableJob">;
|
||||||
|
loginService: Pick<TAuthLoginFactory, "generateUserTokens">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TOrgServiceFactory = ReturnType<typeof orgServiceFactory>;
|
export type TOrgServiceFactory = ReturnType<typeof orgServiceFactory>;
|
||||||
@@ -104,6 +115,9 @@ export type TOrgServiceFactory = ReturnType<typeof orgServiceFactory>;
|
|||||||
export const orgServiceFactory = ({
|
export const orgServiceFactory = ({
|
||||||
userAliasDAL,
|
userAliasDAL,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
|
secretDAL,
|
||||||
|
secretV2BridgeDAL,
|
||||||
|
folderDAL,
|
||||||
userDAL,
|
userDAL,
|
||||||
groupDAL,
|
groupDAL,
|
||||||
orgRoleDAL,
|
orgRoleDAL,
|
||||||
@@ -124,7 +138,9 @@ export const orgServiceFactory = ({
|
|||||||
projectBotDAL,
|
projectBotDAL,
|
||||||
projectUserMembershipRoleDAL,
|
projectUserMembershipRoleDAL,
|
||||||
identityMetadataDAL,
|
identityMetadataDAL,
|
||||||
projectBotService
|
projectBotService,
|
||||||
|
queueService,
|
||||||
|
loginService
|
||||||
}: TOrgServiceFactoryDep) => {
|
}: TOrgServiceFactoryDep) => {
|
||||||
/*
|
/*
|
||||||
* Get organization details by the organization id
|
* Get organization details by the organization id
|
||||||
@@ -419,24 +435,88 @@ export const orgServiceFactory = ({
|
|||||||
/*
|
/*
|
||||||
* Delete organization by id
|
* Delete organization by id
|
||||||
* */
|
* */
|
||||||
const deleteOrganizationById = async (
|
const deleteOrganizationById = async ({
|
||||||
userId: string,
|
userId,
|
||||||
orgId: string,
|
authorizationHeader,
|
||||||
actorAuthMethod: ActorAuthMethod,
|
userAgentHeader,
|
||||||
actorOrgId: string | undefined
|
ipAddress,
|
||||||
) => {
|
orgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
}: {
|
||||||
|
userId: string;
|
||||||
|
authorizationHeader?: string;
|
||||||
|
userAgentHeader?: string;
|
||||||
|
ipAddress: string;
|
||||||
|
orgId: string;
|
||||||
|
actorAuthMethod: ActorAuthMethod;
|
||||||
|
actorOrgId: string | undefined;
|
||||||
|
}) => {
|
||||||
const { membership } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
|
const { membership } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
|
||||||
if ((membership.role as OrgMembershipRole) !== OrgMembershipRole.Admin)
|
if ((membership.role as OrgMembershipRole) !== OrgMembershipRole.Admin) {
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
name: "DeleteOrganizationById",
|
name: "DeleteOrganizationById",
|
||||||
message: "Insufficient privileges"
|
message: "Insufficient privileges"
|
||||||
});
|
});
|
||||||
|
|
||||||
const organization = await orgDAL.deleteById(orgId);
|
|
||||||
if (organization.customerId) {
|
|
||||||
await licenseService.removeOrgCustomer(organization.customerId);
|
|
||||||
}
|
}
|
||||||
return organization;
|
|
||||||
|
if (!authorizationHeader) {
|
||||||
|
throw new UnauthorizedError({ name: "Authorization header not set on request." });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!userAgentHeader) {
|
||||||
|
throw new BadRequestError({ name: "User agent not set on request." });
|
||||||
|
}
|
||||||
|
|
||||||
|
const cfg = getConfig();
|
||||||
|
const authToken = authorizationHeader.replace("Bearer ", "");
|
||||||
|
|
||||||
|
const decodedToken = jwt.verify(authToken, cfg.AUTH_SECRET) as AuthModeJwtTokenPayload;
|
||||||
|
if (!decodedToken.authMethod) throw new UnauthorizedError({ name: "Auth method not found on existing token" });
|
||||||
|
|
||||||
|
const response = await orgDAL.transaction(async (tx) => {
|
||||||
|
const projects = await projectDAL.find({ orgId }, { tx });
|
||||||
|
|
||||||
|
for await (const project of projects) {
|
||||||
|
await fnDeleteProjectSecretReminders(project.id, {
|
||||||
|
secretDAL,
|
||||||
|
secretV2BridgeDAL,
|
||||||
|
queueService,
|
||||||
|
projectBotService,
|
||||||
|
folderDAL
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const deletedOrg = await orgDAL.deleteById(orgId, tx);
|
||||||
|
|
||||||
|
if (deletedOrg.customerId) {
|
||||||
|
await licenseService.removeOrgCustomer(deletedOrg.customerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate new tokens without the organization ID present
|
||||||
|
const user = await userDAL.findById(userId, tx);
|
||||||
|
const { access: accessToken, refresh: refreshToken } = await loginService.generateUserTokens(
|
||||||
|
{
|
||||||
|
user,
|
||||||
|
authMethod: decodedToken.authMethod,
|
||||||
|
ip: ipAddress,
|
||||||
|
userAgent: userAgentHeader,
|
||||||
|
isMfaVerified: decodedToken.isMfaVerified,
|
||||||
|
mfaMethod: decodedToken.mfaMethod
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
organization: deletedOrg,
|
||||||
|
tokens: {
|
||||||
|
accessToken,
|
||||||
|
refreshToken
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
};
|
};
|
||||||
/*
|
/*
|
||||||
* Org membership management
|
* Org membership management
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export const projectDALFactory = (db: TDbClient) => {
|
|||||||
.join(TableName.Project, `${TableName.GroupProjectMembership}.projectId`, `${TableName.Project}.id`)
|
.join(TableName.Project, `${TableName.GroupProjectMembership}.projectId`, `${TableName.Project}.id`)
|
||||||
.where(`${TableName.Project}.orgId`, orgId)
|
.where(`${TableName.Project}.orgId`, orgId)
|
||||||
.andWhere((qb) => {
|
.andWhere((qb) => {
|
||||||
if (projectType) {
|
if (projectType !== "all") {
|
||||||
void qb.where(`${TableName.Project}.type`, projectType);
|
void qb.where(`${TableName.Project}.type`, projectType);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/
|
|||||||
import { groupBy } from "@app/lib/fn";
|
import { groupBy } from "@app/lib/fn";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
import { TQueueServiceFactory } from "@app/queue";
|
||||||
|
|
||||||
import { ActorType } from "../auth/auth-type";
|
import { ActorType } from "../auth/auth-type";
|
||||||
import { TCertificateDALFactory } from "../certificate/certificate-dal";
|
import { TCertificateDALFactory } from "../certificate/certificate-dal";
|
||||||
@@ -31,13 +32,17 @@ import { TOrgServiceFactory } from "../org/org-service";
|
|||||||
import { TPkiAlertDALFactory } from "../pki-alert/pki-alert-dal";
|
import { TPkiAlertDALFactory } from "../pki-alert/pki-alert-dal";
|
||||||
import { TPkiCollectionDALFactory } from "../pki-collection/pki-collection-dal";
|
import { TPkiCollectionDALFactory } from "../pki-collection/pki-collection-dal";
|
||||||
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
||||||
|
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
|
||||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||||
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
||||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||||
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
||||||
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
|
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
|
||||||
import { getPredefinedRoles } from "../project-role/project-role-fns";
|
import { getPredefinedRoles } from "../project-role/project-role-fns";
|
||||||
|
import { TSecretDALFactory } from "../secret/secret-dal";
|
||||||
|
import { fnDeleteProjectSecretReminders } from "../secret/secret-fns";
|
||||||
import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
|
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
|
||||||
import { TProjectSlackConfigDALFactory } from "../slack/project-slack-config-dal";
|
import { TProjectSlackConfigDALFactory } from "../slack/project-slack-config-dal";
|
||||||
import { TSlackIntegrationDALFactory } from "../slack/slack-integration-dal";
|
import { TSlackIntegrationDALFactory } from "../slack/slack-integration-dal";
|
||||||
import { TUserDALFactory } from "../user/user-dal";
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
@@ -80,7 +85,10 @@ type TProjectServiceFactoryDep = {
|
|||||||
projectDAL: TProjectDALFactory;
|
projectDAL: TProjectDALFactory;
|
||||||
projectQueue: TProjectQueueFactory;
|
projectQueue: TProjectQueueFactory;
|
||||||
userDAL: TUserDALFactory;
|
userDAL: TUserDALFactory;
|
||||||
folderDAL: TSecretFolderDALFactory;
|
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
||||||
|
folderDAL: Pick<TSecretFolderDALFactory, "insertMany" | "findByProjectId">;
|
||||||
|
secretDAL: Pick<TSecretDALFactory, "find">;
|
||||||
|
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find">;
|
||||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "insertMany" | "find">;
|
projectEnvDAL: Pick<TProjectEnvDALFactory, "insertMany" | "find">;
|
||||||
identityOrgMembershipDAL: TIdentityOrgDALFactory;
|
identityOrgMembershipDAL: TIdentityOrgDALFactory;
|
||||||
identityProjectDAL: TIdentityProjectDALFactory;
|
identityProjectDAL: TIdentityProjectDALFactory;
|
||||||
@@ -101,6 +109,8 @@ type TProjectServiceFactoryDep = {
|
|||||||
permissionService: TPermissionServiceFactory;
|
permissionService: TPermissionServiceFactory;
|
||||||
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
|
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
|
queueService: Pick<TQueueServiceFactory, "stopRepeatableJob">;
|
||||||
|
|
||||||
orgDAL: Pick<TOrgDALFactory, "findOne">;
|
orgDAL: Pick<TOrgDALFactory, "findOne">;
|
||||||
keyStore: Pick<TKeyStoreFactory, "deleteItem">;
|
keyStore: Pick<TKeyStoreFactory, "deleteItem">;
|
||||||
projectBotDAL: Pick<TProjectBotDALFactory, "create">;
|
projectBotDAL: Pick<TProjectBotDALFactory, "create">;
|
||||||
@@ -121,9 +131,13 @@ export type TProjectServiceFactory = ReturnType<typeof projectServiceFactory>;
|
|||||||
|
|
||||||
export const projectServiceFactory = ({
|
export const projectServiceFactory = ({
|
||||||
projectDAL,
|
projectDAL,
|
||||||
|
secretDAL,
|
||||||
|
secretV2BridgeDAL,
|
||||||
projectQueue,
|
projectQueue,
|
||||||
projectKeyDAL,
|
projectKeyDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
|
queueService,
|
||||||
|
projectBotService,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
userDAL,
|
userDAL,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
@@ -436,6 +450,14 @@ export const projectServiceFactory = ({
|
|||||||
await userDAL.deleteById(projectGhostUser.id, tx);
|
await userDAL.deleteById(projectGhostUser.id, tx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await fnDeleteProjectSecretReminders(project.id, {
|
||||||
|
secretDAL,
|
||||||
|
secretV2BridgeDAL,
|
||||||
|
queueService,
|
||||||
|
projectBotService,
|
||||||
|
folderDAL
|
||||||
|
});
|
||||||
|
|
||||||
return delProject;
|
return delProject;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -453,7 +475,12 @@ export const projectServiceFactory = ({
|
|||||||
const workspaces = await projectDAL.findAllProjects(actorId, actorOrgId, type);
|
const workspaces = await projectDAL.findAllProjects(actorId, actorOrgId, type);
|
||||||
|
|
||||||
if (includeRoles) {
|
if (includeRoles) {
|
||||||
const { permission } = await permissionService.getUserOrgPermission(actorId, actorOrgId, actorAuthMethod);
|
const { permission } = await permissionService.getUserOrgPermission(
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
// `includeRoles` is specifically used by organization admins when inviting new users to the organizations to avoid looping redundant api calls.
|
// `includeRoles` is specifically used by organization admins when inviting new users to the organizations to avoid looping redundant api calls.
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
|||||||
|
|
||||||
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
|
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
|
||||||
import { TIdentityUaClientSecretDALFactory } from "../identity-ua/identity-ua-client-secret-dal";
|
import { TIdentityUaClientSecretDALFactory } from "../identity-ua/identity-ua-client-secret-dal";
|
||||||
|
import { TSecretDALFactory } from "../secret/secret-dal";
|
||||||
import { TSecretVersionDALFactory } from "../secret/secret-version-dal";
|
import { TSecretVersionDALFactory } from "../secret/secret-version-dal";
|
||||||
import { TSecretFolderVersionDALFactory } from "../secret-folder/secret-folder-version-dal";
|
import { TSecretFolderVersionDALFactory } from "../secret-folder/secret-folder-version-dal";
|
||||||
import { TSecretSharingDALFactory } from "../secret-sharing/secret-sharing-dal";
|
import { TSecretSharingDALFactory } from "../secret-sharing/secret-sharing-dal";
|
||||||
@@ -16,6 +17,7 @@ type TDailyResourceCleanUpQueueServiceFactoryDep = {
|
|||||||
identityUniversalAuthClientSecretDAL: Pick<TIdentityUaClientSecretDALFactory, "removeExpiredClientSecrets">;
|
identityUniversalAuthClientSecretDAL: Pick<TIdentityUaClientSecretDALFactory, "removeExpiredClientSecrets">;
|
||||||
secretVersionDAL: Pick<TSecretVersionDALFactory, "pruneExcessVersions">;
|
secretVersionDAL: Pick<TSecretVersionDALFactory, "pruneExcessVersions">;
|
||||||
secretVersionV2DAL: Pick<TSecretVersionV2DALFactory, "pruneExcessVersions">;
|
secretVersionV2DAL: Pick<TSecretVersionV2DALFactory, "pruneExcessVersions">;
|
||||||
|
secretDAL: Pick<TSecretDALFactory, "pruneSecretReminders">;
|
||||||
secretFolderVersionDAL: Pick<TSecretFolderVersionDALFactory, "pruneExcessVersions">;
|
secretFolderVersionDAL: Pick<TSecretFolderVersionDALFactory, "pruneExcessVersions">;
|
||||||
snapshotDAL: Pick<TSnapshotDALFactory, "pruneExcessSnapshots">;
|
snapshotDAL: Pick<TSnapshotDALFactory, "pruneExcessSnapshots">;
|
||||||
secretSharingDAL: Pick<TSecretSharingDALFactory, "pruneExpiredSharedSecrets">;
|
secretSharingDAL: Pick<TSecretSharingDALFactory, "pruneExpiredSharedSecrets">;
|
||||||
@@ -30,6 +32,7 @@ export const dailyResourceCleanUpQueueServiceFactory = ({
|
|||||||
snapshotDAL,
|
snapshotDAL,
|
||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
secretFolderVersionDAL,
|
secretFolderVersionDAL,
|
||||||
|
secretDAL,
|
||||||
identityAccessTokenDAL,
|
identityAccessTokenDAL,
|
||||||
secretSharingDAL,
|
secretSharingDAL,
|
||||||
secretVersionV2DAL,
|
secretVersionV2DAL,
|
||||||
@@ -37,6 +40,7 @@ export const dailyResourceCleanUpQueueServiceFactory = ({
|
|||||||
}: TDailyResourceCleanUpQueueServiceFactoryDep) => {
|
}: TDailyResourceCleanUpQueueServiceFactoryDep) => {
|
||||||
queueService.start(QueueName.DailyResourceCleanUp, async () => {
|
queueService.start(QueueName.DailyResourceCleanUp, async () => {
|
||||||
logger.info(`${QueueName.DailyResourceCleanUp}: queue task started`);
|
logger.info(`${QueueName.DailyResourceCleanUp}: queue task started`);
|
||||||
|
await secretDAL.pruneSecretReminders(queueService);
|
||||||
await auditLogDAL.pruneAuditLog();
|
await auditLogDAL.pruneAuditLog();
|
||||||
await identityAccessTokenDAL.removeExpiredTokens();
|
await identityAccessTokenDAL.removeExpiredTokens();
|
||||||
await identityUniversalAuthClientSecretDAL.removeExpiredClientSecrets();
|
await identityUniversalAuthClientSecretDAL.removeExpiredClientSecrets();
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { TDbClient } from "@app/db";
|
|||||||
import { SecretsSchema, SecretType, TableName, TSecrets, TSecretsUpdate } from "@app/db/schemas";
|
import { SecretsSchema, SecretType, TableName, TSecrets, TSecretsUpdate } from "@app/db/schemas";
|
||||||
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
|
||||||
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { QueueName, TQueueServiceFactory } from "@app/queue";
|
||||||
|
|
||||||
export type TSecretDALFactory = ReturnType<typeof secretDALFactory>;
|
export type TSecretDALFactory = ReturnType<typeof secretDALFactory>;
|
||||||
|
|
||||||
@@ -339,6 +341,94 @@ export const secretDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const pruneSecretReminders = async (queueService: TQueueServiceFactory) => {
|
||||||
|
const REMINDER_PRUNE_BATCH_SIZE = 5_000;
|
||||||
|
const MAX_RETRY_ON_FAILURE = 3;
|
||||||
|
let numberOfRetryOnFailure = 0;
|
||||||
|
let deletedReminderCount = 0;
|
||||||
|
|
||||||
|
logger.info(`${QueueName.DailyResourceCleanUp}: secret reminders started`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const repeatableJobs = await queueService.getRepeatableJobs(QueueName.SecretReminder);
|
||||||
|
const reminderJobs = repeatableJobs
|
||||||
|
.map((job) => ({ secretId: job.id?.replace("reminder-", "") as string, jobKey: job.key }))
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
if (reminderJobs.length === 0) {
|
||||||
|
logger.info(`${QueueName.DailyResourceCleanUp}: no reminder jobs found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let offset = 0; offset < reminderJobs.length; offset += REMINDER_PRUNE_BATCH_SIZE) {
|
||||||
|
try {
|
||||||
|
const batchIds = reminderJobs.slice(offset, offset + REMINDER_PRUNE_BATCH_SIZE).map((r) => r.secretId);
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
$in: {
|
||||||
|
id: batchIds
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const opts = {
|
||||||
|
limit: REMINDER_PRUNE_BATCH_SIZE
|
||||||
|
};
|
||||||
|
|
||||||
|
// Find existing secrets with pagination
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const [secrets, secretsV2] = await Promise.all([
|
||||||
|
ormify(db, TableName.Secret).find(payload, opts),
|
||||||
|
ormify(db, TableName.SecretV2).find(payload, opts)
|
||||||
|
]);
|
||||||
|
|
||||||
|
const foundSecretIds = new Set([
|
||||||
|
...secrets.map((secret) => secret.id),
|
||||||
|
...secretsV2.map((secret) => secret.id)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Find IDs that don't exist in either table
|
||||||
|
const secretIdsNotFound = batchIds.filter((secretId) => !foundSecretIds.has(secretId));
|
||||||
|
|
||||||
|
// Delete reminders for non-existent secrets
|
||||||
|
for (const secretId of secretIdsNotFound) {
|
||||||
|
const jobKey = reminderJobs.find((r) => r.secretId === secretId)?.jobKey;
|
||||||
|
|
||||||
|
if (jobKey) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await queueService.stopRepeatableJobByKey(QueueName.SecretReminder, jobKey);
|
||||||
|
deletedReminderCount += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
numberOfRetryOnFailure = 0;
|
||||||
|
} catch (error) {
|
||||||
|
numberOfRetryOnFailure += 1;
|
||||||
|
logger.error(error, `Failed to process batch at offset ${offset}`);
|
||||||
|
|
||||||
|
if (numberOfRetryOnFailure >= MAX_RETRY_ON_FAILURE) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retry the current batch
|
||||||
|
offset -= REMINDER_PRUNE_BATCH_SIZE;
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-promise-executor-return, @typescript-eslint/no-loop-func, no-await-in-loop
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500 * numberOfRetryOnFailure));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Small delay between batches
|
||||||
|
// eslint-disable-next-line no-promise-executor-return, @typescript-eslint/no-loop-func, no-await-in-loop
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error, "Failed to complete secret reminder pruning");
|
||||||
|
} finally {
|
||||||
|
logger.info(
|
||||||
|
`${QueueName.DailyResourceCleanUp}: secret reminders completed. Deleted ${deletedReminderCount} reminders`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...secretOrm,
|
...secretOrm,
|
||||||
update,
|
update,
|
||||||
@@ -352,6 +442,7 @@ export const secretDALFactory = (db: TDbClient) => {
|
|||||||
findByBlindIndexes,
|
findByBlindIndexes,
|
||||||
upsertSecretReferences,
|
upsertSecretReferences,
|
||||||
findReferencedSecretReferences,
|
findReferencedSecretReferences,
|
||||||
findAllProjectSecretValues
|
findAllProjectSecretValues,
|
||||||
|
pruneSecretReminders
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,9 +19,11 @@ import {
|
|||||||
decryptSymmetric128BitHexKeyUTF8,
|
decryptSymmetric128BitHexKeyUTF8,
|
||||||
encryptSymmetric128BitHexKeyUTF8
|
encryptSymmetric128BitHexKeyUTF8
|
||||||
} from "@app/lib/crypto";
|
} from "@app/lib/crypto";
|
||||||
|
import { daysToMillisecond, secondsToMillis } from "@app/lib/dates";
|
||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { groupBy, unique } from "@app/lib/fn";
|
import { groupBy, unique } from "@app/lib/fn";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||||
import {
|
import {
|
||||||
fnSecretBulkInsert as fnSecretV2BridgeBulkInsert,
|
fnSecretBulkInsert as fnSecretV2BridgeBulkInsert,
|
||||||
fnSecretBulkUpdate as fnSecretV2BridgeBulkUpdate,
|
fnSecretBulkUpdate as fnSecretV2BridgeBulkUpdate,
|
||||||
@@ -31,8 +33,10 @@ import {
|
|||||||
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||||
import { KmsDataKey } from "../kms/kms-types";
|
import { KmsDataKey } from "../kms/kms-types";
|
||||||
import { getBotKeyFnFactory } from "../project-bot/project-bot-fns";
|
import { getBotKeyFnFactory } from "../project-bot/project-bot-fns";
|
||||||
|
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
|
||||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||||
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
|
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
|
||||||
import { TSecretDALFactory } from "./secret-dal";
|
import { TSecretDALFactory } from "./secret-dal";
|
||||||
import {
|
import {
|
||||||
TCreateManySecretsRawFn,
|
TCreateManySecretsRawFn,
|
||||||
@@ -1138,3 +1142,49 @@ export const decryptSecretWithBot = (
|
|||||||
secretComment
|
secretComment
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type TFnDeleteProjectSecretReminders = {
|
||||||
|
secretDAL: Pick<TSecretDALFactory, "find">;
|
||||||
|
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find">;
|
||||||
|
queueService: Pick<TQueueServiceFactory, "stopRepeatableJob">;
|
||||||
|
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
||||||
|
folderDAL: Pick<TSecretFolderDALFactory, "findByProjectId">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fnDeleteProjectSecretReminders = async (
|
||||||
|
projectId: string,
|
||||||
|
{ secretDAL, secretV2BridgeDAL, queueService, projectBotService, folderDAL }: TFnDeleteProjectSecretReminders
|
||||||
|
) => {
|
||||||
|
const projectFolders = await folderDAL.findByProjectId(projectId);
|
||||||
|
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId, false);
|
||||||
|
|
||||||
|
const projectSecrets = shouldUseSecretV2Bridge
|
||||||
|
? await secretV2BridgeDAL.find({
|
||||||
|
$in: { folderId: projectFolders.map((folder) => folder.id) },
|
||||||
|
$notNull: ["reminderRepeatDays"]
|
||||||
|
})
|
||||||
|
: await secretDAL.find({
|
||||||
|
$in: { folderId: projectFolders.map((folder) => folder.id) },
|
||||||
|
$notNull: ["secretReminderRepeatDays"]
|
||||||
|
});
|
||||||
|
|
||||||
|
const appCfg = getConfig();
|
||||||
|
for await (const secret of projectSecrets) {
|
||||||
|
const repeatDays = shouldUseSecretV2Bridge
|
||||||
|
? (secret as { reminderRepeatDays: number }).reminderRepeatDays
|
||||||
|
: (secret as { secretReminderRepeatDays: number }).secretReminderRepeatDays;
|
||||||
|
|
||||||
|
// We're using the queue service directly to get around conflicting imports.
|
||||||
|
if (repeatDays) {
|
||||||
|
await queueService.stopRepeatableJob(
|
||||||
|
QueueName.SecretReminder,
|
||||||
|
QueueJobs.SecretReminder,
|
||||||
|
{
|
||||||
|
// on prod it this will be in days, in development this will be second
|
||||||
|
every: appCfg.NODE_ENV === "development" ? secondsToMillis(repeatDays) : daysToMillisecond(repeatDays)
|
||||||
|
},
|
||||||
|
`reminder-${secret.id}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -248,7 +248,9 @@ export const secretQueueFactory = ({
|
|||||||
? secondsToMillis(newSecret.secretReminderRepeatDays)
|
? secondsToMillis(newSecret.secretReminderRepeatDays)
|
||||||
: daysToMillisecond(newSecret.secretReminderRepeatDays),
|
: daysToMillisecond(newSecret.secretReminderRepeatDays),
|
||||||
immediately: true
|
immediately: true
|
||||||
}
|
},
|
||||||
|
removeOnComplete: true,
|
||||||
|
removeOnFail: true
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -491,8 +491,8 @@ export const secretServiceFactory = ({
|
|||||||
secretDAL
|
secretDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const deletedSecret = await secretDAL.transaction(async (tx) =>
|
const deletedSecret = await secretDAL.transaction(async (tx) => {
|
||||||
fnSecretBulkDelete({
|
const secrets = await fnSecretBulkDelete({
|
||||||
projectId,
|
projectId,
|
||||||
folderId,
|
folderId,
|
||||||
actorId,
|
actorId,
|
||||||
@@ -505,8 +505,19 @@ export const secretServiceFactory = ({
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
tx
|
tx
|
||||||
})
|
});
|
||||||
);
|
|
||||||
|
for await (const secret of secrets) {
|
||||||
|
if (secret.secretReminderRepeatDays !== null && secret.secretReminderRepeatDays !== undefined) {
|
||||||
|
await secretQueueService.removeSecretReminder({
|
||||||
|
repeatDays: secret.secretReminderRepeatDays,
|
||||||
|
secretId: secret.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return secrets;
|
||||||
|
});
|
||||||
|
|
||||||
if (inputSecret.type === SecretType.Shared) {
|
if (inputSecret.type === SecretType.Shared) {
|
||||||
await snapshotService.performSnapshot(folderId);
|
await snapshotService.performSnapshot(folderId);
|
||||||
@@ -971,8 +982,8 @@ export const secretServiceFactory = ({
|
|||||||
secretDAL
|
secretDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretsDeleted = await secretDAL.transaction(async (tx) =>
|
const secretsDeleted = await secretDAL.transaction(async (tx) => {
|
||||||
fnSecretBulkDelete({
|
const secrets = await fnSecretBulkDelete({
|
||||||
secretDAL,
|
secretDAL,
|
||||||
secretQueueService,
|
secretQueueService,
|
||||||
inputSecrets: inputSecrets.map(({ type, secretName }) => ({
|
inputSecrets: inputSecrets.map(({ type, secretName }) => ({
|
||||||
@@ -983,8 +994,19 @@ export const secretServiceFactory = ({
|
|||||||
folderId,
|
folderId,
|
||||||
actorId,
|
actorId,
|
||||||
tx
|
tx
|
||||||
})
|
});
|
||||||
);
|
|
||||||
|
for await (const secret of secrets) {
|
||||||
|
if (secret.secretReminderRepeatDays !== null && secret.secretReminderRepeatDays !== undefined) {
|
||||||
|
await secretQueueService.removeSecretReminder({
|
||||||
|
repeatDays: secret.secretReminderRepeatDays,
|
||||||
|
secretId: secret.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return secrets;
|
||||||
|
});
|
||||||
|
|
||||||
await snapshotService.performSnapshot(folderId);
|
await snapshotService.performSnapshot(folderId);
|
||||||
await secretQueueService.syncSecrets({
|
await secretQueueService.syncSecrets({
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 494 KiB |
|
After Width: | Height: | Size: 537 KiB |
|
After Width: | Height: | Size: 538 KiB |
|
Before Width: | Height: | Size: 339 KiB After Width: | Height: | Size: 555 KiB |
@@ -11,21 +11,30 @@ Prerequisites:
|
|||||||
<Step title="Authorize Infisical for CircleCI">
|
<Step title="Authorize Infisical for CircleCI">
|
||||||
Obtain an API token in User Settings > Personal API Tokens
|
Obtain an API token in User Settings > Personal API Tokens
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Navigate to your project's integrations tab in Infisical.
|
Navigate to your project's integrations tab in Infisical.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Press on the CircleCI tile and input your CircleCI API token to grant Infisical access to your CircleCI account.
|
Press on the CircleCI tile and input your CircleCI API token to grant Infisical access to your CircleCI account.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Start integration">
|
<Step title="Start integration">
|
||||||
Select which Infisical environment secrets you want to sync to which CircleCI project and press create integration to start syncing secrets to CircleCI.
|
Select which Infisical environment secrets you want to sync to which CircleCI project or context.
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Project">
|
||||||
|

|
||||||
|
</Tab>
|
||||||
|
<Tab title="Context">
|
||||||
|

|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
Finally, press create integration to start syncing secrets to CircleCI.
|
||||||
|

|
||||||
|
|
||||||

|
|
||||||

|
|
||||||
</Step>
|
</Step>
|
||||||
</Steps>
|
</Steps>
|
||||||
@@ -43,13 +43,28 @@ The operator can be install via [Helm](https://helm.sh) or [kubectl](https://git
|
|||||||
|
|
||||||
**Namespace-scoped Installation**
|
**Namespace-scoped Installation**
|
||||||
|
|
||||||
The operator can be configured to watch and manage secrets in a specific namespace instead of having cluster-wide access.
|
The operator can be configured to watch and manage secrets in a specific namespace instead of having cluster-wide access. This is useful for:
|
||||||
|
|
||||||
|
- **Enhanced Security**: Limit the operator's permissions to only specific namespaces instead of cluster-wide access
|
||||||
|
- **Multi-tenant Clusters**: Run separate operator instances for different teams or applications
|
||||||
|
- **Resource Isolation**: Ensure operators in different namespaces don't interfere with each other
|
||||||
|
- **Development & Testing**: Run development and production operators side by side in isolated namespaces
|
||||||
|
|
||||||
|
**Note**: For multiple namespace-scoped installations, only the first installation should install CRDs. Subsequent installations should set `installCRDs: false` to avoid conflicts.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
helm install operator infisical-helm-charts/secrets-operator \
|
# First namespace installation (with CRDs)
|
||||||
--namespace your-namespace \
|
helm install operator-namespace1 infisical-helm-charts/secrets-operator \
|
||||||
--set scopedNamespace=your-namespace \
|
--namespace first-namespace \
|
||||||
|
--set scopedNamespace=first-namespace \
|
||||||
--set scopedRBAC=true
|
--set scopedRBAC=true
|
||||||
|
|
||||||
|
# Subsequent namespace installations
|
||||||
|
helm install operator-namespace2 infisical-helm-charts/secrets-operator \
|
||||||
|
--namespace another-namespace \
|
||||||
|
--set scopedNamespace=another-namespace \
|
||||||
|
--set scopedRBAC=true \
|
||||||
|
--set installCRDs=false
|
||||||
```
|
```
|
||||||
|
|
||||||
When scoped to a namespace, the operator will:
|
When scoped to a namespace, the operator will:
|
||||||
@@ -61,10 +76,15 @@ The operator can be install via [Helm](https://helm.sh) or [kubectl](https://git
|
|||||||
The default configuration gives cluster-wide access:
|
The default configuration gives cluster-wide access:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
installCRDs: true # Install CRDs (set to false for additional namespace installations)
|
||||||
scopedNamespace: "" # Empty for cluster-wide access
|
scopedNamespace: "" # Empty for cluster-wide access
|
||||||
scopedRBAC: false # Cluster-wide permissions
|
scopedRBAC: false # Cluster-wide permissions
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If you want to install operators in multiple namespaces simultaneously:
|
||||||
|
- Make sure to set `installCRDs: false` for all but one of the installations to avoid conflicts, as CRDs are cluster-wide resources.
|
||||||
|
- Use unique release names for each installation (e.g., operator-namespace1, operator-namespace2).
|
||||||
|
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab title="Kubectl">
|
<Tab title="Kubectl">
|
||||||
For production deployments, it is highly recommended to set the version of the Kubernetes operator manually instead of pointing to the latest version.
|
For production deployments, it is highly recommended to set the version of the Kubernetes operator manually instead of pointing to the latest version.
|
||||||
@@ -714,6 +734,7 @@ Define secret keys and their corresponding templates.
|
|||||||
Each data value uses a Golang template with access to all secrets retrieved from the specified scope.
|
Each data value uses a Golang template with access to all secrets retrieved from the specified scope.
|
||||||
|
|
||||||
Secrets are structured as follows:
|
Secrets are structured as follows:
|
||||||
|
|
||||||
```golang
|
```golang
|
||||||
type TemplateSecret struct {
|
type TemplateSecret struct {
|
||||||
Value string `json:"value"`
|
Value string `json:"value"`
|
||||||
@@ -722,6 +743,7 @@ type TemplateSecret struct {
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### Example template configuration:
|
#### Example template configuration:
|
||||||
|
|
||||||
```golang
|
```golang
|
||||||
managedSecretReference:
|
managedSecretReference:
|
||||||
secretName: managed-secret
|
secretName: managed-secret
|
||||||
@@ -733,19 +755,23 @@ type TemplateSecret struct {
|
|||||||
```
|
```
|
||||||
|
|
||||||
When you run the following command:
|
When you run the following command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
kubectl get secret managed-secret -o jsonpath='{.data}'
|
kubectl get secret managed-secret -o jsonpath='{.data}'
|
||||||
```
|
```
|
||||||
|
|
||||||
You'll receive Kubernetes secrets output that includes the NEW_KEY:
|
You'll receive Kubernetes secrets output that includes the NEW_KEY:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
{... "KEY":"d29ybGQ=","NEW_KEY":"LyBoZWxsbw=="}
|
{... "KEY":"d29ybGQ=","NEW_KEY":"LyBoZWxsbw=="}
|
||||||
```
|
```
|
||||||
|
|
||||||
When you set `includeAllSecrets` as `false` the Kubernetes secrets outputs will be:
|
When you set `includeAllSecrets` as `false` the Kubernetes secrets outputs will be:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
{"NEW_KEY":"LyBoZWxsbw=="}
|
{"NEW_KEY":"LyBoZWxsbw=="}
|
||||||
```
|
```
|
||||||
|
|
||||||
</Accordion>
|
</Accordion>
|
||||||
<Accordion title="managedSecretReference.creationPolicy">
|
<Accordion title="managedSecretReference.creationPolicy">
|
||||||
Creation polices allow you to control whether or not owner references should be added to the managed Kubernetes secret that is generated by the Infisical operator.
|
Creation polices allow you to control whether or not owner references should be added to the managed Kubernetes secret that is generated by the Infisical operator.
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 7.8 KiB |
@@ -9,6 +9,7 @@ import { twMerge } from "tailwind-merge";
|
|||||||
|
|
||||||
import { useOrganization, useWorkspace } from "@app/context";
|
import { useOrganization, useWorkspace } from "@app/context";
|
||||||
import { useToggle } from "@app/hooks";
|
import { useToggle } from "@app/hooks";
|
||||||
|
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||||
|
|
||||||
import { createNotification } from "../notifications";
|
import { createNotification } from "../notifications";
|
||||||
import { IconButton, Select, SelectItem, Tooltip } from "../v2";
|
import { IconButton, Select, SelectItem, Tooltip } from "../v2";
|
||||||
@@ -69,7 +70,11 @@ export default function NavHeader({
|
|||||||
<div className="mr-2 flex h-5 w-5 min-w-[1.25rem] items-center justify-center rounded-md bg-primary text-sm text-black">
|
<div className="mr-2 flex h-5 w-5 min-w-[1.25rem] items-center justify-center rounded-md bg-primary text-sm text-black">
|
||||||
{currentOrg?.name?.charAt(0)}
|
{currentOrg?.name?.charAt(0)}
|
||||||
</div>
|
</div>
|
||||||
<Link passHref legacyBehavior href={`/org/${currentOrg?.id}/overview`}>
|
<Link
|
||||||
|
passHref
|
||||||
|
legacyBehavior
|
||||||
|
href={`/org/${currentOrg?.id}/${ProjectType.SecretManager}/overview`}
|
||||||
|
>
|
||||||
<a className="truncate pl-0.5 text-sm font-semibold text-primary/80 hover:text-primary">
|
<a className="truncate pl-0.5 text-sm font-semibold text-primary/80 hover:text-primary">
|
||||||
{currentOrg?.name}
|
{currentOrg?.name}
|
||||||
</a>
|
</a>
|
||||||
@@ -93,7 +98,10 @@ export default function NavHeader({
|
|||||||
<Link
|
<Link
|
||||||
passHref
|
passHref
|
||||||
legacyBehavior
|
legacyBehavior
|
||||||
href={{ pathname: "/project/[id]/secrets/overview", query: { id: router.query.id } }}
|
href={{
|
||||||
|
pathname: `/${ProjectType.SecretManager}/[id]/secrets/overview`,
|
||||||
|
query: { id: router.query.id }
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<a className="text-sm font-semibold text-primary/80 hover:text-primary">{pageName}</a>
|
<a className="text-sm font-semibold text-primary/80 hover:text-primary">{pageName}</a>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -130,7 +138,7 @@ export default function NavHeader({
|
|||||||
passHref
|
passHref
|
||||||
legacyBehavior
|
legacyBehavior
|
||||||
href={{
|
href={{
|
||||||
pathname: "/project/[id]/secrets/[env]",
|
pathname: `/${ProjectType.SecretManager}/[id]/secrets/[env]`,
|
||||||
query: { id: router.query.id, env: router.query.env }
|
query: { id: router.query.id, env: router.query.env }
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -199,7 +207,10 @@ export default function NavHeader({
|
|||||||
<Link
|
<Link
|
||||||
passHref
|
passHref
|
||||||
legacyBehavior
|
legacyBehavior
|
||||||
href={{ pathname: "/project/[id]/secrets/[env]", query }}
|
href={{
|
||||||
|
pathname: `/${ProjectType.SecretManager}/[id]/secrets/[env]`,
|
||||||
|
query
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ ModalContent.displayName = "ModalContent";
|
|||||||
|
|
||||||
export type ModalProps = Omit<DialogPrimitive.DialogProps, "open"> & { isOpen?: boolean };
|
export type ModalProps = Omit<DialogPrimitive.DialogProps, "open"> & { isOpen?: boolean };
|
||||||
export const Modal = ({ isOpen, ...props }: ModalProps) => (
|
export const Modal = ({ isOpen, ...props }: ModalProps) => (
|
||||||
<DialogPrimitive.Root open={isOpen} {...props} />
|
<DialogPrimitive.Root open={isOpen} {...props} modal/>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const ModalTrigger = DialogPrimitive.Trigger;
|
export const ModalTrigger = DialogPrimitive.Trigger;
|
||||||
|
|||||||
@@ -77,11 +77,16 @@ export const selectOrganization = async (data: {
|
|||||||
export const useSelectOrganization = () => {
|
export const useSelectOrganization = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async (details: { organizationId: string; userAgent?: UserAgentType }) => {
|
mutationFn: async (details: {
|
||||||
|
organizationId: string;
|
||||||
|
userAgent?: UserAgentType;
|
||||||
|
forceSetCredentials?: boolean;
|
||||||
|
}) => {
|
||||||
const data = await selectOrganization(details);
|
const data = await selectOrganization(details);
|
||||||
|
|
||||||
// If a custom user agent is set, then this session is meant for another consuming application, not the web application.
|
// If a custom user agent is set, then this session is meant for another consuming application, not the web application.
|
||||||
if (!details.userAgent && !data.isMfaEnabled) {
|
if ((!details.userAgent && !data.isMfaEnabled) || details.forceSetCredentials) {
|
||||||
|
localStorage.setItem("orgData.id", details.organizationId);
|
||||||
SecurityClient.setToken(data.token);
|
SecurityClient.setToken(data.token);
|
||||||
SecurityClient.setProviderAuthToken("");
|
SecurityClient.setProviderAuthToken("");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export {
|
|||||||
useGetIntegrationAuthBitBucketWorkspaces,
|
useGetIntegrationAuthBitBucketWorkspaces,
|
||||||
useGetIntegrationAuthById,
|
useGetIntegrationAuthById,
|
||||||
useGetIntegrationAuthChecklyGroups,
|
useGetIntegrationAuthChecklyGroups,
|
||||||
|
useGetIntegrationAuthCircleCIOrganizations,
|
||||||
useGetIntegrationAuthGithubEnvs,
|
useGetIntegrationAuthGithubEnvs,
|
||||||
useGetIntegrationAuthGithubOrgs,
|
useGetIntegrationAuthGithubOrgs,
|
||||||
useGetIntegrationAuthNorthflankSecretGroups,
|
useGetIntegrationAuthNorthflankSecretGroups,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
BitBucketEnvironment,
|
BitBucketEnvironment,
|
||||||
BitBucketWorkspace,
|
BitBucketWorkspace,
|
||||||
ChecklyGroup,
|
ChecklyGroup,
|
||||||
|
CircleCIOrganization,
|
||||||
Environment,
|
Environment,
|
||||||
HerokuPipelineCoupling,
|
HerokuPipelineCoupling,
|
||||||
IntegrationAuth,
|
IntegrationAuth,
|
||||||
@@ -128,7 +129,9 @@ const integrationAuthKeys = {
|
|||||||
integrationAuthId,
|
integrationAuthId,
|
||||||
...params
|
...params
|
||||||
}: TGetIntegrationAuthOctopusDeployScopeValuesDTO) =>
|
}: TGetIntegrationAuthOctopusDeployScopeValuesDTO) =>
|
||||||
[{ integrationAuthId }, "getIntegrationAuthOctopusDeployScopeValues", params] as const
|
[{ integrationAuthId }, "getIntegrationAuthOctopusDeployScopeValues", params] as const,
|
||||||
|
getIntegrationAuthCircleCIOrganizations: (integrationAuthId: string) =>
|
||||||
|
[{ integrationAuthId }, "getIntegrationAuthCircleCIOrganizations"] as const
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchIntegrationAuthById = async (integrationAuthId: string) => {
|
const fetchIntegrationAuthById = async (integrationAuthId: string) => {
|
||||||
@@ -510,6 +513,15 @@ const fetchIntegrationAuthOctopusDeployScopeValues = async ({
|
|||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchIntegrationAuthCircleCIOrganizations = async (integrationAuthId: string) => {
|
||||||
|
const {
|
||||||
|
data: { organizations }
|
||||||
|
} = await apiRequest.get<{
|
||||||
|
organizations: CircleCIOrganization[];
|
||||||
|
}>(`/api/v1/integration-auth/${integrationAuthId}/circleci/organizations`);
|
||||||
|
return organizations;
|
||||||
|
};
|
||||||
|
|
||||||
export const useGetIntegrationAuthById = (integrationAuthId: string) => {
|
export const useGetIntegrationAuthById = (integrationAuthId: string) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: integrationAuthKeys.getIntegrationAuthById(integrationAuthId),
|
queryKey: integrationAuthKeys.getIntegrationAuthById(integrationAuthId),
|
||||||
@@ -884,6 +896,13 @@ export const useGetIntegrationAuthTeamCityBuildConfigs = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useGetIntegrationAuthCircleCIOrganizations = (integrationAuthId: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: integrationAuthKeys.getIntegrationAuthCircleCIOrganizations(integrationAuthId),
|
||||||
|
queryFn: () => fetchIntegrationAuthCircleCIOrganizations(integrationAuthId)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const useAuthorizeIntegration = () => {
|
export const useAuthorizeIntegration = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
|||||||
@@ -105,6 +105,19 @@ export enum OctopusDeployScope {
|
|||||||
// tenant, variable set
|
// tenant, variable set
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CircleCIOrganization = {
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
projects: {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
}[];
|
||||||
|
contexts: {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
export type TGetIntegrationAuthOctopusDeployScopeValuesDTO = {
|
export type TGetIntegrationAuthOctopusDeployScopeValuesDTO = {
|
||||||
integrationAuthId: string;
|
integrationAuthId: string;
|
||||||
spaceId: string;
|
spaceId: string;
|
||||||
@@ -125,3 +138,8 @@ export type TOctopusDeployVariableSetScopeValues = {
|
|||||||
Name: string;
|
Name: string;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum CircleCiScope {
|
||||||
|
Context = "context",
|
||||||
|
Project = "project"
|
||||||
|
}
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ export const useCreateIntegration = () => {
|
|||||||
key: string;
|
key: string;
|
||||||
value: string;
|
value: string;
|
||||||
}[];
|
}[];
|
||||||
|
azureLabel?: string;
|
||||||
githubVisibility?: string;
|
githubVisibility?: string;
|
||||||
githubVisibilityRepoIds?: string[];
|
githubVisibilityRepoIds?: string[];
|
||||||
kmsKeyId?: string;
|
kmsKeyId?: string;
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export type TIntegration = {
|
|||||||
key: string;
|
key: string;
|
||||||
value: string;
|
value: string;
|
||||||
}[];
|
}[];
|
||||||
|
azureLabel?: string;
|
||||||
|
|
||||||
kmsKeyId?: string;
|
kmsKeyId?: string;
|
||||||
secretSuffix?: string;
|
secretSuffix?: string;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useMutation, useQuery, useQueryClient, UseQueryOptions } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient, UseQueryOptions } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||||
|
|
||||||
@@ -67,7 +68,7 @@ export const useCreateOrg = (options: { invalidate: boolean } = { invalidate: tr
|
|||||||
mutationFn: async ({ name }: { name: string }) => {
|
mutationFn: async ({ name }: { name: string }) => {
|
||||||
const {
|
const {
|
||||||
data: { organization }
|
data: { organization }
|
||||||
} = await apiRequest.post("/api/v2/organizations", {
|
} = await apiRequest.post<{ organization: { id: string } }>("/api/v2/organizations", {
|
||||||
name
|
name
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -437,10 +438,13 @@ export const useDeleteOrgById = () => {
|
|||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async ({ organizationId }: { organizationId: string }) => {
|
mutationFn: async ({ organizationId }: { organizationId: string }) => {
|
||||||
const {
|
const {
|
||||||
data: { organization }
|
data: { organization, accessToken }
|
||||||
} = await apiRequest.delete<{ organization: Organization }>(
|
} = await apiRequest.delete<{ organization: Organization; accessToken: string }>(
|
||||||
`/api/v2/organizations/${organizationId}`
|
`/api/v2/organizations/${organizationId}`
|
||||||
);
|
);
|
||||||
|
SecurityClient.setToken(accessToken);
|
||||||
|
localStorage.removeItem("orgData.id");
|
||||||
|
|
||||||
return organization;
|
return organization;
|
||||||
},
|
},
|
||||||
onSuccess(_, dto) {
|
onSuccess(_, dto) {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
|
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
|
||||||
import { useCreateIntegration } from "@app/hooks/api";
|
import { useCreateIntegration } from "@app/hooks/api";
|
||||||
import { IntegrationSyncBehavior } from "@app/hooks/api/integrations/types";
|
import { IntegrationSyncBehavior } from "@app/hooks/api/integrations/types";
|
||||||
@@ -19,9 +20,11 @@ import {
|
|||||||
Card,
|
Card,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
FormControl,
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
Input,
|
Input,
|
||||||
Select,
|
Select,
|
||||||
SelectItem
|
SelectItem,
|
||||||
|
Switch
|
||||||
} from "../../../components/v2";
|
} from "../../../components/v2";
|
||||||
import { useGetIntegrationAuthById } from "../../../hooks/api/integrationAuth";
|
import { useGetIntegrationAuthById } from "../../../hooks/api/integrationAuth";
|
||||||
import { useGetWorkspaceById } from "../../../hooks/api/workspace";
|
import { useGetWorkspaceById } from "../../../hooks/api/workspace";
|
||||||
@@ -39,7 +42,9 @@ const schema = z.object({
|
|||||||
secretPath: z.string().trim().min(1, { message: "Secret path is required" }),
|
secretPath: z.string().trim().min(1, { message: "Secret path is required" }),
|
||||||
sourceEnvironment: z.string().trim().min(1, { message: "Source environment is required" }),
|
sourceEnvironment: z.string().trim().min(1, { message: "Source environment is required" }),
|
||||||
initialSyncBehavior: z.nativeEnum(IntegrationSyncBehavior),
|
initialSyncBehavior: z.nativeEnum(IntegrationSyncBehavior),
|
||||||
secretPrefix: z.string().default("")
|
secretPrefix: z.string().default(""),
|
||||||
|
useLabels: z.boolean().default(false),
|
||||||
|
azureLabel: z.string().min(1).optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
type TFormSchema = z.infer<typeof schema>;
|
type TFormSchema = z.infer<typeof schema>;
|
||||||
@@ -60,6 +65,7 @@ export default function AzureAppConfigurationCreateIntegration() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
|
watch,
|
||||||
setValue,
|
setValue,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { isSubmitting }
|
formState: { isSubmitting }
|
||||||
@@ -85,16 +91,28 @@ export default function AzureAppConfigurationCreateIntegration() {
|
|||||||
}
|
}
|
||||||
}, [workspace]);
|
}, [workspace]);
|
||||||
|
|
||||||
|
const shouldUseLabels = watch("useLabels");
|
||||||
|
|
||||||
const handleIntegrationSubmit = async ({
|
const handleIntegrationSubmit = async ({
|
||||||
secretPath,
|
secretPath,
|
||||||
|
useLabels,
|
||||||
sourceEnvironment,
|
sourceEnvironment,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
initialSyncBehavior,
|
initialSyncBehavior,
|
||||||
secretPrefix
|
secretPrefix,
|
||||||
|
azureLabel
|
||||||
}: TFormSchema) => {
|
}: TFormSchema) => {
|
||||||
try {
|
try {
|
||||||
if (!integrationAuth?.id) return;
|
if (!integrationAuth?.id) return;
|
||||||
|
|
||||||
|
if (useLabels && !azureLabel) {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Label must be provided when 'Use Labels' is enabled"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
integrationAuthId: integrationAuth?.id,
|
integrationAuthId: integrationAuth?.id,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
@@ -103,7 +121,8 @@ export default function AzureAppConfigurationCreateIntegration() {
|
|||||||
secretPath,
|
secretPath,
|
||||||
metadata: {
|
metadata: {
|
||||||
initialSyncBehavior,
|
initialSyncBehavior,
|
||||||
secretPrefix
|
secretPrefix,
|
||||||
|
...(useLabels && { azureLabel })
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -155,6 +174,7 @@ export default function AzureAppConfigurationCreateIntegration() {
|
|||||||
</div>
|
</div>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<div className="px-6">
|
<div className="px-6">
|
||||||
|
<div className="">
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="sourceEnvironment"
|
name="sourceEnvironment"
|
||||||
@@ -184,6 +204,40 @@ export default function AzureAppConfigurationCreateIntegration() {
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div className="mb-2 flex w-full flex-col gap-1">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="useLabels"
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<Switch
|
||||||
|
id="use-environment-labels"
|
||||||
|
onCheckedChange={(isChecked) => onChange(isChecked)}
|
||||||
|
isChecked={value}
|
||||||
|
>
|
||||||
|
<FormLabel label="Use Labels" />
|
||||||
|
</Switch>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{shouldUseLabels && (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="azureLabel"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
className=""
|
||||||
|
// label="Label"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="pre-prod" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="secretPath"
|
name="secretPath"
|
||||||
|
|||||||
@@ -1,155 +1,150 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import Head from "next/head";
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import {
|
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||||
faArrowUpRightFromSquare,
|
|
||||||
faBookOpen,
|
|
||||||
faBugs,
|
|
||||||
faCircleInfo
|
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import queryString from "query-string";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { useCreateIntegration } from "@app/hooks/api";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
|
FilterableSelect,
|
||||||
FormControl,
|
FormControl,
|
||||||
Input,
|
|
||||||
Select,
|
Select,
|
||||||
SelectItem
|
SelectItem,
|
||||||
} from "../../../components/v2";
|
Spinner
|
||||||
import {
|
} from "@app/components/v2";
|
||||||
useGetIntegrationAuthApps,
|
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
|
||||||
useGetIntegrationAuthById
|
import { useWorkspace } from "@app/context";
|
||||||
} from "../../../hooks/api/integrationAuth";
|
import { useCreateIntegration } from "@app/hooks/api";
|
||||||
import { useGetWorkspaceById } from "../../../hooks/api/workspace";
|
import { useGetIntegrationAuthCircleCIOrganizations } from "@app/hooks/api/integrationAuth";
|
||||||
|
import { CircleCiScope } from "@app/hooks/api/integrationAuth/types";
|
||||||
|
|
||||||
|
const formSchema = z.discriminatedUnion("scope", [
|
||||||
|
z.object({
|
||||||
|
scope: z.literal(CircleCiScope.Context),
|
||||||
|
secretPath: z.string().default("/"),
|
||||||
|
sourceEnvironment: z.object({ name: z.string(), slug: z.string() }),
|
||||||
|
targetOrg: z.object({ name: z.string().min(1), slug: z.string().min(1) }),
|
||||||
|
targetContext: z.object({ name: z.string().min(1), id: z.string().min(1) })
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
scope: z.literal(CircleCiScope.Project),
|
||||||
|
secretPath: z.string().default("/"),
|
||||||
|
sourceEnvironment: z.object({ name: z.string(), slug: z.string() }),
|
||||||
|
targetOrg: z.object({ name: z.string().min(1), slug: z.string().min(1) }),
|
||||||
|
targetProject: z.object({ name: z.string().min(1), id: z.string().min(1) })
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
type TFormData = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
export default function CircleCICreateIntegrationPage() {
|
export default function CircleCICreateIntegrationPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { mutateAsync } = useCreateIntegration();
|
const { mutateAsync, isLoading: isCreatingIntegration } = useCreateIntegration();
|
||||||
|
const { currentWorkspace, isLoading: isProjectLoading } = useWorkspace();
|
||||||
|
|
||||||
const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]);
|
const integrationAuthId = router.query.integrationAuthId as string;
|
||||||
|
|
||||||
const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? "");
|
const { control, watch, handleSubmit, setValue } = useForm<TFormData>({
|
||||||
const { data: integrationAuth, isLoading: isintegrationAuthLoading } = useGetIntegrationAuthById(
|
resolver: zodResolver(formSchema),
|
||||||
(integrationAuthId as string) ?? ""
|
defaultValues: {
|
||||||
);
|
secretPath: "/",
|
||||||
const { data: integrationAuthApps, isLoading: isIntegrationAuthAppsLoading } =
|
sourceEnvironment: currentWorkspace?.environments[0],
|
||||||
useGetIntegrationAuthApps({
|
scope: CircleCiScope.Project
|
||||||
integrationAuthId: (integrationAuthId as string) ?? ""
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
|
const selectedScope = watch("scope");
|
||||||
const [targetOrganization, setTargetOrganization] = useState("");
|
const selectedOrg = watch("targetOrg");
|
||||||
const [secretPath, setSecretPath] = useState("/");
|
|
||||||
|
|
||||||
const [targetProjectId, setTargetProjectId] = useState("");
|
const { data: circleCIOrganizations, isLoading: isCircleCIOrganizationsLoading } =
|
||||||
|
useGetIntegrationAuthCircleCIOrganizations(integrationAuthId);
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const selectedOrganizationEntry = selectedOrg
|
||||||
|
? circleCIOrganizations?.find((org) => org.slug === selectedOrg.slug)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
useEffect(() => {
|
const onSubmit = async (data: TFormData) => {
|
||||||
if (workspace) {
|
|
||||||
setSelectedSourceEnvironment(workspace.environments[0].slug);
|
|
||||||
}
|
|
||||||
}, [workspace]);
|
|
||||||
|
|
||||||
const handleButtonClick = async () => {
|
|
||||||
try {
|
try {
|
||||||
if (!integrationAuth?.id) return;
|
if (data.scope === CircleCiScope.Context) {
|
||||||
|
|
||||||
if (!targetProjectId || targetOrganization === "none") {
|
|
||||||
createNotification({
|
|
||||||
type: "error",
|
|
||||||
text: "Please select a project"
|
|
||||||
});
|
|
||||||
setIsLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsLoading(true);
|
|
||||||
|
|
||||||
const selectedApp = integrationAuthApps?.find(
|
|
||||||
(integrationAuthApp) => integrationAuthApp.appId === targetProjectId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!selectedApp) {
|
|
||||||
createNotification({
|
|
||||||
type: "error",
|
|
||||||
text: "Invalid project selected"
|
|
||||||
});
|
|
||||||
setIsLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
integrationAuthId: integrationAuth?.id,
|
scope: data.scope,
|
||||||
|
integrationAuthId,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
app: selectedApp.name, // project name
|
sourceEnvironment: data.sourceEnvironment.slug,
|
||||||
owner: selectedApp.owner, // organization name
|
app: data.targetContext.name,
|
||||||
appId: selectedApp.appId, // project id (used for syncing)
|
appId: data.targetContext.id,
|
||||||
sourceEnvironment: selectedSourceEnvironment,
|
owner: data.targetOrg.name,
|
||||||
secretPath
|
secretPath: data.secretPath
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
await mutateAsync({
|
||||||
|
scope: data.scope,
|
||||||
|
integrationAuthId,
|
||||||
|
isActive: true,
|
||||||
|
app: data.targetProject.name, // project name
|
||||||
|
owner: data.targetOrg.name, // organization name
|
||||||
|
appId: data.targetProject.id, // project id (used for syncing)
|
||||||
|
sourceEnvironment: data.sourceEnvironment.slug,
|
||||||
|
secretPath: data.secretPath
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
setIsLoading(false);
|
createNotification({
|
||||||
|
type: "success",
|
||||||
router.push(`/integrations/${localStorage.getItem("projectData.id")}`);
|
text: "Successfully created integration"
|
||||||
|
});
|
||||||
|
router.push(`/integrations/${currentWorkspace?.id}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Failed to create integration"
|
||||||
|
});
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredProjects = useMemo(() => {
|
if (isProjectLoading || isCircleCIOrganizationsLoading)
|
||||||
if (!integrationAuthApps) return [];
|
return (
|
||||||
|
<div className="flex h-full w-full items-center justify-center p-24">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
return integrationAuthApps.filter((integrationAuthApp) => {
|
return (
|
||||||
return integrationAuthApp.owner === targetOrganization;
|
<form
|
||||||
});
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
}, [integrationAuthApps, targetOrganization]);
|
className="flex h-full w-full items-center justify-center"
|
||||||
|
|
||||||
const filteredOrganizations = useMemo(() => {
|
|
||||||
const organizations = new Set<string>();
|
|
||||||
|
|
||||||
if (integrationAuthApps) {
|
|
||||||
integrationAuthApps.forEach((integrationAuthApp) => {
|
|
||||||
if (!integrationAuthApp.owner) return;
|
|
||||||
organizations.add(integrationAuthApp.owner);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return Array.from(organizations);
|
|
||||||
}, [integrationAuthApps]);
|
|
||||||
|
|
||||||
return integrationAuth && workspace && selectedSourceEnvironment && integrationAuthApps ? (
|
|
||||||
<div className="flex h-full w-full flex-col items-center justify-center">
|
|
||||||
<Head>
|
|
||||||
<title>Set Up CircleCI Integration</title>
|
|
||||||
<link rel="icon" href="/infisical.ico" />
|
|
||||||
</Head>
|
|
||||||
<Card className="max-w-lg rounded-md border border-mineshaft-600">
|
|
||||||
<CardTitle
|
|
||||||
className="px-6 text-left text-xl"
|
|
||||||
subTitle="Choose which environment or folder in Infisical you want to sync to CircleCI environment variables."
|
|
||||||
>
|
>
|
||||||
<div className="flex flex-row items-center">
|
<Card className="max-w-lg rounded-md p-8 pt-4">
|
||||||
<div className="flex items-center pb-0.5">
|
<CardTitle
|
||||||
|
className="w-full px-0 text-left text-xl"
|
||||||
|
subTitle="Choose which environment or folder in Infisical you want to sync to CircleCI."
|
||||||
|
>
|
||||||
|
<div className="flex w-full flex-row items-center justify-between">
|
||||||
|
<div className="flex flex-row items-center gap-1.5">
|
||||||
<Image
|
<Image
|
||||||
src="/images/integrations/CircleCI.png"
|
src="/images/integrations/CircleCI.png"
|
||||||
height={30}
|
height={30}
|
||||||
width={30}
|
width={30}
|
||||||
alt="CircleCI logo"
|
alt="CircleCI logo"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<span className="">CircleCI Context Integration </span>
|
||||||
</div>
|
</div>
|
||||||
<span className="ml-1.5">CircleCI Integration </span>
|
|
||||||
<Link href="https://infisical.com/docs/integrations/cicd/circleci" passHref>
|
<Link
|
||||||
<a target="_blank" rel="noopener noreferrer">
|
href="https://infisical.com/docs/integrations/cicd/circleci"
|
||||||
<div className="ml-2 mb-1 inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
passHref
|
||||||
|
>
|
||||||
|
<div className="ml-2 mb-1 flex cursor-pointer flex-row items-center gap-0.5 rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
|
||||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||||
Docs
|
Docs
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
@@ -157,137 +152,158 @@ export default function CircleCICreateIntegrationPage() {
|
|||||||
className="ml-1.5 mb-[0.07rem] text-xxs"
|
className="ml-1.5 mb-[0.07rem] text-xxs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
|
<Controller
|
||||||
<FormControl label="Project Environment" className="px-6">
|
control={control}
|
||||||
<Select
|
name="sourceEnvironment"
|
||||||
value={selectedSourceEnvironment}
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
<FormControl
|
||||||
className="w-full border border-mineshaft-500"
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
label="Project Environment"
|
||||||
>
|
>
|
||||||
{workspace?.environments.map((sourceEnvironment) => (
|
<FilterableSelect
|
||||||
<SelectItem
|
getOptionValue={(option) => option.slug}
|
||||||
value={sourceEnvironment.slug}
|
value={value}
|
||||||
key={`source-environment-${sourceEnvironment.slug}`}
|
getOptionLabel={(option) => option.name}
|
||||||
>
|
onChange={onChange}
|
||||||
{sourceEnvironment.name}
|
options={currentWorkspace?.environments}
|
||||||
</SelectItem>
|
placeholder="Select a project environment"
|
||||||
))}
|
isDisabled={!currentWorkspace?.environments.length}
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
<FormControl label="Secrets Path" className="px-6">
|
|
||||||
<Input
|
|
||||||
value={secretPath}
|
|
||||||
onChange={(evt) => setSecretPath(evt.target.value)}
|
|
||||||
placeholder="Provide a path, default is /"
|
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="secretPath"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl label="Secrets Path" errorText={error?.message} isError={Boolean(error)}>
|
||||||
|
<SecretPathInput {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="targetOrg"
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
label="CircleCI Organization"
|
||||||
|
>
|
||||||
|
<FilterableSelect
|
||||||
|
getOptionValue={(option) => option.slug}
|
||||||
|
value={value}
|
||||||
|
getOptionLabel={(option) => option.name}
|
||||||
|
onChange={(e) => {
|
||||||
|
setValue("targetProject", {
|
||||||
|
name: "",
|
||||||
|
id: ""
|
||||||
|
});
|
||||||
|
setValue("targetContext", {
|
||||||
|
name: "",
|
||||||
|
id: ""
|
||||||
|
});
|
||||||
|
|
||||||
<FormControl label="CircleCI Organization" className="px-6">
|
onChange(e);
|
||||||
|
}}
|
||||||
|
options={circleCIOrganizations}
|
||||||
|
placeholder={
|
||||||
|
circleCIOrganizations?.length
|
||||||
|
? "Select an organization..."
|
||||||
|
: "No organizations found..."
|
||||||
|
}
|
||||||
|
isDisabled={!circleCIOrganizations?.length}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="scope"
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl label="Scope" errorText={error?.message} isError={Boolean(error)}>
|
||||||
<Select
|
<Select
|
||||||
value={targetOrganization}
|
defaultValue={field.value}
|
||||||
onValueChange={(val) => {
|
onValueChange={(e) => {
|
||||||
setTargetOrganization(val);
|
onChange(e);
|
||||||
setTargetProjectId("none");
|
|
||||||
}}
|
}}
|
||||||
className="w-full border border-mineshaft-500"
|
className="w-full border border-mineshaft-500"
|
||||||
isDisabled={filteredOrganizations.length === 0}
|
|
||||||
>
|
>
|
||||||
{filteredOrganizations.length > 0 ? (
|
<SelectItem value={CircleCiScope.Project}>Project</SelectItem>
|
||||||
filteredOrganizations.map((org) => (
|
<SelectItem value={CircleCiScope.Context}>Context</SelectItem>
|
||||||
<SelectItem value={org} key={`target-org-${org}`}>
|
|
||||||
{org}
|
|
||||||
</SelectItem>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<SelectItem value="none" key="target-app-none">
|
|
||||||
No organizations found
|
|
||||||
</SelectItem>
|
|
||||||
)}
|
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
{targetOrganization && (
|
|
||||||
<FormControl label="CircleCI Project ID" className="px-6">
|
|
||||||
<Select
|
|
||||||
value={targetProjectId}
|
|
||||||
onValueChange={(val) => {
|
|
||||||
setTargetProjectId(val);
|
|
||||||
}}
|
|
||||||
className="w-full border border-mineshaft-500"
|
|
||||||
isDisabled={filteredProjects.length === 0}
|
|
||||||
>
|
|
||||||
{filteredProjects.length > 0 ? (
|
|
||||||
filteredProjects.map((project) => (
|
|
||||||
<SelectItem value={project.appId!} key={`target-project-${project.owner}`}>
|
|
||||||
{project.name}
|
|
||||||
</SelectItem>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<SelectItem value="none" key="target-app-none">
|
|
||||||
No projects found
|
|
||||||
</SelectItem>
|
|
||||||
)}
|
)}
|
||||||
</Select>
|
/>
|
||||||
|
{selectedScope === CircleCiScope.Context && selectedOrganizationEntry && (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="targetContext"
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
label="CircleCI Context"
|
||||||
|
>
|
||||||
|
<FilterableSelect
|
||||||
|
value={value}
|
||||||
|
getOptionValue={(option) => option.id!}
|
||||||
|
getOptionLabel={(option) => option.name}
|
||||||
|
onChange={onChange}
|
||||||
|
options={selectedOrganizationEntry?.contexts}
|
||||||
|
placeholder={
|
||||||
|
selectedOrganizationEntry.contexts?.length
|
||||||
|
? "Select a context..."
|
||||||
|
: "No contexts found..."
|
||||||
|
}
|
||||||
|
isDisabled={!selectedOrganizationEntry.contexts?.length}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{selectedScope === CircleCiScope.Project && selectedOrganizationEntry && (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="targetProject"
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
label="CircleCI Project"
|
||||||
|
>
|
||||||
|
<FilterableSelect
|
||||||
|
value={value}
|
||||||
|
getOptionValue={(option) => option.id!}
|
||||||
|
getOptionLabel={(option) => option.name}
|
||||||
|
onChange={onChange}
|
||||||
|
options={selectedOrganizationEntry?.projects}
|
||||||
|
placeholder={
|
||||||
|
selectedOrganizationEntry.projects?.length
|
||||||
|
? "Select a project..."
|
||||||
|
: "No projects found..."
|
||||||
|
}
|
||||||
|
isDisabled={!selectedOrganizationEntry.projects?.length}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
onClick={handleButtonClick}
|
type="submit"
|
||||||
colorSchema="primary"
|
colorSchema="primary"
|
||||||
variant="outline_bg"
|
className="mt-4"
|
||||||
className="mb-6 mt-2 ml-auto mr-6 w-min"
|
isLoading={isCreatingIntegration}
|
||||||
isLoading={isLoading}
|
isDisabled={isCreatingIntegration}
|
||||||
isDisabled={integrationAuthApps.length === 0}
|
|
||||||
>
|
>
|
||||||
Create Integration
|
Create Integration
|
||||||
</Button>
|
</Button>
|
||||||
</Card>
|
</Card>
|
||||||
<div className="mt-6 w-full max-w-md border-t border-mineshaft-800" />
|
</form>
|
||||||
<div className="mt-6 flex w-full max-w-lg flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4">
|
|
||||||
<div className="flex flex-row items-center">
|
|
||||||
<FontAwesomeIcon icon={faCircleInfo} className="text-xl text-mineshaft-200" />{" "}
|
|
||||||
<span className="text-md ml-3 text-mineshaft-100">Pro Tip</span>
|
|
||||||
</div>
|
|
||||||
<span className="mt-4 text-sm text-mineshaft-300">
|
|
||||||
After creating an integration, your secrets will start syncing immediately. This might
|
|
||||||
cause an unexpected override of current secrets in CircleCI with secrets from Infisical.
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
|
||||||
<Head>
|
|
||||||
<title>Set Up CircleCI Integration</title>
|
|
||||||
<link rel="icon" href="/infisical.ico" />
|
|
||||||
</Head>
|
|
||||||
{isIntegrationAuthAppsLoading || isintegrationAuthLoading ? (
|
|
||||||
<img
|
|
||||||
src="/images/loading/loading.gif"
|
|
||||||
height={70}
|
|
||||||
width={120}
|
|
||||||
alt="infisical loading indicator"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="flex h-max max-w-md flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-6 text-center text-mineshaft-200">
|
|
||||||
<FontAwesomeIcon icon={faBugs} className="inlineli my-2 text-6xl" />
|
|
||||||
<p>
|
|
||||||
Something went wrong. Please contact{" "}
|
|
||||||
<a
|
|
||||||
className="inline cursor-pointer text-mineshaft-100 underline decoration-primary-500 underline-offset-4 opacity-80 duration-200 hover:opacity-100"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
href="mailto:[email protected]"
|
|
||||||
>
|
|
||||||
support@infisical.com
|
|
||||||
</a>{" "}
|
|
||||||
if the issue persists.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { integrationSlugNameMapping } from "public/data/frequentConstants";
|
import { integrationSlugNameMapping } from "public/data/frequentConstants";
|
||||||
|
|
||||||
import { FormLabel } from "@app/components/v2";
|
import { FormLabel } from "@app/components/v2";
|
||||||
|
import { CircleCiScope } from "@app/hooks/api/integrationAuth/types";
|
||||||
import { IntegrationMappingBehavior, TIntegrationWithEnv } from "@app/hooks/api/integrations/types";
|
import { IntegrationMappingBehavior, TIntegrationWithEnv } from "@app/hooks/api/integrations/types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -46,6 +47,11 @@ export const IntegrationConnectionSection = ({ integration }: Props) => {
|
|||||||
case "qovery":
|
case "qovery":
|
||||||
return integration.scope;
|
return integration.scope;
|
||||||
case "circleci":
|
case "circleci":
|
||||||
|
if (integration.scope === CircleCiScope.Context) {
|
||||||
|
return "Context";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Project";
|
||||||
case "terraform-cloud":
|
case "terraform-cloud":
|
||||||
return "Project";
|
return "Project";
|
||||||
case "aws-secret-manager":
|
case "aws-secret-manager":
|
||||||
@@ -77,7 +83,6 @@ export const IntegrationConnectionSection = ({ integration }: Props) => {
|
|||||||
return `${integration.owner}`;
|
return `${integration.owner}`;
|
||||||
}
|
}
|
||||||
return `${integration.owner}/${integration.app}`;
|
return `${integration.owner}/${integration.app}`;
|
||||||
|
|
||||||
case "aws-parameter-store":
|
case "aws-parameter-store":
|
||||||
case "rundeck":
|
case "rundeck":
|
||||||
return `${integration.path}`;
|
return `${integration.path}`;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const metadataMappings: Record<keyof NonNullable<TIntegrationWithEnv["metadata"]
|
|||||||
githubVisibilityRepoIds: "Github Visibility Repo Ids",
|
githubVisibilityRepoIds: "Github Visibility Repo Ids",
|
||||||
shouldAutoRedeploy: "Auto Redeploy Target Application When Secrets Change",
|
shouldAutoRedeploy: "Auto Redeploy Target Application When Secrets Change",
|
||||||
secretAWSTag: "Tags For Secrets Stored In AWS",
|
secretAWSTag: "Tags For Secrets Stored In AWS",
|
||||||
|
azureLabel: "Azure Label",
|
||||||
kmsKeyId: "AWS KMS Key ID",
|
kmsKeyId: "AWS KMS Key ID",
|
||||||
secretSuffix: "Secret Suffix",
|
secretSuffix: "Secret Suffix",
|
||||||
secretPrefix: "Secret Prefix",
|
secretPrefix: "Secret Prefix",
|
||||||
@@ -86,7 +87,7 @@ export const IntegrationSettingsSection = ({ integration }: Props) => {
|
|||||||
Object.entries(integration.metadata).map(([key, value]) => (
|
Object.entries(integration.metadata).map(([key, value]) => (
|
||||||
<div key={key} className="flex flex-col">
|
<div key={key} className="flex flex-col">
|
||||||
<p className="text-sm text-gray-400">
|
<p className="text-sm text-gray-400">
|
||||||
{metadataMappings[key as keyof typeof metadataMappings]}
|
{!!value && metadataMappings[key as keyof typeof metadataMappings]}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-gray-200">{renderValue(key as MetadataKey, value)}</p>
|
<p className="text-sm text-gray-200">{renderValue(key as MetadataKey, value)}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { FormLabel } from "@app/components/v2";
|
import { FormLabel } from "@app/components/v2";
|
||||||
|
import { CircleCiScope } from "@app/hooks/api/integrationAuth/types";
|
||||||
import { IntegrationMappingBehavior, TIntegration } from "@app/hooks/api/integrations/types";
|
import { IntegrationMappingBehavior, TIntegration } from "@app/hooks/api/integrations/types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -52,7 +53,8 @@ export const IntegrationDetails = ({ integration }: Props) => {
|
|||||||
<FormLabel
|
<FormLabel
|
||||||
label={
|
label={
|
||||||
(integration.integration === "qovery" && integration?.scope) ||
|
(integration.integration === "qovery" && integration?.scope) ||
|
||||||
(integration.integration === "circleci" && "Project") ||
|
(integration.integration === "circleci" &&
|
||||||
|
(integration.scope === CircleCiScope.Context ? "Context" : "Project")) ||
|
||||||
(integration.integration === "bitbucket" && "Repository") ||
|
(integration.integration === "bitbucket" && "Repository") ||
|
||||||
(integration.integration === "octopus-deploy" && "Project") ||
|
(integration.integration === "octopus-deploy" && "Project") ||
|
||||||
(integration.integration === "aws-secret-manager" && "Secret") ||
|
(integration.integration === "aws-secret-manager" && "Secret") ||
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { ProjectType } from "@app/hooks/api/workspace/types";
|
|||||||
import { queryClient } from "@app/reactQuery";
|
import { queryClient } from "@app/reactQuery";
|
||||||
|
|
||||||
export const navigateUserToOrg = async (router: NextRouter, organizationId?: string) => {
|
export const navigateUserToOrg = async (router: NextRouter, organizationId?: string) => {
|
||||||
const userOrgs = await fetchOrganizations();
|
const userOrgs = await fetchOrganizations().catch(() => []);
|
||||||
|
|
||||||
const nonAuthEnforcedOrgs = userOrgs.filter((org) => !org.authEnforced);
|
const nonAuthEnforcedOrgs = userOrgs.filter((org) => !org.authEnforced);
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import z from "zod";
|
|||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
|
import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
|
||||||
import { useCreateOrg, useSelectOrganization } from "@app/hooks/api";
|
import { useCreateOrg, useGetOrganizations, useSelectOrganization } from "@app/hooks/api";
|
||||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||||
|
|
||||||
const schema = z
|
const schema = z
|
||||||
@@ -23,9 +23,10 @@ interface CreateOrgModalProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const CreateOrgModal: FC<CreateOrgModalProps> = ({ isOpen, onClose }) => {
|
export const CreateOrgModal: FC<CreateOrgModalProps> = ({ isOpen, onClose }) => {
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
const { refetch: refetchOrganizations } = useGetOrganizations();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
@@ -50,19 +51,21 @@ export const CreateOrgModal: FC<CreateOrgModalProps> = ({ isOpen, onClose }) =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
await selectOrg({
|
await selectOrg({
|
||||||
organizationId: organization.id
|
organizationId: organization.id,
|
||||||
|
forceSetCredentials: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await refetchOrganizations();
|
||||||
|
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Successfully created organization",
|
text: "Successfully created organization",
|
||||||
type: "success"
|
type: "success"
|
||||||
});
|
});
|
||||||
|
|
||||||
if (router.isReady) router.push(`/org/${organization.id}/${ProjectType.SecretManager}/overview`);
|
if (router.isReady)
|
||||||
|
router.push(`/org/${organization.id}/${ProjectType.SecretManager}/overview`);
|
||||||
else window.location.href = `/org/${organization.id}/${ProjectType.SecretManager}/overview`;
|
else window.location.href = `/org/${organization.id}/${ProjectType.SecretManager}/overview`;
|
||||||
|
|
||||||
localStorage.setItem("orgData.id", organization.id);
|
|
||||||
|
|
||||||
reset();
|
reset();
|
||||||
onClose();
|
onClose();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -1125,6 +1125,7 @@ export const SecretOverviewPage = () => {
|
|||||||
bodyClassName="overflow-visible"
|
bodyClassName="overflow-visible"
|
||||||
title="Create Secrets"
|
title="Create Secrets"
|
||||||
subTitle="Create a secret across multiple environments"
|
subTitle="Create a secret across multiple environments"
|
||||||
|
onPointerDownOutside={(e) => e.preventDefault()}
|
||||||
>
|
>
|
||||||
<CreateSecretForm
|
<CreateSecretForm
|
||||||
secretPath={secretPath}
|
secretPath={secretPath}
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ type: application
|
|||||||
# This is the chart version. This version number should be incremented each time you make changes
|
# This is the chart version. This version number should be incremented each time you make changes
|
||||||
# to the chart and its templates, including the app version.
|
# to the chart and its templates, including the app version.
|
||||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||||
version: v0.7.6
|
version: v0.7.7
|
||||||
# This is the version number of the application being deployed. This version number should be
|
# This is the version number of the application being deployed. This version number should be
|
||||||
# incremented each time you make changes to the application. Versions are not expected to
|
# incremented each time you make changes to the application. Versions are not expected to
|
||||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||||
# It is recommended to use it with quotes.
|
# It is recommended to use it with quotes.
|
||||||
appVersion: "v0.7.6"
|
appVersion: "v0.7.7"
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
{{- if .Values.installCRDs }}
|
||||||
apiVersion: apiextensions.k8s.io/v1
|
apiVersion: apiextensions.k8s.io/v1
|
||||||
kind: CustomResourceDefinition
|
kind: CustomResourceDefinition
|
||||||
metadata:
|
metadata:
|
||||||
@@ -425,3 +426,4 @@ status:
|
|||||||
plural: ""
|
plural: ""
|
||||||
conditions: []
|
conditions: []
|
||||||
storedVersions: []
|
storedVersions: []
|
||||||
|
{{- end }}
|
||||||
@@ -32,7 +32,7 @@ controllerManager:
|
|||||||
- ALL
|
- ALL
|
||||||
image:
|
image:
|
||||||
repository: infisical/kubernetes-operator
|
repository: infisical/kubernetes-operator
|
||||||
tag: v0.7.6
|
tag: v0.7.7
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
cpu: 500m
|
cpu: 500m
|
||||||
@@ -48,6 +48,7 @@ controllerManager:
|
|||||||
kubernetesClusterDomain: cluster.local
|
kubernetesClusterDomain: cluster.local
|
||||||
scopedNamespace: ""
|
scopedNamespace: ""
|
||||||
scopedRBAC: false
|
scopedRBAC: false
|
||||||
|
installCRDs: true
|
||||||
metricsService:
|
metricsService:
|
||||||
ports:
|
ports:
|
||||||
- name: https
|
- name: https
|
||||||
|
|||||||