mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-11 12:11:38 +00:00
Compare commits
43 Commits
fix/cli-jw
...
infisical/
Author | SHA1 | Date | |
---|---|---|---|
b3ace353ce | |||
48353ab201 | |||
2137d13157 | |||
647e13d654 | |||
bb2a933a39 | |||
6f75debb9c | |||
90588bc3c9 | |||
4a09fc5e63 | |||
f0ec8c883f | |||
b30706607f | |||
2a3d19dcb2 | |||
b4ff620b44 | |||
23f1888123 | |||
7764f63299 | |||
cb3365afd4 | |||
58705ffc3f | |||
67e57d8993 | |||
90ff13a6b5 | |||
36145a15c1 | |||
4f64ed6b42 | |||
d47959ca83 | |||
3b2953ca58 | |||
1daa503e0e | |||
d69e8d2a8d | |||
7c7af347fc | |||
8d6712aa58 | |||
a767870ad6 | |||
a0c432628a | |||
08a74a63b5 | |||
8329240822 | |||
ec3cbb9460 | |||
f167ba0fb8 | |||
f291aa1c01 | |||
9ac4453523 | |||
7e9743b4c2 | |||
34cf544b3a | |||
12fd063cd5 | |||
8fb6063686 | |||
459b262865 | |||
f27d4ee973 | |||
7a13c27055 | |||
e7ac783b10 | |||
01ef498397 |
@ -50,6 +50,8 @@ export const initDbConnection = ({
|
|||||||
}
|
}
|
||||||
: false
|
: false
|
||||||
},
|
},
|
||||||
|
// https://knexjs.org/guide/#pool
|
||||||
|
pool: { min: 0, max: 10 },
|
||||||
migrations: {
|
migrations: {
|
||||||
tableName: "infisical_migrations"
|
tableName: "infisical_migrations"
|
||||||
}
|
}
|
||||||
@ -70,7 +72,8 @@ export const initDbConnection = ({
|
|||||||
},
|
},
|
||||||
migrations: {
|
migrations: {
|
||||||
tableName: "infisical_migrations"
|
tableName: "infisical_migrations"
|
||||||
}
|
},
|
||||||
|
pool: { min: 0, max: 10 }
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -89,7 +89,7 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
schema: {
|
schema: {
|
||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
projectSlug: z.string().trim(),
|
projectSlug: z.string().trim(),
|
||||||
authorProjectMembershipId: z.string().trim().optional(),
|
authorUserId: z.string().trim().optional(),
|
||||||
envSlug: z.string().trim().optional()
|
envSlug: z.string().trim().optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
@ -143,7 +143,7 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const { requests } = await server.services.accessApprovalRequest.listApprovalRequests({
|
const { requests } = await server.services.accessApprovalRequest.listApprovalRequests({
|
||||||
projectSlug: req.query.projectSlug,
|
projectSlug: req.query.projectSlug,
|
||||||
authorProjectMembershipId: req.query.authorProjectMembershipId,
|
authorUserId: req.query.authorUserId,
|
||||||
envSlug: req.query.envSlug,
|
envSlug: req.query.envSlug,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
|
@ -30,6 +30,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
workspaceId: z.string().trim(),
|
workspaceId: z.string().trim(),
|
||||||
environment: z.string().trim().optional(),
|
environment: z.string().trim().optional(),
|
||||||
committer: z.string().trim().optional(),
|
committer: z.string().trim().optional(),
|
||||||
|
search: z.string().trim().optional(),
|
||||||
status: z.nativeEnum(RequestState).optional(),
|
status: z.nativeEnum(RequestState).optional(),
|
||||||
limit: z.coerce.number().default(20),
|
limit: z.coerce.number().default(20),
|
||||||
offset: z.coerce.number().default(0)
|
offset: z.coerce.number().default(0)
|
||||||
@ -66,13 +67,14 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
userId: z.string().nullable().optional()
|
userId: z.string().nullable().optional()
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
}).array()
|
}).array(),
|
||||||
|
totalCount: z.number()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const approvals = await server.services.secretApprovalRequest.getSecretApprovals({
|
const { approvals, totalCount } = await server.services.secretApprovalRequest.getSecretApprovals({
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
@ -80,7 +82,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
...req.query,
|
...req.query,
|
||||||
projectId: req.query.workspaceId
|
projectId: req.query.workspaceId
|
||||||
});
|
});
|
||||||
return { approvals };
|
return { approvals, totalCount };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -725,16 +725,17 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR
|
|||||||
)
|
)
|
||||||
|
|
||||||
.where(`${TableName.Environment}.projectId`, projectId)
|
.where(`${TableName.Environment}.projectId`, projectId)
|
||||||
.where(`${TableName.AccessApprovalPolicy}.deletedAt`, null)
|
|
||||||
.select(selectAllTableCols(TableName.AccessApprovalRequest))
|
.select(selectAllTableCols(TableName.AccessApprovalRequest))
|
||||||
.select(db.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus"))
|
.select(db.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus"))
|
||||||
.select(db.ref("reviewerUserId").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerUserId"));
|
.select(db.ref("reviewerUserId").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerUserId"))
|
||||||
|
.select(db.ref("deletedAt").withSchema(TableName.AccessApprovalPolicy).as("policyDeletedAt"));
|
||||||
|
|
||||||
const formattedRequests = sqlNestRelationships({
|
const formattedRequests = sqlNestRelationships({
|
||||||
data: accessRequests,
|
data: accessRequests,
|
||||||
key: "id",
|
key: "id",
|
||||||
parentMapper: (doc) => ({
|
parentMapper: (doc) => ({
|
||||||
...AccessApprovalRequestsSchema.parse(doc)
|
...AccessApprovalRequestsSchema.parse(doc),
|
||||||
|
isPolicyDeleted: Boolean(doc.policyDeletedAt)
|
||||||
}),
|
}),
|
||||||
childrenMapper: [
|
childrenMapper: [
|
||||||
{
|
{
|
||||||
@ -751,7 +752,8 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR
|
|||||||
(req) =>
|
(req) =>
|
||||||
!req.privilegeId &&
|
!req.privilegeId &&
|
||||||
!req.reviewers.some((r) => r.status === ApprovalStatus.REJECTED) &&
|
!req.reviewers.some((r) => r.status === ApprovalStatus.REJECTED) &&
|
||||||
req.status === ApprovalStatus.PENDING
|
req.status === ApprovalStatus.PENDING &&
|
||||||
|
!req.isPolicyDeleted
|
||||||
);
|
);
|
||||||
|
|
||||||
// an approval is finalized if there are any rejections, a privilege ID is set or the number of approvals is equal to the number of approvals required.
|
// an approval is finalized if there are any rejections, a privilege ID is set or the number of approvals is equal to the number of approvals required.
|
||||||
@ -759,7 +761,8 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR
|
|||||||
(req) =>
|
(req) =>
|
||||||
req.privilegeId ||
|
req.privilegeId ||
|
||||||
req.reviewers.some((r) => r.status === ApprovalStatus.REJECTED) ||
|
req.reviewers.some((r) => r.status === ApprovalStatus.REJECTED) ||
|
||||||
req.status !== ApprovalStatus.PENDING
|
req.status !== ApprovalStatus.PENDING ||
|
||||||
|
req.isPolicyDeleted
|
||||||
);
|
);
|
||||||
|
|
||||||
return { pendingCount: pendingApprovals.length, finalizedCount: finalizedApprovals.length };
|
return { pendingCount: pendingApprovals.length, finalizedCount: finalizedApprovals.length };
|
||||||
|
@ -275,7 +275,7 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
|
|
||||||
const listApprovalRequests: TAccessApprovalRequestServiceFactory["listApprovalRequests"] = async ({
|
const listApprovalRequests: TAccessApprovalRequestServiceFactory["listApprovalRequests"] = async ({
|
||||||
projectSlug,
|
projectSlug,
|
||||||
authorProjectMembershipId,
|
authorUserId,
|
||||||
envSlug,
|
envSlug,
|
||||||
actor,
|
actor,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
@ -300,8 +300,8 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
const policies = await accessApprovalPolicyDAL.find({ projectId: project.id });
|
const policies = await accessApprovalPolicyDAL.find({ projectId: project.id });
|
||||||
let requests = await accessApprovalRequestDAL.findRequestsWithPrivilegeByPolicyIds(policies.map((p) => p.id));
|
let requests = await accessApprovalRequestDAL.findRequestsWithPrivilegeByPolicyIds(policies.map((p) => p.id));
|
||||||
|
|
||||||
if (authorProjectMembershipId) {
|
if (authorUserId) {
|
||||||
requests = requests.filter((request) => request.requestedByUserId === actorId);
|
requests = requests.filter((request) => request.requestedByUserId === authorUserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (envSlug) {
|
if (envSlug) {
|
||||||
|
@ -31,7 +31,7 @@ export type TCreateAccessApprovalRequestDTO = {
|
|||||||
|
|
||||||
export type TListApprovalRequestsDTO = {
|
export type TListApprovalRequestsDTO = {
|
||||||
projectSlug: string;
|
projectSlug: string;
|
||||||
authorProjectMembershipId?: string;
|
authorUserId?: string;
|
||||||
envSlug?: string;
|
envSlug?: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
133
backend/src/ee/services/dynamic-secret/providers/github.ts
Normal file
133
backend/src/ee/services/dynamic-secret/providers/github.ts
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import * as jwt from "jsonwebtoken";
|
||||||
|
|
||||||
|
import { BadRequestError, InternalServerError } from "@app/lib/errors";
|
||||||
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
|
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||||
|
|
||||||
|
import { DynamicSecretGithubSchema, TDynamicProviderFns } from "./models";
|
||||||
|
|
||||||
|
interface GitHubInstallationTokenResponse {
|
||||||
|
token: string;
|
||||||
|
expires_at: string; // ISO 8601 timestamp e.g., "2024-01-15T12:00:00Z"
|
||||||
|
permissions?: Record<string, string>;
|
||||||
|
repository_selection?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TGithubProviderInputs {
|
||||||
|
appId: number;
|
||||||
|
installationId: number;
|
||||||
|
privateKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GithubProvider = (): TDynamicProviderFns => {
|
||||||
|
const validateProviderInputs = async (inputs: unknown) => {
|
||||||
|
const providerInputs = await DynamicSecretGithubSchema.parseAsync(inputs);
|
||||||
|
return providerInputs;
|
||||||
|
};
|
||||||
|
|
||||||
|
const $generateGitHubInstallationAccessToken = async (
|
||||||
|
credentials: TGithubProviderInputs
|
||||||
|
): Promise<GitHubInstallationTokenResponse> => {
|
||||||
|
const { appId, installationId, privateKey } = credentials;
|
||||||
|
|
||||||
|
const nowInSeconds = Math.floor(Date.now() / 1000);
|
||||||
|
const jwtPayload = {
|
||||||
|
iat: nowInSeconds - 5,
|
||||||
|
exp: nowInSeconds + 60,
|
||||||
|
iss: String(appId)
|
||||||
|
};
|
||||||
|
|
||||||
|
let appJwt: string;
|
||||||
|
try {
|
||||||
|
appJwt = jwt.sign(jwtPayload, privateKey, { algorithm: "RS256" });
|
||||||
|
} catch (error) {
|
||||||
|
let message = "Failed to sign JWT.";
|
||||||
|
if (error instanceof jwt.JsonWebTokenError) {
|
||||||
|
message += ` JsonWebTokenError: ${error.message}`;
|
||||||
|
}
|
||||||
|
throw new InternalServerError({
|
||||||
|
message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenUrl = `${IntegrationUrls.GITHUB_API_URL}/app/installations/${String(installationId)}/access_tokens`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post<GitHubInstallationTokenResponse>(tokenUrl, undefined, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${appJwt}`,
|
||||||
|
Accept: "application/vnd.github.v3+json",
|
||||||
|
"X-GitHub-Api-Version": "2022-11-28"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 201 && response.data.token) {
|
||||||
|
return response.data; // Includes token, expires_at, permissions, repository_selection
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InternalServerError({
|
||||||
|
message: `GitHub API responded with unexpected status ${response.status}: ${JSON.stringify(response.data)}`
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
let message = "Failed to fetch GitHub installation access token.";
|
||||||
|
if (axios.isAxiosError(error) && error.response) {
|
||||||
|
const githubErrorMsg =
|
||||||
|
(error.response.data as { message?: string })?.message || JSON.stringify(error.response.data);
|
||||||
|
message += ` GitHub API Error: ${error.response.status} - ${githubErrorMsg}`;
|
||||||
|
|
||||||
|
// Classify as BadRequestError for auth-related issues (401, 403, 404) which might be due to user input
|
||||||
|
if ([401, 403, 404].includes(error.response.status)) {
|
||||||
|
throw new BadRequestError({ message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InternalServerError({ message });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateConnection = async (inputs: unknown) => {
|
||||||
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
await $generateGitHubInstallationAccessToken(providerInputs);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const create = async (data: { inputs: unknown }) => {
|
||||||
|
const { inputs } = data;
|
||||||
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
|
||||||
|
const ghTokenData = await $generateGitHubInstallationAccessToken(providerInputs);
|
||||||
|
const entityId = alphaNumericNanoId(32);
|
||||||
|
|
||||||
|
return {
|
||||||
|
entityId,
|
||||||
|
data: {
|
||||||
|
TOKEN: ghTokenData.token,
|
||||||
|
EXPIRES_AT: ghTokenData.expires_at,
|
||||||
|
PERMISSIONS: ghTokenData.permissions,
|
||||||
|
REPOSITORY_SELECTION: ghTokenData.repository_selection
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const revoke = async () => {
|
||||||
|
// GitHub installation tokens cannot be revoked.
|
||||||
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"Github dynamic secret does not support revocation because GitHub itself cannot revoke installation tokens"
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const renew = async () => {
|
||||||
|
// No renewal
|
||||||
|
throw new BadRequestError({ message: "Github dynamic secret does not support renewal" });
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
validateProviderInputs,
|
||||||
|
validateConnection,
|
||||||
|
create,
|
||||||
|
revoke,
|
||||||
|
renew
|
||||||
|
};
|
||||||
|
};
|
@ -7,6 +7,7 @@ import { AzureEntraIDProvider } from "./azure-entra-id";
|
|||||||
import { CassandraProvider } from "./cassandra";
|
import { CassandraProvider } from "./cassandra";
|
||||||
import { ElasticSearchProvider } from "./elastic-search";
|
import { ElasticSearchProvider } from "./elastic-search";
|
||||||
import { GcpIamProvider } from "./gcp-iam";
|
import { GcpIamProvider } from "./gcp-iam";
|
||||||
|
import { GithubProvider } from "./github";
|
||||||
import { KubernetesProvider } from "./kubernetes";
|
import { KubernetesProvider } from "./kubernetes";
|
||||||
import { LdapProvider } from "./ldap";
|
import { LdapProvider } from "./ldap";
|
||||||
import { DynamicSecretProviders, TDynamicProviderFns } from "./models";
|
import { DynamicSecretProviders, TDynamicProviderFns } from "./models";
|
||||||
@ -44,5 +45,6 @@ export const buildDynamicSecretProviders = ({
|
|||||||
[DynamicSecretProviders.SapAse]: SapAseProvider(),
|
[DynamicSecretProviders.SapAse]: SapAseProvider(),
|
||||||
[DynamicSecretProviders.Kubernetes]: KubernetesProvider({ gatewayService }),
|
[DynamicSecretProviders.Kubernetes]: KubernetesProvider({ gatewayService }),
|
||||||
[DynamicSecretProviders.Vertica]: VerticaProvider({ gatewayService }),
|
[DynamicSecretProviders.Vertica]: VerticaProvider({ gatewayService }),
|
||||||
[DynamicSecretProviders.GcpIam]: GcpIamProvider()
|
[DynamicSecretProviders.GcpIam]: GcpIamProvider(),
|
||||||
|
[DynamicSecretProviders.Github]: GithubProvider()
|
||||||
});
|
});
|
||||||
|
@ -52,9 +52,8 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
gatewayId: string;
|
gatewayId: string;
|
||||||
targetHost: string;
|
targetHost: string;
|
||||||
targetPort: number;
|
targetPort: number;
|
||||||
caCert?: string;
|
httpsAgent?: https.Agent;
|
||||||
reviewTokenThroughGateway: boolean;
|
reviewTokenThroughGateway: boolean;
|
||||||
enableSsl: boolean;
|
|
||||||
},
|
},
|
||||||
gatewayCallback: (host: string, port: number, httpsAgent?: https.Agent) => Promise<T>
|
gatewayCallback: (host: string, port: number, httpsAgent?: https.Agent) => Promise<T>
|
||||||
): Promise<T> => {
|
): Promise<T> => {
|
||||||
@ -85,10 +84,7 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
key: relayDetails.privateKey.toString()
|
key: relayDetails.privateKey.toString()
|
||||||
},
|
},
|
||||||
// we always pass this, because its needed for both tcp and http protocol
|
// we always pass this, because its needed for both tcp and http protocol
|
||||||
httpsAgent: new https.Agent({
|
httpsAgent: inputs.httpsAgent
|
||||||
ca: inputs.caCert,
|
|
||||||
rejectUnauthorized: inputs.enableSsl
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -311,6 +307,14 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
const k8sHost = `${url.protocol}//${url.hostname}`;
|
const k8sHost = `${url.protocol}//${url.hostname}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const httpsAgent =
|
||||||
|
providerInputs.ca && providerInputs.sslEnabled
|
||||||
|
? new https.Agent({
|
||||||
|
ca: providerInputs.ca,
|
||||||
|
rejectUnauthorized: true
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
|
|
||||||
if (providerInputs.gatewayId) {
|
if (providerInputs.gatewayId) {
|
||||||
if (providerInputs.authMethod === KubernetesAuthMethod.Gateway) {
|
if (providerInputs.authMethod === KubernetesAuthMethod.Gateway) {
|
||||||
await $gatewayProxyWrapper(
|
await $gatewayProxyWrapper(
|
||||||
@ -318,8 +322,7 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
gatewayId: providerInputs.gatewayId,
|
gatewayId: providerInputs.gatewayId,
|
||||||
targetHost: k8sHost,
|
targetHost: k8sHost,
|
||||||
targetPort: k8sPort,
|
targetPort: k8sPort,
|
||||||
enableSsl: providerInputs.sslEnabled,
|
httpsAgent,
|
||||||
caCert: providerInputs.ca,
|
|
||||||
reviewTokenThroughGateway: true
|
reviewTokenThroughGateway: true
|
||||||
},
|
},
|
||||||
providerInputs.credentialType === KubernetesCredentialType.Static
|
providerInputs.credentialType === KubernetesCredentialType.Static
|
||||||
@ -332,8 +335,7 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
gatewayId: providerInputs.gatewayId,
|
gatewayId: providerInputs.gatewayId,
|
||||||
targetHost: k8sGatewayHost,
|
targetHost: k8sGatewayHost,
|
||||||
targetPort: k8sPort,
|
targetPort: k8sPort,
|
||||||
enableSsl: providerInputs.sslEnabled,
|
httpsAgent,
|
||||||
caCert: providerInputs.ca,
|
|
||||||
reviewTokenThroughGateway: false
|
reviewTokenThroughGateway: false
|
||||||
},
|
},
|
||||||
providerInputs.credentialType === KubernetesCredentialType.Static
|
providerInputs.credentialType === KubernetesCredentialType.Static
|
||||||
@ -342,9 +344,9 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (providerInputs.credentialType === KubernetesCredentialType.Static) {
|
} else if (providerInputs.credentialType === KubernetesCredentialType.Static) {
|
||||||
await serviceAccountStaticCallback(k8sHost, k8sPort);
|
await serviceAccountStaticCallback(k8sHost, k8sPort, httpsAgent);
|
||||||
} else {
|
} else {
|
||||||
await serviceAccountDynamicCallback(k8sHost, k8sPort);
|
await serviceAccountDynamicCallback(k8sHost, k8sPort, httpsAgent);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@ -546,6 +548,15 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
let tokenData;
|
let tokenData;
|
||||||
|
|
||||||
|
const httpsAgent =
|
||||||
|
providerInputs.ca && providerInputs.sslEnabled
|
||||||
|
? new https.Agent({
|
||||||
|
ca: providerInputs.ca,
|
||||||
|
rejectUnauthorized: true
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
|
|
||||||
if (providerInputs.gatewayId) {
|
if (providerInputs.gatewayId) {
|
||||||
if (providerInputs.authMethod === KubernetesAuthMethod.Gateway) {
|
if (providerInputs.authMethod === KubernetesAuthMethod.Gateway) {
|
||||||
tokenData = await $gatewayProxyWrapper(
|
tokenData = await $gatewayProxyWrapper(
|
||||||
@ -553,8 +564,7 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
gatewayId: providerInputs.gatewayId,
|
gatewayId: providerInputs.gatewayId,
|
||||||
targetHost: k8sHost,
|
targetHost: k8sHost,
|
||||||
targetPort: k8sPort,
|
targetPort: k8sPort,
|
||||||
enableSsl: providerInputs.sslEnabled,
|
httpsAgent,
|
||||||
caCert: providerInputs.ca,
|
|
||||||
reviewTokenThroughGateway: true
|
reviewTokenThroughGateway: true
|
||||||
},
|
},
|
||||||
providerInputs.credentialType === KubernetesCredentialType.Static
|
providerInputs.credentialType === KubernetesCredentialType.Static
|
||||||
@ -567,8 +577,7 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
gatewayId: providerInputs.gatewayId,
|
gatewayId: providerInputs.gatewayId,
|
||||||
targetHost: k8sGatewayHost,
|
targetHost: k8sGatewayHost,
|
||||||
targetPort: k8sPort,
|
targetPort: k8sPort,
|
||||||
enableSsl: providerInputs.sslEnabled,
|
httpsAgent,
|
||||||
caCert: providerInputs.ca,
|
|
||||||
reviewTokenThroughGateway: false
|
reviewTokenThroughGateway: false
|
||||||
},
|
},
|
||||||
providerInputs.credentialType === KubernetesCredentialType.Static
|
providerInputs.credentialType === KubernetesCredentialType.Static
|
||||||
@ -579,8 +588,8 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
} else {
|
} else {
|
||||||
tokenData =
|
tokenData =
|
||||||
providerInputs.credentialType === KubernetesCredentialType.Static
|
providerInputs.credentialType === KubernetesCredentialType.Static
|
||||||
? await tokenRequestStaticCallback(k8sHost, k8sPort)
|
? await tokenRequestStaticCallback(k8sHost, k8sPort, httpsAgent)
|
||||||
: await serviceAccountDynamicCallback(k8sHost, k8sPort);
|
: await serviceAccountDynamicCallback(k8sHost, k8sPort, httpsAgent);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -684,6 +693,14 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
const k8sPort = url.port ? Number(url.port) : 443;
|
const k8sPort = url.port ? Number(url.port) : 443;
|
||||||
const k8sHost = `${url.protocol}//${url.hostname}`;
|
const k8sHost = `${url.protocol}//${url.hostname}`;
|
||||||
|
|
||||||
|
const httpsAgent =
|
||||||
|
providerInputs.ca && providerInputs.sslEnabled
|
||||||
|
? new https.Agent({
|
||||||
|
ca: providerInputs.ca,
|
||||||
|
rejectUnauthorized: true
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
|
|
||||||
if (providerInputs.gatewayId) {
|
if (providerInputs.gatewayId) {
|
||||||
if (providerInputs.authMethod === KubernetesAuthMethod.Gateway) {
|
if (providerInputs.authMethod === KubernetesAuthMethod.Gateway) {
|
||||||
await $gatewayProxyWrapper(
|
await $gatewayProxyWrapper(
|
||||||
@ -691,8 +708,7 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
gatewayId: providerInputs.gatewayId,
|
gatewayId: providerInputs.gatewayId,
|
||||||
targetHost: k8sHost,
|
targetHost: k8sHost,
|
||||||
targetPort: k8sPort,
|
targetPort: k8sPort,
|
||||||
enableSsl: providerInputs.sslEnabled,
|
httpsAgent,
|
||||||
caCert: providerInputs.ca,
|
|
||||||
reviewTokenThroughGateway: true
|
reviewTokenThroughGateway: true
|
||||||
},
|
},
|
||||||
serviceAccountDynamicCallback
|
serviceAccountDynamicCallback
|
||||||
@ -703,15 +719,14 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
gatewayId: providerInputs.gatewayId,
|
gatewayId: providerInputs.gatewayId,
|
||||||
targetHost: k8sGatewayHost,
|
targetHost: k8sGatewayHost,
|
||||||
targetPort: k8sPort,
|
targetPort: k8sPort,
|
||||||
enableSsl: providerInputs.sslEnabled,
|
httpsAgent,
|
||||||
caCert: providerInputs.ca,
|
|
||||||
reviewTokenThroughGateway: false
|
reviewTokenThroughGateway: false
|
||||||
},
|
},
|
||||||
serviceAccountDynamicCallback
|
serviceAccountDynamicCallback
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await serviceAccountDynamicCallback(k8sHost, k8sPort);
|
await serviceAccountDynamicCallback(k8sHost, k8sPort, httpsAgent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -477,6 +477,23 @@ export const DynamicSecretGcpIamSchema = z.object({
|
|||||||
serviceAccountEmail: z.string().email().trim().min(1, "Service account email required").max(128)
|
serviceAccountEmail: z.string().email().trim().min(1, "Service account email required").max(128)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const DynamicSecretGithubSchema = z.object({
|
||||||
|
appId: z.number().min(1).describe("The ID of your GitHub App."),
|
||||||
|
installationId: z.number().min(1).describe("The ID of the GitHub App installation."),
|
||||||
|
privateKey: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1)
|
||||||
|
.refine(
|
||||||
|
(val) =>
|
||||||
|
new RE2(
|
||||||
|
/^-----BEGIN(?:(?: RSA| PGP| ENCRYPTED)? PRIVATE KEY)-----\s*[\s\S]*?-----END(?:(?: RSA| PGP| ENCRYPTED)? PRIVATE KEY)-----$/
|
||||||
|
).test(val),
|
||||||
|
"Invalid PEM format for private key"
|
||||||
|
)
|
||||||
|
.describe("The private key generated for your GitHub App.")
|
||||||
|
});
|
||||||
|
|
||||||
export enum DynamicSecretProviders {
|
export enum DynamicSecretProviders {
|
||||||
SqlDatabase = "sql-database",
|
SqlDatabase = "sql-database",
|
||||||
Cassandra = "cassandra",
|
Cassandra = "cassandra",
|
||||||
@ -495,7 +512,8 @@ export enum DynamicSecretProviders {
|
|||||||
SapAse = "sap-ase",
|
SapAse = "sap-ase",
|
||||||
Kubernetes = "kubernetes",
|
Kubernetes = "kubernetes",
|
||||||
Vertica = "vertica",
|
Vertica = "vertica",
|
||||||
GcpIam = "gcp-iam"
|
GcpIam = "gcp-iam",
|
||||||
|
Github = "github"
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
||||||
@ -516,7 +534,8 @@ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
|||||||
z.object({ type: z.literal(DynamicSecretProviders.Totp), inputs: DynamicSecretTotpSchema }),
|
z.object({ type: z.literal(DynamicSecretProviders.Totp), inputs: DynamicSecretTotpSchema }),
|
||||||
z.object({ type: z.literal(DynamicSecretProviders.Kubernetes), inputs: DynamicSecretKubernetesSchema }),
|
z.object({ type: z.literal(DynamicSecretProviders.Kubernetes), inputs: DynamicSecretKubernetesSchema }),
|
||||||
z.object({ type: z.literal(DynamicSecretProviders.Vertica), inputs: DynamicSecretVerticaSchema }),
|
z.object({ type: z.literal(DynamicSecretProviders.Vertica), inputs: DynamicSecretVerticaSchema }),
|
||||||
z.object({ type: z.literal(DynamicSecretProviders.GcpIam), inputs: DynamicSecretGcpIamSchema })
|
z.object({ type: z.literal(DynamicSecretProviders.GcpIam), inputs: DynamicSecretGcpIamSchema }),
|
||||||
|
z.object({ type: z.literal(DynamicSecretProviders.Github), inputs: DynamicSecretGithubSchema })
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export type TDynamicProviderFns = {
|
export type TDynamicProviderFns = {
|
||||||
|
@ -24,6 +24,7 @@ type TFindQueryFilter = {
|
|||||||
committer?: string;
|
committer?: string;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
offset?: number;
|
offset?: number;
|
||||||
|
search?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||||
@ -314,7 +315,6 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
.where(`${TableName.SecretApprovalPolicyApprover}.approverUserId`, userId)
|
.where(`${TableName.SecretApprovalPolicyApprover}.approverUserId`, userId)
|
||||||
.orWhere(`${TableName.SecretApprovalRequest}.committerUserId`, userId)
|
.orWhere(`${TableName.SecretApprovalRequest}.committerUserId`, userId)
|
||||||
)
|
)
|
||||||
.andWhere((bd) => void bd.where(`${TableName.SecretApprovalPolicy}.deletedAt`, null))
|
|
||||||
.select("status", `${TableName.SecretApprovalRequest}.id`)
|
.select("status", `${TableName.SecretApprovalRequest}.id`)
|
||||||
.groupBy(`${TableName.SecretApprovalRequest}.id`, "status")
|
.groupBy(`${TableName.SecretApprovalRequest}.id`, "status")
|
||||||
.count("status")
|
.count("status")
|
||||||
@ -340,13 +340,13 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const findByProjectId = async (
|
const findByProjectId = async (
|
||||||
{ status, limit = 20, offset = 0, projectId, committer, environment, userId }: TFindQueryFilter,
|
{ status, limit = 20, offset = 0, projectId, committer, environment, userId, search }: TFindQueryFilter,
|
||||||
tx?: Knex
|
tx?: Knex
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
// akhilmhdh: If ever u wanted a 1 to so many relationship connected with pagination
|
// akhilmhdh: If ever u wanted a 1 to so many relationship connected with pagination
|
||||||
// this is the place u wanna look at.
|
// this is the place u wanna look at.
|
||||||
const query = (tx || db.replicaNode())(TableName.SecretApprovalRequest)
|
const innerQuery = (tx || db.replicaNode())(TableName.SecretApprovalRequest)
|
||||||
.join(TableName.SecretFolder, `${TableName.SecretApprovalRequest}.folderId`, `${TableName.SecretFolder}.id`)
|
.join(TableName.SecretFolder, `${TableName.SecretApprovalRequest}.folderId`, `${TableName.SecretFolder}.id`)
|
||||||
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
||||||
.join(
|
.join(
|
||||||
@ -435,7 +435,30 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("firstName").withSchema("committerUser").as("committerUserFirstName"),
|
db.ref("firstName").withSchema("committerUser").as("committerUserFirstName"),
|
||||||
db.ref("lastName").withSchema("committerUser").as("committerUserLastName")
|
db.ref("lastName").withSchema("committerUser").as("committerUserLastName")
|
||||||
)
|
)
|
||||||
.orderBy("createdAt", "desc");
|
.distinctOn(`${TableName.SecretApprovalRequest}.id`)
|
||||||
|
.as("inner");
|
||||||
|
|
||||||
|
const query = (tx || db)
|
||||||
|
.select("*")
|
||||||
|
.select(db.raw("count(*) OVER() as total_count"))
|
||||||
|
.from(innerQuery)
|
||||||
|
.orderBy("createdAt", "desc") as typeof innerQuery;
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
void query.where((qb) => {
|
||||||
|
void qb
|
||||||
|
.whereRaw(`CONCAT_WS(' ', ??, ??) ilike ?`, [
|
||||||
|
db.ref("firstName").withSchema("committerUser"),
|
||||||
|
db.ref("lastName").withSchema("committerUser"),
|
||||||
|
`%${search}%`
|
||||||
|
])
|
||||||
|
.orWhereRaw(`?? ilike ?`, [db.ref("username").withSchema("committerUser"), `%${search}%`])
|
||||||
|
.orWhereRaw(`?? ilike ?`, [db.ref("email").withSchema("committerUser"), `%${search}%`])
|
||||||
|
.orWhereILike(`${TableName.Environment}.name`, `%${search}%`)
|
||||||
|
.orWhereILike(`${TableName.Environment}.slug`, `%${search}%`)
|
||||||
|
.orWhereILike(`${TableName.SecretApprovalPolicy}.secretPath`, `%${search}%`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const docs = await (tx || db)
|
const docs = await (tx || db)
|
||||||
.with("w", query)
|
.with("w", query)
|
||||||
@ -443,6 +466,10 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
.from<Awaited<typeof query>[number]>("w")
|
.from<Awaited<typeof query>[number]>("w")
|
||||||
.where("w.rank", ">=", offset)
|
.where("w.rank", ">=", offset)
|
||||||
.andWhere("w.rank", "<", offset + limit);
|
.andWhere("w.rank", "<", offset + limit);
|
||||||
|
|
||||||
|
// @ts-expect-error knex does not infer
|
||||||
|
const totalCount = Number(docs[0]?.total_count || 0);
|
||||||
|
|
||||||
const formattedDoc = sqlNestRelationships({
|
const formattedDoc = sqlNestRelationships({
|
||||||
data: docs,
|
data: docs,
|
||||||
key: "id",
|
key: "id",
|
||||||
@ -504,23 +531,26 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
return formattedDoc.map((el) => ({
|
return {
|
||||||
|
approvals: formattedDoc.map((el) => ({
|
||||||
...el,
|
...el,
|
||||||
policy: { ...el.policy, approvers: el.approvers, bypassers: el.bypassers }
|
policy: { ...el.policy, approvers: el.approvers, bypassers: el.bypassers }
|
||||||
}));
|
})),
|
||||||
|
totalCount
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "FindSAR" });
|
throw new DatabaseError({ error, name: "FindSAR" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const findByProjectIdBridgeSecretV2 = async (
|
const findByProjectIdBridgeSecretV2 = async (
|
||||||
{ status, limit = 20, offset = 0, projectId, committer, environment, userId }: TFindQueryFilter,
|
{ status, limit = 20, offset = 0, projectId, committer, environment, userId, search }: TFindQueryFilter,
|
||||||
tx?: Knex
|
tx?: Knex
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
// akhilmhdh: If ever u wanted a 1 to so many relationship connected with pagination
|
// akhilmhdh: If ever u wanted a 1 to so many relationship connected with pagination
|
||||||
// this is the place u wanna look at.
|
// this is the place u wanna look at.
|
||||||
const query = (tx || db.replicaNode())(TableName.SecretApprovalRequest)
|
const innerQuery = (tx || db.replicaNode())(TableName.SecretApprovalRequest)
|
||||||
.join(TableName.SecretFolder, `${TableName.SecretApprovalRequest}.folderId`, `${TableName.SecretFolder}.id`)
|
.join(TableName.SecretFolder, `${TableName.SecretApprovalRequest}.folderId`, `${TableName.SecretFolder}.id`)
|
||||||
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
||||||
.join(
|
.join(
|
||||||
@ -609,14 +639,42 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("firstName").withSchema("committerUser").as("committerUserFirstName"),
|
db.ref("firstName").withSchema("committerUser").as("committerUserFirstName"),
|
||||||
db.ref("lastName").withSchema("committerUser").as("committerUserLastName")
|
db.ref("lastName").withSchema("committerUser").as("committerUserLastName")
|
||||||
)
|
)
|
||||||
.orderBy("createdAt", "desc");
|
.distinctOn(`${TableName.SecretApprovalRequest}.id`)
|
||||||
|
.as("inner");
|
||||||
|
|
||||||
|
const query = (tx || db)
|
||||||
|
.select("*")
|
||||||
|
.select(db.raw("count(*) OVER() as total_count"))
|
||||||
|
.from(innerQuery)
|
||||||
|
.orderBy("createdAt", "desc") as typeof innerQuery;
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
void query.where((qb) => {
|
||||||
|
void qb
|
||||||
|
.whereRaw(`CONCAT_WS(' ', ??, ??) ilike ?`, [
|
||||||
|
db.ref("firstName").withSchema("committerUser"),
|
||||||
|
db.ref("lastName").withSchema("committerUser"),
|
||||||
|
`%${search}%`
|
||||||
|
])
|
||||||
|
.orWhereRaw(`?? ilike ?`, [db.ref("username").withSchema("committerUser"), `%${search}%`])
|
||||||
|
.orWhereRaw(`?? ilike ?`, [db.ref("email").withSchema("committerUser"), `%${search}%`])
|
||||||
|
.orWhereILike(`${TableName.Environment}.name`, `%${search}%`)
|
||||||
|
.orWhereILike(`${TableName.Environment}.slug`, `%${search}%`)
|
||||||
|
.orWhereILike(`${TableName.SecretApprovalPolicy}.secretPath`, `%${search}%`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const rankOffset = offset + 1;
|
||||||
const docs = await (tx || db)
|
const docs = await (tx || db)
|
||||||
.with("w", query)
|
.with("w", query)
|
||||||
.select("*")
|
.select("*")
|
||||||
.from<Awaited<typeof query>[number]>("w")
|
.from<Awaited<typeof query>[number]>("w")
|
||||||
.where("w.rank", ">=", offset)
|
.where("w.rank", ">=", rankOffset)
|
||||||
.andWhere("w.rank", "<", offset + limit);
|
.andWhere("w.rank", "<", rankOffset + limit);
|
||||||
|
|
||||||
|
// @ts-expect-error knex does not infer
|
||||||
|
const totalCount = Number(docs[0]?.total_count || 0);
|
||||||
|
|
||||||
const formattedDoc = sqlNestRelationships({
|
const formattedDoc = sqlNestRelationships({
|
||||||
data: docs,
|
data: docs,
|
||||||
key: "id",
|
key: "id",
|
||||||
@ -682,10 +740,13 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
return formattedDoc.map((el) => ({
|
return {
|
||||||
|
approvals: formattedDoc.map((el) => ({
|
||||||
...el,
|
...el,
|
||||||
policy: { ...el.policy, approvers: el.approvers, bypassers: el.bypassers }
|
policy: { ...el.policy, approvers: el.approvers, bypassers: el.bypassers }
|
||||||
}));
|
})),
|
||||||
|
totalCount
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "FindSAR" });
|
throw new DatabaseError({ error, name: "FindSAR" });
|
||||||
}
|
}
|
||||||
|
@ -194,7 +194,8 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
environment,
|
environment,
|
||||||
committer,
|
committer,
|
||||||
limit,
|
limit,
|
||||||
offset
|
offset,
|
||||||
|
search
|
||||||
}: TListApprovalsDTO) => {
|
}: TListApprovalsDTO) => {
|
||||||
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
||||||
|
|
||||||
@ -208,6 +209,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
|
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
|
||||||
|
|
||||||
if (shouldUseSecretV2Bridge) {
|
if (shouldUseSecretV2Bridge) {
|
||||||
return secretApprovalRequestDAL.findByProjectIdBridgeSecretV2({
|
return secretApprovalRequestDAL.findByProjectIdBridgeSecretV2({
|
||||||
projectId,
|
projectId,
|
||||||
@ -216,19 +218,21 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
status,
|
status,
|
||||||
userId: actorId,
|
userId: actorId,
|
||||||
limit,
|
limit,
|
||||||
offset
|
offset,
|
||||||
|
search
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const approvals = await secretApprovalRequestDAL.findByProjectId({
|
|
||||||
|
return secretApprovalRequestDAL.findByProjectId({
|
||||||
projectId,
|
projectId,
|
||||||
committer,
|
committer,
|
||||||
environment,
|
environment,
|
||||||
status,
|
status,
|
||||||
userId: actorId,
|
userId: actorId,
|
||||||
limit,
|
limit,
|
||||||
offset
|
offset,
|
||||||
|
search
|
||||||
});
|
});
|
||||||
return approvals;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSecretApprovalDetails = async ({
|
const getSecretApprovalDetails = async ({
|
||||||
|
@ -93,6 +93,7 @@ export type TListApprovalsDTO = {
|
|||||||
committer?: string;
|
committer?: string;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
offset?: number;
|
offset?: number;
|
||||||
|
search?: string;
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
export type TSecretApprovalDetailsDTO = {
|
export type TSecretApprovalDetailsDTO = {
|
||||||
|
@ -101,9 +101,9 @@ const envSchema = z
|
|||||||
LOOPS_API_KEY: zpStr(z.string().optional()),
|
LOOPS_API_KEY: zpStr(z.string().optional()),
|
||||||
// jwt options
|
// jwt options
|
||||||
AUTH_SECRET: zpStr(z.string()).default(process.env.JWT_AUTH_SECRET), // for those still using old JWT_AUTH_SECRET
|
AUTH_SECRET: zpStr(z.string()).default(process.env.JWT_AUTH_SECRET), // for those still using old JWT_AUTH_SECRET
|
||||||
JWT_AUTH_LIFETIME: zpStr(z.string().default("1d")),
|
JWT_AUTH_LIFETIME: zpStr(z.string().default("10d")),
|
||||||
JWT_SIGNUP_LIFETIME: zpStr(z.string().default("15m")),
|
JWT_SIGNUP_LIFETIME: zpStr(z.string().default("15m")),
|
||||||
JWT_REFRESH_LIFETIME: zpStr(z.string().default("14d")),
|
JWT_REFRESH_LIFETIME: zpStr(z.string().default("90d")),
|
||||||
JWT_INVITE_LIFETIME: zpStr(z.string().default("1d")),
|
JWT_INVITE_LIFETIME: zpStr(z.string().default("1d")),
|
||||||
JWT_MFA_LIFETIME: zpStr(z.string().default("5m")),
|
JWT_MFA_LIFETIME: zpStr(z.string().default("5m")),
|
||||||
JWT_PROVIDER_AUTH_LIFETIME: zpStr(z.string().default("15m")),
|
JWT_PROVIDER_AUTH_LIFETIME: zpStr(z.string().default("15m")),
|
||||||
|
@ -107,7 +107,7 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
|
|||||||
server.addHook("onRequest", async (req) => {
|
server.addHook("onRequest", async (req) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
|
|
||||||
if (req.url.includes(".well-known/est") || req.url.includes("/api/v3/auth/") || req.url === "/api/v1/auth/token") {
|
if (req.url.includes(".well-known/est") || req.url.includes("/api/v3/auth/")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,7 +83,7 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
config: {
|
config: {
|
||||||
rateLimit: smtpRateLimit({
|
rateLimit: smtpRateLimit({
|
||||||
keyGenerator: (req) =>
|
keyGenerator: (req) =>
|
||||||
(req.body as { membershipId?: string })?.membershipId?.trim().substring(0, 100) ?? req.realIp
|
(req.body as { membershipId?: string })?.membershipId?.trim().substring(0, 100) || req.realIp
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
@ -81,7 +81,7 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
|
|||||||
url: "/email/password-reset",
|
url: "/email/password-reset",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: smtpRateLimit({
|
rateLimit: smtpRateLimit({
|
||||||
keyGenerator: (req) => (req.body as { email?: string })?.email?.trim().substring(0, 100) ?? req.realIp
|
keyGenerator: (req) => (req.body as { email?: string })?.email?.trim().substring(0, 100) || req.realIp
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
@ -107,7 +107,9 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/email/password-reset-verify",
|
url: "/email/password-reset-verify",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: authRateLimit
|
rateLimit: smtpRateLimit({
|
||||||
|
keyGenerator: (req) => (req.body as { email?: string })?.email?.trim().substring(0, 100) || req.realIp
|
||||||
|
})
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
body: z.object({
|
body: z.object({
|
||||||
|
@ -2,7 +2,7 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { AuthTokenSessionsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
|
import { AuthTokenSessionsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
|
||||||
import { ApiKeysSchema } from "@app/db/schemas/api-keys";
|
import { ApiKeysSchema } from "@app/db/schemas/api-keys";
|
||||||
import { authRateLimit, readLimit, smtpRateLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, smtpRateLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMethod, AuthMode, MfaMethod } from "@app/services/auth/auth-type";
|
import { AuthMethod, AuthMode, MfaMethod } from "@app/services/auth/auth-type";
|
||||||
import { sanitizedOrganizationSchema } from "@app/services/org/org-schema";
|
import { sanitizedOrganizationSchema } from "@app/services/org/org-schema";
|
||||||
@ -13,7 +13,7 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
|
|||||||
url: "/me/emails/code",
|
url: "/me/emails/code",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: smtpRateLimit({
|
rateLimit: smtpRateLimit({
|
||||||
keyGenerator: (req) => (req.body as { username?: string })?.username?.trim().substring(0, 100) ?? req.realIp
|
keyGenerator: (req) => (req.body as { username?: string })?.username?.trim().substring(0, 100) || req.realIp
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
@ -34,7 +34,9 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/me/emails/verify",
|
url: "/me/emails/verify",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: authRateLimit
|
rateLimit: smtpRateLimit({
|
||||||
|
keyGenerator: (req) => (req.body as { username?: string })?.username?.trim().substring(0, 100) || req.realIp
|
||||||
|
})
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
body: z.object({
|
body: z.object({
|
||||||
|
@ -50,8 +50,7 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
|
|||||||
200: z.object({
|
200: z.object({
|
||||||
token: z.string(),
|
token: z.string(),
|
||||||
isMfaEnabled: z.boolean(),
|
isMfaEnabled: z.boolean(),
|
||||||
mfaMethod: z.string().optional(),
|
mfaMethod: z.string().optional()
|
||||||
RefreshToken: z.string().optional()
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -102,7 +101,7 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
|
|||||||
maxAge: 0
|
maxAge: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
return { token: tokens.access, isMfaEnabled: false, RefreshToken: tokens.refresh };
|
return { token: tokens.access, isMfaEnabled: false };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -130,8 +129,7 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
|
|||||||
encryptedPrivateKey: z.string(),
|
encryptedPrivateKey: z.string(),
|
||||||
iv: z.string(),
|
iv: z.string(),
|
||||||
tag: z.string(),
|
tag: z.string(),
|
||||||
token: z.string(),
|
token: z.string()
|
||||||
RefreshToken: z.string().optional()
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -174,8 +172,7 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
|
|||||||
tag: data.user.tag,
|
tag: data.user.tag,
|
||||||
protectedKey: data.user.protectedKey || null,
|
protectedKey: data.user.protectedKey || null,
|
||||||
protectedKeyIV: data.user.protectedKeyIV || null,
|
protectedKeyIV: data.user.protectedKeyIV || null,
|
||||||
protectedKeyTag: data.user.protectedKeyTag || null,
|
protectedKeyTag: data.user.protectedKeyTag || null
|
||||||
RefreshToken: data.token.refresh
|
|
||||||
} as const;
|
} as const;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -14,7 +14,7 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: smtpRateLimit({
|
rateLimit: smtpRateLimit({
|
||||||
keyGenerator: (req) => (req.body as { email?: string })?.email?.trim().substring(0, 100) ?? req.realIp
|
keyGenerator: (req) => (req.body as { email?: string })?.email?.trim().substring(0, 100) || req.realIp
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
@ -55,7 +55,9 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
|
|||||||
url: "/email/verify",
|
url: "/email/verify",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: authRateLimit
|
rateLimit: smtpRateLimit({
|
||||||
|
keyGenerator: (req) => (req.body as { email?: string })?.email?.trim().substring(0, 100) || req.realIp
|
||||||
|
})
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
body: z.object({
|
body: z.object({
|
||||||
|
@ -21,7 +21,7 @@ type LoginTwoRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type LoginTwoResponse struct {
|
type LoginTwoResponse struct {
|
||||||
JWTToken string `json:"token"`
|
JTWToken string `json:"token"`
|
||||||
RefreshToken string `json:"refreshToken"`
|
RefreshToken string `json:"refreshToken"`
|
||||||
PublicKey string `json:"publicKey"`
|
PublicKey string `json:"publicKey"`
|
||||||
EncryptedPrivateKey string `json:"encryptedPrivateKey"`
|
EncryptedPrivateKey string `json:"encryptedPrivateKey"`
|
||||||
|
@ -87,7 +87,7 @@ func getDynamicSecretList(cmd *cobra.Command, args []string) {
|
|||||||
loggedInUserDetails = util.EstablishUserLoginSession()
|
loggedInUserDetails = util.EstablishUserLoginSession()
|
||||||
}
|
}
|
||||||
|
|
||||||
infisicalToken = loggedInUserDetails.UserCredentials.JWTToken
|
infisicalToken = loggedInUserDetails.UserCredentials.JTWToken
|
||||||
}
|
}
|
||||||
|
|
||||||
httpClient.SetAuthToken(infisicalToken)
|
httpClient.SetAuthToken(infisicalToken)
|
||||||
@ -211,7 +211,7 @@ func createDynamicSecretLeaseByName(cmd *cobra.Command, args []string) {
|
|||||||
if loggedInUserDetails.LoginExpired {
|
if loggedInUserDetails.LoginExpired {
|
||||||
loggedInUserDetails = util.EstablishUserLoginSession()
|
loggedInUserDetails = util.EstablishUserLoginSession()
|
||||||
}
|
}
|
||||||
infisicalToken = loggedInUserDetails.UserCredentials.JWTToken
|
infisicalToken = loggedInUserDetails.UserCredentials.JTWToken
|
||||||
}
|
}
|
||||||
|
|
||||||
httpClient.SetAuthToken(infisicalToken)
|
httpClient.SetAuthToken(infisicalToken)
|
||||||
@ -363,7 +363,7 @@ func renewDynamicSecretLeaseByName(cmd *cobra.Command, args []string) {
|
|||||||
loggedInUserDetails = util.EstablishUserLoginSession()
|
loggedInUserDetails = util.EstablishUserLoginSession()
|
||||||
}
|
}
|
||||||
|
|
||||||
infisicalToken = loggedInUserDetails.UserCredentials.JWTToken
|
infisicalToken = loggedInUserDetails.UserCredentials.JTWToken
|
||||||
}
|
}
|
||||||
|
|
||||||
httpClient.SetAuthToken(infisicalToken)
|
httpClient.SetAuthToken(infisicalToken)
|
||||||
@ -478,7 +478,7 @@ func revokeDynamicSecretLeaseByName(cmd *cobra.Command, args []string) {
|
|||||||
loggedInUserDetails = util.EstablishUserLoginSession()
|
loggedInUserDetails = util.EstablishUserLoginSession()
|
||||||
}
|
}
|
||||||
|
|
||||||
infisicalToken = loggedInUserDetails.UserCredentials.JWTToken
|
infisicalToken = loggedInUserDetails.UserCredentials.JTWToken
|
||||||
}
|
}
|
||||||
|
|
||||||
httpClient.SetAuthToken(infisicalToken)
|
httpClient.SetAuthToken(infisicalToken)
|
||||||
@ -592,7 +592,7 @@ func listDynamicSecretLeaseByName(cmd *cobra.Command, args []string) {
|
|||||||
if loggedInUserDetails.LoginExpired {
|
if loggedInUserDetails.LoginExpired {
|
||||||
loggedInUserDetails = util.EstablishUserLoginSession()
|
loggedInUserDetails = util.EstablishUserLoginSession()
|
||||||
}
|
}
|
||||||
infisicalToken = loggedInUserDetails.UserCredentials.JWTToken
|
infisicalToken = loggedInUserDetails.UserCredentials.JTWToken
|
||||||
}
|
}
|
||||||
|
|
||||||
httpClient.SetAuthToken(infisicalToken)
|
httpClient.SetAuthToken(infisicalToken)
|
||||||
|
@ -115,7 +115,7 @@ var exportCmd = &cobra.Command{
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err)
|
util.HandleError(err)
|
||||||
}
|
}
|
||||||
accessToken = loggedInUserDetails.UserCredentials.JWTToken
|
accessToken = loggedInUserDetails.UserCredentials.JTWToken
|
||||||
}
|
}
|
||||||
|
|
||||||
processedTemplate, err := ProcessTemplate(1, templatePath, nil, accessToken, "", &newEtag, dynamicSecretLeases)
|
processedTemplate, err := ProcessTemplate(1, templatePath, nil, accessToken, "", &newEtag, dynamicSecretLeases)
|
||||||
|
@ -53,7 +53,7 @@ var initCmd = &cobra.Command{
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "Unable to get resty client with custom headers")
|
util.HandleError(err, "Unable to get resty client with custom headers")
|
||||||
}
|
}
|
||||||
httpClient.SetAuthToken(userCreds.UserCredentials.JWTToken)
|
httpClient.SetAuthToken(userCreds.UserCredentials.JTWToken)
|
||||||
|
|
||||||
organizationResponse, err := api.CallGetAllOrganizations(httpClient)
|
organizationResponse, err := api.CallGetAllOrganizations(httpClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -124,7 +124,7 @@ var initCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
// set the config jwt token to the new token
|
// set the config jwt token to the new token
|
||||||
userCreds.UserCredentials.JWTToken = tokenResponse.Token
|
userCreds.UserCredentials.JTWToken = tokenResponse.Token
|
||||||
err = util.StoreUserCredsInKeyRing(&userCreds.UserCredentials)
|
err = util.StoreUserCredsInKeyRing(&userCreds.UserCredentials)
|
||||||
httpClient.SetAuthToken(tokenResponse.Token)
|
httpClient.SetAuthToken(tokenResponse.Token)
|
||||||
|
|
||||||
|
@ -111,7 +111,7 @@ var loginCmd = &cobra.Command{
|
|||||||
infisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{
|
infisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{
|
||||||
SiteUrl: config.INFISICAL_URL,
|
SiteUrl: config.INFISICAL_URL,
|
||||||
UserAgent: api.USER_AGENT,
|
UserAgent: api.USER_AGENT,
|
||||||
AutoTokenRefresh: true,
|
AutoTokenRefresh: false,
|
||||||
CustomHeaders: customHeaders,
|
CustomHeaders: customHeaders,
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -437,8 +437,7 @@ func cliDefaultLogin(userCredentialsToBeStored *models.UserCredentials) {
|
|||||||
//updating usercredentials
|
//updating usercredentials
|
||||||
userCredentialsToBeStored.Email = email
|
userCredentialsToBeStored.Email = email
|
||||||
userCredentialsToBeStored.PrivateKey = string(decryptedPrivateKey)
|
userCredentialsToBeStored.PrivateKey = string(decryptedPrivateKey)
|
||||||
userCredentialsToBeStored.JWTToken = newJwtToken
|
userCredentialsToBeStored.JTWToken = newJwtToken
|
||||||
userCredentialsToBeStored.RefreshToken = loginTwoResponse.RefreshToken
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@ -863,7 +862,7 @@ func askToPasteJwtToken(success chan models.UserCredentials, failure chan error)
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// verify JWT
|
// verify JTW
|
||||||
httpClient, err := util.GetRestyClientWithCustomHeaders()
|
httpClient, err := util.GetRestyClientWithCustomHeaders()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
failure <- err
|
failure <- err
|
||||||
@ -872,7 +871,7 @@ func askToPasteJwtToken(success chan models.UserCredentials, failure chan error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
httpClient.
|
httpClient.
|
||||||
SetAuthToken(userCredentials.JWTToken).
|
SetAuthToken(userCredentials.JTWToken).
|
||||||
SetHeader("Accept", "application/json")
|
SetHeader("Accept", "application/json")
|
||||||
|
|
||||||
isAuthenticated := api.CallIsAuthenticated(httpClient)
|
isAuthenticated := api.CallIsAuthenticated(httpClient)
|
||||||
|
@ -245,7 +245,7 @@ var secretsSetCmd = &cobra.Command{
|
|||||||
|
|
||||||
secretOperations, err = util.SetRawSecrets(processedArgs, secretType, environmentName, secretsPath, projectId, &models.TokenDetails{
|
secretOperations, err = util.SetRawSecrets(processedArgs, secretType, environmentName, secretsPath, projectId, &models.TokenDetails{
|
||||||
Type: "",
|
Type: "",
|
||||||
Token: loggedInUserDetails.UserCredentials.JWTToken,
|
Token: loggedInUserDetails.UserCredentials.JTWToken,
|
||||||
}, file)
|
}, file)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -330,7 +330,7 @@ var secretsDeleteCmd = &cobra.Command{
|
|||||||
loggedInUserDetails = util.EstablishUserLoginSession()
|
loggedInUserDetails = util.EstablishUserLoginSession()
|
||||||
}
|
}
|
||||||
|
|
||||||
httpClient.SetAuthToken(loggedInUserDetails.UserCredentials.JWTToken)
|
httpClient.SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, secretName := range args {
|
for _, secretName := range args {
|
||||||
|
@ -186,7 +186,7 @@ func issueCredentials(cmd *cobra.Command, args []string) {
|
|||||||
if loggedInUserDetails.LoginExpired {
|
if loggedInUserDetails.LoginExpired {
|
||||||
loggedInUserDetails = util.EstablishUserLoginSession()
|
loggedInUserDetails = util.EstablishUserLoginSession()
|
||||||
}
|
}
|
||||||
infisicalToken = loggedInUserDetails.UserCredentials.JWTToken
|
infisicalToken = loggedInUserDetails.UserCredentials.JTWToken
|
||||||
}
|
}
|
||||||
|
|
||||||
certificateTemplateId, err := cmd.Flags().GetString("certificateTemplateId")
|
certificateTemplateId, err := cmd.Flags().GetString("certificateTemplateId")
|
||||||
@ -419,7 +419,7 @@ func signKey(cmd *cobra.Command, args []string) {
|
|||||||
if loggedInUserDetails.LoginExpired {
|
if loggedInUserDetails.LoginExpired {
|
||||||
loggedInUserDetails = util.EstablishUserLoginSession()
|
loggedInUserDetails = util.EstablishUserLoginSession()
|
||||||
}
|
}
|
||||||
infisicalToken = loggedInUserDetails.UserCredentials.JWTToken
|
infisicalToken = loggedInUserDetails.UserCredentials.JTWToken
|
||||||
}
|
}
|
||||||
|
|
||||||
certificateTemplateId, err := cmd.Flags().GetString("certificateTemplateId")
|
certificateTemplateId, err := cmd.Flags().GetString("certificateTemplateId")
|
||||||
@ -628,7 +628,7 @@ func sshConnect(cmd *cobra.Command, args []string) {
|
|||||||
if loggedInUserDetails.LoginExpired {
|
if loggedInUserDetails.LoginExpired {
|
||||||
loggedInUserDetails = util.EstablishUserLoginSession()
|
loggedInUserDetails = util.EstablishUserLoginSession()
|
||||||
}
|
}
|
||||||
infisicalToken = loggedInUserDetails.UserCredentials.JWTToken
|
infisicalToken = loggedInUserDetails.UserCredentials.JTWToken
|
||||||
}
|
}
|
||||||
|
|
||||||
writeHostCaToFile, err := cmd.Flags().GetBool("write-host-ca-to-file")
|
writeHostCaToFile, err := cmd.Flags().GetBool("write-host-ca-to-file")
|
||||||
@ -881,7 +881,7 @@ func sshAddHost(cmd *cobra.Command, args []string) {
|
|||||||
if loggedInUserDetails.LoginExpired {
|
if loggedInUserDetails.LoginExpired {
|
||||||
loggedInUserDetails = util.EstablishUserLoginSession()
|
loggedInUserDetails = util.EstablishUserLoginSession()
|
||||||
}
|
}
|
||||||
infisicalToken = loggedInUserDetails.UserCredentials.JWTToken
|
infisicalToken = loggedInUserDetails.UserCredentials.JTWToken
|
||||||
}
|
}
|
||||||
|
|
||||||
projectId, err := cmd.Flags().GetString("projectId")
|
projectId, err := cmd.Flags().GetString("projectId")
|
||||||
|
@ -115,7 +115,7 @@ var tokensCreateCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
workspaceKey, err := util.GetPlainTextWorkspaceKey(loggedInUserDetails.UserCredentials.JWTToken, loggedInUserDetails.UserCredentials.PrivateKey, workspaceId)
|
workspaceKey, err := util.GetPlainTextWorkspaceKey(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, workspaceId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "Unable to get workspace key needed to create service token")
|
util.HandleError(err, "Unable to get workspace key needed to create service token")
|
||||||
}
|
}
|
||||||
@ -140,7 +140,7 @@ var tokensCreateCmd = &cobra.Command{
|
|||||||
util.HandleError(err, "Unable to get resty client with custom headers")
|
util.HandleError(err, "Unable to get resty client with custom headers")
|
||||||
}
|
}
|
||||||
|
|
||||||
httpClient.SetAuthToken(loggedInUserDetails.UserCredentials.JWTToken).
|
httpClient.SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken).
|
||||||
SetHeader("Accept", "application/json")
|
SetHeader("Accept", "application/json")
|
||||||
|
|
||||||
createServiceTokenResponse, err := api.CallCreateServiceToken(httpClient, api.CreateServiceTokenRequest{
|
createServiceTokenResponse, err := api.CallCreateServiceToken(httpClient, api.CreateServiceTokenRequest{
|
||||||
|
@ -118,7 +118,7 @@ var userGetTokenCmd = &cobra.Command{
|
|||||||
util.HandleError(err, "[infisical user get token]: Unable to get logged in user token")
|
util.HandleError(err, "[infisical user get token]: Unable to get logged in user token")
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenParts := strings.Split(loggedInUserDetails.UserCredentials.JWTToken, ".")
|
tokenParts := strings.Split(loggedInUserDetails.UserCredentials.JTWToken, ".")
|
||||||
if len(tokenParts) != 3 {
|
if len(tokenParts) != 3 {
|
||||||
util.HandleError(errors.New("invalid token format"), "[infisical user get token]: Invalid token format")
|
util.HandleError(errors.New("invalid token format"), "[infisical user get token]: Invalid token format")
|
||||||
}
|
}
|
||||||
@ -136,7 +136,7 @@ var userGetTokenCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Session ID:", tokenPayload.TokenVersionId)
|
fmt.Println("Session ID:", tokenPayload.TokenVersionId)
|
||||||
fmt.Println("Token:", loggedInUserDetails.UserCredentials.JWTToken)
|
fmt.Println("Token:", loggedInUserDetails.UserCredentials.JTWToken)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ import "time"
|
|||||||
type UserCredentials struct {
|
type UserCredentials struct {
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
PrivateKey string `json:"privateKey"`
|
PrivateKey string `json:"privateKey"`
|
||||||
JWTToken string `json:"JTWToken"`
|
JTWToken string `json:"JTWToken"`
|
||||||
RefreshToken string `json:"RefreshToken"`
|
RefreshToken string `json:"RefreshToken"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,7 +9,6 @@ import (
|
|||||||
"github.com/Infisical/infisical-merge/packages/api"
|
"github.com/Infisical/infisical-merge/packages/api"
|
||||||
"github.com/Infisical/infisical-merge/packages/config"
|
"github.com/Infisical/infisical-merge/packages/config"
|
||||||
"github.com/Infisical/infisical-merge/packages/models"
|
"github.com/Infisical/infisical-merge/packages/models"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"github.com/zalando/go-keyring"
|
"github.com/zalando/go-keyring"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -91,22 +90,23 @@ func GetCurrentLoggedInUserDetails(setConfigVariables bool) (LoggedInUserDetails
|
|||||||
}
|
}
|
||||||
|
|
||||||
httpClient.
|
httpClient.
|
||||||
SetAuthToken(userCreds.JWTToken).
|
SetAuthToken(userCreds.JTWToken).
|
||||||
SetHeader("Accept", "application/json")
|
SetHeader("Accept", "application/json")
|
||||||
|
|
||||||
isAuthenticated := api.CallIsAuthenticated(httpClient)
|
isAuthenticated := api.CallIsAuthenticated(httpClient)
|
||||||
if !isAuthenticated {
|
// TODO: add refresh token
|
||||||
accessTokenResponse, refreshErr := api.CallGetNewAccessTokenWithRefreshToken(httpClient, userCreds.RefreshToken)
|
// if !isAuthenticated {
|
||||||
if refreshErr == nil && accessTokenResponse.Token != "" {
|
// accessTokenResponse, err := api.CallGetNewAccessTokenWithRefreshToken(httpClient, userCreds.RefreshToken)
|
||||||
isAuthenticated = true
|
// if err == nil && accessTokenResponse.Token != "" {
|
||||||
userCreds.JWTToken = accessTokenResponse.Token
|
// isAuthenticated = true
|
||||||
}
|
// userCreds.JTWToken = accessTokenResponse.Token
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
err = StoreUserCredsInKeyRing(&userCreds)
|
// err = StoreUserCredsInKeyRing(&userCreds)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
log.Debug().Msg("unable to store your user credentials with new access token")
|
// log.Debug().Msg("unable to store your user credentials with new access token")
|
||||||
}
|
// }
|
||||||
|
|
||||||
if !isAuthenticated {
|
if !isAuthenticated {
|
||||||
return LoggedInUserDetails{
|
return LoggedInUserDetails{
|
||||||
|
@ -35,7 +35,7 @@ func GetAllFolders(params models.GetAllFoldersParameters) ([]models.SingleFolder
|
|||||||
params.WorkspaceId = workspaceFile.WorkspaceId
|
params.WorkspaceId = workspaceFile.WorkspaceId
|
||||||
}
|
}
|
||||||
|
|
||||||
folders, err := GetFoldersViaJWT(loggedInUserDetails.UserCredentials.JWTToken, params.WorkspaceId, params.Environment, params.FoldersPath)
|
folders, err := GetFoldersViaJTW(loggedInUserDetails.UserCredentials.JTWToken, params.WorkspaceId, params.Environment, params.FoldersPath)
|
||||||
folderErr = err
|
folderErr = err
|
||||||
foldersToReturn = folders
|
foldersToReturn = folders
|
||||||
} else if params.InfisicalToken != "" {
|
} else if params.InfisicalToken != "" {
|
||||||
@ -60,14 +60,14 @@ func GetAllFolders(params models.GetAllFoldersParameters) ([]models.SingleFolder
|
|||||||
return foldersToReturn, folderErr
|
return foldersToReturn, folderErr
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetFoldersViaJWT(JWTToken string, workspaceId string, environmentName string, foldersPath string) ([]models.SingleFolder, error) {
|
func GetFoldersViaJTW(JTWToken string, workspaceId string, environmentName string, foldersPath string) ([]models.SingleFolder, error) {
|
||||||
// set up resty client
|
// set up resty client
|
||||||
httpClient, err := GetRestyClientWithCustomHeaders()
|
httpClient, err := GetRestyClientWithCustomHeaders()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
httpClient.SetAuthToken(JWTToken).
|
httpClient.SetAuthToken(JTWToken).
|
||||||
SetHeader("Accept", "application/json")
|
SetHeader("Accept", "application/json")
|
||||||
|
|
||||||
getFoldersRequest := api.GetFoldersV1Request{
|
getFoldersRequest := api.GetFoldersV1Request{
|
||||||
@ -194,7 +194,7 @@ func CreateFolder(params models.CreateFolderParameters) (models.SingleFolder, er
|
|||||||
loggedInUserDetails = EstablishUserLoginSession()
|
loggedInUserDetails = EstablishUserLoginSession()
|
||||||
}
|
}
|
||||||
|
|
||||||
params.InfisicalToken = loggedInUserDetails.UserCredentials.JWTToken
|
params.InfisicalToken = loggedInUserDetails.UserCredentials.JTWToken
|
||||||
}
|
}
|
||||||
|
|
||||||
// set up resty client
|
// set up resty client
|
||||||
@ -243,7 +243,7 @@ func DeleteFolder(params models.DeleteFolderParameters) ([]models.SingleFolder,
|
|||||||
loggedInUserDetails = EstablishUserLoginSession()
|
loggedInUserDetails = EstablishUserLoginSession()
|
||||||
}
|
}
|
||||||
|
|
||||||
params.InfisicalToken = loggedInUserDetails.UserCredentials.JWTToken
|
params.InfisicalToken = loggedInUserDetails.UserCredentials.JTWToken
|
||||||
}
|
}
|
||||||
|
|
||||||
// set up resty client
|
// set up resty client
|
||||||
|
@ -302,9 +302,9 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectCo
|
|||||||
params.WorkspaceId = infisicalDotJson.WorkspaceId
|
params.WorkspaceId = infisicalDotJson.WorkspaceId
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := GetPlainTextSecretsV3(loggedInUserDetails.UserCredentials.JWTToken, params.WorkspaceId,
|
res, err := GetPlainTextSecretsV3(loggedInUserDetails.UserCredentials.JTWToken, params.WorkspaceId,
|
||||||
params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive, params.TagSlugs, true)
|
params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive, params.TagSlugs, true)
|
||||||
log.Debug().Msgf("GetAllEnvironmentVariables: Trying to fetch secrets JWT token [err=%s]", err)
|
log.Debug().Msgf("GetAllEnvironmentVariables: Trying to fetch secrets JTW token [err=%s]", err)
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
backupEncryptionKey, err := GetBackupEncryptionKey()
|
backupEncryptionKey, err := GetBackupEncryptionKey()
|
||||||
|
2230
docs/docs.json
Normal file
2230
docs/docs.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -187,7 +187,7 @@ We'll now use the Infisical-Vercel integration send secrets from Infisical to Ve
|
|||||||
|
|
||||||
### Infisical-Vercel integration
|
### Infisical-Vercel integration
|
||||||
|
|
||||||
To begin we have to import the Next.js app into Vercel as a project. [Follow these instructions](https://nextjs.org/learn/basics/deploying-nextjs-app/deploy) to deploy the Next.js app to Vercel.
|
To begin we have to import the Next.js app into Vercel as a project. [Follow these instructions](https://vercel.com/docs/frameworks/nextjs) to deploy the Next.js app to Vercel.
|
||||||
|
|
||||||
Next, navigate to your project's integrations tab in Infisical and press on the Vercel tile to grant Infisical access to your Vercel account.
|
Next, navigate to your project's integrations tab in Infisical and press on the Vercel tile to grant Infisical access to your Vercel account.
|
||||||
|
|
||||||
|
112
docs/documentation/platform/dynamic-secrets/github.mdx
Normal file
112
docs/documentation/platform/dynamic-secrets/github.mdx
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
---
|
||||||
|
title: "GitHub"
|
||||||
|
description: "Learn how to dynamically generate GitHub App tokens."
|
||||||
|
---
|
||||||
|
|
||||||
|
The Infisical GitHub dynamic secret allows you to generate short-lived tokens for a GitHub App on demand based on service account permissions.
|
||||||
|
|
||||||
|
## Setup GitHub App
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Create an application on GitHub">
|
||||||
|
Navigate to [GitHub App settings](https://github.com/settings/apps) and click **New GitHub App**.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Give the application a name and a homepage URL. These values do not need to be anything specific.
|
||||||
|
|
||||||
|
Disable webhook by unchecking the Active checkbox.
|
||||||
|

|
||||||
|
|
||||||
|
Configure the app's permissions to grant the necessary access for the dynamic secret's short-lived tokens based on your use case.
|
||||||
|
|
||||||
|
Create the GitHub Application.
|
||||||
|

|
||||||
|
|
||||||
|
<Note>
|
||||||
|
If you have a GitHub organization, you can create an application under it
|
||||||
|
in your organization Settings > Developer settings > GitHub Apps > New GitHub App.
|
||||||
|
</Note>
|
||||||
|
</Step>
|
||||||
|
<Step title="Save app credentials">
|
||||||
|
Copy the **App ID** and generate a new **Private Key** for your GitHub Application.
|
||||||
|

|
||||||
|
|
||||||
|
Save these for later steps.
|
||||||
|
</Step>
|
||||||
|
<Step title="Install app">
|
||||||
|
Install your application to whichever repositories and organizations that you want the dynamic secret to access.
|
||||||
|

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

|
||||||
|
|
||||||
|
Once you've installed the app, **copy the installation ID** from the URL and save it for later steps.
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
## Set up Dynamic Secrets with GitHub
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Open Secret Overview Dashboard">
|
||||||
|
Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret.
|
||||||
|
</Step>
|
||||||
|
<Step title="Click on the 'Add Dynamic Secret' button">
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Select 'GitHub'">
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Provide the inputs for dynamic secret parameters">
|
||||||
|
<ParamField path="Secret Name" type="string" required>
|
||||||
|
Name by which you want the secret to be referenced
|
||||||
|
</ParamField>
|
||||||
|
<ParamField path="App ID" type="string" required>
|
||||||
|
The ID of the app created in earlier steps.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField path="App Private Key PEM" type="string" required>
|
||||||
|
The Private Key of the app created in earlier steps.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField path="Installation ID" type="string" required>
|
||||||
|
The ID of the installation from earlier steps.
|
||||||
|
</ParamField>
|
||||||
|
</Step>
|
||||||
|
<Step title="Click `Submit`">
|
||||||
|
After submitting the form, you will see a dynamic secret created in the dashboard.
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Generate dynamic secrets">
|
||||||
|
Once you've successfully configured the dynamic secret, you're ready to generate on-demand credentials.
|
||||||
|
To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item.
|
||||||
|
Alternatively, you can initiate the creation of a new lease by selecting 'New Lease' from the dynamic secret lease list section.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
When generating these secrets, the TTL will be fixed to 1 hour.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Once you click the `Submit` button, a new secret lease will be generated and the credentials from it will be shown to you.
|
||||||
|
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
## Audit or Revoke Leases
|
||||||
|
|
||||||
|
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
|
||||||
|
|
||||||
|
This will allow you to see the expiration time of the lease or delete a lease before its set time to live.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<Warning>
|
||||||
|
GitHub App tokens cannot be revoked. As such, revoking a token on Infisical does not invalidate the GitHub token; it remains active until it expires.
|
||||||
|
</Warning>
|
||||||
|
|
||||||
|
## Renew Leases
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
GitHub App tokens cannot be renewed because they are fixed to a lifetime of 1 hour.
|
||||||
|
</Note>
|
@ -49,11 +49,21 @@ In the following steps, we explore how to install the Infisical PKI Issuer using
|
|||||||
```
|
```
|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Install the Issuer Controller">
|
<Step title="Install the Issuer Controller">
|
||||||
Install the Infisical PKI Issuer controller into your Kubernetes cluster by running the following command:
|
Install the Infisical PKI Issuer controller into your Kubernetes cluster using one of the following methods:
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Helm">
|
||||||
|
```bash
|
||||||
|
helm repo add infisical-helm-charts 'https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/'
|
||||||
|
helm install infisical-pki-issuer infisical-helm-charts/infisical-pki-issuer
|
||||||
|
```
|
||||||
|
</Tab>
|
||||||
|
<Tab title="kubectl">
|
||||||
```bash
|
```bash
|
||||||
kubectl apply -f https://raw.githubusercontent.com/Infisical/infisical-issuer/main/build/install.yaml
|
kubectl apply -f https://raw.githubusercontent.com/Infisical/infisical-issuer/main/build/install.yaml
|
||||||
```
|
```
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Create Kubernetes Secret for Infisical PKI Issuer">
|
<Step title="Create Kubernetes Secret for Infisical PKI Issuer">
|
||||||
Start by creating a Kubernetes `Secret` containing the **Client Secret** from step 1. As mentioned previously, this will be used by the Infisical PKI issuer to authenticate with Infisical.
|
Start by creating a Kubernetes `Secret` containing the **Client Secret** from step 1. As mentioned previously, this will be used by the Infisical PKI issuer to authenticate with Infisical.
|
||||||
|
Binary file not shown.
After Width: | Height: | Size: 147 KiB |
BIN
docs/images/platform/dynamic-secrets/github/install-app.png
Normal file
BIN
docs/images/platform/dynamic-secrets/github/install-app.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 205 KiB |
BIN
docs/images/platform/dynamic-secrets/github/installation.png
Normal file
BIN
docs/images/platform/dynamic-secrets/github/installation.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 518 KiB |
BIN
docs/images/platform/dynamic-secrets/github/lease.png
Normal file
BIN
docs/images/platform/dynamic-secrets/github/lease.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 47 KiB |
BIN
docs/images/platform/dynamic-secrets/github/modal.png
Normal file
BIN
docs/images/platform/dynamic-secrets/github/modal.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 145 KiB |
128
docs/internals/architecture/cloud.mdx
Normal file
128
docs/internals/architecture/cloud.mdx
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
---
|
||||||
|
title: "Infisical Cloud"
|
||||||
|
description: "Architecture overview of Infisical's US and EU cloud deployments"
|
||||||
|
---
|
||||||
|
|
||||||
|
This document provides an overview of Infisical's cloud architecture for our US and EU deployments, detailing the core components and how they interact to provide security and infrastructure services.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Infisical Cloud operates on AWS infrastructure using containerized services deployed via Amazon ECS (Elastic Container Service). Our US and EU deployments use identical architectural patterns to ensure consistency and reliability across regions.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
A typical Infisical Cloud deployment consists of the following components:
|
||||||
|
|
||||||
|
### Application Services
|
||||||
|
|
||||||
|
- **Infisical Core**: Main application server running the Infisical backend API
|
||||||
|
- **License API**: Dedicated API service for license management with separate RDS instance (shared between US/EU)
|
||||||
|
- **Application Load Balancer**: Routes incoming traffic to application containers with SSL termination and host-based routing
|
||||||
|
|
||||||
|
### Data Layer
|
||||||
|
|
||||||
|
- **Amazon RDS (PostgreSQL)**:
|
||||||
|
- **Main RDS Instance**: Primary database for secrets, users, and metadata (Multi-AZ, encryption enabled)
|
||||||
|
- **License API RDS Instance**: Dedicated database for license management services
|
||||||
|
- **Amazon ElastiCache (Redis)**:
|
||||||
|
- **Main Redis Cluster**: Multi-AZ replication group for core application caching and queuing
|
||||||
|
- **License API Redis**: Dedicated cache for license services
|
||||||
|
- Redis 7 engine with CloudWatch logging and snapshot backups
|
||||||
|
|
||||||
|
### Infrastructure
|
||||||
|
|
||||||
|
- **ECS Fargate**: Serverless container platform running application services
|
||||||
|
- **AWS Global Accelerator**: Global traffic routing and performance optimization
|
||||||
|
- **Cloudflare**: DNS management and routing
|
||||||
|
- **AWS SSM Parameter Store**: Stores application configuration and secrets
|
||||||
|
- **CloudWatch**: Centralized logging and monitoring
|
||||||
|
|
||||||
|
## System Layout
|
||||||
|
|
||||||
|
### Service Architecture
|
||||||
|
|
||||||
|
The Infisical application runs as multiple containerized services on ECS:
|
||||||
|
|
||||||
|
- **Main Server**: Auto-scaling containerized application services
|
||||||
|
- **License API**: Dedicated service with separate infrastructure (shared globally)
|
||||||
|
- **Monitoring**: AWS OTel Collector and Datadog Agent sidecars
|
||||||
|
|
||||||
|
Container images are pulled from Docker Hub and managed via GitHub Actions for deployments.
|
||||||
|
|
||||||
|
### Network Configuration
|
||||||
|
|
||||||
|
Services are deployed in private subnets with the following connectivity:
|
||||||
|
|
||||||
|
- External traffic → Application Load Balancer → ECS Services
|
||||||
|
- Main server exposes port 8080
|
||||||
|
- License API exposes port 4000 (portal.infisical.com, license.infisical.com)
|
||||||
|
- Service-to-service communication via AWS Service Connect
|
||||||
|
|
||||||
|
### Data Flow
|
||||||
|
|
||||||
|
1. **DNS resolution** via Cloudflare routes traffic to AWS Global Accelerator
|
||||||
|
2. **Global Accelerator** optimizes routing to the nearest AWS region
|
||||||
|
3. **Client requests** are routed through the Application Load Balancer to ECS containers
|
||||||
|
4. **Application logic** processes requests in the Infisical Core service
|
||||||
|
5. **Data persistence** occurs via encrypted connections to RDS
|
||||||
|
6. **Caching** utilizes ElastiCache for performance optimization
|
||||||
|
7. **Configuration** is retrieved from AWS SSM Parameter Store
|
||||||
|
|
||||||
|
## Regional Deployments
|
||||||
|
|
||||||
|
Each region operates in a separate AWS account, providing strong isolation boundaries for security, compliance, and operational independence.
|
||||||
|
|
||||||
|
### US Cloud (us.infisical.com or app.infisical.com)
|
||||||
|
|
||||||
|
- **AWS Account**: Dedicated US AWS account
|
||||||
|
- **Infrastructure**: ECS-based containerized deployment
|
||||||
|
- **Monitoring**: Integrated with Datadog for observability and security monitoring
|
||||||
|
|
||||||
|
### EU Cloud (eu.infisical.com)
|
||||||
|
|
||||||
|
- **AWS Account**: Dedicated EU AWS account
|
||||||
|
- **Infrastructure**: ECS-based containerized deployment
|
||||||
|
- **Monitoring**: Integrated with Datadog for observability and security monitoring
|
||||||
|
|
||||||
|
## Configuration Management
|
||||||
|
|
||||||
|
Application configuration and secrets are managed through AWS SSM Parameter Store, with deployment automation handled via GitHub Actions.
|
||||||
|
|
||||||
|
## Monitoring and Observability
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
|
||||||
|
- **CloudWatch**: 365-day retention for application logs
|
||||||
|
- **Health Checks**: HTTP endpoint monitoring for service health
|
||||||
|
|
||||||
|
### Metrics
|
||||||
|
|
||||||
|
- **AWS OTel Collector**: Prometheus metrics collection
|
||||||
|
- **Datadog Agent**: Application performance monitoring and infrastructure metrics
|
||||||
|
|
||||||
|
## Container Management
|
||||||
|
|
||||||
|
- **Images**: `infisical/staging_infisical` and `infisical/license-api` from Docker Hub
|
||||||
|
- **Deployment**: Automated via GitHub Actions updating SSM parameter for image tags
|
||||||
|
- **Registry Access**: Docker Hub credentials stored in AWS Secrets Manager
|
||||||
|
- **Platform**: ECS Fargate serverless container platform
|
||||||
|
|
||||||
|
## Security Overview
|
||||||
|
|
||||||
|
### Data Protection
|
||||||
|
|
||||||
|
- **Encryption**: All secrets encrypted at rest and in transit
|
||||||
|
- **Network Isolation**: Services deployed in private subnets with controlled access
|
||||||
|
- **Authentication**: API tokens and service accounts for secure access
|
||||||
|
- **Audit Logging**: Comprehensive audit trails for all secret operations
|
||||||
|
|
||||||
|
### Network Architecture
|
||||||
|
|
||||||
|
- **VPC Design**: Dedicated VPC with public and private subnets across multiple Availability Zones
|
||||||
|
- **NAT Gateway**: Controlled outbound connectivity from private subnets
|
||||||
|
- **Load Balancing**: Application Load Balancer with SSL termination and health checks
|
||||||
|
- **Security Groups**: Restrictive firewall rules and controlled network access
|
||||||
|
- **High Availability**: Multi-AZ deployment with automatic failover
|
||||||
|
- **Network Monitoring**: VPC Flow Logs with 365-day retention for traffic analysis
|
2214
docs/mint.json
2214
docs/mint.json
File diff suppressed because it is too large
Load Diff
@ -1,3 +1,7 @@
|
|||||||
|
* {
|
||||||
|
border-radius: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
#navbar .max-w-8xl {
|
#navbar .max-w-8xl {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
border-bottom: 1px solid #ebebeb;
|
border-bottom: 1px solid #ebebeb;
|
||||||
@ -26,24 +30,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#sidebar li > div.mt-2 {
|
#sidebar li > div.mt-2 {
|
||||||
border-radius: 0;
|
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar li > a.text-primary {
|
#sidebar li > a.text-primary {
|
||||||
border-radius: 0;
|
|
||||||
background-color: #FBFFCC;
|
background-color: #FBFFCC;
|
||||||
border-left: 4px solid #EFFF33;
|
border-left: 4px solid #EFFF33;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar li > a.mt-2 {
|
#sidebar li > a.mt-2 {
|
||||||
border-radius: 0;
|
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar li > a.leading-6 {
|
#sidebar li > a.leading-6 {
|
||||||
border-radius: 0;
|
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,65 +68,26 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#content-area .mt-8 .block{
|
#content-area .mt-8 .block{
|
||||||
border-radius: 0;
|
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
background-color: #FCFBFA;
|
background-color: #FCFBFA;
|
||||||
border-color: #ebebeb;
|
border-color: #ebebeb;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* #content-area:hover .mt-8 .block:hover{
|
/* #content-area:hover .mt-8 .block:hover{
|
||||||
border-radius: 0;
|
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
background-color: #FDFFE5;
|
background-color: #FDFFE5;
|
||||||
border-color: #EFFF33;
|
border-color: #EFFF33;
|
||||||
} */
|
} */
|
||||||
|
|
||||||
#content-area .mt-8 .rounded-xl{
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#content-area .mt-8 .rounded-lg{
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#content-area .mt-6 .rounded-xl{
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#content-area .mt-6 .rounded-lg{
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#content-area .mt-6 .rounded-md{
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#content-area .mt-8 .rounded-md{
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#content-area div.my-4{
|
#content-area div.my-4{
|
||||||
border-radius: 0;
|
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#content-area div.flex-1 {
|
/* #content-area div.flex-1 {
|
||||||
/* text-transform: uppercase; */
|
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
} */
|
||||||
|
|
||||||
#content-area button {
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#content-area a {
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#content-area .not-prose {
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* .eyebrow {
|
/* .eyebrow {
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
@ -26,7 +26,7 @@ export const TtlFormLabel = ({ label }: { label: string }) => (
|
|||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faQuestionCircle}
|
icon={faQuestionCircle}
|
||||||
size="sm"
|
size="sm"
|
||||||
className="relative bottom-1 right-1"
|
className="relative bottom-px right-1"
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
|
@ -94,7 +94,7 @@ export const DropdownMenuItem = <T extends ElementType = "button">({
|
|||||||
className={twMerge(
|
className={twMerge(
|
||||||
"block cursor-pointer rounded-sm px-4 py-2 font-inter text-xs text-mineshaft-200 outline-none data-[highlighted]:bg-mineshaft-700",
|
"block cursor-pointer rounded-sm px-4 py-2 font-inter text-xs text-mineshaft-200 outline-none data-[highlighted]:bg-mineshaft-700",
|
||||||
className,
|
className,
|
||||||
isDisabled ? "pointer-events-none opacity-50" : ""
|
isDisabled ? "pointer-events-none cursor-not-allowed opacity-50" : ""
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Item type="button" role="menuitem" className="flex w-full items-center" ref={inputRef}>
|
<Item type="button" role="menuitem" className="flex w-full items-center" ref={inputRef}>
|
||||||
|
@ -1,12 +1,20 @@
|
|||||||
|
import { IconDefinition } from "@fortawesome/free-brands-svg-icons";
|
||||||
|
import { faArrowRightToBracket, faEdit } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
import { PolicyType } from "@app/hooks/api/policies/enums";
|
import { PolicyType } from "@app/hooks/api/policies/enums";
|
||||||
|
|
||||||
export const policyDetails: Record<PolicyType, { name: string; className: string }> = {
|
export const policyDetails: Record<
|
||||||
|
PolicyType,
|
||||||
|
{ name: string; className: string; icon: IconDefinition }
|
||||||
|
> = {
|
||||||
[PolicyType.AccessPolicy]: {
|
[PolicyType.AccessPolicy]: {
|
||||||
className: "bg-lime-900 text-lime-100",
|
className: "bg-green/20 text-green",
|
||||||
name: "Access Policy"
|
name: "Access Policy",
|
||||||
|
icon: faArrowRightToBracket
|
||||||
},
|
},
|
||||||
[PolicyType.ChangePolicy]: {
|
[PolicyType.ChangePolicy]: {
|
||||||
className: "bg-indigo-900 text-indigo-100",
|
className: "bg-yellow/20 text-yellow",
|
||||||
name: "Change Policy"
|
name: "Change Policy",
|
||||||
|
icon: faEdit
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -65,11 +65,11 @@ const fetchApprovalPolicies = async ({ projectSlug }: TGetAccessApprovalRequests
|
|||||||
const fetchApprovalRequests = async ({
|
const fetchApprovalRequests = async ({
|
||||||
projectSlug,
|
projectSlug,
|
||||||
envSlug,
|
envSlug,
|
||||||
authorProjectMembershipId
|
authorUserId
|
||||||
}: TGetAccessApprovalRequestsDTO) => {
|
}: TGetAccessApprovalRequestsDTO) => {
|
||||||
const { data } = await apiRequest.get<{ requests: TAccessApprovalRequest[] }>(
|
const { data } = await apiRequest.get<{ requests: TAccessApprovalRequest[] }>(
|
||||||
"/api/v1/access-approvals/requests",
|
"/api/v1/access-approvals/requests",
|
||||||
{ params: { projectSlug, envSlug, authorProjectMembershipId } }
|
{ params: { projectSlug, envSlug, authorUserId } }
|
||||||
);
|
);
|
||||||
|
|
||||||
return data.requests.map((request) => ({
|
return data.requests.map((request) => ({
|
||||||
@ -109,12 +109,12 @@ export const useGetAccessRequestsCount = ({
|
|||||||
export const useGetAccessApprovalPolicies = ({
|
export const useGetAccessApprovalPolicies = ({
|
||||||
projectSlug,
|
projectSlug,
|
||||||
envSlug,
|
envSlug,
|
||||||
authorProjectMembershipId,
|
authorUserId,
|
||||||
options = {}
|
options = {}
|
||||||
}: TGetAccessApprovalRequestsDTO & TReactQueryOptions) =>
|
}: TGetAccessApprovalRequestsDTO & TReactQueryOptions) =>
|
||||||
useQuery({
|
useQuery({
|
||||||
queryKey: accessApprovalKeys.getAccessApprovalPolicies(projectSlug),
|
queryKey: accessApprovalKeys.getAccessApprovalPolicies(projectSlug),
|
||||||
queryFn: () => fetchApprovalPolicies({ projectSlug, envSlug, authorProjectMembershipId }),
|
queryFn: () => fetchApprovalPolicies({ projectSlug, envSlug, authorUserId }),
|
||||||
...options,
|
...options,
|
||||||
enabled: Boolean(projectSlug) && (options?.enabled ?? true)
|
enabled: Boolean(projectSlug) && (options?.enabled ?? true)
|
||||||
});
|
});
|
||||||
@ -122,16 +122,13 @@ export const useGetAccessApprovalPolicies = ({
|
|||||||
export const useGetAccessApprovalRequests = ({
|
export const useGetAccessApprovalRequests = ({
|
||||||
projectSlug,
|
projectSlug,
|
||||||
envSlug,
|
envSlug,
|
||||||
authorProjectMembershipId,
|
authorUserId,
|
||||||
options = {}
|
options = {}
|
||||||
}: TGetAccessApprovalRequestsDTO & TReactQueryOptions) =>
|
}: TGetAccessApprovalRequestsDTO & TReactQueryOptions) =>
|
||||||
useQuery({
|
useQuery({
|
||||||
queryKey: accessApprovalKeys.getAccessApprovalRequests(
|
queryKey: accessApprovalKeys.getAccessApprovalRequests(projectSlug, envSlug, authorUserId),
|
||||||
projectSlug,
|
queryFn: () => fetchApprovalRequests({ projectSlug, envSlug, authorUserId }),
|
||||||
envSlug,
|
|
||||||
authorProjectMembershipId
|
|
||||||
),
|
|
||||||
queryFn: () => fetchApprovalRequests({ projectSlug, envSlug, authorProjectMembershipId }),
|
|
||||||
...options,
|
...options,
|
||||||
enabled: Boolean(projectSlug) && (options?.enabled ?? true)
|
enabled: Boolean(projectSlug) && (options?.enabled ?? true),
|
||||||
|
placeholderData: (previousData) => previousData
|
||||||
});
|
});
|
||||||
|
@ -148,7 +148,7 @@ export type TCreateAccessRequestDTO = {
|
|||||||
export type TGetAccessApprovalRequestsDTO = {
|
export type TGetAccessApprovalRequestsDTO = {
|
||||||
projectSlug: string;
|
projectSlug: string;
|
||||||
envSlug?: string;
|
envSlug?: string;
|
||||||
authorProjectMembershipId?: string;
|
authorUserId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TGetAccessPolicyApprovalCountDTO = {
|
export type TGetAccessPolicyApprovalCountDTO = {
|
||||||
|
@ -73,7 +73,6 @@ export const selectOrganization = async (data: {
|
|||||||
}) => {
|
}) => {
|
||||||
const { data: res } = await apiRequest.post<{
|
const { data: res } = await apiRequest.post<{
|
||||||
token: string;
|
token: string;
|
||||||
RefreshToken: string;
|
|
||||||
isMfaEnabled: boolean;
|
isMfaEnabled: boolean;
|
||||||
mfaMethod?: MfaMethod;
|
mfaMethod?: MfaMethod;
|
||||||
}>("/api/v3/auth/select-organization", data);
|
}>("/api/v3/auth/select-organization", data);
|
||||||
|
@ -12,7 +12,7 @@ export type TDynamicSecret = {
|
|||||||
defaultTTL: string;
|
defaultTTL: string;
|
||||||
status?: DynamicSecretStatus;
|
status?: DynamicSecretStatus;
|
||||||
statusDetails?: string;
|
statusDetails?: string;
|
||||||
maxTTL: string;
|
maxTTL?: string;
|
||||||
usernameTemplate?: string | null;
|
usernameTemplate?: string | null;
|
||||||
metadata?: { key: string; value: string }[];
|
metadata?: { key: string; value: string }[];
|
||||||
tags?: { key: string; value: string }[];
|
tags?: { key: string; value: string }[];
|
||||||
@ -36,7 +36,8 @@ export enum DynamicSecretProviders {
|
|||||||
SapAse = "sap-ase",
|
SapAse = "sap-ase",
|
||||||
Kubernetes = "kubernetes",
|
Kubernetes = "kubernetes",
|
||||||
Vertica = "vertica",
|
Vertica = "vertica",
|
||||||
GcpIam = "gcp-iam"
|
GcpIam = "gcp-iam",
|
||||||
|
Github = "github"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum KubernetesDynamicSecretCredentialType {
|
export enum KubernetesDynamicSecretCredentialType {
|
||||||
@ -335,6 +336,14 @@ export type TDynamicSecretProvider =
|
|||||||
inputs: {
|
inputs: {
|
||||||
serviceAccountEmail: string;
|
serviceAccountEmail: string;
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: DynamicSecretProviders.Github;
|
||||||
|
inputs: {
|
||||||
|
appId: number;
|
||||||
|
installationId: number;
|
||||||
|
privateKey: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCreateDynamicSecretDTO = {
|
export type TCreateDynamicSecretDTO = {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable no-param-reassign */
|
/* eslint-disable no-param-reassign */
|
||||||
import { useInfiniteQuery, useQuery, UseQueryOptions } from "@tanstack/react-query";
|
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
decryptAssymmetric,
|
decryptAssymmetric,
|
||||||
@ -25,10 +25,11 @@ export const secretApprovalRequestKeys = {
|
|||||||
status,
|
status,
|
||||||
committer,
|
committer,
|
||||||
offset,
|
offset,
|
||||||
limit
|
limit,
|
||||||
|
search
|
||||||
}: TGetSecretApprovalRequestList) =>
|
}: TGetSecretApprovalRequestList) =>
|
||||||
[
|
[
|
||||||
{ workspaceId, environment, status, committer, offset, limit },
|
{ workspaceId, environment, status, committer, offset, limit, search },
|
||||||
"secret-approval-requests"
|
"secret-approval-requests"
|
||||||
] as const,
|
] as const,
|
||||||
detail: ({ id }: Omit<TGetSecretApprovalRequestDetails, "decryptKey">) =>
|
detail: ({ id }: Omit<TGetSecretApprovalRequestDetails, "decryptKey">) =>
|
||||||
@ -118,23 +119,25 @@ const fetchSecretApprovalRequestList = async ({
|
|||||||
committer,
|
committer,
|
||||||
status = "open",
|
status = "open",
|
||||||
limit = 20,
|
limit = 20,
|
||||||
offset
|
offset = 0,
|
||||||
|
search = ""
|
||||||
}: TGetSecretApprovalRequestList) => {
|
}: TGetSecretApprovalRequestList) => {
|
||||||
const { data } = await apiRequest.get<{ approvals: TSecretApprovalRequest[] }>(
|
const { data } = await apiRequest.get<{
|
||||||
"/api/v1/secret-approval-requests",
|
approvals: TSecretApprovalRequest[];
|
||||||
{
|
totalCount: number;
|
||||||
|
}>("/api/v1/secret-approval-requests", {
|
||||||
params: {
|
params: {
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
environment,
|
||||||
committer,
|
committer,
|
||||||
status,
|
status,
|
||||||
limit,
|
limit,
|
||||||
offset
|
offset,
|
||||||
|
search
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
return data.approvals;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGetSecretApprovalRequests = ({
|
export const useGetSecretApprovalRequests = ({
|
||||||
@ -143,31 +146,32 @@ export const useGetSecretApprovalRequests = ({
|
|||||||
options = {},
|
options = {},
|
||||||
status,
|
status,
|
||||||
limit = 20,
|
limit = 20,
|
||||||
|
offset = 0,
|
||||||
|
search,
|
||||||
committer
|
committer
|
||||||
}: TGetSecretApprovalRequestList & TReactQueryOptions) =>
|
}: TGetSecretApprovalRequestList & TReactQueryOptions) =>
|
||||||
useInfiniteQuery({
|
useQuery({
|
||||||
initialPageParam: 0,
|
|
||||||
queryKey: secretApprovalRequestKeys.list({
|
queryKey: secretApprovalRequestKeys.list({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
environment,
|
||||||
committer,
|
committer,
|
||||||
status
|
status,
|
||||||
|
limit,
|
||||||
|
search,
|
||||||
|
offset
|
||||||
}),
|
}),
|
||||||
queryFn: ({ pageParam }) =>
|
queryFn: () =>
|
||||||
fetchSecretApprovalRequestList({
|
fetchSecretApprovalRequestList({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
environment,
|
||||||
status,
|
status,
|
||||||
committer,
|
committer,
|
||||||
limit,
|
limit,
|
||||||
offset: pageParam
|
offset,
|
||||||
|
search
|
||||||
}),
|
}),
|
||||||
enabled: Boolean(workspaceId) && (options?.enabled ?? true),
|
enabled: Boolean(workspaceId) && (options?.enabled ?? true),
|
||||||
getNextPageParam: (lastPage, pages) => {
|
placeholderData: (previousData) => previousData
|
||||||
if (lastPage.length && lastPage.length < limit) return undefined;
|
|
||||||
|
|
||||||
return lastPage?.length !== 0 ? pages.length * limit : undefined;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const fetchSecretApprovalRequestDetails = async ({
|
const fetchSecretApprovalRequestDetails = async ({
|
||||||
|
@ -113,6 +113,7 @@ export type TGetSecretApprovalRequestList = {
|
|||||||
committer?: string;
|
committer?: string;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
offset?: number;
|
offset?: number;
|
||||||
|
search?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TGetSecretApprovalRequestCount = {
|
export type TGetSecretApprovalRequestCount = {
|
||||||
|
@ -352,9 +352,9 @@ export const ProjectLayout = () => {
|
|||||||
secretApprovalReqCount?.open ||
|
secretApprovalReqCount?.open ||
|
||||||
accessApprovalRequestCount?.pendingCount
|
accessApprovalRequestCount?.pendingCount
|
||||||
) && (
|
) && (
|
||||||
<span className="ml-2 rounded border border-primary-400 bg-primary-600 px-1 py-0.5 text-xs font-semibold text-black">
|
<Badge variant="primary" className="ml-1.5">
|
||||||
{pendingRequestsCount}
|
{pendingRequestsCount}
|
||||||
</span>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
|
@ -76,9 +76,7 @@ export const PasswordStep = ({
|
|||||||
// case: organization ID is present from the provider auth token -- select the org and use the new jwt token in the CLI, then navigate to the org
|
// case: organization ID is present from the provider auth token -- select the org and use the new jwt token in the CLI, then navigate to the org
|
||||||
if (organizationId) {
|
if (organizationId) {
|
||||||
const finishWithOrgWorkflow = async () => {
|
const finishWithOrgWorkflow = async () => {
|
||||||
const { token, isMfaEnabled, mfaMethod, RefreshToken } = await selectOrganization({
|
const { token, isMfaEnabled, mfaMethod } = await selectOrganization({ organizationId });
|
||||||
organizationId
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isMfaEnabled) {
|
if (isMfaEnabled) {
|
||||||
SecurityClient.setMfaToken(token);
|
SecurityClient.setMfaToken(token);
|
||||||
@ -96,11 +94,10 @@ export const PasswordStep = ({
|
|||||||
const payload = {
|
const payload = {
|
||||||
privateKey,
|
privateKey,
|
||||||
email,
|
email,
|
||||||
JTWToken: token,
|
JTWToken: token
|
||||||
RefreshToken
|
|
||||||
};
|
};
|
||||||
await instance.post(cliUrl, payload).catch(() => {
|
await instance.post(cliUrl, payload).catch(() => {
|
||||||
// if error happens to communicate we set the token with an expiry in session storage
|
// if error happens to communicate we set the token with an expiry in sessino storage
|
||||||
// the cli-redirect page has logic to show this to user and ask them to paste it in terminal
|
// the cli-redirect page has logic to show this to user and ask them to paste it in terminal
|
||||||
sessionStorage.setItem(
|
sessionStorage.setItem(
|
||||||
SessionStorageKeys.CLI_TERMINAL_TOKEN,
|
SessionStorageKeys.CLI_TERMINAL_TOKEN,
|
||||||
@ -190,7 +187,7 @@ export const PasswordStep = ({
|
|||||||
// case: organization ID is present from the provider auth token -- select the org and use the new jwt token in the CLI, then navigate to the org
|
// case: organization ID is present from the provider auth token -- select the org and use the new jwt token in the CLI, then navigate to the org
|
||||||
if (organizationId) {
|
if (organizationId) {
|
||||||
const finishWithOrgWorkflow = async () => {
|
const finishWithOrgWorkflow = async () => {
|
||||||
const { token, isMfaEnabled, mfaMethod, RefreshToken } = await selectOrganization({
|
const { token, isMfaEnabled, mfaMethod } = await selectOrganization({
|
||||||
organizationId
|
organizationId
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -209,11 +206,10 @@ export const PasswordStep = ({
|
|||||||
const instance = axios.create();
|
const instance = axios.create();
|
||||||
const payload = {
|
const payload = {
|
||||||
...isCliLoginSuccessful.loginResponse,
|
...isCliLoginSuccessful.loginResponse,
|
||||||
JTWToken: token,
|
JTWToken: token
|
||||||
RefreshToken
|
|
||||||
};
|
};
|
||||||
await instance.post(cliUrl, payload).catch(() => {
|
await instance.post(cliUrl, payload).catch(() => {
|
||||||
// if error happens to communicate we set the token with an expiry in session storage
|
// if error happens to communicate we set the token with an expiry in sessino storage
|
||||||
// the cli-redirect page has logic to show this to user and ask them to paste it in terminal
|
// the cli-redirect page has logic to show this to user and ask them to paste it in terminal
|
||||||
sessionStorage.setItem(
|
sessionStorage.setItem(
|
||||||
SessionStorageKeys.CLI_TERMINAL_TOKEN,
|
SessionStorageKeys.CLI_TERMINAL_TOKEN,
|
||||||
|
@ -112,7 +112,7 @@ export const SelectOrganizationSection = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { token, isMfaEnabled, mfaMethod, RefreshToken } = await selectOrg
|
const { token, isMfaEnabled, mfaMethod } = await selectOrg
|
||||||
.mutateAsync({
|
.mutateAsync({
|
||||||
organizationId: organization.id,
|
organizationId: organization.id,
|
||||||
userAgent: callbackPort ? UserAgentType.CLI : undefined
|
userAgent: callbackPort ? UserAgentType.CLI : undefined
|
||||||
@ -151,14 +151,13 @@ export const SelectOrganizationSection = () => {
|
|||||||
const payload = {
|
const payload = {
|
||||||
JTWToken: token,
|
JTWToken: token,
|
||||||
email: user?.email,
|
email: user?.email,
|
||||||
privateKey,
|
privateKey
|
||||||
RefreshToken
|
|
||||||
} as IsCliLoginSuccessful["loginResponse"];
|
} as IsCliLoginSuccessful["loginResponse"];
|
||||||
|
|
||||||
// send request to server endpoint
|
// send request to server endpoint
|
||||||
const instance = axios.create();
|
const instance = axios.create();
|
||||||
await instance.post(`http://127.0.0.1:${callbackPort}/`, payload).catch(() => {
|
await instance.post(`http://127.0.0.1:${callbackPort}/`, payload).catch(() => {
|
||||||
// if error happens to communicate we set the token with an expiry in session storage
|
// if error happens to communicate we set the token with an expiry in sessino storage
|
||||||
// the cli-redirect page has logic to show this to user and ask them to paste it in terminal
|
// the cli-redirect page has logic to show this to user and ask them to paste it in terminal
|
||||||
sessionStorage.setItem(
|
sessionStorage.setItem(
|
||||||
SessionStorageKeys.CLI_TERMINAL_TOKEN,
|
SessionStorageKeys.CLI_TERMINAL_TOKEN,
|
||||||
|
@ -19,41 +19,38 @@ import { UsePopUpState } from "@app/hooks/usePopUp";
|
|||||||
const taxIDTypes = [
|
const taxIDTypes = [
|
||||||
{ label: "Australia ABN", value: "au_abn" },
|
{ label: "Australia ABN", value: "au_abn" },
|
||||||
{ label: "Australia ARN", value: "au_arn" },
|
{ label: "Australia ARN", value: "au_arn" },
|
||||||
{ label: "Bulgaria UIC", value: "bg_uic" },
|
|
||||||
{ label: "Brazil CNPJ", value: "br_cnpj" },
|
{ label: "Brazil CNPJ", value: "br_cnpj" },
|
||||||
{ label: "Brazil CPF", value: "br_cpf" },
|
{ label: "Brazil CPF", value: "br_cpf" },
|
||||||
|
{ label: "Bulgaria UIC", value: "bg_uic" },
|
||||||
{ label: "Canada BN", value: "ca_bn" },
|
{ label: "Canada BN", value: "ca_bn" },
|
||||||
{ label: "Canada GST/HST", value: "ca_gst_hst" },
|
{ label: "Canada GST/HST", value: "ca_gst_hst" },
|
||||||
{ label: "Canada PST BC", value: "ca_pst_bc" },
|
{ label: "Canada PST BC", value: "ca_pst_bc" },
|
||||||
{ label: "Canada PST MB", value: "ca_pst_mb" },
|
{ label: "Canada PST MB", value: "ca_pst_mb" },
|
||||||
{ label: "Canada PST SK", value: "ca_pst_sk" },
|
{ label: "Canada PST SK", value: "ca_pst_sk" },
|
||||||
{ label: "Canada QST", value: "ca_qst" },
|
{ label: "Canada QST", value: "ca_qst" },
|
||||||
{ label: "Switzerland VAT", value: "ch_vat" },
|
|
||||||
{ label: "Chile TIN", value: "cl_tin" },
|
{ label: "Chile TIN", value: "cl_tin" },
|
||||||
{ label: "Egypt TIN", value: "eg_tin" },
|
{ label: "Egypt TIN", value: "eg_tin" },
|
||||||
{ label: "Spain CIF", value: "es_cif" },
|
|
||||||
{ label: "EU OSS VAT", value: "eu_oss_vat" },
|
{ label: "EU OSS VAT", value: "eu_oss_vat" },
|
||||||
{ label: "EU VAT", value: "eu_vat" },
|
{ label: "EU VAT", value: "eu_vat" },
|
||||||
{ label: "GB VAT", value: "gb_vat" },
|
{ label: "GB VAT", value: "gb_vat" },
|
||||||
{ label: "Georgia VAT", value: "ge_vat" },
|
{ label: "Georgia VAT", value: "ge_vat" },
|
||||||
{ label: "Hong Kong BR", value: "hk_br" },
|
{ label: "Hong Kong BR", value: "hk_br" },
|
||||||
{ label: "Hungary TIN", value: "hu_tin" },
|
{ label: "Hungary TIN", value: "hu_tin" },
|
||||||
|
{ label: "Iceland VAT", value: "is_vat" },
|
||||||
|
{ label: "India GST", value: "in_gst" },
|
||||||
{ label: "Indonesia NPWP", value: "id_npwp" },
|
{ label: "Indonesia NPWP", value: "id_npwp" },
|
||||||
{ label: "Israel VAT", value: "il_vat" },
|
{ label: "Israel VAT", value: "il_vat" },
|
||||||
{ label: "India GST", value: "in_gst" },
|
|
||||||
{ label: "Iceland VAT", value: "is_vat" },
|
|
||||||
{ label: "Japan CN", value: "jp_cn" },
|
{ label: "Japan CN", value: "jp_cn" },
|
||||||
{ label: "Japan RN", value: "jp_rn" },
|
{ label: "Japan RN", value: "jp_rn" },
|
||||||
{ label: "Japan TRN", value: "jp_trn" },
|
{ label: "Japan TRN", value: "jp_trn" },
|
||||||
{ label: "Kenya PIN", value: "ke_pin" },
|
{ label: "Kenya PIN", value: "ke_pin" },
|
||||||
{ label: "South Korea BRN", value: "kr_brn" },
|
|
||||||
{ label: "Liechtenstein UID", value: "li_uid" },
|
{ label: "Liechtenstein UID", value: "li_uid" },
|
||||||
{ label: "Mexico RFC", value: "mx_rfc" },
|
|
||||||
{ label: "Malaysia FRP", value: "my_frp" },
|
{ label: "Malaysia FRP", value: "my_frp" },
|
||||||
{ label: "Malaysia ITN", value: "my_itn" },
|
{ label: "Malaysia ITN", value: "my_itn" },
|
||||||
{ label: "Malaysia SST", value: "my_sst" },
|
{ label: "Malaysia SST", value: "my_sst" },
|
||||||
{ label: "Norway VAT", value: "no_vat" },
|
{ label: "Mexico RFC", value: "mx_rfc" },
|
||||||
{ label: "New Zealand GST", value: "nz_gst" },
|
{ label: "New Zealand GST", value: "nz_gst" },
|
||||||
|
{ label: "Norway VAT", value: "no_vat" },
|
||||||
{ label: "Philippines TIN", value: "ph_tin" },
|
{ label: "Philippines TIN", value: "ph_tin" },
|
||||||
{ label: "Russia INN", value: "ru_inn" },
|
{ label: "Russia INN", value: "ru_inn" },
|
||||||
{ label: "Russia KPP", value: "ru_kpp" },
|
{ label: "Russia KPP", value: "ru_kpp" },
|
||||||
@ -61,12 +58,15 @@ const taxIDTypes = [
|
|||||||
{ label: "Singapore GST", value: "sg_gst" },
|
{ label: "Singapore GST", value: "sg_gst" },
|
||||||
{ label: "Singapore UEN", value: "sg_uen" },
|
{ label: "Singapore UEN", value: "sg_uen" },
|
||||||
{ label: "Slovenia TIN", value: "si_tin" },
|
{ label: "Slovenia TIN", value: "si_tin" },
|
||||||
|
{ label: "South Africa VAT", value: "za_vat" },
|
||||||
|
{ label: "South Korea BRN", value: "kr_brn" },
|
||||||
|
{ label: "Spain CIF", value: "es_cif" },
|
||||||
|
{ label: "Switzerland VAT", value: "ch_vat" },
|
||||||
|
{ label: "Taiwan VAT", value: "tw_vat" },
|
||||||
{ label: "Thailand VAT", value: "th_vat" },
|
{ label: "Thailand VAT", value: "th_vat" },
|
||||||
{ label: "Turkey TIN", value: "tr_tin" },
|
{ label: "Turkey TIN", value: "tr_tin" },
|
||||||
{ label: "Taiwan VAT", value: "tw_vat" },
|
|
||||||
{ label: "Ukraine VAT", value: "ua_vat" },
|
|
||||||
{ label: "US EIN", value: "us_ein" },
|
{ label: "US EIN", value: "us_ein" },
|
||||||
{ label: "South Africa VAT", value: "za_vat" }
|
{ label: "Ukraine VAT", value: "ua_vat" }
|
||||||
];
|
];
|
||||||
|
|
||||||
const schema = z
|
const schema = z
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
|
|
||||||
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||||
import { Badge } from "@app/components/v2/Badge";
|
import { Badge } from "@app/components/v2/Badge";
|
||||||
@ -45,21 +43,7 @@ export const SecretApprovalsPage = () => {
|
|||||||
<PageHeader
|
<PageHeader
|
||||||
title="Approval Workflows"
|
title="Approval Workflows"
|
||||||
description="Create approval policies for any modifications to secrets in sensitive environments and folders."
|
description="Create approval policies for any modifications to secrets in sensitive environments and folders."
|
||||||
>
|
|
||||||
<a
|
|
||||||
href="https://infisical.com/docs/documentation/platform/pr-workflows"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<span className="flex w-max cursor-pointer items-center rounded-md border border-mineshaft-500 bg-mineshaft-600 px-4 py-2 text-mineshaft-200 duration-200 hover:border-primary/40 hover:bg-primary/10 hover:text-white">
|
|
||||||
Documentation
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faArrowUpRightFromSquare}
|
|
||||||
className="mb-[0.06rem] ml-1 text-xs"
|
|
||||||
/>
|
/>
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</PageHeader>
|
|
||||||
<Tabs defaultValue={defaultTab}>
|
<Tabs defaultValue={defaultTab}>
|
||||||
<TabList>
|
<TabList>
|
||||||
<Tab value={TabSection.SecretApprovalRequests}>
|
<Tab value={TabSection.SecretApprovalRequests}>
|
||||||
|
@ -2,15 +2,25 @@
|
|||||||
/* eslint-disable react/jsx-no-useless-fragment */
|
/* eslint-disable react/jsx-no-useless-fragment */
|
||||||
import { useCallback, useMemo, useState } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
|
faArrowUpRightFromSquare,
|
||||||
|
faBan,
|
||||||
|
faBookOpen,
|
||||||
faCheck,
|
faCheck,
|
||||||
faCheckCircle,
|
faCheckCircle,
|
||||||
faChevronDown,
|
faChevronDown,
|
||||||
|
faClipboardCheck,
|
||||||
faLock,
|
faLock,
|
||||||
faPlus
|
faMagnifyingGlass,
|
||||||
|
faPlus,
|
||||||
|
faSearch,
|
||||||
|
faStopwatch,
|
||||||
|
faUser,
|
||||||
|
IconDefinition
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { formatDistance } from "date-fns";
|
import { format, formatDistance } from "date-fns";
|
||||||
import { AnimatePresence, motion } from "framer-motion";
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||||
import {
|
import {
|
||||||
@ -21,6 +31,8 @@ import {
|
|||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
|
Input,
|
||||||
|
Pagination,
|
||||||
Tooltip
|
Tooltip
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { Badge } from "@app/components/v2/Badge";
|
import { Badge } from "@app/components/v2/Badge";
|
||||||
@ -32,7 +44,12 @@ import {
|
|||||||
useUser,
|
useUser,
|
||||||
useWorkspace
|
useWorkspace
|
||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
import { usePopUp } from "@app/hooks";
|
import {
|
||||||
|
getUserTablePreference,
|
||||||
|
PreferenceKey,
|
||||||
|
setUserTablePreference
|
||||||
|
} from "@app/helpers/userTablePreferences";
|
||||||
|
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
|
||||||
import { useGetWorkspaceUsers } from "@app/hooks/api";
|
import { useGetWorkspaceUsers } from "@app/hooks/api";
|
||||||
import {
|
import {
|
||||||
accessApprovalKeys,
|
accessApprovalKeys,
|
||||||
@ -48,28 +65,21 @@ import { ApprovalStatus, TWorkspaceUser } from "@app/hooks/api/types";
|
|||||||
import { RequestAccessModal } from "./components/RequestAccessModal";
|
import { RequestAccessModal } from "./components/RequestAccessModal";
|
||||||
import { ReviewAccessRequestModal } from "./components/ReviewAccessModal";
|
import { ReviewAccessRequestModal } from "./components/ReviewAccessModal";
|
||||||
|
|
||||||
const generateRequestText = (request: TAccessApprovalRequest, userId: string) => {
|
const generateRequestText = (request: TAccessApprovalRequest) => {
|
||||||
const { isTemporary } = request;
|
const { isTemporary } = request;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full items-center justify-between text-sm">
|
<div className="flex items-center justify-between text-sm">
|
||||||
<div>
|
<div>
|
||||||
Requested {isTemporary ? "temporary" : "permanent"} access to{" "}
|
Requested {isTemporary ? "temporary" : "permanent"} access to{" "}
|
||||||
<code className="mx-1 rounded-sm bg-primary-500/20 px-1.5 py-0.5 font-mono text-xs text-primary">
|
<code className="mx-1 rounded bg-mineshaft-600 px-1.5 py-0.5 font-mono text-[13px] text-mineshaft-200">
|
||||||
{request.policy.secretPath}
|
{request.policy.secretPath}
|
||||||
</code>
|
</code>{" "}
|
||||||
in
|
in{" "}
|
||||||
<code className="mx-1 rounded-sm bg-primary-500/20 px-1.5 py-0.5 font-mono text-xs text-primary">
|
<code className="mx-1 rounded bg-mineshaft-600 px-1.5 py-0.5 font-mono text-[13px] text-mineshaft-200">
|
||||||
{request.environmentName}
|
{request.environmentName}
|
||||||
</code>
|
</code>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
{request.requestedByUserId === userId && (
|
|
||||||
<span className="text-xs text-gray-500">
|
|
||||||
<Badge className="ml-1">Requested By You</Badge>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -120,30 +130,64 @@ export const AccessApprovalRequest = ({
|
|||||||
projectSlug
|
projectSlug
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: requests, refetch: refetchRequests } = useGetAccessApprovalRequests({
|
const {
|
||||||
|
data: requests,
|
||||||
|
refetch: refetchRequests,
|
||||||
|
isPending: areRequestsPending
|
||||||
|
} = useGetAccessApprovalRequests({
|
||||||
projectSlug,
|
projectSlug,
|
||||||
authorProjectMembershipId: requestedByFilter,
|
authorUserId: requestedByFilter,
|
||||||
envSlug: envFilter
|
envSlug: envFilter
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { search, setSearch, setPage, page, perPage, setPerPage, offset } = usePagination("", {
|
||||||
|
initPerPage: getUserTablePreference("accessRequestsTable", PreferenceKey.PerPage, 20)
|
||||||
|
});
|
||||||
|
|
||||||
|
const handlePerPageChange = (newPerPage: number) => {
|
||||||
|
setPerPage(newPerPage);
|
||||||
|
setUserTablePreference("accessRequestsTable", PreferenceKey.PerPage, newPerPage);
|
||||||
|
};
|
||||||
|
|
||||||
const filteredRequests = useMemo(() => {
|
const filteredRequests = useMemo(() => {
|
||||||
|
let accessRequests: typeof requests;
|
||||||
|
|
||||||
if (statusFilter === "open")
|
if (statusFilter === "open")
|
||||||
return requests?.filter(
|
accessRequests = requests?.filter(
|
||||||
(request) =>
|
(request) =>
|
||||||
!request.policy.deletedAt &&
|
!request.policy.deletedAt &&
|
||||||
!request.isApproved &&
|
!request.isApproved &&
|
||||||
!request.reviewers.some((reviewer) => reviewer.status === ApprovalStatus.REJECTED)
|
!request.reviewers.some((reviewer) => reviewer.status === ApprovalStatus.REJECTED)
|
||||||
);
|
);
|
||||||
if (statusFilter === "close")
|
if (statusFilter === "close")
|
||||||
return requests?.filter(
|
accessRequests = requests?.filter(
|
||||||
(request) =>
|
(request) =>
|
||||||
request.policy.deletedAt ||
|
request.policy.deletedAt ||
|
||||||
request.isApproved ||
|
request.isApproved ||
|
||||||
request.reviewers.some((reviewer) => reviewer.status === ApprovalStatus.REJECTED)
|
request.reviewers.some((reviewer) => reviewer.status === ApprovalStatus.REJECTED)
|
||||||
);
|
);
|
||||||
|
|
||||||
return requests;
|
return (
|
||||||
}, [requests, statusFilter, requestedByFilter, envFilter]);
|
accessRequests?.filter((request) => {
|
||||||
|
const { environmentName, requestedByUser } = request;
|
||||||
|
|
||||||
|
const searchValue = search.trim().toLowerCase();
|
||||||
|
|
||||||
|
return (
|
||||||
|
environmentName?.toLowerCase().includes(searchValue) ||
|
||||||
|
`${requestedByUser?.email ?? ""} ${requestedByUser?.firstName ?? ""} ${requestedByUser?.lastName ?? ""}`
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(searchValue)
|
||||||
|
);
|
||||||
|
}) ?? []
|
||||||
|
);
|
||||||
|
}, [requests, statusFilter, requestedByFilter, envFilter, search]);
|
||||||
|
|
||||||
|
useResetPageHelper({
|
||||||
|
totalCount: filteredRequests.length,
|
||||||
|
offset,
|
||||||
|
setPage
|
||||||
|
});
|
||||||
|
|
||||||
const generateRequestDetails = useCallback(
|
const generateRequestDetails = useCallback(
|
||||||
(request: TAccessApprovalRequest) => {
|
(request: TAccessApprovalRequest) => {
|
||||||
@ -162,9 +206,15 @@ export const AccessApprovalRequest = ({
|
|||||||
const canBypass =
|
const canBypass =
|
||||||
!request.policy.bypassers.length || request.policy.bypassers.includes(user.id);
|
!request.policy.bypassers.length || request.policy.bypassers.includes(user.id);
|
||||||
|
|
||||||
let displayData: { label: string; type: "primary" | "danger" | "success" } = {
|
let displayData: {
|
||||||
|
label: string;
|
||||||
|
type: "primary" | "danger" | "success";
|
||||||
|
tooltipContent?: string;
|
||||||
|
icon: IconDefinition | null;
|
||||||
|
} = {
|
||||||
label: "",
|
label: "",
|
||||||
type: "primary"
|
type: "primary",
|
||||||
|
icon: null
|
||||||
};
|
};
|
||||||
|
|
||||||
const isExpired =
|
const isExpired =
|
||||||
@ -172,20 +222,42 @@ export const AccessApprovalRequest = ({
|
|||||||
request.isApproved &&
|
request.isApproved &&
|
||||||
new Date() > new Date(request.privilege.temporaryAccessEndTime || ("" as string));
|
new Date() > new Date(request.privilege.temporaryAccessEndTime || ("" as string));
|
||||||
|
|
||||||
if (isExpired) displayData = { label: "Access Expired", type: "danger" };
|
if (isExpired)
|
||||||
else if (isAccepted) displayData = { label: "Access Granted", type: "success" };
|
displayData = {
|
||||||
else if (isRejectedByAnyone) displayData = { label: "Rejected", type: "danger" };
|
label: "Access Expired",
|
||||||
|
type: "danger",
|
||||||
|
icon: faStopwatch,
|
||||||
|
tooltipContent: request.privilege?.temporaryAccessEndTime
|
||||||
|
? `Expired ${format(request.privilege.temporaryAccessEndTime, "M/d/yyyy h:mm aa")}`
|
||||||
|
: undefined
|
||||||
|
};
|
||||||
|
else if (isAccepted)
|
||||||
|
displayData = {
|
||||||
|
label: "Access Granted",
|
||||||
|
type: "success",
|
||||||
|
icon: faCheck,
|
||||||
|
tooltipContent: `Granted ${format(request.updatedAt, "M/d/yyyy h:mm aa")}`
|
||||||
|
};
|
||||||
|
else if (isRejectedByAnyone)
|
||||||
|
displayData = {
|
||||||
|
label: "Rejected",
|
||||||
|
type: "danger",
|
||||||
|
icon: faBan,
|
||||||
|
tooltipContent: `Rejected ${format(request.updatedAt, "M/d/yyyy h:mm aa")}`
|
||||||
|
};
|
||||||
else if (userReviewStatus === ApprovalStatus.APPROVED) {
|
else if (userReviewStatus === ApprovalStatus.APPROVED) {
|
||||||
displayData = {
|
displayData = {
|
||||||
label: `Pending ${request.policy.approvals - request.reviewers.length} review${
|
label: `Pending ${request.policy.approvals - request.reviewers.length} review${
|
||||||
request.policy.approvals - request.reviewers.length > 1 ? "s" : ""
|
request.policy.approvals - request.reviewers.length > 1 ? "s" : ""
|
||||||
}`,
|
}`,
|
||||||
type: "primary"
|
type: "primary",
|
||||||
|
icon: faClipboardCheck
|
||||||
};
|
};
|
||||||
} else if (!isReviewedByUser)
|
} else if (!isReviewedByUser)
|
||||||
displayData = {
|
displayData = {
|
||||||
label: "Review Required",
|
label: "Review Required",
|
||||||
type: "primary"
|
type: "primary",
|
||||||
|
icon: faClipboardCheck
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -225,16 +297,42 @@ export const AccessApprovalRequest = ({
|
|||||||
[generateRequestDetails, membersGroupById, user, setSelectedRequest, handlePopUpOpen]
|
[generateRequestDetails, membersGroupById, user, setSelectedRequest, handlePopUpOpen]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isFiltered = Boolean(search || envFilter || requestedByFilter);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
<motion.div
|
||||||
|
key="approval-changes-list"
|
||||||
|
transition={{ duration: 0.1 }}
|
||||||
|
initial={{ opacity: 0, translateX: 30 }}
|
||||||
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
|
exit={{ opacity: 0, translateX: 30 }}
|
||||||
|
className="rounded-md text-gray-300"
|
||||||
|
>
|
||||||
|
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<div className="mb-6 flex items-end justify-between">
|
<div className="flex items-start gap-1">
|
||||||
<div className="flex flex-col">
|
<p className="text-xl font-semibold text-mineshaft-100">Access Requests</p>
|
||||||
<span className="text-xl font-semibold text-mineshaft-100">Access Requests</span>
|
<a
|
||||||
<div className="mt-2 text-sm text-bunker-300">
|
href="https://infisical.com/docs/documentation/platform/access-controls/access-requests"
|
||||||
Request access to secrets in sensitive environments and folders.
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<div className="ml-1 mt-[0.32rem] inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
|
||||||
|
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||||
|
<span>Docs</span>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faArrowUpRightFromSquare}
|
||||||
|
className="mb-[0.07rem] ml-1.5 text-[10px]"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-bunker-300">
|
||||||
|
Request and review access to secrets in sensitive environments and folders
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content="To submit Access Requests, your project needs to create Access Request policies first."
|
content="To submit Access Requests, your project needs to create Access Request policies first."
|
||||||
isDisabled={policiesLoading || !!policies?.length}
|
isDisabled={policiesLoading || !!policies?.length}
|
||||||
@ -247,25 +345,23 @@ export const AccessApprovalRequest = ({
|
|||||||
}
|
}
|
||||||
handlePopUpOpen("requestAccess");
|
handlePopUpOpen("requestAccess");
|
||||||
}}
|
}}
|
||||||
|
colorSchema="secondary"
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
isDisabled={policiesLoading || !policies?.length}
|
isDisabled={policiesLoading || !policies?.length}
|
||||||
>
|
>
|
||||||
Request access
|
Request Access
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<Input
|
||||||
|
value={search}
|
||||||
<AnimatePresence>
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
<motion.div
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
key="approval-changes-list"
|
placeholder="Search approval requests by requesting user or environment..."
|
||||||
transition={{ duration: 0.1 }}
|
className="flex-1"
|
||||||
initial={{ opacity: 0, translateX: 30 }}
|
containerClassName="mb-4"
|
||||||
animate={{ opacity: 1, translateX: 0 }}
|
/>
|
||||||
exit={{ opacity: 0, translateX: 30 }}
|
<div className="flex items-center space-x-8 rounded-t-md border-x border-t border-mineshaft-600 bg-mineshaft-800 px-8 py-3 text-sm">
|
||||||
className="rounded-md text-gray-300"
|
|
||||||
>
|
|
||||||
<div className="flex items-center space-x-8 rounded-t-md border-x border-t border-mineshaft-600 bg-mineshaft-800 p-4 px-8">
|
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
@ -273,17 +369,19 @@ export const AccessApprovalRequest = ({
|
|||||||
onKeyDown={(evt) => {
|
onKeyDown={(evt) => {
|
||||||
if (evt.key === "Enter") setStatusFilter("open");
|
if (evt.key === "Enter") setStatusFilter("open");
|
||||||
}}
|
}}
|
||||||
className={
|
className={twMerge(
|
||||||
statusFilter === "close" ? "text-gray-500 duration-100 hover:text-gray-400" : ""
|
"font-medium",
|
||||||
}
|
statusFilter === "close" && "text-gray-500 duration-100 hover:text-gray-400"
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faLock} className="mr-2" />
|
<FontAwesomeIcon icon={faLock} className="mr-2" />
|
||||||
{!!requestCount && requestCount?.pendingCount} Pending
|
{!!requestCount && requestCount?.pendingCount} Pending
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={
|
className={twMerge(
|
||||||
statusFilter === "open" ? "text-gray-500 duration-100 hover:text-gray-400" : ""
|
"font-medium",
|
||||||
}
|
statusFilter === "open" && "text-gray-500 duration-100 hover:text-gray-400"
|
||||||
|
)}
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={() => setStatusFilter("close")}
|
onClick={() => setStatusFilter("close")}
|
||||||
@ -292,7 +390,7 @@ export const AccessApprovalRequest = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faCheck} className="mr-2" />
|
<FontAwesomeIcon icon={faCheck} className="mr-2" />
|
||||||
{!!requestCount && requestCount.finalizedCount} Completed
|
{!!requestCount && requestCount.finalizedCount} Closed
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-grow justify-end space-x-8">
|
<div className="flex flex-grow justify-end space-x-8">
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
@ -300,14 +398,20 @@ export const AccessApprovalRequest = ({
|
|||||||
<Button
|
<Button
|
||||||
variant="plain"
|
variant="plain"
|
||||||
colorSchema="secondary"
|
colorSchema="secondary"
|
||||||
className="text-bunker-300"
|
className={envFilter ? "text-white" : "text-bunker-300"}
|
||||||
rightIcon={<FontAwesomeIcon icon={faChevronDown} size="sm" className="ml-2" />}
|
rightIcon={<FontAwesomeIcon icon={faChevronDown} size="sm" className="ml-2" />}
|
||||||
>
|
>
|
||||||
Environments
|
Environments
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent
|
||||||
<DropdownMenuLabel>Select an environment</DropdownMenuLabel>
|
align="end"
|
||||||
|
sideOffset={1}
|
||||||
|
className="thin-scrollbar max-h-[20rem] overflow-y-auto"
|
||||||
|
>
|
||||||
|
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
|
||||||
|
Select an Environment
|
||||||
|
</DropdownMenuLabel>
|
||||||
{currentWorkspace?.environments.map(({ slug, name }) => (
|
{currentWorkspace?.environments.map(({ slug, name }) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => setEnvFilter((state) => (state === slug ? undefined : slug))}
|
onClick={() => setEnvFilter((state) => (state === slug ? undefined : slug))}
|
||||||
@ -337,15 +441,27 @@ export const AccessApprovalRequest = ({
|
|||||||
Requested By
|
Requested By
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent
|
||||||
<DropdownMenuLabel>Select an author</DropdownMenuLabel>
|
align="end"
|
||||||
|
sideOffset={1}
|
||||||
|
className="thin-scrollbar max-h-[20rem] overflow-y-auto"
|
||||||
|
>
|
||||||
|
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
|
||||||
|
Select Requesting User
|
||||||
|
</DropdownMenuLabel>
|
||||||
{members?.map(({ user: membershipUser, id }) => (
|
{members?.map(({ user: membershipUser, id }) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setRequestedByFilter((state) => (state === id ? undefined : id))
|
setRequestedByFilter((state) =>
|
||||||
|
state === membershipUser.id ? undefined : membershipUser.id
|
||||||
|
)
|
||||||
}
|
}
|
||||||
key={`request-filter-member-${id}`}
|
key={`request-filter-member-${id}`}
|
||||||
icon={requestedByFilter === id && <FontAwesomeIcon icon={faCheckCircle} />}
|
icon={
|
||||||
|
requestedByFilter === membershipUser.id && (
|
||||||
|
<FontAwesomeIcon icon={faCheckCircle} />
|
||||||
|
)
|
||||||
|
}
|
||||||
iconPos="right"
|
iconPos="right"
|
||||||
>
|
>
|
||||||
{membershipUser.username}
|
{membershipUser.username}
|
||||||
@ -357,19 +473,26 @@ export const AccessApprovalRequest = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col rounded-b-md border-x border-b border-t border-mineshaft-600 bg-mineshaft-800">
|
<div className="flex flex-col rounded-b-md border-x border-b border-t border-mineshaft-600 bg-mineshaft-800">
|
||||||
{filteredRequests?.length === 0 && (
|
{filteredRequests?.length === 0 && !isFiltered && (
|
||||||
<div className="py-12">
|
<div className="py-12">
|
||||||
<EmptyState title="No more access requests pending." />
|
<EmptyState
|
||||||
|
title={`No ${statusFilter === "open" ? "Pending" : "Closed"} Access Requests`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{Boolean(!filteredRequests?.length && isFiltered && !areRequestsPending) && (
|
||||||
|
<div className="py-12">
|
||||||
|
<EmptyState title="No Requests Match Filters" icon={faSearch} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!!filteredRequests?.length &&
|
{!!filteredRequests?.length &&
|
||||||
filteredRequests?.map((request) => {
|
filteredRequests?.slice(offset, perPage * page).map((request) => {
|
||||||
const details = generateRequestDetails(request);
|
const details = generateRequestDetails(request);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={request.id}
|
key={request.id}
|
||||||
className="flex w-full cursor-pointer px-8 py-4 hover:bg-mineshaft-700 aria-disabled:opacity-80"
|
className="flex w-full cursor-pointer border-b border-mineshaft-600 px-8 py-3 last:border-b-0 hover:bg-mineshaft-700 aria-disabled:opacity-80"
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={() => handleSelectRequest(request)}
|
onClick={() => handleSelectRequest(request)}
|
||||||
@ -379,14 +502,18 @@ export const AccessApprovalRequest = ({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="w-full">
|
<div className="flex w-full items-center justify-between">
|
||||||
<div className="flex w-full flex-col justify-between">
|
<div className="flex w-full flex-col justify-between">
|
||||||
<div className="mb-1 flex w-full items-center">
|
<div className="mb-1 flex w-full items-center">
|
||||||
<FontAwesomeIcon icon={faLock} className="mr-2" />
|
<FontAwesomeIcon
|
||||||
{generateRequestText(request, user.id)}
|
icon={faLock}
|
||||||
|
size="xs"
|
||||||
|
className="mr-1.5 text-mineshaft-300"
|
||||||
|
/>
|
||||||
|
{generateRequestText(request)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="text-xs text-gray-500">
|
<div className="text-xs leading-3 text-gray-500">
|
||||||
{membersGroupById?.[request.requestedByUserId]?.user && (
|
{membersGroupById?.[request.requestedByUserId]?.user && (
|
||||||
<>
|
<>
|
||||||
Requested {formatDistance(new Date(request.createdAt), new Date())}{" "}
|
Requested {formatDistance(new Date(request.createdAt), new Date())}{" "}
|
||||||
@ -397,21 +524,45 @@ export const AccessApprovalRequest = ({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{request.requestedByUserId === user.id && (
|
||||||
|
<div className="flex items-center gap-1.5 whitespace-nowrap text-xs text-bunker-300">
|
||||||
|
<FontAwesomeIcon icon={faUser} size="sm" />
|
||||||
|
<span>Requested By You</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Tooltip content={details.displayData.tooltipContent}>
|
||||||
<div>
|
<div>
|
||||||
<Badge variant={details.displayData.type}>
|
<Badge
|
||||||
{details.displayData.label}
|
className="flex items-center gap-1.5 whitespace-nowrap"
|
||||||
|
variant={details.displayData.type}
|
||||||
|
>
|
||||||
|
{details.displayData.icon && (
|
||||||
|
<FontAwesomeIcon icon={details.displayData.icon} />
|
||||||
|
)}
|
||||||
|
<span>{details.displayData.label}</span>
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
{Boolean(filteredRequests.length) && (
|
||||||
|
<Pagination
|
||||||
|
className="border-none"
|
||||||
|
count={filteredRequests.length}
|
||||||
|
page={page}
|
||||||
|
perPage={perPage}
|
||||||
|
onChangePage={setPage}
|
||||||
|
onChangePerPage={handlePerPageChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
|
||||||
</AnimatePresence>
|
|
||||||
|
|
||||||
{!!policies && (
|
{!!policies && (
|
||||||
<RequestAccessModal
|
<RequestAccessModal
|
||||||
policies={policies}
|
policies={policies}
|
||||||
@ -452,6 +603,7 @@ export const AccessApprovalRequest = ({
|
|||||||
isOpen={popUp.upgradePlan.isOpen}
|
isOpen={popUp.upgradePlan.isOpen}
|
||||||
onOpenChange={() => handlePopUpClose("upgradePlan")}
|
onOpenChange={() => handlePopUpClose("upgradePlan")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,11 +1,19 @@
|
|||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
|
faArrowDown,
|
||||||
|
faArrowUp,
|
||||||
|
faArrowUpRightFromSquare,
|
||||||
|
faBookOpen,
|
||||||
faCheckCircle,
|
faCheckCircle,
|
||||||
faChevronDown,
|
|
||||||
faFileShield,
|
faFileShield,
|
||||||
faPlus
|
faFilter,
|
||||||
|
faMagnifyingGlass,
|
||||||
|
faPlus,
|
||||||
|
faSearch
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
@ -19,8 +27,9 @@ import {
|
|||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
Modal,
|
IconButton,
|
||||||
ModalContent,
|
Input,
|
||||||
|
Pagination,
|
||||||
Table,
|
Table,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
TableSkeleton,
|
TableSkeleton,
|
||||||
@ -38,7 +47,12 @@ import {
|
|||||||
useWorkspace
|
useWorkspace
|
||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
import { ProjectPermissionActions } from "@app/context/ProjectPermissionContext/types";
|
import { ProjectPermissionActions } from "@app/context/ProjectPermissionContext/types";
|
||||||
import { usePopUp } from "@app/hooks";
|
import {
|
||||||
|
getUserTablePreference,
|
||||||
|
PreferenceKey,
|
||||||
|
setUserTablePreference
|
||||||
|
} from "@app/helpers/userTablePreferences";
|
||||||
|
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
|
||||||
import {
|
import {
|
||||||
useDeleteAccessApprovalPolicy,
|
useDeleteAccessApprovalPolicy,
|
||||||
useDeleteSecretApprovalPolicy,
|
useDeleteSecretApprovalPolicy,
|
||||||
@ -47,6 +61,7 @@ import {
|
|||||||
useListWorkspaceGroups
|
useListWorkspaceGroups
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { useGetAccessApprovalPolicies } from "@app/hooks/api/accessApproval/queries";
|
import { useGetAccessApprovalPolicies } from "@app/hooks/api/accessApproval/queries";
|
||||||
|
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||||
import { PolicyType } from "@app/hooks/api/policies/enums";
|
import { PolicyType } from "@app/hooks/api/policies/enums";
|
||||||
import { TAccessApprovalPolicy, Workspace } from "@app/hooks/api/types";
|
import { TAccessApprovalPolicy, Workspace } from "@app/hooks/api/types";
|
||||||
|
|
||||||
@ -57,6 +72,18 @@ interface IProps {
|
|||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum PolicyOrderBy {
|
||||||
|
Name = "name",
|
||||||
|
Environment = "environment",
|
||||||
|
SecretPath = "secret-path",
|
||||||
|
Type = "type"
|
||||||
|
}
|
||||||
|
|
||||||
|
type PolicyFilters = {
|
||||||
|
type: null | PolicyType;
|
||||||
|
environmentIds: string[];
|
||||||
|
};
|
||||||
|
|
||||||
const useApprovalPolicies = (permission: TProjectPermission, currentWorkspace?: Workspace) => {
|
const useApprovalPolicies = (permission: TProjectPermission, currentWorkspace?: Workspace) => {
|
||||||
const { data: accessPolicies, isPending: isAccessPoliciesLoading } = useGetAccessApprovalPolicies(
|
const { data: accessPolicies, isPending: isAccessPoliciesLoading } = useGetAccessApprovalPolicies(
|
||||||
{
|
{
|
||||||
@ -112,11 +139,79 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
|
|||||||
currentWorkspace
|
currentWorkspace
|
||||||
);
|
);
|
||||||
|
|
||||||
const [filterType, setFilterType] = useState<string | null>(null);
|
const [filters, setFilters] = useState<PolicyFilters>({
|
||||||
|
type: null,
|
||||||
|
environmentIds: []
|
||||||
|
});
|
||||||
|
|
||||||
const filteredPolicies = useMemo(() => {
|
const {
|
||||||
return filterType ? policies.filter((policy) => policy.policyType === filterType) : policies;
|
search,
|
||||||
}, [policies, filterType]);
|
setSearch,
|
||||||
|
setPage,
|
||||||
|
page,
|
||||||
|
perPage,
|
||||||
|
setPerPage,
|
||||||
|
offset,
|
||||||
|
orderDirection,
|
||||||
|
orderBy,
|
||||||
|
setOrderBy,
|
||||||
|
setOrderDirection,
|
||||||
|
toggleOrderDirection
|
||||||
|
} = usePagination<PolicyOrderBy>(PolicyOrderBy.Name, {
|
||||||
|
initPerPage: getUserTablePreference("approvalPoliciesTable", PreferenceKey.PerPage, 20)
|
||||||
|
});
|
||||||
|
|
||||||
|
const handlePerPageChange = (newPerPage: number) => {
|
||||||
|
setPerPage(newPerPage);
|
||||||
|
setUserTablePreference("approvalPoliciesTable", PreferenceKey.PerPage, newPerPage);
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredPolicies = useMemo(
|
||||||
|
() =>
|
||||||
|
policies
|
||||||
|
.filter(({ policyType, environment, name, secretPath }) => {
|
||||||
|
if (filters.type && policyType !== filters.type) return false;
|
||||||
|
|
||||||
|
if (filters.environmentIds.length && !filters.environmentIds.includes(environment.id))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const searchValue = search.trim().toLowerCase();
|
||||||
|
|
||||||
|
return (
|
||||||
|
name.toLowerCase().includes(searchValue) ||
|
||||||
|
environment.name.toLowerCase().includes(searchValue) ||
|
||||||
|
(secretPath ?? "*").toLowerCase().includes(searchValue)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.sort((a, b) => {
|
||||||
|
const [policyOne, policyTwo] = orderDirection === OrderByDirection.ASC ? [a, b] : [b, a];
|
||||||
|
|
||||||
|
switch (orderBy) {
|
||||||
|
case PolicyOrderBy.Type:
|
||||||
|
return policyOne.policyType
|
||||||
|
.toLowerCase()
|
||||||
|
.localeCompare(policyTwo.policyType.toLowerCase());
|
||||||
|
case PolicyOrderBy.Environment:
|
||||||
|
return policyOne.environment.name
|
||||||
|
.toLowerCase()
|
||||||
|
.localeCompare(policyTwo.environment.name.toLowerCase());
|
||||||
|
case PolicyOrderBy.SecretPath:
|
||||||
|
return (policyOne.secretPath ?? "*")
|
||||||
|
.toLowerCase()
|
||||||
|
.localeCompare((policyTwo.secretPath ?? "*").toLowerCase());
|
||||||
|
case PolicyOrderBy.Name:
|
||||||
|
default:
|
||||||
|
return policyOne.name.toLowerCase().localeCompare(policyTwo.name.toLowerCase());
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
[policies, filters, search, orderBy, orderDirection]
|
||||||
|
);
|
||||||
|
|
||||||
|
useResetPageHelper({
|
||||||
|
totalCount: filteredPolicies.length,
|
||||||
|
offset,
|
||||||
|
setPage
|
||||||
|
});
|
||||||
|
|
||||||
const { mutateAsync: deleteSecretApprovalPolicy } = useDeleteSecretApprovalPolicy();
|
const { mutateAsync: deleteSecretApprovalPolicy } = useDeleteSecretApprovalPolicy();
|
||||||
const { mutateAsync: deleteAccessApprovalPolicy } = useDeleteAccessApprovalPolicy();
|
const { mutateAsync: deleteAccessApprovalPolicy } = useDeleteAccessApprovalPolicy();
|
||||||
@ -151,16 +246,57 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isTableFiltered = filters.type !== null || Boolean(filters.environmentIds.length);
|
||||||
|
|
||||||
|
const handleSort = (column: PolicyOrderBy) => {
|
||||||
|
if (column === orderBy) {
|
||||||
|
toggleOrderDirection();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setOrderBy(column);
|
||||||
|
setOrderDirection(OrderByDirection.ASC);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getClassName = (col: PolicyOrderBy) => twMerge("ml-2", orderBy === col ? "" : "opacity-30");
|
||||||
|
|
||||||
|
const getColSortIcon = (col: PolicyOrderBy) =>
|
||||||
|
orderDirection === OrderByDirection.DESC && orderBy === col ? faArrowUp : faArrowDown;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
<motion.div
|
||||||
|
key="approval-changes-list"
|
||||||
|
transition={{ duration: 0.1 }}
|
||||||
|
initial={{ opacity: 0, translateX: 30 }}
|
||||||
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
|
exit={{ opacity: 0, translateX: 30 }}
|
||||||
|
className="rounded-md text-gray-300"
|
||||||
|
>
|
||||||
|
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<div className="mb-6 flex items-end justify-between">
|
<div className="flex items-start gap-1">
|
||||||
<div className="flex flex-col">
|
<p className="text-xl font-semibold text-mineshaft-100">Policies</p>
|
||||||
<span className="text-xl font-semibold text-mineshaft-100">Policies</span>
|
<a
|
||||||
<div className="mt-2 text-sm text-bunker-300">
|
href="https://infisical.com/docs/documentation/platform/pr-workflows"
|
||||||
Implement granular policies for access requests and secrets management.
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<div className="ml-1 mt-[0.32rem] inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
|
||||||
|
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||||
|
<span>Docs</span>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faArrowUpRightFromSquare}
|
||||||
|
className="mb-[0.07rem] ml-1.5 text-[10px]"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-bunker-300">
|
||||||
|
Implement granular policies for access requests and secrets management
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Create}
|
I={ProjectPermissionActions.Create}
|
||||||
a={ProjectPermissionSub.SecretApproval}
|
a={ProjectPermissionSub.SecretApproval}
|
||||||
@ -174,6 +310,7 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
|
|||||||
}
|
}
|
||||||
handlePopUpOpen("policyForm");
|
handlePopUpOpen("policyForm");
|
||||||
}}
|
}}
|
||||||
|
colorSchema="secondary"
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
isDisabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
>
|
>
|
||||||
@ -182,41 +319,54 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
|
|||||||
)}
|
)}
|
||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="mb-4 flex items-center gap-2">
|
||||||
<TableContainer>
|
<Input
|
||||||
<Table>
|
value={search}
|
||||||
<THead>
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
<Tr>
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
<Th>Name</Th>
|
placeholder="Search policies by name, type, environment or secret path..."
|
||||||
<Th>Environment</Th>
|
className="flex-1"
|
||||||
<Th>Secret Path</Th>
|
/>
|
||||||
<Th>
|
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<IconButton
|
||||||
|
ariaLabel="Filter findings"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
colorSchema="secondary"
|
size="sm"
|
||||||
className="text-xs font-semibold uppercase text-bunker-300"
|
className={twMerge(
|
||||||
rightIcon={
|
"flex h-10 w-11 items-center justify-center overflow-hidden border border-mineshaft-600 bg-mineshaft-800 p-0 transition-all hover:border-primary/60 hover:bg-primary/10",
|
||||||
<FontAwesomeIcon icon={faChevronDown} size="sm" className="ml-2" />
|
isTableFiltered && "border-primary/50 text-primary"
|
||||||
}
|
)}
|
||||||
>
|
>
|
||||||
Type
|
<FontAwesomeIcon icon={faFilter} />
|
||||||
</Button>
|
</IconButton>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent
|
||||||
<DropdownMenuLabel>Select a type</DropdownMenuLabel>
|
className="thin-scrollbar max-h-[70vh] overflow-y-auto"
|
||||||
|
align="end"
|
||||||
|
>
|
||||||
|
<DropdownMenuLabel>Policy Type</DropdownMenuLabel>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => setFilterType(null)}
|
onClick={() =>
|
||||||
icon={!filterType && <FontAwesomeIcon icon={faCheckCircle} />}
|
setFilters((prev) => ({
|
||||||
|
...prev,
|
||||||
|
type: null
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
icon={!filters && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||||
iconPos="right"
|
iconPos="right"
|
||||||
>
|
>
|
||||||
All
|
All
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => setFilterType(PolicyType.AccessPolicy)}
|
onClick={() =>
|
||||||
|
setFilters((prev) => ({
|
||||||
|
...prev,
|
||||||
|
type: PolicyType.AccessPolicy
|
||||||
|
}))
|
||||||
|
}
|
||||||
icon={
|
icon={
|
||||||
filterType === PolicyType.AccessPolicy && (
|
filters.type === PolicyType.AccessPolicy && (
|
||||||
<FontAwesomeIcon icon={faCheckCircle} />
|
<FontAwesomeIcon icon={faCheckCircle} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -225,9 +375,14 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
|
|||||||
Access Policy
|
Access Policy
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => setFilterType(PolicyType.ChangePolicy)}
|
onClick={() =>
|
||||||
|
setFilters((prev) => ({
|
||||||
|
...prev,
|
||||||
|
type: PolicyType.ChangePolicy
|
||||||
|
}))
|
||||||
|
}
|
||||||
icon={
|
icon={
|
||||||
filterType === PolicyType.ChangePolicy && (
|
filters.type === PolicyType.ChangePolicy && (
|
||||||
<FontAwesomeIcon icon={faCheckCircle} />
|
<FontAwesomeIcon icon={faCheckCircle} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -235,25 +390,110 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
|
|||||||
>
|
>
|
||||||
Change Policy
|
Change Policy
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuLabel>Environment</DropdownMenuLabel>
|
||||||
|
{currentWorkspace.environments.map((env) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setFilters((prev) => ({
|
||||||
|
...prev,
|
||||||
|
environmentIds: prev.environmentIds.includes(env.id)
|
||||||
|
? prev.environmentIds.filter((i) => i !== env.id)
|
||||||
|
: [...prev.environmentIds, env.id]
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
key={env.id}
|
||||||
|
icon={
|
||||||
|
filters.environmentIds.includes(env.id) && (
|
||||||
|
<FontAwesomeIcon className="text-primary" icon={faCheckCircle} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
iconPos="right"
|
||||||
|
>
|
||||||
|
<span className="capitalize">{env.name}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th>
|
||||||
|
<div className="flex items-center">
|
||||||
|
Name
|
||||||
|
<IconButton
|
||||||
|
variant="plain"
|
||||||
|
className={getClassName(PolicyOrderBy.Name)}
|
||||||
|
ariaLabel="sort"
|
||||||
|
onClick={() => handleSort(PolicyOrderBy.Name)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={getColSortIcon(PolicyOrderBy.Name)} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
</Th>
|
</Th>
|
||||||
<Th />
|
<Th>
|
||||||
|
<div className="flex items-center">
|
||||||
|
Environment
|
||||||
|
<IconButton
|
||||||
|
variant="plain"
|
||||||
|
className={getClassName(PolicyOrderBy.Environment)}
|
||||||
|
ariaLabel="sort"
|
||||||
|
onClick={() => handleSort(PolicyOrderBy.Environment)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={getColSortIcon(PolicyOrderBy.Environment)} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</Th>
|
||||||
|
<Th>
|
||||||
|
<div className="flex items-center">
|
||||||
|
Secret Path
|
||||||
|
<IconButton
|
||||||
|
variant="plain"
|
||||||
|
className={getClassName(PolicyOrderBy.SecretPath)}
|
||||||
|
ariaLabel="sort"
|
||||||
|
onClick={() => handleSort(PolicyOrderBy.SecretPath)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={getColSortIcon(PolicyOrderBy.SecretPath)} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</Th>
|
||||||
|
<Th>
|
||||||
|
<div className="flex items-center">
|
||||||
|
Type
|
||||||
|
<IconButton
|
||||||
|
variant="plain"
|
||||||
|
className={getClassName(PolicyOrderBy.Type)}
|
||||||
|
ariaLabel="sort"
|
||||||
|
onClick={() => handleSort(PolicyOrderBy.Type)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={getColSortIcon(PolicyOrderBy.Type)} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</Th>
|
||||||
|
<Th className="w-5" />
|
||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
<TBody>
|
<TBody>
|
||||||
{isPoliciesLoading && (
|
{isPoliciesLoading && (
|
||||||
<TableSkeleton columns={6} innerKey="secret-policies" className="bg-mineshaft-700" />
|
<TableSkeleton
|
||||||
|
columns={5}
|
||||||
|
innerKey="secret-policies"
|
||||||
|
className="bg-mineshaft-700"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{!isPoliciesLoading && !filteredPolicies?.length && (
|
{!isPoliciesLoading && !policies?.length && (
|
||||||
<Tr>
|
<Tr>
|
||||||
<Td colSpan={6}>
|
<Td colSpan={5}>
|
||||||
<EmptyState title="No policies found" icon={faFileShield} />
|
<EmptyState title="No Policies Found" icon={faFileShield} />
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
)}
|
)}
|
||||||
{!!currentWorkspace &&
|
{!!currentWorkspace &&
|
||||||
filteredPolicies?.map((policy) => (
|
filteredPolicies
|
||||||
|
?.slice(offset, perPage * page)
|
||||||
|
.map((policy) => (
|
||||||
<ApprovalPolicyRow
|
<ApprovalPolicyRow
|
||||||
policy={policy}
|
policy={policy}
|
||||||
key={policy.id}
|
key={policy.id}
|
||||||
@ -265,20 +505,21 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
|
|||||||
))}
|
))}
|
||||||
</TBody>
|
</TBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
{Boolean(!filteredPolicies.length && policies.length && !isPoliciesLoading) && (
|
||||||
|
<EmptyState title="No Policies Match Search" icon={faSearch} />
|
||||||
|
)}
|
||||||
|
{Boolean(filteredPolicies.length) && (
|
||||||
|
<Pagination
|
||||||
|
count={filteredPolicies.length}
|
||||||
|
page={page}
|
||||||
|
perPage={perPage}
|
||||||
|
onChangePage={setPage}
|
||||||
|
onChangePerPage={handlePerPageChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
<Modal
|
</div>
|
||||||
isOpen={popUp.policyForm.isOpen}
|
</motion.div>
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("policyForm", isOpen)}
|
|
||||||
>
|
|
||||||
<ModalContent
|
|
||||||
className="max-w-3xl"
|
|
||||||
title={
|
|
||||||
popUp.policyForm.data
|
|
||||||
? `Edit ${popUp?.policyForm?.data?.name || "Policy"}`
|
|
||||||
: "Create Policy"
|
|
||||||
}
|
|
||||||
id="policy-form"
|
|
||||||
>
|
|
||||||
<AccessPolicyForm
|
<AccessPolicyForm
|
||||||
projectId={currentWorkspace.id}
|
projectId={currentWorkspace.id}
|
||||||
projectSlug={currentWorkspace.slug}
|
projectSlug={currentWorkspace.slug}
|
||||||
@ -287,8 +528,6 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
|
|||||||
members={members}
|
members={members}
|
||||||
editValues={popUp.policyForm.data as TAccessApprovalPolicy}
|
editValues={popUp.policyForm.data as TAccessApprovalPolicy}
|
||||||
/>
|
/>
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
<DeleteActionModal
|
<DeleteActionModal
|
||||||
isOpen={popUp.deletePolicy.isOpen}
|
isOpen={popUp.deletePolicy.isOpen}
|
||||||
deleteKey="remove"
|
deleteKey="remove"
|
||||||
@ -301,6 +540,6 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
|
|||||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||||
text="You can add secret approval policy if you switch to Infisical's Enterprise plan."
|
text="You can add secret approval policy if you switch to Infisical's Enterprise plan."
|
||||||
/>
|
/>
|
||||||
</div>
|
</AnimatePresence>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { RefObject, useMemo, useRef, useState } from "react";
|
||||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||||
import { faGripVertical, faTrash } from "@fortawesome/free-solid-svg-icons";
|
import { faGripVertical, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
@ -13,6 +13,8 @@ import {
|
|||||||
FormControl,
|
FormControl,
|
||||||
IconButton,
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
Select,
|
Select,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
Switch,
|
Switch,
|
||||||
@ -110,20 +112,20 @@ const formSchema = z
|
|||||||
|
|
||||||
type TFormSchema = z.infer<typeof formSchema>;
|
type TFormSchema = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
export const AccessPolicyForm = ({
|
const Form = ({
|
||||||
isOpen,
|
|
||||||
onToggle,
|
onToggle,
|
||||||
members = [],
|
members = [],
|
||||||
projectId,
|
projectId,
|
||||||
projectSlug,
|
projectSlug,
|
||||||
editValues
|
editValues,
|
||||||
}: Props) => {
|
modalContainer,
|
||||||
|
isEditMode
|
||||||
|
}: Props & { modalContainer: RefObject<HTMLDivElement>; isEditMode: boolean }) => {
|
||||||
const [draggedItem, setDraggedItem] = useState<number | null>(null);
|
const [draggedItem, setDraggedItem] = useState<number | null>(null);
|
||||||
const [dragOverItem, setDragOverItem] = useState<number | null>(null);
|
const [dragOverItem, setDragOverItem] = useState<number | null>(null);
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
|
||||||
watch,
|
watch,
|
||||||
formState: { isSubmitting }
|
formState: { isSubmitting }
|
||||||
} = useForm<TFormSchema>({
|
} = useForm<TFormSchema>({
|
||||||
@ -188,13 +190,8 @@ export const AccessPolicyForm = ({
|
|||||||
const { data: groups } = useListWorkspaceGroups(projectId);
|
const { data: groups } = useListWorkspaceGroups(projectId);
|
||||||
|
|
||||||
const environments = currentWorkspace?.environments || [];
|
const environments = currentWorkspace?.environments || [];
|
||||||
const isEditMode = Boolean(editValues);
|
|
||||||
const isAccessPolicyType = watch("policyType") === PolicyType.AccessPolicy;
|
const isAccessPolicyType = watch("policyType") === PolicyType.AccessPolicy;
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isOpen || !isEditMode) reset({});
|
|
||||||
}, [isOpen, isEditMode]);
|
|
||||||
|
|
||||||
const { mutateAsync: createAccessApprovalPolicy } = useCreateAccessApprovalPolicy();
|
const { mutateAsync: createAccessApprovalPolicy } = useCreateAccessApprovalPolicy();
|
||||||
const { mutateAsync: updateAccessApprovalPolicy } = useUpdateAccessApprovalPolicy();
|
const { mutateAsync: updateAccessApprovalPolicy } = useUpdateAccessApprovalPolicy();
|
||||||
|
|
||||||
@ -387,6 +384,7 @@ export const AccessPolicyForm = ({
|
|||||||
setDraggedItem(null);
|
setDraggedItem(null);
|
||||||
setDragOverItem(null);
|
setDragOverItem(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col space-y-3">
|
<div className="flex flex-col space-y-3">
|
||||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||||
@ -572,7 +570,7 @@ export const AccessPolicyForm = ({
|
|||||||
className="flex-grow"
|
className="flex-grow"
|
||||||
>
|
>
|
||||||
<FilterableSelect
|
<FilterableSelect
|
||||||
menuPortalTarget={document.getElementById("policy-form")}
|
menuPortalTarget={modalContainer.current}
|
||||||
menuPlacement="top"
|
menuPlacement="top"
|
||||||
isMulti
|
isMulti
|
||||||
placeholder="Select members..."
|
placeholder="Select members..."
|
||||||
@ -602,7 +600,7 @@ export const AccessPolicyForm = ({
|
|||||||
className="flex-grow"
|
className="flex-grow"
|
||||||
>
|
>
|
||||||
<FilterableSelect
|
<FilterableSelect
|
||||||
menuPortalTarget={document.getElementById("policy-form")}
|
menuPortalTarget={modalContainer.current}
|
||||||
menuPlacement="top"
|
menuPlacement="top"
|
||||||
isMulti
|
isMulti
|
||||||
placeholder="Select groups..."
|
placeholder="Select groups..."
|
||||||
@ -813,3 +811,27 @@ export const AccessPolicyForm = ({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const AccessPolicyForm = ({ isOpen, onToggle, editValues, ...props }: Props) => {
|
||||||
|
const modalContainer = useRef<HTMLDivElement>(null);
|
||||||
|
const isEditMode = Boolean(editValues);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onOpenChange={onToggle}>
|
||||||
|
<ModalContent
|
||||||
|
className="max-w-3xl"
|
||||||
|
ref={modalContainer}
|
||||||
|
title={isEditMode ? "Edit Policy" : "Create Policy"}
|
||||||
|
>
|
||||||
|
<Form
|
||||||
|
{...props}
|
||||||
|
isOpen={isOpen}
|
||||||
|
onToggle={onToggle}
|
||||||
|
editValues={editValues}
|
||||||
|
modalContainer={modalContainer}
|
||||||
|
isEditMode={isEditMode}
|
||||||
|
/>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { faEllipsis } from "@fortawesome/free-solid-svg-icons";
|
import { faEdit, faEllipsisV, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
@ -9,6 +9,8 @@ import {
|
|||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
|
GenericFieldLabel,
|
||||||
|
IconButton,
|
||||||
Td,
|
Td,
|
||||||
Tr
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
@ -102,36 +104,47 @@ export const ApprovalPolicyRow = ({
|
|||||||
}}
|
}}
|
||||||
onClick={() => setIsExpanded.toggle()}
|
onClick={() => setIsExpanded.toggle()}
|
||||||
>
|
>
|
||||||
<Td>{policy.name}</Td>
|
<Td>{policy.name || <span className="text-mineshaft-400">Unnamed Policy</span>}</Td>
|
||||||
<Td>{policy.environment.slug}</Td>
|
<Td>{policy.environment.name}</Td>
|
||||||
<Td>{policy.secretPath || "*"}</Td>
|
<Td>{policy.secretPath || "*"}</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<Badge className={policyDetails[policy.policyType].className}>
|
<Badge
|
||||||
{policyDetails[policy.policyType].name}
|
className={twMerge(
|
||||||
|
policyDetails[policy.policyType].className,
|
||||||
|
"flex w-min items-center gap-1.5 whitespace-nowrap"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={policyDetails[policy.policyType].icon} />
|
||||||
|
<span>{policyDetails[policy.policyType].name}</span>
|
||||||
</Badge>
|
</Badge>
|
||||||
</Td>
|
</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild className="cursor-pointer rounded-lg">
|
<DropdownMenuTrigger asChild className="cursor-pointer rounded-lg">
|
||||||
<div className="flex items-center justify-center transition-transform duration-300 ease-in-out hover:scale-125 hover:text-primary-400 data-[state=open]:scale-125 data-[state=open]:text-primary-400">
|
<DropdownMenuTrigger asChild>
|
||||||
<FontAwesomeIcon size="sm" icon={faEllipsis} />
|
<IconButton
|
||||||
</div>
|
ariaLabel="Options"
|
||||||
|
colorSchema="secondary"
|
||||||
|
className="w-6"
|
||||||
|
variant="plain"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faEllipsisV} />
|
||||||
|
</IconButton>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="center" className="min-w-[100%] p-1">
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent sideOffset={2} align="end" className="min-w-[12rem] p-1">
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Edit}
|
I={ProjectPermissionActions.Edit}
|
||||||
a={ProjectPermissionSub.SecretApproval}
|
a={ProjectPermissionSub.SecretApproval}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className={twMerge(
|
|
||||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
|
||||||
)}
|
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onEdit();
|
onEdit();
|
||||||
}}
|
}}
|
||||||
disabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
|
icon={<FontAwesomeIcon icon={faEdit} />}
|
||||||
>
|
>
|
||||||
Edit Policy
|
Edit Policy
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@ -143,16 +156,12 @@ export const ApprovalPolicyRow = ({
|
|||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className={twMerge(
|
|
||||||
isAllowed
|
|
||||||
? "hover:!bg-red-500 hover:!text-white"
|
|
||||||
: "pointer-events-none cursor-not-allowed opacity-50"
|
|
||||||
)}
|
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onDelete();
|
onDelete();
|
||||||
}}
|
}}
|
||||||
disabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
|
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||||
>
|
>
|
||||||
Delete Policy
|
Delete Policy
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@ -162,45 +171,41 @@ export const ApprovalPolicyRow = ({
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
{isExpanded && (
|
|
||||||
<Tr>
|
<Tr>
|
||||||
<Td colSpan={5} className="rounded bg-mineshaft-900">
|
<Td colSpan={6} className="!border-none p-0">
|
||||||
<div className="mb-4 border-b-2 border-mineshaft-500 py-2 text-lg">Approvers</div>
|
<div
|
||||||
|
className={`w-full overflow-hidden bg-mineshaft-900/75 transition-all duration-500 ease-in-out ${
|
||||||
|
isExpanded ? "thin-scrollbar max-h-[26rem] !overflow-y-auto opacity-100" : "max-h-0"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="mb-4 border-b-2 border-mineshaft-500 pb-2">Approvers</div>
|
||||||
{labels?.map((el, index) => (
|
{labels?.map((el, index) => (
|
||||||
<div
|
<div
|
||||||
key={`approval-list-${index + 1}`}
|
key={`approval-list-${index + 1}`}
|
||||||
className="relative mb-2 flex rounded border border-mineshaft-500 bg-mineshaft-700 p-4"
|
className="relative mb-2 flex rounded border border-mineshaft-500 bg-mineshaft-800 p-4"
|
||||||
>
|
>
|
||||||
<div>
|
<div className="my-auto mr-8 flex h-8 w-8 items-center justify-center rounded border border-mineshaft-400 bg-bunker-500/50 text-white">
|
||||||
<div className="mr-8 flex h-8 w-8 items-center justify-center border border-bunker-300 bg-bunker-800 text-white">
|
<div>{index + 1}</div>
|
||||||
<div className="text-lg">{index + 1}</div>
|
|
||||||
</div>
|
</div>
|
||||||
{index !== labels.length - 1 && (
|
{index !== labels.length - 1 && (
|
||||||
<div className="absolute bottom-0 left-8 h-6 border-r border-gray-400" />
|
<div className="absolute bottom-0 left-8 h-[1.25rem] border-r border-mineshaft-400" />
|
||||||
)}
|
)}
|
||||||
{index !== 0 && (
|
{index !== 0 && (
|
||||||
<div className="absolute left-8 top-0 h-4 border-r border-gray-400" />
|
<div className="absolute left-8 top-0 h-[1.25rem] border-r border-mineshaft-400" />
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
<div className="grid flex-grow grid-cols-3">
|
<div className="grid flex-grow grid-cols-3">
|
||||||
<div>
|
<GenericFieldLabel label="Users">{el.userLabels}</GenericFieldLabel>
|
||||||
<div className="mb-1 text-xs font-semibold uppercase">Users</div>
|
<GenericFieldLabel label="Groups">{el.groupLabels}</GenericFieldLabel>
|
||||||
<div>{el.userLabels || "-"}</div>
|
<GenericFieldLabel label="Approvals Required">{el.approvals}</GenericFieldLabel>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="mb-1 text-xs font-semibold uppercase">Groups</div>
|
|
||||||
<div>{el.groupLabels || "-"}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="mb-1 text-xs font-semibold uppercase">Approvals Required</div>
|
|
||||||
<div>{el.approvals || "-"}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,14 +1,19 @@
|
|||||||
import { Fragment, useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
|
faArrowUpRightFromSquare,
|
||||||
|
faBookOpen,
|
||||||
faCheck,
|
faCheck,
|
||||||
faCheckCircle,
|
faCheckCircle,
|
||||||
faChevronDown,
|
faChevronDown,
|
||||||
faCodeBranch
|
faCodeBranch,
|
||||||
|
faMagnifyingGlass,
|
||||||
|
faSearch
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { useSearch } from "@tanstack/react-router";
|
import { useSearch } from "@tanstack/react-router";
|
||||||
import { formatDistance } from "date-fns";
|
import { formatDistance } from "date-fns";
|
||||||
import { AnimatePresence, motion } from "framer-motion";
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@ -18,6 +23,8 @@ import {
|
|||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
|
Input,
|
||||||
|
Pagination,
|
||||||
Skeleton
|
Skeleton
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { ROUTE_PATHS } from "@app/const/routes";
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
@ -28,6 +35,12 @@ import {
|
|||||||
useUser,
|
useUser,
|
||||||
useWorkspace
|
useWorkspace
|
||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
|
import {
|
||||||
|
getUserTablePreference,
|
||||||
|
PreferenceKey,
|
||||||
|
setUserTablePreference
|
||||||
|
} from "@app/helpers/userTablePreferences";
|
||||||
|
import { usePagination } from "@app/hooks";
|
||||||
import {
|
import {
|
||||||
useGetSecretApprovalRequestCount,
|
useGetSecretApprovalRequestCount,
|
||||||
useGetSecretApprovalRequests,
|
useGetSecretApprovalRequests,
|
||||||
@ -52,18 +65,41 @@ export const SecretApprovalRequest = () => {
|
|||||||
const [usingUrlRequestId, setUsingUrlRequestId] = useState(false);
|
const [usingUrlRequestId, setUsingUrlRequestId] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: secretApprovalRequests,
|
debouncedSearch: debouncedSearchFilter,
|
||||||
isFetchingNextPage: isFetchingNextApprovalRequest,
|
search: searchFilter,
|
||||||
fetchNextPage: fetchNextApprovalRequest,
|
setSearch: setSearchFilter,
|
||||||
hasNextPage: hasNextApprovalPage,
|
setPage,
|
||||||
|
page,
|
||||||
|
perPage,
|
||||||
|
setPerPage,
|
||||||
|
offset,
|
||||||
|
limit
|
||||||
|
} = usePagination("", {
|
||||||
|
initPerPage: getUserTablePreference("changeRequestsTable", PreferenceKey.PerPage, 20)
|
||||||
|
});
|
||||||
|
|
||||||
|
const handlePerPageChange = (newPerPage: number) => {
|
||||||
|
setPerPage(newPerPage);
|
||||||
|
setUserTablePreference("changeRequestsTable", PreferenceKey.PerPage, newPerPage);
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
data,
|
||||||
isPending: isApprovalRequestLoading,
|
isPending: isApprovalRequestLoading,
|
||||||
refetch
|
refetch
|
||||||
} = useGetSecretApprovalRequests({
|
} = useGetSecretApprovalRequests({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
status: statusFilter,
|
status: statusFilter,
|
||||||
environment: envFilter,
|
environment: envFilter,
|
||||||
committer: committerFilter
|
committer: committerFilter,
|
||||||
|
search: debouncedSearchFilter,
|
||||||
|
limit,
|
||||||
|
offset
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const totalApprovalCount = data?.totalCount ?? 0;
|
||||||
|
const secretApprovalRequests = data?.approvals ?? [];
|
||||||
|
|
||||||
const { data: secretApprovalRequestCount, isSuccess: isSecretApprovalReqCountSuccess } =
|
const { data: secretApprovalRequestCount, isSuccess: isSecretApprovalReqCountSuccess } =
|
||||||
useGetSecretApprovalRequestCount({ workspaceId });
|
useGetSecretApprovalRequestCount({ workspaceId });
|
||||||
const { user: userSession } = useUser();
|
const { user: userSession } = useUser();
|
||||||
@ -88,8 +124,9 @@ export const SecretApprovalRequest = () => {
|
|||||||
refetch();
|
refetch();
|
||||||
};
|
};
|
||||||
|
|
||||||
const isRequestListEmpty =
|
const isRequestListEmpty = !isApprovalRequestLoading && secretApprovalRequests?.length === 0;
|
||||||
!isApprovalRequestLoading && secretApprovalRequests?.pages[0]?.length === 0;
|
|
||||||
|
const isFiltered = Boolean(searchFilter || envFilter || committerFilter);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
@ -116,7 +153,38 @@ export const SecretApprovalRequest = () => {
|
|||||||
exit={{ opacity: 0, translateX: 30 }}
|
exit={{ opacity: 0, translateX: 30 }}
|
||||||
className="rounded-md text-gray-300"
|
className="rounded-md text-gray-300"
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-8 rounded-t-md border-x border-t border-mineshaft-600 bg-mineshaft-800 p-4 px-8">
|
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-start gap-1">
|
||||||
|
<p className="text-xl font-semibold text-mineshaft-100">Change Requests</p>
|
||||||
|
<a
|
||||||
|
href="https://infisical.com/docs/documentation/platform/pr-workflows"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<div className="ml-1 mt-[0.32rem] inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
|
||||||
|
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||||
|
<span>Docs</span>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faArrowUpRightFromSquare}
|
||||||
|
className="mb-[0.07rem] ml-1.5 text-[10px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-bunker-300">Review pending and closed change requests</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
value={searchFilter}
|
||||||
|
onChange={(e) => setSearchFilter(e.target.value)}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
|
placeholder="Search change requests by author, environment or policy path..."
|
||||||
|
className="flex-1"
|
||||||
|
containerClassName="mb-4"
|
||||||
|
/>
|
||||||
|
<div className="flex items-center space-x-8 rounded-t-md border-x border-t border-mineshaft-600 bg-mineshaft-800 px-8 py-3 text-sm">
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
@ -124,17 +192,19 @@ export const SecretApprovalRequest = () => {
|
|||||||
onKeyDown={(evt) => {
|
onKeyDown={(evt) => {
|
||||||
if (evt.key === "Enter") setStatusFilter("open");
|
if (evt.key === "Enter") setStatusFilter("open");
|
||||||
}}
|
}}
|
||||||
className={
|
className={twMerge(
|
||||||
statusFilter === "close" ? "text-gray-500 duration-100 hover:text-gray-400" : ""
|
"font-medium",
|
||||||
}
|
statusFilter === "close" && "text-gray-500 duration-100 hover:text-gray-400"
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faCodeBranch} className="mr-2" />
|
<FontAwesomeIcon icon={faCodeBranch} className="mr-2" />
|
||||||
{isSecretApprovalReqCountSuccess && secretApprovalRequestCount?.open} Open
|
{isSecretApprovalReqCountSuccess && secretApprovalRequestCount?.open} Open
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={
|
className={twMerge(
|
||||||
statusFilter === "open" ? "text-gray-500 duration-100 hover:text-gray-400" : ""
|
"font-medium",
|
||||||
}
|
statusFilter === "open" && "text-gray-500 duration-100 hover:text-gray-400"
|
||||||
|
)}
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={() => setStatusFilter("close")}
|
onClick={() => setStatusFilter("close")}
|
||||||
@ -152,13 +222,21 @@ export const SecretApprovalRequest = () => {
|
|||||||
variant="plain"
|
variant="plain"
|
||||||
colorSchema="secondary"
|
colorSchema="secondary"
|
||||||
className={envFilter ? "text-white" : "text-bunker-300"}
|
className={envFilter ? "text-white" : "text-bunker-300"}
|
||||||
rightIcon={<FontAwesomeIcon icon={faChevronDown} size="sm" className="ml-2" />}
|
rightIcon={
|
||||||
|
<FontAwesomeIcon icon={faChevronDown} size="sm" className="ml-2" />
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Environments
|
Environments
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent
|
||||||
<DropdownMenuLabel>Select an environment</DropdownMenuLabel>
|
align="end"
|
||||||
|
sideOffset={1}
|
||||||
|
className="thin-scrollbar max-h-[20rem] overflow-y-auto"
|
||||||
|
>
|
||||||
|
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
|
||||||
|
Select an Environment
|
||||||
|
</DropdownMenuLabel>
|
||||||
{currentWorkspace?.environments.map(({ slug, name }) => (
|
{currentWorkspace?.environments.map(({ slug, name }) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => setEnvFilter((state) => (state === slug ? undefined : slug))}
|
onClick={() => setEnvFilter((state) => (state === slug ? undefined : slug))}
|
||||||
@ -188,8 +266,14 @@ export const SecretApprovalRequest = () => {
|
|||||||
Author
|
Author
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent
|
||||||
<DropdownMenuLabel>Select an author</DropdownMenuLabel>
|
align="end"
|
||||||
|
sideOffset={1}
|
||||||
|
className="thin-scrollbar max-h-[20rem] overflow-y-auto"
|
||||||
|
>
|
||||||
|
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
|
||||||
|
Select an Author
|
||||||
|
</DropdownMenuLabel>
|
||||||
{members?.map(({ user, id }) => (
|
{members?.map(({ user, id }) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
@ -210,14 +294,14 @@ export const SecretApprovalRequest = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col rounded-b-md border-x border-b border-t border-mineshaft-600 bg-mineshaft-800">
|
<div className="flex flex-col rounded-b-md border-x border-b border-t border-mineshaft-600 bg-mineshaft-800">
|
||||||
{isRequestListEmpty && (
|
{isRequestListEmpty && !isFiltered && (
|
||||||
<div className="py-12">
|
<div className="py-12">
|
||||||
<EmptyState title="No more requests pending." />
|
<EmptyState
|
||||||
|
title={`No ${statusFilter === "open" ? "Open" : "Closed"} Change Requests`}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{secretApprovalRequests?.pages?.map((group, i) => (
|
{secretApprovalRequests.map((secretApproval) => {
|
||||||
<Fragment key={`secret-approval-request-${i + 1}`}>
|
|
||||||
{group?.map((secretApproval) => {
|
|
||||||
const {
|
const {
|
||||||
id: reqId,
|
id: reqId,
|
||||||
commits,
|
commits,
|
||||||
@ -233,7 +317,7 @@ export const SecretApprovalRequest = () => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={reqId}
|
key={reqId}
|
||||||
className="flex flex-col px-8 py-4 hover:bg-mineshaft-700"
|
className="flex flex-col border-b border-mineshaft-600 px-8 py-3 last:border-b-0 hover:bg-mineshaft-700"
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={() => setSelectedApprovalId(secretApproval.id)}
|
onClick={() => setSelectedApprovalId(secretApproval.id)}
|
||||||
@ -241,14 +325,18 @@ export const SecretApprovalRequest = () => {
|
|||||||
if (evt.key === "Enter") setSelectedApprovalId(secretApproval.id);
|
if (evt.key === "Enter") setSelectedApprovalId(secretApproval.id);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="mb-1">
|
<div className="mb-1 text-sm">
|
||||||
<FontAwesomeIcon icon={faCodeBranch} className="mr-2" />
|
<FontAwesomeIcon
|
||||||
|
icon={faCodeBranch}
|
||||||
|
size="sm"
|
||||||
|
className="mr-1.5 text-mineshaft-300"
|
||||||
|
/>
|
||||||
{secretApproval.isReplicated
|
{secretApproval.isReplicated
|
||||||
? `${commits.length} secret pending import`
|
? `${commits.length} secret pending import`
|
||||||
: generateCommitText(commits)}
|
: generateCommitText(commits)}
|
||||||
<span className="text-xs text-bunker-300"> #{secretApproval.slug}</span>
|
<span className="text-xs text-bunker-300"> #{secretApproval.slug}</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs text-gray-500">
|
<span className="text-xs leading-3 text-gray-500">
|
||||||
Opened {formatDistance(new Date(createdAt), new Date())} ago by{" "}
|
Opened {formatDistance(new Date(createdAt), new Date())} ago by{" "}
|
||||||
{committerUser?.firstName || ""} {committerUser?.lastName || ""} (
|
{committerUser?.firstName || ""} {committerUser?.lastName || ""} (
|
||||||
{committerUser?.email})
|
{committerUser?.email})
|
||||||
@ -257,9 +345,24 @@ export const SecretApprovalRequest = () => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Fragment>
|
{Boolean(
|
||||||
))}
|
!secretApprovalRequests.length && isFiltered && !isApprovalRequestLoading
|
||||||
{(isFetchingNextApprovalRequest || isApprovalRequestLoading) && (
|
) && (
|
||||||
|
<div className="py-12">
|
||||||
|
<EmptyState title="No Requests Match Filters" icon={faSearch} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{Boolean(totalApprovalCount) && (
|
||||||
|
<Pagination
|
||||||
|
className="border-none"
|
||||||
|
count={totalApprovalCount}
|
||||||
|
page={page}
|
||||||
|
perPage={perPage}
|
||||||
|
onChangePage={setPage}
|
||||||
|
onChangePerPage={handlePerPageChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isApprovalRequestLoading && (
|
||||||
<div>
|
<div>
|
||||||
{Array.apply(0, Array(3)).map((_x, index) => (
|
{Array.apply(0, Array(3)).map((_x, index) => (
|
||||||
<div
|
<div
|
||||||
@ -276,18 +379,7 @@ export const SecretApprovalRequest = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{hasNextApprovalPage && (
|
</div>
|
||||||
<Button
|
|
||||||
className="mt-4 text-sm"
|
|
||||||
isFullWidth
|
|
||||||
variant="star"
|
|
||||||
isLoading={isFetchingNextApprovalRequest}
|
|
||||||
isDisabled={isFetchingNextApprovalRequest || !hasNextApprovalPage}
|
|
||||||
onClick={() => fetchNextApprovalRequest()}
|
|
||||||
>
|
|
||||||
{hasNextApprovalPage ? "Load More" : "End of history"}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
@ -56,27 +56,24 @@ export const generateCommitText = (commits: { op: CommitType }[] = [], isReplica
|
|||||||
if (score[CommitType.CREATE])
|
if (score[CommitType.CREATE])
|
||||||
text.push(
|
text.push(
|
||||||
<span key="created-commit">
|
<span key="created-commit">
|
||||||
{score[CommitType.CREATE]} secret{score[CommitType.CREATE] !== 1 && "s"}
|
{score[CommitType.CREATE]} Secret{score[CommitType.CREATE] !== 1 && "s"}
|
||||||
<span style={{ color: "#60DD00" }}> created</span>
|
<span className="text-green-600"> Created</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
if (score[CommitType.UPDATE])
|
if (score[CommitType.UPDATE])
|
||||||
text.push(
|
text.push(
|
||||||
<span key="updated-commit">
|
<span key="updated-commit">
|
||||||
{Boolean(text.length) && ", "}
|
{Boolean(text.length) && ", "}
|
||||||
{score[CommitType.UPDATE]} secret{score[CommitType.UPDATE] !== 1 && "s"}
|
{score[CommitType.UPDATE]} Secret{score[CommitType.UPDATE] !== 1 && "s"}
|
||||||
<span style={{ color: "#F8EB30" }} className="text-orange-600">
|
<span className="text-yellow-600"> Updated</span>
|
||||||
{" "}
|
|
||||||
updated
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
if (score[CommitType.DELETE])
|
if (score[CommitType.DELETE])
|
||||||
text.push(
|
text.push(
|
||||||
<span className="deleted-commit">
|
<span className="deleted-commit">
|
||||||
{Boolean(text.length) && "and"}
|
{Boolean(text.length) && "and"}
|
||||||
{score[CommitType.DELETE]} secret{score[CommitType.UPDATE] !== 1 && "s"}
|
{score[CommitType.DELETE]} Secret{score[CommitType.DELETE] !== 1 && "s"}
|
||||||
<span style={{ color: "#F83030" }}> deleted</span>
|
<span className="text-red-600"> Deleted</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
return text;
|
return text;
|
||||||
|
@ -37,7 +37,10 @@ const formSchema = z.object({
|
|||||||
policyArns: z.string().trim().optional(),
|
policyArns: z.string().trim().optional(),
|
||||||
tags: z
|
tags: z
|
||||||
.array(
|
.array(
|
||||||
z.object({ key: z.string().trim().min(1).max(128), value: z.string().trim().min(1).max(256) })
|
z.object({
|
||||||
|
key: z.string().trim().min(1).max(128),
|
||||||
|
value: z.string().trim().min(1).max(256)
|
||||||
|
})
|
||||||
)
|
)
|
||||||
.optional()
|
.optional()
|
||||||
}),
|
}),
|
||||||
@ -52,7 +55,10 @@ const formSchema = z.object({
|
|||||||
policyArns: z.string().trim().optional(),
|
policyArns: z.string().trim().optional(),
|
||||||
tags: z
|
tags: z
|
||||||
.array(
|
.array(
|
||||||
z.object({ key: z.string().trim().min(1).max(128), value: z.string().trim().min(1).max(256) })
|
z.object({
|
||||||
|
key: z.string().trim().min(1).max(128),
|
||||||
|
value: z.string().trim().min(1).max(256)
|
||||||
|
})
|
||||||
)
|
)
|
||||||
.optional()
|
.optional()
|
||||||
})
|
})
|
||||||
|
@ -11,7 +11,7 @@ import {
|
|||||||
SiSnowflake
|
SiSnowflake
|
||||||
} from "react-icons/si";
|
} from "react-icons/si";
|
||||||
import { VscAzure } from "react-icons/vsc";
|
import { VscAzure } from "react-icons/vsc";
|
||||||
import { faAws, faGoogle } from "@fortawesome/free-brands-svg-icons";
|
import { faAws, faGithub, faGoogle } from "@fortawesome/free-brands-svg-icons";
|
||||||
import { faClock, faDatabase } from "@fortawesome/free-solid-svg-icons";
|
import { faClock, faDatabase } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { AnimatePresence, motion } from "framer-motion";
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
@ -26,6 +26,7 @@ import { AzureEntraIdInputForm } from "./AzureEntraIdInputForm";
|
|||||||
import { CassandraInputForm } from "./CassandraInputForm";
|
import { CassandraInputForm } from "./CassandraInputForm";
|
||||||
import { ElasticSearchInputForm } from "./ElasticSearchInputForm";
|
import { ElasticSearchInputForm } from "./ElasticSearchInputForm";
|
||||||
import { GcpIamInputForm } from "./GcpIamInputForm";
|
import { GcpIamInputForm } from "./GcpIamInputForm";
|
||||||
|
import { GithubInputForm } from "./GithubInputForm";
|
||||||
import { KubernetesInputForm } from "./KubernetesInputForm";
|
import { KubernetesInputForm } from "./KubernetesInputForm";
|
||||||
import { LdapInputForm } from "./LdapInputForm";
|
import { LdapInputForm } from "./LdapInputForm";
|
||||||
import { MongoAtlasInputForm } from "./MongoAtlasInputForm";
|
import { MongoAtlasInputForm } from "./MongoAtlasInputForm";
|
||||||
@ -143,6 +144,11 @@ const DYNAMIC_SECRET_LIST = [
|
|||||||
icon: <FontAwesomeIcon icon={faGoogle} size="lg" />,
|
icon: <FontAwesomeIcon icon={faGoogle} size="lg" />,
|
||||||
provider: DynamicSecretProviders.GcpIam,
|
provider: DynamicSecretProviders.GcpIam,
|
||||||
title: "GCP IAM"
|
title: "GCP IAM"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <FontAwesomeIcon icon={faGithub} size="lg" />,
|
||||||
|
provider: DynamicSecretProviders.Github,
|
||||||
|
title: "GitHub"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -548,6 +554,25 @@ export const CreateDynamicSecretForm = ({
|
|||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
{wizardStep === WizardSteps.ProviderInputs &&
|
||||||
|
selectedProvider === DynamicSecretProviders.Github && (
|
||||||
|
<motion.div
|
||||||
|
key="dynamic-github-step"
|
||||||
|
transition={{ duration: 0.1 }}
|
||||||
|
initial={{ opacity: 0, translateX: 30 }}
|
||||||
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
|
exit={{ opacity: 0, translateX: -30 }}
|
||||||
|
>
|
||||||
|
<GithubInputForm
|
||||||
|
onCompleted={handleFormReset}
|
||||||
|
onCancel={handleFormReset}
|
||||||
|
projectSlug={projectSlug}
|
||||||
|
secretPath={secretPath}
|
||||||
|
environments={environments}
|
||||||
|
isSingleEnvironmentMode={isSingleEnvironmentMode}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@ -0,0 +1,234 @@
|
|||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
FilterableSelect,
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
Input,
|
||||||
|
SecretInput,
|
||||||
|
Tooltip
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useCreateDynamicSecret } from "@app/hooks/api";
|
||||||
|
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||||
|
import { WorkspaceEnv } from "@app/hooks/api/types";
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
provider: z.object({
|
||||||
|
appId: z.coerce.number().min(1, "Required"),
|
||||||
|
installationId: z.coerce.number().min(1, "Required"),
|
||||||
|
privateKey: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, "Required")
|
||||||
|
.refine(
|
||||||
|
(val) =>
|
||||||
|
/^-----BEGIN(?:(?: RSA| PGP| ENCRYPTED)? PRIVATE KEY)-----\s*[\s\S]*?-----END(?:(?: RSA| PGP| ENCRYPTED)? PRIVATE KEY)-----$/.test(
|
||||||
|
val
|
||||||
|
),
|
||||||
|
"Invalid PEM format for private key"
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
|
||||||
|
environment: z.object({ name: z.string(), slug: z.string() })
|
||||||
|
});
|
||||||
|
type TForm = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onCompleted: () => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
secretPath: string;
|
||||||
|
projectSlug: string;
|
||||||
|
environments: WorkspaceEnv[];
|
||||||
|
isSingleEnvironmentMode?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GithubInputForm = ({
|
||||||
|
onCompleted,
|
||||||
|
onCancel,
|
||||||
|
environments,
|
||||||
|
secretPath,
|
||||||
|
projectSlug,
|
||||||
|
isSingleEnvironmentMode
|
||||||
|
}: Props) => {
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
formState: { isSubmitting },
|
||||||
|
handleSubmit
|
||||||
|
} = useForm<TForm>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
environment: isSingleEnvironmentMode && environments.length > 0 ? environments[0] : undefined
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const createDynamicSecret = useCreateDynamicSecret();
|
||||||
|
|
||||||
|
const handleCreateDynamicSecret = async ({ name, provider, environment }: TForm) => {
|
||||||
|
if (createDynamicSecret.isPending) return;
|
||||||
|
try {
|
||||||
|
await createDynamicSecret.mutateAsync({
|
||||||
|
provider: {
|
||||||
|
type: DynamicSecretProviders.Github,
|
||||||
|
inputs: {
|
||||||
|
...provider
|
||||||
|
}
|
||||||
|
},
|
||||||
|
defaultTTL: "1h", // Github is limited to 1 hour tokens
|
||||||
|
name,
|
||||||
|
path: secretPath,
|
||||||
|
projectSlug,
|
||||||
|
environmentSlug: environment.slug
|
||||||
|
});
|
||||||
|
onCompleted();
|
||||||
|
} catch {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Failed to create dynamic secret"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<form onSubmit={handleSubmit(handleCreateDynamicSecret)} autoComplete="off">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="flex-grow">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
name="name"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Secret Name"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="dynamic-secret" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-32">
|
||||||
|
<FormControl
|
||||||
|
label={
|
||||||
|
<FormLabel
|
||||||
|
label="Default TTL"
|
||||||
|
icon={
|
||||||
|
<Tooltip content="Github token TTL is fixed to 1 hour">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faQuestionCircle}
|
||||||
|
size="sm"
|
||||||
|
className="relative bottom-px right-1"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Input disabled value="1h" className="pointer-events-none opacity-50" />
|
||||||
|
</FormControl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
|
||||||
|
Configuration
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="provider.appId"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="App ID"
|
||||||
|
className="flex-grow"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
isRequired
|
||||||
|
>
|
||||||
|
<Input placeholder="0000000" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="provider.installationId"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Installation ID"
|
||||||
|
className="flex-grow"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
isRequired
|
||||||
|
>
|
||||||
|
<Input placeholder="00000000" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="provider.privateKey"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="App Private Key PEM"
|
||||||
|
className="flex-grow"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
isRequired
|
||||||
|
>
|
||||||
|
<SecretInput
|
||||||
|
{...field}
|
||||||
|
containerClassName="text-gray-400 group-focus-within:!border-primary-400/50 border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!isSingleEnvironmentMode && (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="environment"
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Environment"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<FilterableSelect
|
||||||
|
options={environments}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
placeholder="Select the environment to create secret in..."
|
||||||
|
getOptionLabel={(option) => option.name}
|
||||||
|
getOptionValue={(option) => option.slug}
|
||||||
|
menuPlacement="top"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 flex items-center space-x-4">
|
||||||
|
<Button type="submit" isLoading={isSubmitting}>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline_bg" onClick={onCancel}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -368,6 +368,22 @@ const renderOutputForm = (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (provider === DynamicSecretProviders.Github) {
|
||||||
|
const { TOKEN } = data as {
|
||||||
|
TOKEN: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<OutputDisplay
|
||||||
|
label="Token"
|
||||||
|
value={TOKEN}
|
||||||
|
helperText="Important: Copy these credentials now. You will not be able to see them again after you close the modal."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -608,6 +624,9 @@ export const CreateDynamicSecretLease = ({
|
|||||||
return <Spinner className="mx-auto h-40 text-mineshaft-700" />;
|
return <Spinner className="mx-auto h-40 text-mineshaft-700" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Github tokens are fixed to 1 hour
|
||||||
|
const fixedTtl = provider === DynamicSecretProviders.Github;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
@ -629,8 +648,11 @@ export const CreateDynamicSecretLease = ({
|
|||||||
label={<TtlFormLabel label="Default TTL" />}
|
label={<TtlFormLabel label="Default TTL" />}
|
||||||
isError={Boolean(error?.message)}
|
isError={Boolean(error?.message)}
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
|
helperText={
|
||||||
|
fixedTtl ? `This provider has a fixed TTL of ${field.value}` : undefined
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Input {...field} />
|
<Input {...field} isDisabled={fixedTtl} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
@ -29,7 +29,7 @@ import {
|
|||||||
import { ProjectPermissionDynamicSecretActions, ProjectPermissionSub } from "@app/context";
|
import { ProjectPermissionDynamicSecretActions, ProjectPermissionSub } from "@app/context";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
import { useGetDynamicSecretLeases, useRevokeDynamicSecretLease } from "@app/hooks/api";
|
import { useGetDynamicSecretLeases, useRevokeDynamicSecretLease } from "@app/hooks/api";
|
||||||
import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
|
import { DynamicSecretProviders, TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
|
||||||
import { DynamicSecretLeaseStatus } from "@app/hooks/api/dynamicSecretLease/types";
|
import { DynamicSecretLeaseStatus } from "@app/hooks/api/dynamicSecretLease/types";
|
||||||
|
|
||||||
import { RenewDynamicSecretLease } from "./RenewDynamicSecretLease";
|
import { RenewDynamicSecretLease } from "./RenewDynamicSecretLease";
|
||||||
@ -44,6 +44,8 @@ type Props = {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DYNAMIC_SECRETS_WITHOUT_RENEWAL = [DynamicSecretProviders.Github];
|
||||||
|
|
||||||
export const DynamicSecretLease = ({
|
export const DynamicSecretLease = ({
|
||||||
projectSlug,
|
projectSlug,
|
||||||
dynamicSecretName,
|
dynamicSecretName,
|
||||||
@ -94,6 +96,8 @@ export const DynamicSecretLease = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const canRenew = !DYNAMIC_SECRETS_WITHOUT_RENEWAL.includes(dynamicSecret.type);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<TableContainer>
|
<TableContainer>
|
||||||
@ -141,6 +145,7 @@ export const DynamicSecretLease = ({
|
|||||||
</Td>
|
</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
|
{canRenew && (
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionDynamicSecretActions.Lease}
|
I={ProjectPermissionDynamicSecretActions.Lease}
|
||||||
a={subject(ProjectPermissionSub.DynamicSecrets, {
|
a={subject(ProjectPermissionSub.DynamicSecrets, {
|
||||||
@ -153,7 +158,7 @@ export const DynamicSecretLease = ({
|
|||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<IconButton
|
<IconButton
|
||||||
ariaLabel="edit-folder"
|
ariaLabel="renew-lease"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="p-0"
|
className="p-0"
|
||||||
@ -164,6 +169,7 @@ export const DynamicSecretLease = ({
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
|
)}
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionDynamicSecretActions.Lease}
|
I={ProjectPermissionDynamicSecretActions.Lease}
|
||||||
a={subject(ProjectPermissionSub.DynamicSecrets, {
|
a={subject(ProjectPermissionSub.DynamicSecrets, {
|
||||||
|
@ -26,7 +26,7 @@ const formSchema = z.object({
|
|||||||
policyArns: z.string().trim().optional(),
|
policyArns: z.string().trim().optional(),
|
||||||
tags: z
|
tags: z
|
||||||
.array(z.object({ key: z.string().trim().min(1), value: z.string().trim().min(1) }))
|
.array(z.object({ key: z.string().trim().min(1), value: z.string().trim().min(1) }))
|
||||||
.optional(),
|
.optional()
|
||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
method: z.literal(DynamicSecretAwsIamAuth.AssumeRole),
|
method: z.literal(DynamicSecretAwsIamAuth.AssumeRole),
|
||||||
@ -97,7 +97,7 @@ export const EditDynamicSecretAwsIamForm = ({
|
|||||||
usernameTemplate: dynamicSecret?.usernameTemplate || "{{randomUsername}}",
|
usernameTemplate: dynamicSecret?.usernameTemplate || "{{randomUsername}}",
|
||||||
inputs: {
|
inputs: {
|
||||||
...(dynamicSecret.inputs as TForm["inputs"])
|
...(dynamicSecret.inputs as TForm["inputs"])
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const isAccessKeyMethod = watch("inputs.method") === DynamicSecretAwsIamAuth.AccessKey;
|
const isAccessKeyMethod = watch("inputs.method") === DynamicSecretAwsIamAuth.AccessKey;
|
||||||
@ -125,8 +125,7 @@ export const EditDynamicSecretAwsIamForm = ({
|
|||||||
defaultTTL,
|
defaultTTL,
|
||||||
inputs,
|
inputs,
|
||||||
newName: newName === dynamicSecret.name ? undefined : newName,
|
newName: newName === dynamicSecret.name ? undefined : newName,
|
||||||
usernameTemplate:
|
usernameTemplate: !usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate
|
||||||
!usernameTemplate || isDefaultUsernameTemplate ? null : usernameTemplate
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
onClose();
|
onClose();
|
||||||
|
@ -10,6 +10,7 @@ import { EditDynamicSecretAzureEntraIdForm } from "./EditDynamicSecretAzureEntra
|
|||||||
import { EditDynamicSecretCassandraForm } from "./EditDynamicSecretCassandraForm";
|
import { EditDynamicSecretCassandraForm } from "./EditDynamicSecretCassandraForm";
|
||||||
import { EditDynamicSecretElasticSearchForm } from "./EditDynamicSecretElasticSearchForm";
|
import { EditDynamicSecretElasticSearchForm } from "./EditDynamicSecretElasticSearchForm";
|
||||||
import { EditDynamicSecretGcpIamForm } from "./EditDynamicSecretGcpIamForm";
|
import { EditDynamicSecretGcpIamForm } from "./EditDynamicSecretGcpIamForm";
|
||||||
|
import { EditDynamicSecretGithubForm } from "./EditDynamicSecretGithubForm";
|
||||||
import { EditDynamicSecretKubernetesForm } from "./EditDynamicSecretKubernetesForm";
|
import { EditDynamicSecretKubernetesForm } from "./EditDynamicSecretKubernetesForm";
|
||||||
import { EditDynamicSecretLdapForm } from "./EditDynamicSecretLdapForm";
|
import { EditDynamicSecretLdapForm } from "./EditDynamicSecretLdapForm";
|
||||||
import { EditDynamicSecretMongoAtlasForm } from "./EditDynamicSecretMongoAtlasForm";
|
import { EditDynamicSecretMongoAtlasForm } from "./EditDynamicSecretMongoAtlasForm";
|
||||||
@ -366,6 +367,23 @@ export const EditDynamicSecretForm = ({
|
|||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
{dynamicSecretDetails?.type === DynamicSecretProviders.Github && (
|
||||||
|
<motion.div
|
||||||
|
key="github-edit"
|
||||||
|
transition={{ duration: 0.1 }}
|
||||||
|
initial={{ opacity: 0, translateX: 30 }}
|
||||||
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
|
exit={{ opacity: 0, translateX: -30 }}
|
||||||
|
>
|
||||||
|
<EditDynamicSecretGithubForm
|
||||||
|
onClose={onClose}
|
||||||
|
projectSlug={projectSlug}
|
||||||
|
secretPath={secretPath}
|
||||||
|
dynamicSecret={dynamicSecretDetails}
|
||||||
|
environment={environment}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,199 @@
|
|||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { Button, FormControl, FormLabel, Input, SecretInput, Tooltip } from "@app/components/v2";
|
||||||
|
import { useUpdateDynamicSecret } from "@app/hooks/api";
|
||||||
|
import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
inputs: z.object({
|
||||||
|
appId: z.coerce.number().min(1, "Required"),
|
||||||
|
installationId: z.coerce.number().min(1, "Required"),
|
||||||
|
privateKey: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, "Required")
|
||||||
|
.refine(
|
||||||
|
(val) =>
|
||||||
|
/^-----BEGIN(?:(?: RSA| PGP| ENCRYPTED)? PRIVATE KEY)-----\s*[\s\S]*?-----END(?:(?: RSA| PGP| ENCRYPTED)? PRIVATE KEY)-----$/.test(
|
||||||
|
val
|
||||||
|
),
|
||||||
|
"Invalid PEM format for private key"
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
newName: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||||
|
});
|
||||||
|
type TForm = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onClose: () => void;
|
||||||
|
dynamicSecret: TDynamicSecret & { inputs: unknown };
|
||||||
|
secretPath: string;
|
||||||
|
environment: string;
|
||||||
|
projectSlug: string;
|
||||||
|
};
|
||||||
|
export const EditDynamicSecretGithubForm = ({
|
||||||
|
onClose,
|
||||||
|
dynamicSecret,
|
||||||
|
secretPath,
|
||||||
|
environment,
|
||||||
|
projectSlug
|
||||||
|
}: Props) => {
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
formState: { isSubmitting },
|
||||||
|
handleSubmit
|
||||||
|
} = useForm<TForm>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
values: {
|
||||||
|
newName: dynamicSecret.name,
|
||||||
|
inputs: {
|
||||||
|
...(dynamicSecret.inputs as TForm["inputs"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateDynamicSecret = useUpdateDynamicSecret();
|
||||||
|
|
||||||
|
const handleUpdateDynamicSecret = async ({ inputs, newName }: TForm) => {
|
||||||
|
if (updateDynamicSecret.isPending) return;
|
||||||
|
try {
|
||||||
|
await updateDynamicSecret.mutateAsync({
|
||||||
|
name: dynamicSecret.name,
|
||||||
|
path: secretPath,
|
||||||
|
projectSlug,
|
||||||
|
environmentSlug: environment,
|
||||||
|
data: {
|
||||||
|
inputs,
|
||||||
|
newName: newName === dynamicSecret.name ? undefined : newName
|
||||||
|
}
|
||||||
|
});
|
||||||
|
onClose();
|
||||||
|
createNotification({
|
||||||
|
type: "success",
|
||||||
|
text: "Successfully updated dynamic secret"
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Failed to update dynamic secret"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<form onSubmit={handleSubmit(handleUpdateDynamicSecret)} autoComplete="off">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="flex-grow">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="newName"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Secret Name"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="dynamic-secret" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-32">
|
||||||
|
<FormControl
|
||||||
|
label={
|
||||||
|
<FormLabel
|
||||||
|
label="Default TTL"
|
||||||
|
icon={
|
||||||
|
<Tooltip content="Github token TTL is fixed to 1 hour">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faQuestionCircle}
|
||||||
|
size="sm"
|
||||||
|
className="relative bottom-px right-1"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Input disabled value="1h" className="pointer-events-none opacity-50" />
|
||||||
|
</FormControl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
|
||||||
|
Configuration
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="inputs.appId"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="App ID"
|
||||||
|
className="flex-grow"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
isRequired
|
||||||
|
>
|
||||||
|
<Input placeholder="0000000" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="inputs.installationId"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Installation ID"
|
||||||
|
className="flex-grow"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
isRequired
|
||||||
|
>
|
||||||
|
<Input placeholder="00000000" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="inputs.privateKey"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="App Private Key PEM"
|
||||||
|
className="flex-grow"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
isRequired
|
||||||
|
>
|
||||||
|
<SecretInput
|
||||||
|
{...field}
|
||||||
|
containerClassName="text-gray-400 group-focus-within:!border-primary-400/50 border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 flex items-center space-x-4">
|
||||||
|
<Button type="submit" isLoading={isSubmitting}>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline_bg" onClick={onClose}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -28,7 +28,7 @@ export const RenewDynamicSecretLease = ({
|
|||||||
environment,
|
environment,
|
||||||
dynamicSecret
|
dynamicSecret
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const maxTtlMs = ms(dynamicSecret.maxTTL);
|
const maxTtlMs = dynamicSecret.maxTTL ? ms(dynamicSecret.maxTTL) : undefined;
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
ttl: z.string().superRefine((val, ctx) => {
|
ttl: z.string().superRefine((val, ctx) => {
|
||||||
@ -39,7 +39,7 @@ export const RenewDynamicSecretLease = ({
|
|||||||
code: z.ZodIssueCode.custom,
|
code: z.ZodIssueCode.custom,
|
||||||
message: "TTL must be greater than 1 second"
|
message: "TTL must be greater than 1 second"
|
||||||
});
|
});
|
||||||
if (valMs > maxTtlMs)
|
if (maxTtlMs && valMs > maxTtlMs)
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
code: z.ZodIssueCode.custom,
|
code: z.ZodIssueCode.custom,
|
||||||
message: `TTL must be less than ${dynamicSecret.maxTTL}`
|
message: `TTL must be less than ${dynamicSecret.maxTTL}`
|
||||||
|
Reference in New Issue
Block a user