mirror of
https://github.com/Infisical/infisical.git
synced 2025-08-11 05:49:05 +00:00
Compare commits
17 Commits
doc/add-bo
...
improve-in
Author | SHA1 | Date | |
---|---|---|---|
|
756b46428a | ||
|
9d3a382b48 | ||
|
1f6a63fa71 | ||
|
9e76fa8230 | ||
|
e2d4816465 | ||
|
37c8fc80f7 | ||
|
5ca521ea6b | ||
|
40de8331a3 | ||
|
561dbb8835 | ||
|
da28f9224b | ||
|
0b7b32bdc3 | ||
|
52ef0e6b81 | ||
|
0f06c4c27a | ||
|
e34deb7bd0 | ||
|
4b6f9fdec2 | ||
|
5df7539f65 | ||
|
2ff211d235 |
@@ -34,6 +34,8 @@ ENV VITE_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
|
|||||||
ARG CAPTCHA_SITE_KEY
|
ARG CAPTCHA_SITE_KEY
|
||||||
ENV VITE_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY
|
ENV VITE_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY
|
||||||
|
|
||||||
|
ENV NODE_OPTIONS="--max-old-space-size=8192"
|
||||||
|
|
||||||
# Build
|
# Build
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
@@ -209,6 +211,11 @@ EXPOSE 443
|
|||||||
RUN grep -v 'import "./lib/telemetry/instrumentation.mjs";' dist/main.mjs > dist/main.mjs.tmp && \
|
RUN grep -v 'import "./lib/telemetry/instrumentation.mjs";' dist/main.mjs > dist/main.mjs.tmp && \
|
||||||
mv dist/main.mjs.tmp dist/main.mjs
|
mv dist/main.mjs.tmp dist/main.mjs
|
||||||
|
|
||||||
|
# The OpenSSL library is installed in different locations in different architectures (x86_64 and arm64).
|
||||||
|
# This is a workaround to avoid errors when the library is not found.
|
||||||
|
RUN ln -sf /usr/local/lib64/ossl-modules /usr/local/lib/ossl-modules || \
|
||||||
|
ln -sf /usr/local/lib/ossl-modules /usr/local/lib64/ossl-modules
|
||||||
|
|
||||||
USER non-root-user
|
USER non-root-user
|
||||||
|
|
||||||
CMD ["./standalone-entrypoint.sh"]
|
CMD ["./standalone-entrypoint.sh"]
|
@@ -579,6 +579,9 @@ export const scimServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const serverCfg = await getServerCfg();
|
const serverCfg = await getServerCfg();
|
||||||
|
const hasEmailChanged = email?.toLowerCase() !== membership.email;
|
||||||
|
const defaultEmailVerified =
|
||||||
|
org.orgAuthMethod === OrgAuthMethod.OIDC ? serverCfg.trustOidcEmails : serverCfg.trustSamlEmails;
|
||||||
await userDAL.transaction(async (tx) => {
|
await userDAL.transaction(async (tx) => {
|
||||||
await userAliasDAL.update(
|
await userAliasDAL.update(
|
||||||
{
|
{
|
||||||
@@ -605,8 +608,7 @@ export const scimServiceFactory = ({
|
|||||||
firstName,
|
firstName,
|
||||||
email: email?.toLowerCase(),
|
email: email?.toLowerCase(),
|
||||||
lastName,
|
lastName,
|
||||||
isEmailVerified:
|
isEmailVerified: hasEmailChanged ? defaultEmailVerified : undefined
|
||||||
org.orgAuthMethod === OrgAuthMethod.OIDC ? serverCfg.trustOidcEmails : serverCfg.trustSamlEmails
|
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
@@ -14,6 +14,11 @@ export const blockLocalAndPrivateIpAddresses = async (url: string) => {
|
|||||||
if (appCfg.isDevelopmentMode) return;
|
if (appCfg.isDevelopmentMode) return;
|
||||||
|
|
||||||
const validUrl = new URL(url);
|
const validUrl = new URL(url);
|
||||||
|
|
||||||
|
if (validUrl.username || validUrl.password) {
|
||||||
|
throw new BadRequestError({ message: "URLs with user credentials (e.g., user:pass@) are not allowed" });
|
||||||
|
}
|
||||||
|
|
||||||
const inputHostIps: string[] = [];
|
const inputHostIps: string[] = [];
|
||||||
if (isIPv4(validUrl.hostname)) {
|
if (isIPv4(validUrl.hostname)) {
|
||||||
inputHostIps.push(validUrl.hostname);
|
inputHostIps.push(validUrl.hostname);
|
||||||
|
@@ -1044,6 +1044,15 @@ export const registerRoutes = async (
|
|||||||
kmsService
|
kmsService
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const gatewayService = gatewayServiceFactory({
|
||||||
|
permissionService,
|
||||||
|
gatewayDAL,
|
||||||
|
kmsService,
|
||||||
|
licenseService,
|
||||||
|
orgGatewayConfigDAL,
|
||||||
|
keyStore
|
||||||
|
});
|
||||||
|
|
||||||
const secretSyncQueue = secretSyncQueueFactory({
|
const secretSyncQueue = secretSyncQueueFactory({
|
||||||
queueService,
|
queueService,
|
||||||
secretSyncDAL,
|
secretSyncDAL,
|
||||||
@@ -1067,7 +1076,8 @@ export const registerRoutes = async (
|
|||||||
secretVersionTagV2BridgeDAL,
|
secretVersionTagV2BridgeDAL,
|
||||||
resourceMetadataDAL,
|
resourceMetadataDAL,
|
||||||
appConnectionDAL,
|
appConnectionDAL,
|
||||||
licenseService
|
licenseService,
|
||||||
|
gatewayService
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretQueueService = secretQueueFactory({
|
const secretQueueService = secretQueueFactory({
|
||||||
@@ -1490,15 +1500,6 @@ export const registerRoutes = async (
|
|||||||
licenseService
|
licenseService
|
||||||
});
|
});
|
||||||
|
|
||||||
const gatewayService = gatewayServiceFactory({
|
|
||||||
permissionService,
|
|
||||||
gatewayDAL,
|
|
||||||
kmsService,
|
|
||||||
licenseService,
|
|
||||||
orgGatewayConfigDAL,
|
|
||||||
keyStore
|
|
||||||
});
|
|
||||||
|
|
||||||
const identityKubernetesAuthService = identityKubernetesAuthServiceFactory({
|
const identityKubernetesAuthService = identityKubernetesAuthServiceFactory({
|
||||||
identityKubernetesAuthDAL,
|
identityKubernetesAuthDAL,
|
||||||
identityOrgMembershipDAL,
|
identityOrgMembershipDAL,
|
||||||
|
@@ -583,7 +583,7 @@ export const appConnectionServiceFactory = ({
|
|||||||
deleteAppConnection,
|
deleteAppConnection,
|
||||||
connectAppConnectionById,
|
connectAppConnectionById,
|
||||||
listAvailableAppConnectionsForUser,
|
listAvailableAppConnectionsForUser,
|
||||||
github: githubConnectionService(connectAppConnectionById),
|
github: githubConnectionService(connectAppConnectionById, gatewayService),
|
||||||
githubRadar: githubRadarConnectionService(connectAppConnectionById),
|
githubRadar: githubRadarConnectionService(connectAppConnectionById),
|
||||||
gcp: gcpConnectionService(connectAppConnectionById),
|
gcp: gcpConnectionService(connectAppConnectionById),
|
||||||
databricks: databricksConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
databricks: databricksConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||||
|
@@ -1,10 +1,16 @@
|
|||||||
import { createAppAuth } from "@octokit/auth-app";
|
import { createAppAuth } from "@octokit/auth-app";
|
||||||
import { Octokit } from "@octokit/rest";
|
import { AxiosError, AxiosRequestConfig, AxiosResponse } from "axios";
|
||||||
import { AxiosResponse } from "axios";
|
import https from "https";
|
||||||
|
import RE2 from "re2";
|
||||||
|
|
||||||
|
import { verifyHostInputValidity } from "@app/ee/services/dynamic-secret/dynamic-secret-fns";
|
||||||
|
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { request } from "@app/lib/config/request";
|
import { request as httpRequest } from "@app/lib/config/request";
|
||||||
import { BadRequestError, ForbiddenRequestError, InternalServerError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, InternalServerError } from "@app/lib/errors";
|
||||||
|
import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||||
import { getAppConnectionMethodName } from "@app/services/app-connection/app-connection-fns";
|
import { getAppConnectionMethodName } from "@app/services/app-connection/app-connection-fns";
|
||||||
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||||
|
|
||||||
@@ -24,123 +30,224 @@ export const getGitHubConnectionListItem = () => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getGitHubClient = (appConnection: TGitHubConnection) => {
|
export const requestWithGitHubGateway = async <T>(
|
||||||
|
appConnection: { gatewayId?: string | null },
|
||||||
|
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">,
|
||||||
|
requestConfig: AxiosRequestConfig
|
||||||
|
): Promise<AxiosResponse<T>> => {
|
||||||
|
const { gatewayId } = appConnection;
|
||||||
|
|
||||||
|
// If gateway isn't set up, don't proxy request
|
||||||
|
if (!gatewayId) {
|
||||||
|
return httpRequest.request(requestConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL(requestConfig.url as string);
|
||||||
|
|
||||||
|
await blockLocalAndPrivateIpAddresses(url.toString());
|
||||||
|
|
||||||
|
const [targetHost] = await verifyHostInputValidity(url.host, true);
|
||||||
|
const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(gatewayId);
|
||||||
|
const [relayHost, relayPort] = relayDetails.relayAddress.split(":");
|
||||||
|
|
||||||
|
return withGatewayProxy(
|
||||||
|
async (proxyPort) => {
|
||||||
|
const httpsAgent = new https.Agent({
|
||||||
|
servername: targetHost
|
||||||
|
});
|
||||||
|
|
||||||
|
url.protocol = "https:";
|
||||||
|
url.host = `localhost:${proxyPort}`;
|
||||||
|
|
||||||
|
const finalRequestConfig: AxiosRequestConfig = {
|
||||||
|
...requestConfig,
|
||||||
|
url: url.toString(),
|
||||||
|
httpsAgent,
|
||||||
|
headers: {
|
||||||
|
...requestConfig.headers,
|
||||||
|
Host: targetHost
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await httpRequest.request(finalRequestConfig);
|
||||||
|
} catch (error) {
|
||||||
|
const axiosError = error as AxiosError;
|
||||||
|
logger.error("Error during GitHub gateway request:", axiosError.message, axiosError.response?.data);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
protocol: GatewayProxyProtocol.Tcp,
|
||||||
|
targetHost,
|
||||||
|
targetPort: 443,
|
||||||
|
relayHost,
|
||||||
|
relayPort: Number(relayPort),
|
||||||
|
identityId: relayDetails.identityId,
|
||||||
|
orgId: relayDetails.orgId,
|
||||||
|
tlsOptions: {
|
||||||
|
ca: relayDetails.certChain,
|
||||||
|
cert: relayDetails.certificate,
|
||||||
|
key: relayDetails.privateKey.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getGitHubAppAuthToken = async (appConnection: TGitHubConnection) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
|
|
||||||
const { method, credentials } = appConnection;
|
|
||||||
|
|
||||||
let client: Octokit;
|
|
||||||
|
|
||||||
const appId = appCfg.INF_APP_CONNECTION_GITHUB_APP_ID;
|
const appId = appCfg.INF_APP_CONNECTION_GITHUB_APP_ID;
|
||||||
const appPrivateKey = appCfg.INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY;
|
const appPrivateKey = appCfg.INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY;
|
||||||
|
|
||||||
switch (method) {
|
if (!appId || !appPrivateKey) {
|
||||||
case GitHubConnectionMethod.App:
|
throw new InternalServerError({
|
||||||
if (!appId || !appPrivateKey) {
|
message: `GitHub App keys are not configured.`
|
||||||
throw new InternalServerError({
|
});
|
||||||
message: `GitHub ${getAppConnectionMethodName(method).replace("GitHub", "")} has not been configured`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
client = new Octokit({
|
|
||||||
authStrategy: createAppAuth,
|
|
||||||
auth: {
|
|
||||||
appId,
|
|
||||||
privateKey: appPrivateKey,
|
|
||||||
installationId: credentials.installationId
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case GitHubConnectionMethod.OAuth:
|
|
||||||
client = new Octokit({
|
|
||||||
auth: credentials.accessToken
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new InternalServerError({
|
|
||||||
message: `Unhandled GitHub connection method: ${method as GitHubConnectionMethod}`
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return client;
|
if (appConnection.method !== GitHubConnectionMethod.App) {
|
||||||
|
throw new InternalServerError({ message: "Cannot generate GitHub App token for non-app connection" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const appAuth = createAppAuth({
|
||||||
|
appId,
|
||||||
|
privateKey: appPrivateKey,
|
||||||
|
installationId: appConnection.credentials.installationId
|
||||||
|
});
|
||||||
|
|
||||||
|
const { token } = await appAuth({ type: "installation" });
|
||||||
|
return token;
|
||||||
|
};
|
||||||
|
|
||||||
|
function extractNextPageUrl(linkHeader: string | undefined): string | null {
|
||||||
|
if (!linkHeader) return null;
|
||||||
|
|
||||||
|
const links = linkHeader.split(",");
|
||||||
|
const nextLink = links.find((link) => link.includes('rel="next"'));
|
||||||
|
|
||||||
|
if (!nextLink) return null;
|
||||||
|
|
||||||
|
const match = new RE2(/<([^>]+)>/).exec(nextLink);
|
||||||
|
return match ? match[1] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const makePaginatedGitHubRequest = async <T, R = T[]>(
|
||||||
|
appConnection: TGitHubConnection,
|
||||||
|
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">,
|
||||||
|
path: string,
|
||||||
|
dataMapper?: (data: R) => T[]
|
||||||
|
): Promise<T[]> => {
|
||||||
|
const { credentials, method } = appConnection;
|
||||||
|
|
||||||
|
const token =
|
||||||
|
method === GitHubConnectionMethod.OAuth ? credentials.accessToken : await getGitHubAppAuthToken(appConnection);
|
||||||
|
let url: string | null = `https://api.${credentials.host || "github.com"}${path}`;
|
||||||
|
let results: T[] = [];
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
while (url && i < 1000) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const response: AxiosResponse<R> = await requestWithGitHubGateway<R>(appConnection, gatewayService, {
|
||||||
|
url,
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/vnd.github+json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"X-GitHub-Api-Version": "2022-11-28"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const items = dataMapper ? dataMapper(response.data) : (response.data as unknown as T[]);
|
||||||
|
results = results.concat(items);
|
||||||
|
|
||||||
|
url = extractNextPageUrl(response.headers.link as string | undefined);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
};
|
};
|
||||||
|
|
||||||
type GitHubOrganization = {
|
type GitHubOrganization = {
|
||||||
login: string;
|
login: string;
|
||||||
id: number;
|
id: number;
|
||||||
|
type: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type GitHubRepository = {
|
type GitHubRepository = {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
owner: GitHubOrganization;
|
owner: GitHubOrganization;
|
||||||
|
permissions?: {
|
||||||
|
admin: boolean;
|
||||||
|
maintain: boolean;
|
||||||
|
push: boolean;
|
||||||
|
triage: boolean;
|
||||||
|
pull: boolean;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getGitHubRepositories = async (appConnection: TGitHubConnection) => {
|
type GitHubEnvironment = {
|
||||||
const client = getGitHubClient(appConnection);
|
id: number;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
let repositories: GitHubRepository[];
|
export const getGitHubRepositories = async (
|
||||||
|
appConnection: TGitHubConnection,
|
||||||
switch (appConnection.method) {
|
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
|
||||||
case GitHubConnectionMethod.App:
|
) => {
|
||||||
repositories = await client.paginate("GET /installation/repositories");
|
if (appConnection.method === GitHubConnectionMethod.App) {
|
||||||
break;
|
return makePaginatedGitHubRequest<GitHubRepository, { repositories: GitHubRepository[] }>(
|
||||||
case GitHubConnectionMethod.OAuth:
|
appConnection,
|
||||||
default:
|
gatewayService,
|
||||||
repositories = (await client.paginate("GET /user/repos")).filter((repo) => repo.permissions?.admin);
|
"/installation/repositories",
|
||||||
break;
|
(data) => data.repositories
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return repositories;
|
const repos = await makePaginatedGitHubRequest<GitHubRepository>(appConnection, gatewayService, "/user/repos");
|
||||||
|
return repos.filter((repo) => repo.permissions?.admin);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getGitHubOrganizations = async (appConnection: TGitHubConnection) => {
|
export const getGitHubOrganizations = async (
|
||||||
const client = getGitHubClient(appConnection);
|
appConnection: TGitHubConnection,
|
||||||
|
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
|
||||||
|
) => {
|
||||||
|
if (appConnection.method === GitHubConnectionMethod.App) {
|
||||||
|
const installationRepositories = await makePaginatedGitHubRequest<
|
||||||
|
GitHubRepository,
|
||||||
|
{ repositories: GitHubRepository[] }
|
||||||
|
>(appConnection, gatewayService, "/installation/repositories", (data) => data.repositories);
|
||||||
|
|
||||||
let organizations: GitHubOrganization[];
|
const organizationMap: Record<string, GitHubOrganization> = {};
|
||||||
|
installationRepositories.forEach((repo) => {
|
||||||
switch (appConnection.method) {
|
if (repo.owner.type === "Organization") {
|
||||||
case GitHubConnectionMethod.App: {
|
organizationMap[repo.owner.id] = repo.owner;
|
||||||
const installationRepositories = await client.paginate("GET /installation/repositories");
|
}
|
||||||
|
|
||||||
const organizationMap: Record<string, GitHubOrganization> = {};
|
|
||||||
|
|
||||||
installationRepositories.forEach((repo) => {
|
|
||||||
if (repo.owner.type === "Organization") {
|
|
||||||
organizationMap[repo.owner.id] = repo.owner;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
organizations = Object.values(organizationMap);
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case GitHubConnectionMethod.OAuth:
|
|
||||||
default:
|
|
||||||
organizations = await client.paginate("GET /user/orgs");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return organizations;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getGitHubEnvironments = async (appConnection: TGitHubConnection, owner: string, repo: string) => {
|
|
||||||
const client = getGitHubClient(appConnection);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const environments = await client.paginate("GET /repos/{owner}/{repo}/environments", {
|
|
||||||
owner,
|
|
||||||
repo
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return environments;
|
return Object.values(organizationMap);
|
||||||
} catch (e) {
|
}
|
||||||
// repo doesn't have envs
|
|
||||||
if ((e as { status: number }).status === 404) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
throw e;
|
return makePaginatedGitHubRequest<GitHubOrganization>(appConnection, gatewayService, "/user/orgs");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getGitHubEnvironments = async (
|
||||||
|
appConnection: TGitHubConnection,
|
||||||
|
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">,
|
||||||
|
owner: string,
|
||||||
|
repo: string
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
return await makePaginatedGitHubRequest<GitHubEnvironment, { environments: GitHubEnvironment[] }>(
|
||||||
|
appConnection,
|
||||||
|
gatewayService,
|
||||||
|
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/environments`,
|
||||||
|
(data) => data.environments
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
const axiosError = error as AxiosError;
|
||||||
|
if (axiosError.response?.status === 404) return [];
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -159,9 +266,11 @@ export function isGithubErrorResponse(data: GithubTokenRespData): data is Github
|
|||||||
return "error" in data;
|
return "error" in data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const validateGitHubConnectionCredentials = async (config: TGitHubConnectionConfig) => {
|
export const validateGitHubConnectionCredentials = async (
|
||||||
|
config: TGitHubConnectionConfig,
|
||||||
|
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
|
||||||
|
) => {
|
||||||
const { credentials, method } = config;
|
const { credentials, method } = config;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID,
|
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID,
|
||||||
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET,
|
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET,
|
||||||
@@ -192,10 +301,13 @@ export const validateGitHubConnectionCredentials = async (config: TGitHubConnect
|
|||||||
}
|
}
|
||||||
|
|
||||||
let tokenResp: AxiosResponse<GithubTokenRespData>;
|
let tokenResp: AxiosResponse<GithubTokenRespData>;
|
||||||
|
const host = credentials.host || "github.com";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
tokenResp = await request.get<GithubTokenRespData>("https://github.com/login/oauth/access_token", {
|
tokenResp = await requestWithGitHubGateway<GithubTokenRespData>(config, gatewayService, {
|
||||||
params: {
|
url: `https://${host}/login/oauth/access_token`,
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
client_id: clientId,
|
client_id: clientId,
|
||||||
client_secret: clientSecret,
|
client_secret: clientSecret,
|
||||||
code: credentials.code,
|
code: credentials.code,
|
||||||
@@ -203,7 +315,7 @@ export const validateGitHubConnectionCredentials = async (config: TGitHubConnect
|
|||||||
},
|
},
|
||||||
headers: {
|
headers: {
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
"Accept-Encoding": "application/json"
|
"Content-Type": "application/json"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -233,7 +345,7 @@ export const validateGitHubConnectionCredentials = async (config: TGitHubConnect
|
|||||||
throw new InternalServerError({ message: `Missing access token: ${tokenResp.data.error}` });
|
throw new InternalServerError({ message: `Missing access token: ${tokenResp.data.error}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
const installationsResp = await request.get<{
|
const installationsResp = await requestWithGitHubGateway<{
|
||||||
installations: {
|
installations: {
|
||||||
id: number;
|
id: number;
|
||||||
account: {
|
account: {
|
||||||
@@ -242,7 +354,8 @@ export const validateGitHubConnectionCredentials = async (config: TGitHubConnect
|
|||||||
id: number;
|
id: number;
|
||||||
};
|
};
|
||||||
}[];
|
}[];
|
||||||
}>(IntegrationUrls.GITHUB_USER_INSTALLATIONS, {
|
}>(config, gatewayService, {
|
||||||
|
url: IntegrationUrls.GITHUB_USER_INSTALLATIONS.replace("api.github.com", `api.${host}`),
|
||||||
headers: {
|
headers: {
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
Authorization: `Bearer ${tokenResp.data.access_token}`,
|
Authorization: `Bearer ${tokenResp.data.access_token}`,
|
||||||
|
@@ -11,20 +11,24 @@ import {
|
|||||||
import { GitHubConnectionMethod } from "./github-connection-enums";
|
import { GitHubConnectionMethod } from "./github-connection-enums";
|
||||||
|
|
||||||
export const GitHubConnectionOAuthInputCredentialsSchema = z.object({
|
export const GitHubConnectionOAuthInputCredentialsSchema = z.object({
|
||||||
code: z.string().trim().min(1, "OAuth code required")
|
code: z.string().trim().min(1, "OAuth code required"),
|
||||||
|
host: z.string().trim().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const GitHubConnectionAppInputCredentialsSchema = z.object({
|
export const GitHubConnectionAppInputCredentialsSchema = z.object({
|
||||||
code: z.string().trim().min(1, "GitHub App code required"),
|
code: z.string().trim().min(1, "GitHub App code required"),
|
||||||
installationId: z.string().min(1, "GitHub App Installation ID required")
|
installationId: z.string().min(1, "GitHub App Installation ID required"),
|
||||||
|
host: z.string().trim().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const GitHubConnectionOAuthOutputCredentialsSchema = z.object({
|
export const GitHubConnectionOAuthOutputCredentialsSchema = z.object({
|
||||||
accessToken: z.string()
|
accessToken: z.string(),
|
||||||
|
host: z.string().trim().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const GitHubConnectionAppOutputCredentialsSchema = z.object({
|
export const GitHubConnectionAppOutputCredentialsSchema = z.object({
|
||||||
installationId: z.string()
|
installationId: z.string(),
|
||||||
|
host: z.string().trim().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ValidateGitHubConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
export const ValidateGitHubConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||||
@@ -43,7 +47,9 @@ export const ValidateGitHubConnectionCredentialsSchema = z.discriminatedUnion("m
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
export const CreateGitHubConnectionSchema = ValidateGitHubConnectionCredentialsSchema.and(
|
export const CreateGitHubConnectionSchema = ValidateGitHubConnectionCredentialsSchema.and(
|
||||||
GenericCreateAppConnectionFieldsSchema(AppConnection.GitHub)
|
GenericCreateAppConnectionFieldsSchema(AppConnection.GitHub, {
|
||||||
|
supportsGateways: true
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
export const UpdateGitHubConnectionSchema = z
|
export const UpdateGitHubConnectionSchema = z
|
||||||
@@ -53,7 +59,11 @@ export const UpdateGitHubConnectionSchema = z
|
|||||||
.optional()
|
.optional()
|
||||||
.describe(AppConnections.UPDATE(AppConnection.GitHub).credentials)
|
.describe(AppConnections.UPDATE(AppConnection.GitHub).credentials)
|
||||||
})
|
})
|
||||||
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.GitHub));
|
.and(
|
||||||
|
GenericUpdateAppConnectionFieldsSchema(AppConnection.GitHub, {
|
||||||
|
supportsGateways: true
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const BaseGitHubConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.GitHub) });
|
const BaseGitHubConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.GitHub) });
|
||||||
|
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
|
||||||
import { OrgServiceActor } from "@app/lib/types";
|
import { OrgServiceActor } from "@app/lib/types";
|
||||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
import {
|
import {
|
||||||
@@ -19,11 +20,14 @@ type TListGitHubEnvironmentsDTO = {
|
|||||||
owner: string;
|
owner: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const githubConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
|
export const githubConnectionService = (
|
||||||
|
getAppConnection: TGetAppConnectionFunc,
|
||||||
|
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
|
||||||
|
) => {
|
||||||
const listRepositories = async (connectionId: string, actor: OrgServiceActor) => {
|
const listRepositories = async (connectionId: string, actor: OrgServiceActor) => {
|
||||||
const appConnection = await getAppConnection(AppConnection.GitHub, connectionId, actor);
|
const appConnection = await getAppConnection(AppConnection.GitHub, connectionId, actor);
|
||||||
|
|
||||||
const repositories = await getGitHubRepositories(appConnection);
|
const repositories = await getGitHubRepositories(appConnection, gatewayService);
|
||||||
|
|
||||||
return repositories;
|
return repositories;
|
||||||
};
|
};
|
||||||
@@ -31,7 +35,7 @@ export const githubConnectionService = (getAppConnection: TGetAppConnectionFunc)
|
|||||||
const listOrganizations = async (connectionId: string, actor: OrgServiceActor) => {
|
const listOrganizations = async (connectionId: string, actor: OrgServiceActor) => {
|
||||||
const appConnection = await getAppConnection(AppConnection.GitHub, connectionId, actor);
|
const appConnection = await getAppConnection(AppConnection.GitHub, connectionId, actor);
|
||||||
|
|
||||||
const organizations = await getGitHubOrganizations(appConnection);
|
const organizations = await getGitHubOrganizations(appConnection, gatewayService);
|
||||||
|
|
||||||
return organizations;
|
return organizations;
|
||||||
};
|
};
|
||||||
@@ -42,7 +46,7 @@ export const githubConnectionService = (getAppConnection: TGetAppConnectionFunc)
|
|||||||
) => {
|
) => {
|
||||||
const appConnection = await getAppConnection(AppConnection.GitHub, connectionId, actor);
|
const appConnection = await getAppConnection(AppConnection.GitHub, connectionId, actor);
|
||||||
|
|
||||||
const environments = await getGitHubEnvironments(appConnection, owner, repo);
|
const environments = await getGitHubEnvironments(appConnection, gatewayService, owner, repo);
|
||||||
|
|
||||||
return environments;
|
return environments;
|
||||||
};
|
};
|
||||||
|
@@ -17,4 +17,7 @@ export type TGitHubConnectionInput = z.infer<typeof CreateGitHubConnectionSchema
|
|||||||
|
|
||||||
export type TValidateGitHubConnectionCredentialsSchema = typeof ValidateGitHubConnectionCredentialsSchema;
|
export type TValidateGitHubConnectionCredentialsSchema = typeof ValidateGitHubConnectionCredentialsSchema;
|
||||||
|
|
||||||
export type TGitHubConnectionConfig = DiscriminativePick<TGitHubConnectionInput, "method" | "app" | "credentials">;
|
export type TGitHubConnectionConfig = DiscriminativePick<
|
||||||
|
TGitHubConnectionInput,
|
||||||
|
"method" | "app" | "credentials" | "gatewayId"
|
||||||
|
>;
|
||||||
|
@@ -1,7 +1,12 @@
|
|||||||
import { Octokit } from "@octokit/rest";
|
|
||||||
import sodium from "libsodium-wrappers";
|
import sodium from "libsodium-wrappers";
|
||||||
|
|
||||||
import { getGitHubClient } from "@app/services/app-connection/github";
|
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
|
||||||
|
import {
|
||||||
|
getGitHubAppAuthToken,
|
||||||
|
GitHubConnectionMethod,
|
||||||
|
makePaginatedGitHubRequest,
|
||||||
|
requestWithGitHubGateway
|
||||||
|
} from "@app/services/app-connection/github";
|
||||||
import { GitHubSyncScope, GitHubSyncVisibility } from "@app/services/secret-sync/github/github-sync-enums";
|
import { GitHubSyncScope, GitHubSyncVisibility } from "@app/services/secret-sync/github/github-sync-enums";
|
||||||
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
|
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
|
||||||
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
|
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
|
||||||
@@ -12,155 +17,165 @@ import { TGitHubPublicKey, TGitHubSecret, TGitHubSecretPayload, TGitHubSyncWithC
|
|||||||
|
|
||||||
// TODO: rate limit handling
|
// TODO: rate limit handling
|
||||||
|
|
||||||
const getEncryptedSecrets = async (client: Octokit, secretSync: TGitHubSyncWithCredentials) => {
|
const getEncryptedSecrets = async (
|
||||||
let encryptedSecrets: TGitHubSecret[];
|
secretSync: TGitHubSyncWithCredentials,
|
||||||
|
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
|
||||||
const { destinationConfig } = secretSync;
|
) => {
|
||||||
|
const { destinationConfig, connection } = secretSync;
|
||||||
|
|
||||||
|
let path: string;
|
||||||
switch (destinationConfig.scope) {
|
switch (destinationConfig.scope) {
|
||||||
case GitHubSyncScope.Organization: {
|
case GitHubSyncScope.Organization: {
|
||||||
encryptedSecrets = await client.paginate("GET /orgs/{org}/actions/secrets", {
|
path = `/orgs/${encodeURIComponent(destinationConfig.org)}/actions/secrets`;
|
||||||
org: destinationConfig.org
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case GitHubSyncScope.Repository: {
|
case GitHubSyncScope.Repository: {
|
||||||
encryptedSecrets = await client.paginate("GET /repos/{owner}/{repo}/actions/secrets", {
|
path = `/repos/${encodeURIComponent(destinationConfig.owner)}/${encodeURIComponent(destinationConfig.repo)}/actions/secrets`;
|
||||||
owner: destinationConfig.owner,
|
|
||||||
repo: destinationConfig.repo
|
|
||||||
});
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case GitHubSyncScope.RepositoryEnvironment:
|
case GitHubSyncScope.RepositoryEnvironment:
|
||||||
default: {
|
default: {
|
||||||
encryptedSecrets = await client.paginate("GET /repos/{owner}/{repo}/environments/{environment_name}/secrets", {
|
path = `/repos/${encodeURIComponent(destinationConfig.owner)}/${encodeURIComponent(destinationConfig.repo)}/environments/${encodeURIComponent(destinationConfig.env)}/secrets`;
|
||||||
owner: destinationConfig.owner,
|
|
||||||
repo: destinationConfig.repo,
|
|
||||||
environment_name: destinationConfig.env
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return encryptedSecrets;
|
return makePaginatedGitHubRequest<TGitHubSecret, { secrets: TGitHubSecret[] }>(
|
||||||
|
connection,
|
||||||
|
gatewayService,
|
||||||
|
path,
|
||||||
|
(data) => data.secrets
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPublicKey = async (client: Octokit, secretSync: TGitHubSyncWithCredentials) => {
|
const getPublicKey = async (
|
||||||
let publicKey: TGitHubPublicKey;
|
secretSync: TGitHubSyncWithCredentials,
|
||||||
|
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">,
|
||||||
const { destinationConfig } = secretSync;
|
token: string
|
||||||
|
) => {
|
||||||
|
const { destinationConfig, connection } = secretSync;
|
||||||
|
|
||||||
|
let path: string;
|
||||||
switch (destinationConfig.scope) {
|
switch (destinationConfig.scope) {
|
||||||
case GitHubSyncScope.Organization: {
|
case GitHubSyncScope.Organization: {
|
||||||
publicKey = (
|
path = `/orgs/${encodeURIComponent(destinationConfig.org)}/actions/secrets/public-key`;
|
||||||
await client.request("GET /orgs/{org}/actions/secrets/public-key", {
|
|
||||||
org: destinationConfig.org
|
|
||||||
})
|
|
||||||
).data;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case GitHubSyncScope.Repository: {
|
case GitHubSyncScope.Repository: {
|
||||||
publicKey = (
|
path = `/repos/${encodeURIComponent(destinationConfig.owner)}/${encodeURIComponent(destinationConfig.repo)}/actions/secrets/public-key`;
|
||||||
await client.request("GET /repos/{owner}/{repo}/actions/secrets/public-key", {
|
|
||||||
owner: destinationConfig.owner,
|
|
||||||
repo: destinationConfig.repo
|
|
||||||
})
|
|
||||||
).data;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case GitHubSyncScope.RepositoryEnvironment:
|
case GitHubSyncScope.RepositoryEnvironment:
|
||||||
default: {
|
default: {
|
||||||
publicKey = (
|
path = `/repos/${encodeURIComponent(destinationConfig.owner)}/${encodeURIComponent(destinationConfig.repo)}/environments/${encodeURIComponent(destinationConfig.env)}/secrets/public-key`;
|
||||||
await client.request("GET /repos/{owner}/{repo}/environments/{environment_name}/secrets/public-key", {
|
|
||||||
owner: destinationConfig.owner,
|
|
||||||
repo: destinationConfig.repo,
|
|
||||||
environment_name: destinationConfig.env
|
|
||||||
})
|
|
||||||
).data;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return publicKey;
|
const response = await requestWithGitHubGateway<TGitHubPublicKey>(connection, gatewayService, {
|
||||||
|
url: `https://api.${connection.credentials.host || "github.com"}${path}`,
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/vnd.github+json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"X-GitHub-Api-Version": "2022-11-28"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteSecret = async (
|
const deleteSecret = async (
|
||||||
client: Octokit,
|
|
||||||
secretSync: TGitHubSyncWithCredentials,
|
secretSync: TGitHubSyncWithCredentials,
|
||||||
|
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">,
|
||||||
|
token: string,
|
||||||
encryptedSecret: TGitHubSecret
|
encryptedSecret: TGitHubSecret
|
||||||
) => {
|
) => {
|
||||||
const { destinationConfig } = secretSync;
|
const { destinationConfig, connection } = secretSync;
|
||||||
|
|
||||||
|
let path: string;
|
||||||
switch (destinationConfig.scope) {
|
switch (destinationConfig.scope) {
|
||||||
case GitHubSyncScope.Organization: {
|
case GitHubSyncScope.Organization: {
|
||||||
await client.request(`DELETE /orgs/{org}/actions/secrets/{secret_name}`, {
|
path = `/orgs/${encodeURIComponent(destinationConfig.org)}/actions/secrets/${encodeURIComponent(encryptedSecret.name)}`;
|
||||||
org: destinationConfig.org,
|
|
||||||
secret_name: encryptedSecret.name
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case GitHubSyncScope.Repository: {
|
case GitHubSyncScope.Repository: {
|
||||||
await client.request("DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}", {
|
path = `/repos/${encodeURIComponent(destinationConfig.owner)}/${encodeURIComponent(destinationConfig.repo)}/actions/secrets/${encodeURIComponent(encryptedSecret.name)}`;
|
||||||
owner: destinationConfig.owner,
|
|
||||||
repo: destinationConfig.repo,
|
|
||||||
secret_name: encryptedSecret.name
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case GitHubSyncScope.RepositoryEnvironment:
|
case GitHubSyncScope.RepositoryEnvironment:
|
||||||
default: {
|
default: {
|
||||||
await client.request("DELETE /repos/{owner}/{repo}/environments/{environment_name}/secrets/{secret_name}", {
|
path = `/repos/${encodeURIComponent(destinationConfig.owner)}/${encodeURIComponent(destinationConfig.repo)}/environments/${encodeURIComponent(destinationConfig.env)}/secrets/${encodeURIComponent(encryptedSecret.name)}`;
|
||||||
owner: destinationConfig.owner,
|
|
||||||
repo: destinationConfig.repo,
|
|
||||||
environment_name: destinationConfig.env,
|
|
||||||
secret_name: encryptedSecret.name
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await requestWithGitHubGateway(connection, gatewayService, {
|
||||||
|
url: `https://api.${connection.credentials.host || "github.com"}${path}`,
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/vnd.github+json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"X-GitHub-Api-Version": "2022-11-28"
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const putSecret = async (client: Octokit, secretSync: TGitHubSyncWithCredentials, payload: TGitHubSecretPayload) => {
|
const putSecret = async (
|
||||||
const { destinationConfig } = secretSync;
|
secretSync: TGitHubSyncWithCredentials,
|
||||||
|
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">,
|
||||||
|
token: string,
|
||||||
|
payload: TGitHubSecretPayload
|
||||||
|
) => {
|
||||||
|
const { destinationConfig, connection } = secretSync;
|
||||||
|
|
||||||
|
let path: string;
|
||||||
|
let body: Record<string, string | number[]> = payload;
|
||||||
|
|
||||||
switch (destinationConfig.scope) {
|
switch (destinationConfig.scope) {
|
||||||
case GitHubSyncScope.Organization: {
|
case GitHubSyncScope.Organization: {
|
||||||
const { visibility, selectedRepositoryIds } = destinationConfig;
|
const { visibility, selectedRepositoryIds } = destinationConfig;
|
||||||
|
path = `/orgs/${encodeURIComponent(destinationConfig.org)}/actions/secrets/${encodeURIComponent(payload.secret_name)}`;
|
||||||
await client.request(`PUT /orgs/{org}/actions/secrets/{secret_name}`, {
|
body = {
|
||||||
org: destinationConfig.org,
|
|
||||||
...payload,
|
...payload,
|
||||||
visibility,
|
visibility,
|
||||||
...(visibility === GitHubSyncVisibility.Selected && {
|
...(visibility === GitHubSyncVisibility.Selected && {
|
||||||
selected_repository_ids: selectedRepositoryIds
|
selected_repository_ids: selectedRepositoryIds
|
||||||
})
|
})
|
||||||
});
|
};
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case GitHubSyncScope.Repository: {
|
case GitHubSyncScope.Repository: {
|
||||||
await client.request("PUT /repos/{owner}/{repo}/actions/secrets/{secret_name}", {
|
path = `/repos/${encodeURIComponent(destinationConfig.owner)}/${encodeURIComponent(destinationConfig.repo)}/actions/secrets/${encodeURIComponent(payload.secret_name)}`;
|
||||||
owner: destinationConfig.owner,
|
|
||||||
repo: destinationConfig.repo,
|
|
||||||
...payload
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case GitHubSyncScope.RepositoryEnvironment:
|
case GitHubSyncScope.RepositoryEnvironment:
|
||||||
default: {
|
default: {
|
||||||
await client.request("PUT /repos/{owner}/{repo}/environments/{environment_name}/secrets/{secret_name}", {
|
path = `/repos/${encodeURIComponent(destinationConfig.owner)}/${encodeURIComponent(destinationConfig.repo)}/environments/${encodeURIComponent(destinationConfig.env)}/secrets/${encodeURIComponent(payload.secret_name)}`;
|
||||||
owner: destinationConfig.owner,
|
|
||||||
repo: destinationConfig.repo,
|
|
||||||
environment_name: destinationConfig.env,
|
|
||||||
...payload
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await requestWithGitHubGateway(connection, gatewayService, {
|
||||||
|
url: `https://api.${connection.credentials.host || "github.com"}${path}`,
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/vnd.github+json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"X-GitHub-Api-Version": "2022-11-28"
|
||||||
|
},
|
||||||
|
data: body
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GithubSyncFns = {
|
export const GithubSyncFns = {
|
||||||
syncSecrets: async (secretSync: TGitHubSyncWithCredentials, secretMap: TSecretMap) => {
|
syncSecrets: async (
|
||||||
|
secretSync: TGitHubSyncWithCredentials,
|
||||||
|
ogSecretMap: TSecretMap,
|
||||||
|
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
|
||||||
|
) => {
|
||||||
|
const secretMap = Object.fromEntries(Object.entries(ogSecretMap).map(([i, v]) => [i.toUpperCase(), v]));
|
||||||
|
|
||||||
switch (secretSync.destinationConfig.scope) {
|
switch (secretSync.destinationConfig.scope) {
|
||||||
case GitHubSyncScope.Organization:
|
case GitHubSyncScope.Organization:
|
||||||
if (Object.values(secretMap).length > 1000) {
|
if (Object.values(secretMap).length > 1000) {
|
||||||
@@ -187,38 +202,40 @@ export const GithubSyncFns = {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = getGitHubClient(secretSync.connection);
|
const { connection } = secretSync;
|
||||||
|
const token =
|
||||||
|
connection.method === GitHubConnectionMethod.OAuth
|
||||||
|
? connection.credentials.accessToken
|
||||||
|
: await getGitHubAppAuthToken(connection);
|
||||||
|
|
||||||
const encryptedSecrets = await getEncryptedSecrets(client, secretSync);
|
const encryptedSecrets = await getEncryptedSecrets(secretSync, gatewayService);
|
||||||
|
const publicKey = await getPublicKey(secretSync, gatewayService, token);
|
||||||
|
|
||||||
const publicKey = await getPublicKey(client, secretSync);
|
await sodium.ready;
|
||||||
|
for await (const key of Object.keys(secretMap)) {
|
||||||
|
// convert secret & base64 key to Uint8Array.
|
||||||
|
const binaryKey = sodium.from_base64(publicKey.key, sodium.base64_variants.ORIGINAL);
|
||||||
|
const binarySecretValue = sodium.from_string(secretMap[key].value);
|
||||||
|
|
||||||
await sodium.ready.then(async () => {
|
// encrypt secret using libsodium
|
||||||
for await (const key of Object.keys(secretMap)) {
|
const encryptedBytes = sodium.crypto_box_seal(binarySecretValue, binaryKey);
|
||||||
// convert secret & base64 key to Uint8Array.
|
|
||||||
const binaryKey = sodium.from_base64(publicKey.key, sodium.base64_variants.ORIGINAL);
|
|
||||||
const binarySecretValue = sodium.from_string(secretMap[key].value);
|
|
||||||
|
|
||||||
// encrypt secret using libsodium
|
// convert encrypted Uint8Array to base64
|
||||||
const encryptedBytes = sodium.crypto_box_seal(binarySecretValue, binaryKey);
|
const encryptedSecretValue = sodium.to_base64(encryptedBytes, sodium.base64_variants.ORIGINAL);
|
||||||
|
|
||||||
// convert encrypted Uint8Array to base64
|
try {
|
||||||
const encryptedSecretValue = sodium.to_base64(encryptedBytes, sodium.base64_variants.ORIGINAL);
|
await putSecret(secretSync, gatewayService, token, {
|
||||||
|
secret_name: key,
|
||||||
try {
|
encrypted_value: encryptedSecretValue,
|
||||||
await putSecret(client, secretSync, {
|
key_id: publicKey.key_id
|
||||||
secret_name: key,
|
});
|
||||||
encrypted_value: encryptedSecretValue,
|
} catch (error) {
|
||||||
key_id: publicKey.key_id
|
throw new SecretSyncError({
|
||||||
});
|
error,
|
||||||
} catch (error) {
|
secretKey: key
|
||||||
throw new SecretSyncError({
|
});
|
||||||
error,
|
|
||||||
secretKey: key
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
if (secretSync.syncOptions.disableSecretDeletion) return;
|
if (secretSync.syncOptions.disableSecretDeletion) return;
|
||||||
|
|
||||||
@@ -228,21 +245,31 @@ export const GithubSyncFns = {
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (!(encryptedSecret.name in secretMap)) {
|
if (!(encryptedSecret.name in secretMap)) {
|
||||||
await deleteSecret(client, secretSync, encryptedSecret);
|
await deleteSecret(secretSync, gatewayService, token, encryptedSecret);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getSecrets: async (secretSync: TGitHubSyncWithCredentials) => {
|
getSecrets: async (secretSync: TGitHubSyncWithCredentials) => {
|
||||||
throw new Error(`${SECRET_SYNC_NAME_MAP[secretSync.destination]} does not support importing secrets.`);
|
throw new Error(`${SECRET_SYNC_NAME_MAP[secretSync.destination]} does not support importing secrets.`);
|
||||||
},
|
},
|
||||||
removeSecrets: async (secretSync: TGitHubSyncWithCredentials, secretMap: TSecretMap) => {
|
removeSecrets: async (
|
||||||
const client = getGitHubClient(secretSync.connection);
|
secretSync: TGitHubSyncWithCredentials,
|
||||||
|
ogSecretMap: TSecretMap,
|
||||||
|
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
|
||||||
|
) => {
|
||||||
|
const secretMap = Object.fromEntries(Object.entries(ogSecretMap).map(([i, v]) => [i.toUpperCase(), v]));
|
||||||
|
|
||||||
const encryptedSecrets = await getEncryptedSecrets(client, secretSync);
|
const { connection } = secretSync;
|
||||||
|
const token =
|
||||||
|
connection.method === GitHubConnectionMethod.OAuth
|
||||||
|
? connection.credentials.accessToken
|
||||||
|
: await getGitHubAppAuthToken(connection);
|
||||||
|
|
||||||
|
const encryptedSecrets = await getEncryptedSecrets(secretSync, gatewayService);
|
||||||
|
|
||||||
for await (const encryptedSecret of encryptedSecrets) {
|
for await (const encryptedSecret of encryptedSecrets) {
|
||||||
if (encryptedSecret.name in secretMap) {
|
if (encryptedSecret.name in secretMap) {
|
||||||
await deleteSecret(client, secretSync, encryptedSecret);
|
await deleteSecret(secretSync, gatewayService, token, encryptedSecret);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
import handlebars from "handlebars";
|
import handlebars from "handlebars";
|
||||||
|
|
||||||
|
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { OCI_VAULT_SYNC_LIST_OPTION, OCIVaultSyncFns } from "@app/ee/services/secret-sync/oci-vault";
|
import { OCI_VAULT_SYNC_LIST_OPTION, OCIVaultSyncFns } from "@app/ee/services/secret-sync/oci-vault";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
@@ -97,6 +98,7 @@ export const listSecretSyncOptions = () => {
|
|||||||
type TSyncSecretDeps = {
|
type TSyncSecretDeps = {
|
||||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">;
|
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">;
|
||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
|
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add schema to secret keys
|
// Add schema to secret keys
|
||||||
@@ -191,7 +193,7 @@ export const SecretSyncFns = {
|
|||||||
syncSecrets: (
|
syncSecrets: (
|
||||||
secretSync: TSecretSyncWithCredentials,
|
secretSync: TSecretSyncWithCredentials,
|
||||||
secretMap: TSecretMap,
|
secretMap: TSecretMap,
|
||||||
{ kmsService, appConnectionDAL }: TSyncSecretDeps
|
{ kmsService, appConnectionDAL, gatewayService }: TSyncSecretDeps
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const schemaSecretMap = addSchema(secretMap, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema);
|
const schemaSecretMap = addSchema(secretMap, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema);
|
||||||
|
|
||||||
@@ -201,7 +203,7 @@ export const SecretSyncFns = {
|
|||||||
case SecretSync.AWSSecretsManager:
|
case SecretSync.AWSSecretsManager:
|
||||||
return AwsSecretsManagerSyncFns.syncSecrets(secretSync, schemaSecretMap);
|
return AwsSecretsManagerSyncFns.syncSecrets(secretSync, schemaSecretMap);
|
||||||
case SecretSync.GitHub:
|
case SecretSync.GitHub:
|
||||||
return GithubSyncFns.syncSecrets(secretSync, schemaSecretMap);
|
return GithubSyncFns.syncSecrets(secretSync, schemaSecretMap, gatewayService);
|
||||||
case SecretSync.GCPSecretManager:
|
case SecretSync.GCPSecretManager:
|
||||||
return GcpSyncFns.syncSecrets(secretSync, schemaSecretMap);
|
return GcpSyncFns.syncSecrets(secretSync, schemaSecretMap);
|
||||||
case SecretSync.AzureKeyVault:
|
case SecretSync.AzureKeyVault:
|
||||||
@@ -395,7 +397,7 @@ export const SecretSyncFns = {
|
|||||||
removeSecrets: (
|
removeSecrets: (
|
||||||
secretSync: TSecretSyncWithCredentials,
|
secretSync: TSecretSyncWithCredentials,
|
||||||
secretMap: TSecretMap,
|
secretMap: TSecretMap,
|
||||||
{ kmsService, appConnectionDAL }: TSyncSecretDeps
|
{ kmsService, appConnectionDAL, gatewayService }: TSyncSecretDeps
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const schemaSecretMap = addSchema(secretMap, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema);
|
const schemaSecretMap = addSchema(secretMap, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema);
|
||||||
|
|
||||||
@@ -405,7 +407,7 @@ export const SecretSyncFns = {
|
|||||||
case SecretSync.AWSSecretsManager:
|
case SecretSync.AWSSecretsManager:
|
||||||
return AwsSecretsManagerSyncFns.removeSecrets(secretSync, schemaSecretMap);
|
return AwsSecretsManagerSyncFns.removeSecrets(secretSync, schemaSecretMap);
|
||||||
case SecretSync.GitHub:
|
case SecretSync.GitHub:
|
||||||
return GithubSyncFns.removeSecrets(secretSync, schemaSecretMap);
|
return GithubSyncFns.removeSecrets(secretSync, schemaSecretMap, gatewayService);
|
||||||
case SecretSync.GCPSecretManager:
|
case SecretSync.GCPSecretManager:
|
||||||
return GcpSyncFns.removeSecrets(secretSync, schemaSecretMap);
|
return GcpSyncFns.removeSecrets(secretSync, schemaSecretMap);
|
||||||
case SecretSync.AzureKeyVault:
|
case SecretSync.AzureKeyVault:
|
||||||
|
@@ -4,6 +4,7 @@ import { Job } from "bullmq";
|
|||||||
|
|
||||||
import { ProjectMembershipRole, SecretType } from "@app/db/schemas";
|
import { ProjectMembershipRole, SecretType } from "@app/db/schemas";
|
||||||
import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
|
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
@@ -96,6 +97,7 @@ type TSecretSyncQueueFactoryDep = {
|
|||||||
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
|
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
|
||||||
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
|
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
|
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
|
||||||
};
|
};
|
||||||
|
|
||||||
type SecretSyncActionJob = Job<
|
type SecretSyncActionJob = Job<
|
||||||
@@ -138,7 +140,8 @@ export const secretSyncQueueFactory = ({
|
|||||||
secretVersionTagV2BridgeDAL,
|
secretVersionTagV2BridgeDAL,
|
||||||
resourceMetadataDAL,
|
resourceMetadataDAL,
|
||||||
folderCommitService,
|
folderCommitService,
|
||||||
licenseService
|
licenseService,
|
||||||
|
gatewayService
|
||||||
}: TSecretSyncQueueFactoryDep) => {
|
}: TSecretSyncQueueFactoryDep) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
|
|
||||||
@@ -353,7 +356,8 @@ export const secretSyncQueueFactory = ({
|
|||||||
|
|
||||||
const importedSecrets = await SecretSyncFns.getSecrets(secretSync, {
|
const importedSecrets = await SecretSyncFns.getSecrets(secretSync, {
|
||||||
appConnectionDAL,
|
appConnectionDAL,
|
||||||
kmsService
|
kmsService,
|
||||||
|
gatewayService
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!Object.keys(importedSecrets).length) return {};
|
if (!Object.keys(importedSecrets).length) return {};
|
||||||
@@ -481,7 +485,8 @@ export const secretSyncQueueFactory = ({
|
|||||||
|
|
||||||
await SecretSyncFns.syncSecrets(secretSyncWithCredentials, secretMap, {
|
await SecretSyncFns.syncSecrets(secretSyncWithCredentials, secretMap, {
|
||||||
appConnectionDAL,
|
appConnectionDAL,
|
||||||
kmsService
|
kmsService,
|
||||||
|
gatewayService
|
||||||
});
|
});
|
||||||
|
|
||||||
isSynced = true;
|
isSynced = true;
|
||||||
@@ -730,7 +735,8 @@ export const secretSyncQueueFactory = ({
|
|||||||
secretMap,
|
secretMap,
|
||||||
{
|
{
|
||||||
appConnectionDAL,
|
appConnectionDAL,
|
||||||
kmsService
|
kmsService,
|
||||||
|
gatewayService
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 586 KiB After Width: | Height: | Size: 0 B |
@@ -94,6 +94,11 @@ Infisical supports two methods for connecting to GitHub.
|
|||||||
</Step>
|
</Step>
|
||||||
<Step title="Authorize Connection">
|
<Step title="Authorize Connection">
|
||||||
Select the **GitHub App** method and click **Connect to GitHub**.
|
Select the **GitHub App** method and click **Connect to GitHub**.
|
||||||
|
|
||||||
|
You may optionally configure GitHub Enterprise options:
|
||||||
|
- **Gateway:** The gateway connected to your private network
|
||||||
|
- **Hostname:** The hostname at which to access your GitHub Enterprise instance
|
||||||
|
|
||||||

|

|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Install GitHub App">
|
<Step title="Install GitHub App">
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { forwardRef, TextareaHTMLAttributes, useCallback, useMemo, useRef, useState } from "react";
|
import { forwardRef, TextareaHTMLAttributes, useCallback, useMemo, useRef, useState } from "react";
|
||||||
import { faCircle, faFolder, faKey } from "@fortawesome/free-solid-svg-icons";
|
import { faFolder, faKey, faLayerGroup } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import * as Popover from "@radix-ui/react-popover";
|
import * as Popover from "@radix-ui/react-popover";
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
|
|||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
const workspaceId = currentWorkspace?.id || "";
|
const workspaceId = currentWorkspace?.id || "";
|
||||||
|
|
||||||
const [debouncedValue] = useDebounce(value, 500);
|
const [debouncedValue] = useDebounce(value, 100);
|
||||||
|
|
||||||
const [highlightedIndex, setHighlightedIndex] = useState(-1);
|
const [highlightedIndex, setHighlightedIndex] = useState(-1);
|
||||||
|
|
||||||
@@ -142,7 +142,7 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
|
|||||||
const suggestions = useMemo(() => {
|
const suggestions = useMemo(() => {
|
||||||
if (!isPopupOpen) return [];
|
if (!isPopupOpen) return [];
|
||||||
// reset highlight whenever recomputation happens
|
// reset highlight whenever recomputation happens
|
||||||
setHighlightedIndex(-1);
|
setHighlightedIndex(0);
|
||||||
const suggestionsArr: ReferenceItem[] = [];
|
const suggestionsArr: ReferenceItem[] = [];
|
||||||
const predicate = suggestionSource.predicate.toLowerCase();
|
const predicate = suggestionSource.predicate.toLowerCase();
|
||||||
|
|
||||||
@@ -298,17 +298,21 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="h-full w-full max-w-60 flex-col items-center justify-center rounded-md text-white"
|
className="h-full w-full flex-col items-center justify-center rounded-md text-white"
|
||||||
ref={popoverContentRef}
|
ref={popoverContentRef}
|
||||||
>
|
>
|
||||||
{suggestions.map((item, i) => {
|
{suggestions.map((item, i) => {
|
||||||
let entryIcon;
|
let entryIcon;
|
||||||
|
let subText;
|
||||||
if (item.type === ReferenceType.SECRET) {
|
if (item.type === ReferenceType.SECRET) {
|
||||||
entryIcon = faKey;
|
entryIcon = <FontAwesomeIcon icon={faKey} className="text-bunker-300" />;
|
||||||
|
subText = "Secret";
|
||||||
} else if (item.type === ReferenceType.ENVIRONMENT) {
|
} else if (item.type === ReferenceType.ENVIRONMENT) {
|
||||||
entryIcon = faCircle;
|
entryIcon = <FontAwesomeIcon icon={faLayerGroup} className="text-green-700" />;
|
||||||
|
subText = "Environment";
|
||||||
} else {
|
} else {
|
||||||
entryIcon = faFolder;
|
entryIcon = <FontAwesomeIcon icon={faFolder} className="text-yellow-700" />;
|
||||||
|
subText = "Folder";
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -327,22 +331,22 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
|
|||||||
}}
|
}}
|
||||||
onMouseEnter={() => setHighlightedIndex(i)}
|
onMouseEnter={() => setHighlightedIndex(i)}
|
||||||
style={{ pointerEvents: "auto" }}
|
style={{ pointerEvents: "auto" }}
|
||||||
className="flex items-center justify-between border-mineshaft-600 text-left"
|
className="flex w-full items-center justify-between border-mineshaft-600 text-left"
|
||||||
key={`secret-reference-secret-${i + 1}`}
|
key={`secret-reference-secret-${i + 1}`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`${
|
className={`${
|
||||||
highlightedIndex === i ? "bg-gray-600" : ""
|
highlightedIndex === i ? "bg-mineshaft-500" : ""
|
||||||
} text-md relative mb-0.5 flex w-full cursor-pointer select-none items-center justify-between rounded-md px-2 py-2 outline-none transition-all hover:bg-mineshaft-500 data-[highlighted]:bg-mineshaft-500`}
|
} text-md relative flex w-full cursor-pointer select-none items-center justify-between px-2 py-2 outline-none transition-all hover:bg-mineshaft-700 data-[highlighted]:bg-mineshaft-700`}
|
||||||
>
|
>
|
||||||
<div className="flex w-full gap-2">
|
<div className="flex w-full items-start gap-2">
|
||||||
<div className="flex items-center text-yellow-700">
|
<div className="mt-1 flex items-center">{entryIcon}</div>
|
||||||
<FontAwesomeIcon
|
<div className="text-md w-10/12 truncate text-left">
|
||||||
icon={entryIcon}
|
{item.label}
|
||||||
size={item.type === ReferenceType.ENVIRONMENT ? "xs" : "1x"}
|
<div className="mb-[0.1rem] text-xs leading-3 text-bunker-400">
|
||||||
/>
|
{subText}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-md w-10/12 truncate text-left">{item.label}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -11,6 +11,7 @@ export type TGitHubConnection = TRootAppConnection & { app: AppConnection.GitHub
|
|||||||
method: GitHubConnectionMethod.OAuth;
|
method: GitHubConnectionMethod.OAuth;
|
||||||
credentials: {
|
credentials: {
|
||||||
code: string;
|
code: string;
|
||||||
|
host?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
@@ -18,6 +19,7 @@ export type TGitHubConnection = TRootAppConnection & { app: AppConnection.GitHub
|
|||||||
credentials: {
|
credentials: {
|
||||||
code: string;
|
code: string;
|
||||||
installationId: string;
|
installationId: string;
|
||||||
|
host?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@@ -11,7 +11,7 @@ import { createNotification } from "@app/components/notifications";
|
|||||||
import attemptCliLogin from "@app/components/utilities/attemptCliLogin";
|
import attemptCliLogin from "@app/components/utilities/attemptCliLogin";
|
||||||
import attemptLogin from "@app/components/utilities/attemptLogin";
|
import attemptLogin from "@app/components/utilities/attemptLogin";
|
||||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||||
import { Button, Input, Spinner } from "@app/components/v2";
|
import { Button, ContentLoader, Input } from "@app/components/v2";
|
||||||
import { envConfig } from "@app/config/env";
|
import { envConfig } from "@app/config/env";
|
||||||
import { SessionStorageKeys } from "@app/const";
|
import { SessionStorageKeys } from "@app/const";
|
||||||
import { useToggle } from "@app/hooks";
|
import { useToggle } from "@app/hooks";
|
||||||
@@ -329,9 +329,8 @@ export const PasswordStep = ({
|
|||||||
|
|
||||||
if (hasExchangedPrivateKey) {
|
if (hasExchangedPrivateKey) {
|
||||||
return (
|
return (
|
||||||
<div className="flex max-h-screen min-h-screen flex-col items-center justify-center gap-2 overflow-y-auto bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700">
|
<div className="fixed left-0 top-0 h-screen w-screen bg-bunker-800">
|
||||||
<Spinner />
|
<ContentLoader />
|
||||||
<p className="text-white opacity-80">Loading, please wait</p>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -1,26 +1,21 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { Spinner } from "@app/components/v2";
|
import { ContentLoader } from "@app/components/v2";
|
||||||
import { useGetMyDuplicateAccount } from "@app/hooks/api";
|
import { useGetMyDuplicateAccount } from "@app/hooks/api";
|
||||||
|
|
||||||
import { EmailDuplicationConfirmation } from "./EmailDuplicationConfirmation";
|
import { EmailDuplicationConfirmation } from "./EmailDuplicationConfirmation";
|
||||||
import { SelectOrganizationSection } from "./SelectOrgSection";
|
import { SelectOrganizationSection } from "./SelectOrgSection";
|
||||||
|
|
||||||
const LoadingScreen = () => {
|
|
||||||
return (
|
|
||||||
<div className="flex max-h-screen min-h-screen flex-col items-center justify-center gap-2 overflow-y-auto bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700">
|
|
||||||
<Spinner />
|
|
||||||
<p className="text-white opacity-80">Loading, please wait</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SelectOrganizationPage = () => {
|
export const SelectOrganizationPage = () => {
|
||||||
const duplicateAccounts = useGetMyDuplicateAccount();
|
const duplicateAccounts = useGetMyDuplicateAccount();
|
||||||
const [removeDuplicateLater, setRemoveDuplicateLater] = useState(false);
|
const [removeDuplicateLater, setRemoveDuplicateLater] = useState(false);
|
||||||
|
|
||||||
if (duplicateAccounts.isPending) {
|
if (duplicateAccounts.isPending) {
|
||||||
return <LoadingScreen />;
|
return (
|
||||||
|
<div className="h-screen w-screen bg-bunker-800">
|
||||||
|
<ContentLoader />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (duplicateAccounts.data?.duplicateAccounts?.length && !removeDuplicateLater) {
|
if (duplicateAccounts.data?.duplicateAccounts?.length && !removeDuplicateLater) {
|
||||||
|
@@ -12,7 +12,7 @@ import { Mfa } from "@app/components/auth/Mfa";
|
|||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { IsCliLoginSuccessful } from "@app/components/utilities/attemptCliLogin";
|
import { IsCliLoginSuccessful } from "@app/components/utilities/attemptCliLogin";
|
||||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||||
import { Button, Spinner } from "@app/components/v2";
|
import { Button, ContentLoader, Spinner } from "@app/components/v2";
|
||||||
import { SessionStorageKeys } from "@app/const";
|
import { SessionStorageKeys } from "@app/const";
|
||||||
import { OrgMembershipRole } from "@app/helpers/roles";
|
import { OrgMembershipRole } from "@app/helpers/roles";
|
||||||
import { useToggle } from "@app/hooks";
|
import { useToggle } from "@app/hooks";
|
||||||
@@ -29,15 +29,6 @@ import { AuthMethod } from "@app/hooks/api/users/types";
|
|||||||
|
|
||||||
import { navigateUserToOrg } from "../LoginPage/Login.utils";
|
import { navigateUserToOrg } from "../LoginPage/Login.utils";
|
||||||
|
|
||||||
const LoadingScreen = () => {
|
|
||||||
return (
|
|
||||||
<div className="flex max-h-screen min-h-screen flex-col items-center justify-center gap-2 overflow-y-auto bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700">
|
|
||||||
<Spinner />
|
|
||||||
<p className="text-white opacity-80">Loading, please wait</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SelectOrganizationSection = () => {
|
export const SelectOrganizationSection = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -228,7 +219,11 @@ export const SelectOrganizationSection = () => {
|
|||||||
!user ||
|
!user ||
|
||||||
((isInitialOrgCheckLoading || defaultSelectedOrg) && !shouldShowMfa)
|
((isInitialOrgCheckLoading || defaultSelectedOrg) && !shouldShowMfa)
|
||||||
) {
|
) {
|
||||||
return <LoadingScreen />;
|
return (
|
||||||
|
<div className="h-screen w-screen bg-bunker-800">
|
||||||
|
<ContentLoader />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@@ -3,11 +3,31 @@ import crypto from "crypto";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { Button, FormControl, ModalClose, Select, SelectItem } from "@app/components/v2";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionContent,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionTrigger,
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
ModalClose,
|
||||||
|
Select,
|
||||||
|
SelectItem,
|
||||||
|
Tooltip
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useSubscription } from "@app/context";
|
||||||
|
import {
|
||||||
|
OrgGatewayPermissionActions,
|
||||||
|
OrgPermissionSubjects
|
||||||
|
} from "@app/context/OrgPermissionContext/types";
|
||||||
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
|
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
|
||||||
import { isInfisicalCloud } from "@app/helpers/platform";
|
import { isInfisicalCloud } from "@app/helpers/platform";
|
||||||
|
import { gatewaysQueryKeys } from "@app/hooks/api";
|
||||||
import {
|
import {
|
||||||
GitHubConnectionMethod,
|
GitHubConnectionMethod,
|
||||||
TGitHubConnection,
|
TGitHubConnection,
|
||||||
@@ -26,7 +46,12 @@ type Props = {
|
|||||||
|
|
||||||
const formSchema = genericAppConnectionFieldsSchema.extend({
|
const formSchema = genericAppConnectionFieldsSchema.extend({
|
||||||
app: z.literal(AppConnection.GitHub),
|
app: z.literal(AppConnection.GitHub),
|
||||||
method: z.nativeEnum(GitHubConnectionMethod)
|
method: z.nativeEnum(GitHubConnectionMethod),
|
||||||
|
credentials: z
|
||||||
|
.object({
|
||||||
|
host: z.string().optional()
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
type FormData = z.infer<typeof formSchema>;
|
type FormData = z.infer<typeof formSchema>;
|
||||||
@@ -44,7 +69,8 @@ export const GitHubConnectionForm = ({ appConnection }: Props) => {
|
|||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: appConnection ?? {
|
defaultValues: appConnection ?? {
|
||||||
app: AppConnection.GitHub,
|
app: AppConnection.GitHub,
|
||||||
method: GitHubConnectionMethod.App
|
method: GitHubConnectionMethod.App,
|
||||||
|
gatewayId: null
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -55,6 +81,9 @@ export const GitHubConnectionForm = ({ appConnection }: Props) => {
|
|||||||
formState: { isSubmitting, isDirty }
|
formState: { isSubmitting, isDirty }
|
||||||
} = form;
|
} = form;
|
||||||
|
|
||||||
|
const { subscription } = useSubscription();
|
||||||
|
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
|
||||||
|
|
||||||
const selectedMethod = watch("method");
|
const selectedMethod = watch("method");
|
||||||
|
|
||||||
const onSubmit = (formData: FormData) => {
|
const onSubmit = (formData: FormData) => {
|
||||||
@@ -66,15 +95,20 @@ export const GitHubConnectionForm = ({ appConnection }: Props) => {
|
|||||||
JSON.stringify({ ...formData, connectionId: appConnection?.id })
|
JSON.stringify({ ...formData, connectionId: appConnection?.id })
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const githubHost =
|
||||||
|
formData.credentials?.host && formData.credentials.host.length > 0
|
||||||
|
? `https://${formData.credentials.host}`
|
||||||
|
: "https://github.com";
|
||||||
|
|
||||||
switch (formData.method) {
|
switch (formData.method) {
|
||||||
case GitHubConnectionMethod.App:
|
case GitHubConnectionMethod.App:
|
||||||
window.location.assign(
|
window.location.assign(
|
||||||
`https://github.com/apps/${appClientSlug}/installations/new?state=${state}`
|
`${githubHost}/apps/${appClientSlug}/installations/new?state=${state}`
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case GitHubConnectionMethod.OAuth:
|
case GitHubConnectionMethod.OAuth:
|
||||||
window.location.assign(
|
window.location.assign(
|
||||||
`https://github.com/login/oauth/authorize?client_id=${oauthClientId}&response_type=code&scope=repo,admin:org&redirect_uri=${window.location.origin}/organization/app-connections/github/oauth/callback&state=${state}`
|
`${githubHost}/login/oauth/authorize?client_id=${oauthClientId}&response_type=code&scope=repo,admin:org&redirect_uri=${window.location.origin}/organization/app-connections/github/oauth/callback&state=${state}`
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -141,6 +175,81 @@ export const GitHubConnectionForm = ({ appConnection }: Props) => {
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
{subscription.gateway && (
|
||||||
|
<Accordion type="single" collapsible className="w-full">
|
||||||
|
<AccordionItem value="enterprise-options" className="data-[state=open]:border-none">
|
||||||
|
<AccordionTrigger className="h-fit flex-none pl-1 text-sm">
|
||||||
|
<div className="order-1 ml-3">GitHub Enterprise Options</div>
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent childrenClassName="px-0">
|
||||||
|
<OrgPermissionCan
|
||||||
|
I={OrgGatewayPermissionActions.AttachGateways}
|
||||||
|
a={OrgPermissionSubjects.Gateway}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="gatewayId"
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
label="Gateway"
|
||||||
|
>
|
||||||
|
<Tooltip
|
||||||
|
isDisabled={isAllowed}
|
||||||
|
content="Restricted access. You don't have permission to attach gateways to resources."
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Select
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
value={value as string}
|
||||||
|
onValueChange={onChange}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
dropdownContainerClassName="max-w-none"
|
||||||
|
isLoading={isGatewaysLoading}
|
||||||
|
placeholder="Default: Internet Gateway"
|
||||||
|
position="popper"
|
||||||
|
>
|
||||||
|
<SelectItem
|
||||||
|
value={null as unknown as string}
|
||||||
|
onClick={() => onChange(undefined)}
|
||||||
|
>
|
||||||
|
Internet Gateway
|
||||||
|
</SelectItem>
|
||||||
|
{gateways?.map((el) => (
|
||||||
|
<SelectItem value={el.id} key={el.id}>
|
||||||
|
{el.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
<Controller
|
||||||
|
name="credentials.host"
|
||||||
|
control={control}
|
||||||
|
shouldUnregister
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
label="Hostname"
|
||||||
|
isOptional
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="github.com" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
)}
|
||||||
<div className="mt-8 flex items-center">
|
<div className="mt-8 flex items-center">
|
||||||
<Button
|
<Button
|
||||||
className="mr-4"
|
className="mr-4"
|
||||||
|
@@ -6,6 +6,7 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
import { Button, FormControl, ModalClose, Select, SelectItem, Tooltip } from "@app/components/v2";
|
import { Button, FormControl, ModalClose, Select, SelectItem, Tooltip } from "@app/components/v2";
|
||||||
|
import { useSubscription } from "@app/context";
|
||||||
import {
|
import {
|
||||||
OrgGatewayPermissionActions,
|
OrgGatewayPermissionActions,
|
||||||
OrgPermissionSubjects
|
OrgPermissionSubjects
|
||||||
@@ -78,6 +79,7 @@ export const MsSqlConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
|||||||
formState: { isSubmitting, isDirty }
|
formState: { isSubmitting, isDirty }
|
||||||
} = form;
|
} = form;
|
||||||
|
|
||||||
|
const { subscription } = useSubscription();
|
||||||
const isPlatformManagedCredentials = appConnection?.isPlatformManagedCredentials ?? false;
|
const isPlatformManagedCredentials = appConnection?.isPlatformManagedCredentials ?? false;
|
||||||
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
|
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
|
||||||
|
|
||||||
@@ -99,55 +101,57 @@ export const MsSqlConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!isUpdate && <GenericAppConnectionsFields />}
|
{!isUpdate && <GenericAppConnectionsFields />}
|
||||||
<OrgPermissionCan
|
{subscription.gateway && (
|
||||||
I={OrgGatewayPermissionActions.AttachGateways}
|
<OrgPermissionCan
|
||||||
a={OrgPermissionSubjects.Gateway}
|
I={OrgGatewayPermissionActions.AttachGateways}
|
||||||
>
|
a={OrgPermissionSubjects.Gateway}
|
||||||
{(isAllowed) => (
|
>
|
||||||
<Controller
|
{(isAllowed) => (
|
||||||
control={control}
|
<Controller
|
||||||
name="gatewayId"
|
control={control}
|
||||||
defaultValue=""
|
name="gatewayId"
|
||||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
defaultValue=""
|
||||||
<FormControl
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
isError={Boolean(error?.message)}
|
<FormControl
|
||||||
errorText={error?.message}
|
isError={Boolean(error?.message)}
|
||||||
label="Gateway"
|
errorText={error?.message}
|
||||||
>
|
label="Gateway"
|
||||||
<Tooltip
|
|
||||||
isDisabled={isAllowed}
|
|
||||||
content="Restricted access. You don't have permission to attach gateways to resources."
|
|
||||||
>
|
>
|
||||||
<div>
|
<Tooltip
|
||||||
<Select
|
isDisabled={isAllowed}
|
||||||
isDisabled={!isAllowed}
|
content="Restricted access. You don't have permission to attach gateways to resources."
|
||||||
value={value as string}
|
>
|
||||||
onValueChange={onChange}
|
<div>
|
||||||
className="w-full border border-mineshaft-500"
|
<Select
|
||||||
dropdownContainerClassName="max-w-none"
|
isDisabled={!isAllowed}
|
||||||
isLoading={isGatewaysLoading}
|
value={value as string}
|
||||||
placeholder="Default: Internet Gateway"
|
onValueChange={onChange}
|
||||||
position="popper"
|
className="w-full border border-mineshaft-500"
|
||||||
>
|
dropdownContainerClassName="max-w-none"
|
||||||
<SelectItem
|
isLoading={isGatewaysLoading}
|
||||||
value={null as unknown as string}
|
placeholder="Default: Internet Gateway"
|
||||||
onClick={() => onChange(undefined)}
|
position="popper"
|
||||||
>
|
>
|
||||||
Internet Gateway
|
<SelectItem
|
||||||
</SelectItem>
|
value={null as unknown as string}
|
||||||
{gateways?.map((el) => (
|
onClick={() => onChange(undefined)}
|
||||||
<SelectItem value={el.id} key={el.id}>
|
>
|
||||||
{el.name}
|
Internet Gateway
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
{gateways?.map((el) => (
|
||||||
</Select>
|
<SelectItem value={el.id} key={el.id}>
|
||||||
</div>
|
{el.name}
|
||||||
</Tooltip>
|
</SelectItem>
|
||||||
</FormControl>
|
))}
|
||||||
)}
|
</Select>
|
||||||
/>
|
</div>
|
||||||
)}
|
</Tooltip>
|
||||||
</OrgPermissionCan>
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
)}
|
||||||
<Controller
|
<Controller
|
||||||
name="method"
|
name="method"
|
||||||
control={control}
|
control={control}
|
||||||
|
@@ -6,6 +6,7 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
import { Button, FormControl, ModalClose, Select, SelectItem, Tooltip } from "@app/components/v2";
|
import { Button, FormControl, ModalClose, Select, SelectItem, Tooltip } from "@app/components/v2";
|
||||||
|
import { useSubscription } from "@app/context";
|
||||||
import {
|
import {
|
||||||
OrgGatewayPermissionActions,
|
OrgGatewayPermissionActions,
|
||||||
OrgPermissionSubjects
|
OrgPermissionSubjects
|
||||||
@@ -75,6 +76,7 @@ export const MySqlConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
|||||||
formState: { isSubmitting, isDirty }
|
formState: { isSubmitting, isDirty }
|
||||||
} = form;
|
} = form;
|
||||||
|
|
||||||
|
const { subscription } = useSubscription();
|
||||||
const isPlatformManagedCredentials = appConnection?.isPlatformManagedCredentials ?? false;
|
const isPlatformManagedCredentials = appConnection?.isPlatformManagedCredentials ?? false;
|
||||||
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
|
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
|
||||||
|
|
||||||
@@ -96,55 +98,57 @@ export const MySqlConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!isUpdate && <GenericAppConnectionsFields />}
|
{!isUpdate && <GenericAppConnectionsFields />}
|
||||||
<OrgPermissionCan
|
{subscription.gateway && (
|
||||||
I={OrgGatewayPermissionActions.AttachGateways}
|
<OrgPermissionCan
|
||||||
a={OrgPermissionSubjects.Gateway}
|
I={OrgGatewayPermissionActions.AttachGateways}
|
||||||
>
|
a={OrgPermissionSubjects.Gateway}
|
||||||
{(isAllowed) => (
|
>
|
||||||
<Controller
|
{(isAllowed) => (
|
||||||
control={control}
|
<Controller
|
||||||
name="gatewayId"
|
control={control}
|
||||||
defaultValue=""
|
name="gatewayId"
|
||||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
defaultValue=""
|
||||||
<FormControl
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
isError={Boolean(error?.message)}
|
<FormControl
|
||||||
errorText={error?.message}
|
isError={Boolean(error?.message)}
|
||||||
label="Gateway"
|
errorText={error?.message}
|
||||||
>
|
label="Gateway"
|
||||||
<Tooltip
|
|
||||||
isDisabled={isAllowed}
|
|
||||||
content="Restricted access. You don't have permission to attach gateways to resources."
|
|
||||||
>
|
>
|
||||||
<div>
|
<Tooltip
|
||||||
<Select
|
isDisabled={isAllowed}
|
||||||
isDisabled={!isAllowed}
|
content="Restricted access. You don't have permission to attach gateways to resources."
|
||||||
value={value as string}
|
>
|
||||||
onValueChange={onChange}
|
<div>
|
||||||
className="w-full border border-mineshaft-500"
|
<Select
|
||||||
dropdownContainerClassName="max-w-none"
|
isDisabled={!isAllowed}
|
||||||
isLoading={isGatewaysLoading}
|
value={value as string}
|
||||||
placeholder="Default: Internet Gateway"
|
onValueChange={onChange}
|
||||||
position="popper"
|
className="w-full border border-mineshaft-500"
|
||||||
>
|
dropdownContainerClassName="max-w-none"
|
||||||
<SelectItem
|
isLoading={isGatewaysLoading}
|
||||||
value={null as unknown as string}
|
placeholder="Default: Internet Gateway"
|
||||||
onClick={() => onChange(undefined)}
|
position="popper"
|
||||||
>
|
>
|
||||||
Internet Gateway
|
<SelectItem
|
||||||
</SelectItem>
|
value={null as unknown as string}
|
||||||
{gateways?.map((el) => (
|
onClick={() => onChange(undefined)}
|
||||||
<SelectItem value={el.id} key={el.id}>
|
>
|
||||||
{el.name}
|
Internet Gateway
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
{gateways?.map((el) => (
|
||||||
</Select>
|
<SelectItem value={el.id} key={el.id}>
|
||||||
</div>
|
{el.name}
|
||||||
</Tooltip>
|
</SelectItem>
|
||||||
</FormControl>
|
))}
|
||||||
)}
|
</Select>
|
||||||
/>
|
</div>
|
||||||
)}
|
</Tooltip>
|
||||||
</OrgPermissionCan>
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
)}
|
||||||
<Controller
|
<Controller
|
||||||
name="method"
|
name="method"
|
||||||
control={control}
|
control={control}
|
||||||
|
@@ -6,6 +6,7 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
import { Button, FormControl, ModalClose, Select, SelectItem, Tooltip } from "@app/components/v2";
|
import { Button, FormControl, ModalClose, Select, SelectItem, Tooltip } from "@app/components/v2";
|
||||||
|
import { useSubscription } from "@app/context";
|
||||||
import {
|
import {
|
||||||
OrgGatewayPermissionActions,
|
OrgGatewayPermissionActions,
|
||||||
OrgPermissionSubjects
|
OrgPermissionSubjects
|
||||||
@@ -75,6 +76,7 @@ export const OracleDBConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
|||||||
formState: { isSubmitting, isDirty }
|
formState: { isSubmitting, isDirty }
|
||||||
} = form;
|
} = form;
|
||||||
|
|
||||||
|
const { subscription } = useSubscription();
|
||||||
const isPlatformManagedCredentials = appConnection?.isPlatformManagedCredentials ?? false;
|
const isPlatformManagedCredentials = appConnection?.isPlatformManagedCredentials ?? false;
|
||||||
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
|
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
|
||||||
|
|
||||||
@@ -96,55 +98,57 @@ export const OracleDBConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!isUpdate && <GenericAppConnectionsFields />}
|
{!isUpdate && <GenericAppConnectionsFields />}
|
||||||
<OrgPermissionCan
|
{subscription.gateway && (
|
||||||
I={OrgGatewayPermissionActions.AttachGateways}
|
<OrgPermissionCan
|
||||||
a={OrgPermissionSubjects.Gateway}
|
I={OrgGatewayPermissionActions.AttachGateways}
|
||||||
>
|
a={OrgPermissionSubjects.Gateway}
|
||||||
{(isAllowed) => (
|
>
|
||||||
<Controller
|
{(isAllowed) => (
|
||||||
control={control}
|
<Controller
|
||||||
name="gatewayId"
|
control={control}
|
||||||
defaultValue=""
|
name="gatewayId"
|
||||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
defaultValue=""
|
||||||
<FormControl
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
isError={Boolean(error?.message)}
|
<FormControl
|
||||||
errorText={error?.message}
|
isError={Boolean(error?.message)}
|
||||||
label="Gateway"
|
errorText={error?.message}
|
||||||
>
|
label="Gateway"
|
||||||
<Tooltip
|
|
||||||
isDisabled={isAllowed}
|
|
||||||
content="Restricted access. You don't have permission to attach gateways to resources."
|
|
||||||
>
|
>
|
||||||
<div>
|
<Tooltip
|
||||||
<Select
|
isDisabled={isAllowed}
|
||||||
isDisabled={!isAllowed}
|
content="Restricted access. You don't have permission to attach gateways to resources."
|
||||||
value={value as string}
|
>
|
||||||
onValueChange={onChange}
|
<div>
|
||||||
className="w-full border border-mineshaft-500"
|
<Select
|
||||||
dropdownContainerClassName="max-w-none"
|
isDisabled={!isAllowed}
|
||||||
isLoading={isGatewaysLoading}
|
value={value as string}
|
||||||
placeholder="Default: Internet Gateway"
|
onValueChange={onChange}
|
||||||
position="popper"
|
className="w-full border border-mineshaft-500"
|
||||||
>
|
dropdownContainerClassName="max-w-none"
|
||||||
<SelectItem
|
isLoading={isGatewaysLoading}
|
||||||
value={null as unknown as string}
|
placeholder="Default: Internet Gateway"
|
||||||
onClick={() => onChange(undefined)}
|
position="popper"
|
||||||
>
|
>
|
||||||
Internet Gateway
|
<SelectItem
|
||||||
</SelectItem>
|
value={null as unknown as string}
|
||||||
{gateways?.map((el) => (
|
onClick={() => onChange(undefined)}
|
||||||
<SelectItem value={el.id} key={el.id}>
|
>
|
||||||
{el.name}
|
Internet Gateway
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
{gateways?.map((el) => (
|
||||||
</Select>
|
<SelectItem value={el.id} key={el.id}>
|
||||||
</div>
|
{el.name}
|
||||||
</Tooltip>
|
</SelectItem>
|
||||||
</FormControl>
|
))}
|
||||||
)}
|
</Select>
|
||||||
/>
|
</div>
|
||||||
)}
|
</Tooltip>
|
||||||
</OrgPermissionCan>
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
)}
|
||||||
<Controller
|
<Controller
|
||||||
name="method"
|
name="method"
|
||||||
control={control}
|
control={control}
|
||||||
|
@@ -6,6 +6,7 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
import { Button, FormControl, ModalClose, Select, SelectItem, Tooltip } from "@app/components/v2";
|
import { Button, FormControl, ModalClose, Select, SelectItem, Tooltip } from "@app/components/v2";
|
||||||
|
import { useSubscription } from "@app/context";
|
||||||
import {
|
import {
|
||||||
OrgGatewayPermissionActions,
|
OrgGatewayPermissionActions,
|
||||||
OrgPermissionSubjects
|
OrgPermissionSubjects
|
||||||
@@ -75,6 +76,7 @@ export const PostgresConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
|||||||
formState: { isSubmitting, isDirty }
|
formState: { isSubmitting, isDirty }
|
||||||
} = form;
|
} = form;
|
||||||
|
|
||||||
|
const { subscription } = useSubscription();
|
||||||
const isPlatformManagedCredentials = appConnection?.isPlatformManagedCredentials ?? false;
|
const isPlatformManagedCredentials = appConnection?.isPlatformManagedCredentials ?? false;
|
||||||
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
|
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
|
||||||
|
|
||||||
@@ -96,55 +98,57 @@ export const PostgresConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!isUpdate && <GenericAppConnectionsFields />}
|
{!isUpdate && <GenericAppConnectionsFields />}
|
||||||
<OrgPermissionCan
|
{subscription.gateway && (
|
||||||
I={OrgGatewayPermissionActions.AttachGateways}
|
<OrgPermissionCan
|
||||||
a={OrgPermissionSubjects.Gateway}
|
I={OrgGatewayPermissionActions.AttachGateways}
|
||||||
>
|
a={OrgPermissionSubjects.Gateway}
|
||||||
{(isAllowed) => (
|
>
|
||||||
<Controller
|
{(isAllowed) => (
|
||||||
control={control}
|
<Controller
|
||||||
name="gatewayId"
|
control={control}
|
||||||
defaultValue=""
|
name="gatewayId"
|
||||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
defaultValue=""
|
||||||
<FormControl
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
isError={Boolean(error?.message)}
|
<FormControl
|
||||||
errorText={error?.message}
|
isError={Boolean(error?.message)}
|
||||||
label="Gateway"
|
errorText={error?.message}
|
||||||
>
|
label="Gateway"
|
||||||
<Tooltip
|
|
||||||
isDisabled={isAllowed}
|
|
||||||
content="Restricted access. You don't have permission to attach gateways to resources."
|
|
||||||
>
|
>
|
||||||
<div>
|
<Tooltip
|
||||||
<Select
|
isDisabled={isAllowed}
|
||||||
isDisabled={!isAllowed}
|
content="Restricted access. You don't have permission to attach gateways to resources."
|
||||||
value={value as string}
|
>
|
||||||
onValueChange={onChange}
|
<div>
|
||||||
className="w-full border border-mineshaft-500"
|
<Select
|
||||||
dropdownContainerClassName="max-w-none"
|
isDisabled={!isAllowed}
|
||||||
isLoading={isGatewaysLoading}
|
value={value as string}
|
||||||
placeholder="Default: Internet Gateway"
|
onValueChange={onChange}
|
||||||
position="popper"
|
className="w-full border border-mineshaft-500"
|
||||||
>
|
dropdownContainerClassName="max-w-none"
|
||||||
<SelectItem
|
isLoading={isGatewaysLoading}
|
||||||
value={null as unknown as string}
|
placeholder="Default: Internet Gateway"
|
||||||
onClick={() => onChange(undefined)}
|
position="popper"
|
||||||
>
|
>
|
||||||
Internet Gateway
|
<SelectItem
|
||||||
</SelectItem>
|
value={null as unknown as string}
|
||||||
{gateways?.map((el) => (
|
onClick={() => onChange(undefined)}
|
||||||
<SelectItem value={el.id} key={el.id}>
|
>
|
||||||
{el.name}
|
Internet Gateway
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
{gateways?.map((el) => (
|
||||||
</Select>
|
<SelectItem value={el.id} key={el.id}>
|
||||||
</div>
|
{el.name}
|
||||||
</Tooltip>
|
</SelectItem>
|
||||||
</FormControl>
|
))}
|
||||||
)}
|
</Select>
|
||||||
/>
|
</div>
|
||||||
)}
|
</Tooltip>
|
||||||
</OrgPermissionCan>
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
)}
|
||||||
<Controller
|
<Controller
|
||||||
name="method"
|
name="method"
|
||||||
control={control}
|
control={control}
|
||||||
|
@@ -30,7 +30,8 @@ type BaseFormData = {
|
|||||||
isUpdate?: boolean;
|
isUpdate?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type GithubFormData = BaseFormData & Pick<TGitHubConnection, "name" | "method" | "description">;
|
type GithubFormData = BaseFormData &
|
||||||
|
Pick<TGitHubConnection, "name" | "method" | "description" | "gatewayId" | "credentials">;
|
||||||
|
|
||||||
type GithubRadarFormData = BaseFormData &
|
type GithubRadarFormData = BaseFormData &
|
||||||
Pick<TGitHubRadarConnection, "name" | "method" | "description">;
|
Pick<TGitHubRadarConnection, "name" | "method" | "description">;
|
||||||
@@ -395,7 +396,7 @@ export const OAuthCallbackPage = () => {
|
|||||||
|
|
||||||
clearState(AppConnection.GitHub);
|
clearState(AppConnection.GitHub);
|
||||||
|
|
||||||
const { connectionId, name, description, returnUrl } = formData;
|
const { connectionId, name, description, returnUrl, gatewayId, credentials } = formData;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (connectionId) {
|
if (connectionId) {
|
||||||
@@ -406,14 +407,18 @@ export const OAuthCallbackPage = () => {
|
|||||||
connectionId,
|
connectionId,
|
||||||
credentials: {
|
credentials: {
|
||||||
code: code as string,
|
code: code as string,
|
||||||
installationId: installationId as string
|
installationId: installationId as string,
|
||||||
}
|
host: credentials.host
|
||||||
|
},
|
||||||
|
gatewayId
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
connectionId,
|
connectionId,
|
||||||
credentials: {
|
credentials: {
|
||||||
code: code as string
|
code: code as string,
|
||||||
}
|
host: credentials.host
|
||||||
|
},
|
||||||
|
gatewayId
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -426,14 +431,18 @@ export const OAuthCallbackPage = () => {
|
|||||||
method: GitHubConnectionMethod.App,
|
method: GitHubConnectionMethod.App,
|
||||||
credentials: {
|
credentials: {
|
||||||
code: code as string,
|
code: code as string,
|
||||||
installationId: installationId as string
|
installationId: installationId as string,
|
||||||
}
|
host: credentials.host
|
||||||
|
},
|
||||||
|
gatewayId
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
method: GitHubConnectionMethod.OAuth,
|
method: GitHubConnectionMethod.OAuth,
|
||||||
credentials: {
|
credentials: {
|
||||||
code: code as string
|
code: code as string,
|
||||||
}
|
host: credentials.host
|
||||||
|
},
|
||||||
|
gatewayId
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user