mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-25 14:07:47 +00:00
Compare commits
167 Commits
identity-a
...
infisical/
Author | SHA1 | Date | |
---|---|---|---|
|
9d66659f72 | ||
|
70c9761abe | ||
|
c9d7559983 | ||
|
66251403bf | ||
|
b9c4407507 | ||
|
624be80768 | ||
|
8d7b5968d3 | ||
|
7154b19703 | ||
|
9ce465b3e2 | ||
|
598e5c0be5 | ||
|
72f08a6b89 | ||
|
55d8762351 | ||
|
3c92ec4dc3 | ||
|
f2224262a4 | ||
|
23eac40740 | ||
|
4ae88c0447 | ||
|
7aecaad050 | ||
|
cf61390e52 | ||
|
3f02481e78 | ||
|
7adc103ed2 | ||
|
5bdbf37171 | ||
|
4f874734ab | ||
|
eb6fd8259b | ||
|
1766a44dd0 | ||
|
624c9ef8da | ||
|
dfd4b13574 | ||
|
22b57b7a74 | ||
|
1ba0b9c204 | ||
|
a903537441 | ||
|
92c4d83714 | ||
|
a6414104ad | ||
|
071f37666e | ||
|
cd5078d8b7 | ||
|
110d0e95b0 | ||
|
a8c0bbb7ca | ||
|
6af8a4fab8 | ||
|
407fd8eda7 | ||
|
9d976de19b | ||
|
43ecd31b74 | ||
|
be99e40050 | ||
|
800d2c0454 | ||
|
6d0534b165 | ||
|
ccee0f5428 | ||
|
14586c7cd0 | ||
|
7090eea716 | ||
|
01d3443139 | ||
|
c4b23a8d4f | ||
|
90a2a11fff | ||
|
95d7c2082c | ||
|
ab5eb4c696 | ||
|
65aeb81934 | ||
|
a406511405 | ||
|
61da0db49e | ||
|
0968893d4b | ||
|
59666740ca | ||
|
9cc7edc869 | ||
|
e1b016f76d | ||
|
1175b9b5af | ||
|
09521144ec | ||
|
8759944077 | ||
|
aac3c355e9 | ||
|
2a28a462a5 | ||
|
3328e0850f | ||
|
216cae9b33 | ||
|
d24a5d96e3 | ||
|
89d4d4bc92 | ||
|
cffcb28bc9 | ||
|
61388753cf | ||
|
a6145120e6 | ||
|
dacffbef08 | ||
|
4db3e5d208 | ||
|
2a84d61862 | ||
|
a5945204ad | ||
|
55b0dc7f81 | ||
|
ba03fc256b | ||
|
ea28c374a7 | ||
|
e99eb47cf4 | ||
|
cf107c0c0d | ||
|
9fcb1c2161 | ||
|
70515a1ca2 | ||
|
955cf9303a | ||
|
a24ef46d7d | ||
|
ee49f714b9 | ||
|
657aca516f | ||
|
b5d60398d6 | ||
|
c3d515bb95 | ||
|
7f89a7c860 | ||
|
23cb05c16d | ||
|
d74b819f57 | ||
|
457056b600 | ||
|
7dc9ea4f6a | ||
|
3b4b520d42 | ||
|
23f605bda7 | ||
|
1c3c8dbdce | ||
|
317c95384e | ||
|
7dd959e124 | ||
|
2049e5668f | ||
|
0a3e99b334 | ||
|
c4ad0aa163 | ||
|
5bb0b7a508 | ||
|
96bcd42753 | ||
|
2c75e23acf | ||
|
907dd4880a | ||
|
6af7c5c371 | ||
|
72468d5428 | ||
|
939ee892e0 | ||
|
c7ec9ff816 | ||
|
554e268f88 | ||
|
a8a27c3045 | ||
|
27af943ee1 | ||
|
9b772ad55a | ||
|
94a1fc2809 | ||
|
10c10642a1 | ||
|
3e0f04273c | ||
|
91f2d0384e | ||
|
811dc8dd75 | ||
|
4ee9375a8d | ||
|
92f697e195 | ||
|
8062f0238b | ||
|
1181c684db | ||
|
dda436bcd9 | ||
|
89124b18d2 | ||
|
effd88c4bd | ||
|
27efc908e2 | ||
|
8e4226038b | ||
|
27425a1a64 | ||
|
18cf3c89c1 | ||
|
49e6d7a861 | ||
|
c4446389b0 | ||
|
7c21dec54d | ||
|
2ea5710896 | ||
|
f9ac7442df | ||
|
a93bfa69c9 | ||
|
08a0550cd7 | ||
|
d7503573b1 | ||
|
b5a89edeed | ||
|
860eaae4c8 | ||
|
c7a4b6c4e9 | ||
|
c12c6dcc6e | ||
|
d0d5556bd0 | ||
|
753c28a2d3 | ||
|
8741414cfa | ||
|
b8d29793ec | ||
|
92013dbfbc | ||
|
c5319588fe | ||
|
9efb8eaf78 | ||
|
dfc973c7f7 | ||
|
3013d1977c | ||
|
f358e8942d | ||
|
58f51411c0 | ||
|
c3970d1ea2 | ||
|
2dc00a638a | ||
|
bab9c1f454 | ||
|
2bd4770fb4 | ||
|
31905fab6e | ||
|
784acf16d0 | ||
|
114b89c952 | ||
|
81420198cb | ||
|
b949708f45 | ||
|
2a6b6b03b9 | ||
|
0ff18e277f | ||
|
e093f70301 | ||
|
8e2ff18f35 | ||
|
3fbfecf7a9 | ||
|
9087def21c | ||
|
586dbd79b0 | ||
|
645dfafba0 |
@@ -26,7 +26,8 @@ SITE_URL=http://localhost:8080
|
|||||||
# Mail/SMTP
|
# Mail/SMTP
|
||||||
SMTP_HOST=
|
SMTP_HOST=
|
||||||
SMTP_PORT=
|
SMTP_PORT=
|
||||||
SMTP_NAME=
|
SMTP_FROM_ADDRESS=
|
||||||
|
SMTP_FROM_NAME=
|
||||||
SMTP_USERNAME=
|
SMTP_USERNAME=
|
||||||
SMTP_PASSWORD=
|
SMTP_PASSWORD=
|
||||||
|
|
||||||
@@ -104,4 +105,7 @@ INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID=
|
|||||||
INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET=
|
INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET=
|
||||||
INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY=
|
INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY=
|
||||||
INF_APP_CONNECTION_GITHUB_APP_SLUG=
|
INF_APP_CONNECTION_GITHUB_APP_SLUG=
|
||||||
INF_APP_CONNECTION_GITHUB_APP_ID=
|
INF_APP_CONNECTION_GITHUB_APP_ID=
|
||||||
|
|
||||||
|
#gcp app
|
||||||
|
INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL=
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
name: Release Helm Charts
|
name: Release Infisical Core Helm chart
|
||||||
|
|
||||||
on: [workflow_dispatch]
|
on: [workflow_dispatch]
|
||||||
|
|
||||||
@@ -17,6 +17,6 @@ jobs:
|
|||||||
- name: Install Cloudsmith CLI
|
- name: Install Cloudsmith CLI
|
||||||
run: pip install --upgrade cloudsmith-cli
|
run: pip install --upgrade cloudsmith-cli
|
||||||
- name: Build and push helm package to Cloudsmith
|
- name: Build and push helm package to Cloudsmith
|
||||||
run: cd helm-charts && sh upload-to-cloudsmith.sh
|
run: cd helm-charts && sh upload-infisical-core-helm-cloudsmith.sh
|
||||||
env:
|
env:
|
||||||
CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}
|
CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}
|
@@ -1,4 +1,4 @@
|
|||||||
name: Release Docker image for K8 operator
|
name: Release image + Helm chart K8s Operator
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
@@ -35,3 +35,18 @@ jobs:
|
|||||||
tags: |
|
tags: |
|
||||||
infisical/kubernetes-operator:latest
|
infisical/kubernetes-operator:latest
|
||||||
infisical/kubernetes-operator:${{ steps.extract_version.outputs.version }}
|
infisical/kubernetes-operator:${{ steps.extract_version.outputs.version }}
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Install Helm
|
||||||
|
uses: azure/setup-helm@v3
|
||||||
|
with:
|
||||||
|
version: v3.10.0
|
||||||
|
- name: Install python
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
- name: Install Cloudsmith CLI
|
||||||
|
run: pip install --upgrade cloudsmith-cli
|
||||||
|
- name: Build and push helm package to Cloudsmith
|
||||||
|
run: cd helm-charts && sh upload-k8s-operator-cloudsmith.sh
|
||||||
|
env:
|
||||||
|
CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}
|
||||||
|
3
Makefile
3
Makefile
@@ -30,3 +30,6 @@ reviewable-api:
|
|||||||
npm run type:check
|
npm run type:check
|
||||||
|
|
||||||
reviewable: reviewable-ui reviewable-api
|
reviewable: reviewable-ui reviewable-api
|
||||||
|
|
||||||
|
up-dev-sso:
|
||||||
|
docker compose -f docker-compose.dev.yml --profile sso up --build
|
||||||
|
@@ -125,7 +125,7 @@ Install pre commit hook to scan each commit before you push to your repository
|
|||||||
infisical scan install --pre-commit-hook
|
infisical scan install --pre-commit-hook
|
||||||
```
|
```
|
||||||
|
|
||||||
Lean about Infisical's code scanning feature [here](https://infisical.com/docs/cli/scanning-overview)
|
Learn about Infisical's code scanning feature [here](https://infisical.com/docs/cli/scanning-overview)
|
||||||
|
|
||||||
## Open-source vs. paid
|
## Open-source vs. paid
|
||||||
|
|
||||||
|
@@ -0,0 +1,23 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasManageGroupMembershipsCol = await knex.schema.hasColumn(TableName.OidcConfig, "manageGroupMemberships");
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.OidcConfig, (tb) => {
|
||||||
|
if (!hasManageGroupMembershipsCol) {
|
||||||
|
tb.boolean("manageGroupMemberships").notNullable().defaultTo(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasManageGroupMembershipsCol = await knex.schema.hasColumn(TableName.OidcConfig, "manageGroupMemberships");
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.OidcConfig, (t) => {
|
||||||
|
if (hasManageGroupMembershipsCol) {
|
||||||
|
t.dropColumn("manageGroupMemberships");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
@@ -0,0 +1,23 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.alterTable(TableName.AppConnection, (t) => {
|
||||||
|
t.unique(["orgId", "name"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.SecretSync, (t) => {
|
||||||
|
t.unique(["projectId", "name"]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.alterTable(TableName.AppConnection, (t) => {
|
||||||
|
t.dropUnique(["orgId", "name"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.SecretSync, (t) => {
|
||||||
|
t.dropUnique(["projectId", "name"]);
|
||||||
|
});
|
||||||
|
}
|
@@ -0,0 +1,36 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasTable = await knex.schema.hasTable(TableName.IdentityGcpAuth);
|
||||||
|
const hasAllowedProjectsColumn = await knex.schema.hasColumn(TableName.IdentityGcpAuth, "allowedProjects");
|
||||||
|
const hasAllowedServiceAccountsColumn = await knex.schema.hasColumn(
|
||||||
|
TableName.IdentityGcpAuth,
|
||||||
|
"allowedServiceAccounts"
|
||||||
|
);
|
||||||
|
const hasAllowedZones = await knex.schema.hasColumn(TableName.IdentityGcpAuth, "allowedZones");
|
||||||
|
if (hasTable) {
|
||||||
|
await knex.schema.alterTable(TableName.IdentityGcpAuth, (t) => {
|
||||||
|
if (hasAllowedProjectsColumn) t.string("allowedProjects", 2500).alter();
|
||||||
|
if (hasAllowedServiceAccountsColumn) t.string("allowedServiceAccounts", 5000).alter();
|
||||||
|
if (hasAllowedZones) t.string("allowedZones", 2500).alter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasTable = await knex.schema.hasTable(TableName.IdentityGcpAuth);
|
||||||
|
const hasAllowedProjectsColumn = await knex.schema.hasColumn(TableName.IdentityGcpAuth, "allowedProjects");
|
||||||
|
const hasAllowedServiceAccountsColumn = await knex.schema.hasColumn(
|
||||||
|
TableName.IdentityGcpAuth,
|
||||||
|
"allowedServiceAccounts"
|
||||||
|
);
|
||||||
|
const hasAllowedZones = await knex.schema.hasColumn(TableName.IdentityGcpAuth, "allowedZones");
|
||||||
|
if (hasTable) {
|
||||||
|
await knex.schema.alterTable(TableName.IdentityGcpAuth, (t) => {
|
||||||
|
if (hasAllowedProjectsColumn) t.string("allowedProjects").alter();
|
||||||
|
if (hasAllowedServiceAccountsColumn) t.string("allowedServiceAccounts").alter();
|
||||||
|
if (hasAllowedZones) t.string("allowedZones").alter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -17,9 +17,9 @@ export const IdentityGcpAuthsSchema = z.object({
|
|||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
identityId: z.string().uuid(),
|
identityId: z.string().uuid(),
|
||||||
type: z.string(),
|
type: z.string(),
|
||||||
allowedServiceAccounts: z.string(),
|
allowedServiceAccounts: z.string().nullable().optional(),
|
||||||
allowedProjects: z.string(),
|
allowedProjects: z.string().nullable().optional(),
|
||||||
allowedZones: z.string()
|
allowedZones: z.string().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TIdentityGcpAuths = z.infer<typeof IdentityGcpAuthsSchema>;
|
export type TIdentityGcpAuths = z.infer<typeof IdentityGcpAuthsSchema>;
|
||||||
|
@@ -27,7 +27,8 @@ export const OidcConfigsSchema = z.object({
|
|||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
orgId: z.string().uuid(),
|
orgId: z.string().uuid(),
|
||||||
lastUsed: z.date().nullable().optional()
|
lastUsed: z.date().nullable().optional(),
|
||||||
|
manageGroupMemberships: z.boolean().default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TOidcConfigs = z.infer<typeof OidcConfigsSchema>;
|
export type TOidcConfigs = z.infer<typeof OidcConfigsSchema>;
|
||||||
|
@@ -153,7 +153,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
|
|||||||
discoveryURL: true,
|
discoveryURL: true,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
orgId: true,
|
orgId: true,
|
||||||
allowedEmailDomains: true
|
allowedEmailDomains: true,
|
||||||
|
manageGroupMemberships: true
|
||||||
}).extend({
|
}).extend({
|
||||||
clientId: z.string(),
|
clientId: z.string(),
|
||||||
clientSecret: z.string()
|
clientSecret: z.string()
|
||||||
@@ -207,7 +208,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
|
|||||||
userinfoEndpoint: z.string().trim(),
|
userinfoEndpoint: z.string().trim(),
|
||||||
clientId: z.string().trim(),
|
clientId: z.string().trim(),
|
||||||
clientSecret: z.string().trim(),
|
clientSecret: z.string().trim(),
|
||||||
isActive: z.boolean()
|
isActive: z.boolean(),
|
||||||
|
manageGroupMemberships: z.boolean().optional()
|
||||||
})
|
})
|
||||||
.partial()
|
.partial()
|
||||||
.merge(z.object({ orgSlug: z.string() })),
|
.merge(z.object({ orgSlug: z.string() })),
|
||||||
@@ -223,7 +225,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
|
|||||||
userinfoEndpoint: true,
|
userinfoEndpoint: true,
|
||||||
orgId: true,
|
orgId: true,
|
||||||
allowedEmailDomains: true,
|
allowedEmailDomains: true,
|
||||||
isActive: true
|
isActive: true,
|
||||||
|
manageGroupMemberships: true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -272,7 +275,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
|
|||||||
clientId: z.string().trim(),
|
clientId: z.string().trim(),
|
||||||
clientSecret: z.string().trim(),
|
clientSecret: z.string().trim(),
|
||||||
isActive: z.boolean(),
|
isActive: z.boolean(),
|
||||||
orgSlug: z.string().trim()
|
orgSlug: z.string().trim(),
|
||||||
|
manageGroupMemberships: z.boolean().optional().default(false)
|
||||||
})
|
})
|
||||||
.superRefine((data, ctx) => {
|
.superRefine((data, ctx) => {
|
||||||
if (data.configurationType === OIDCConfigurationType.CUSTOM) {
|
if (data.configurationType === OIDCConfigurationType.CUSTOM) {
|
||||||
@@ -334,7 +338,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
|
|||||||
userinfoEndpoint: true,
|
userinfoEndpoint: true,
|
||||||
orgId: true,
|
orgId: true,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
allowedEmailDomains: true
|
allowedEmailDomains: true,
|
||||||
|
manageGroupMemberships: true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -350,4 +355,25 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
|
|||||||
return oidc;
|
return oidc;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/manage-group-memberships",
|
||||||
|
schema: {
|
||||||
|
querystring: z.object({
|
||||||
|
orgId: z.string().trim().min(1, "Org ID is required")
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
isEnabled: z.boolean()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const isEnabled = await server.services.oidc.isOidcManageGroupMembershipsEnabled(req.query.orgId, req.permission);
|
||||||
|
|
||||||
|
return { isEnabled };
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
@@ -39,11 +39,13 @@ export const auditLogDALFactory = (db: TDbClient) => {
|
|||||||
offset = 0,
|
offset = 0,
|
||||||
actorId,
|
actorId,
|
||||||
actorType,
|
actorType,
|
||||||
|
secretPath,
|
||||||
eventType,
|
eventType,
|
||||||
eventMetadata
|
eventMetadata
|
||||||
}: Omit<TFindQuery, "actor" | "eventType"> & {
|
}: Omit<TFindQuery, "actor" | "eventType"> & {
|
||||||
actorId?: string;
|
actorId?: string;
|
||||||
actorType?: ActorType;
|
actorType?: ActorType;
|
||||||
|
secretPath?: string;
|
||||||
eventType?: EventType[];
|
eventType?: EventType[];
|
||||||
eventMetadata?: Record<string, string>;
|
eventMetadata?: Record<string, string>;
|
||||||
},
|
},
|
||||||
@@ -88,6 +90,10 @@ export const auditLogDALFactory = (db: TDbClient) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (projectId && secretPath) {
|
||||||
|
void sqlQuery.whereRaw(`"eventMetadata" @> jsonb_build_object('secretPath', ?::text)`, [secretPath]);
|
||||||
|
}
|
||||||
|
|
||||||
// Filter by actor type
|
// Filter by actor type
|
||||||
if (actorType) {
|
if (actorType) {
|
||||||
void sqlQuery.where("actor", actorType);
|
void sqlQuery.where("actor", actorType);
|
||||||
|
@@ -46,10 +46,6 @@ export const auditLogServiceFactory = ({
|
|||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
* NOTE (dangtony98): Update this to organization-level audit log permission check once audit logs are moved
|
|
||||||
* to the organization level ✅
|
|
||||||
*/
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,6 +60,7 @@ export const auditLogServiceFactory = ({
|
|||||||
actorId: filter.auditLogActorId,
|
actorId: filter.auditLogActorId,
|
||||||
actorType: filter.actorType,
|
actorType: filter.actorType,
|
||||||
eventMetadata: filter.eventMetadata,
|
eventMetadata: filter.eventMetadata,
|
||||||
|
secretPath: filter.secretPath,
|
||||||
...(filter.projectId ? { projectId: filter.projectId } : { orgId: actorOrgId })
|
...(filter.projectId ? { projectId: filter.projectId } : { orgId: actorOrgId })
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -32,6 +32,7 @@ export type TListProjectAuditLogDTO = {
|
|||||||
projectId?: string;
|
projectId?: string;
|
||||||
auditLogActorId?: string;
|
auditLogActorId?: string;
|
||||||
actorType?: ActorType;
|
actorType?: ActorType;
|
||||||
|
secretPath?: string;
|
||||||
eventMetadata?: Record<string, string>;
|
eventMetadata?: Record<string, string>;
|
||||||
};
|
};
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
@@ -248,7 +249,9 @@ export enum EventType {
|
|||||||
DELETE_SECRET_SYNC = "delete-secret-sync",
|
DELETE_SECRET_SYNC = "delete-secret-sync",
|
||||||
SECRET_SYNC_SYNC_SECRETS = "secret-sync-sync-secrets",
|
SECRET_SYNC_SYNC_SECRETS = "secret-sync-sync-secrets",
|
||||||
SECRET_SYNC_IMPORT_SECRETS = "secret-sync-import-secrets",
|
SECRET_SYNC_IMPORT_SECRETS = "secret-sync-import-secrets",
|
||||||
SECRET_SYNC_REMOVE_SECRETS = "secret-sync-remove-secrets"
|
SECRET_SYNC_REMOVE_SECRETS = "secret-sync-remove-secrets",
|
||||||
|
OIDC_GROUP_MEMBERSHIP_MAPPING_ASSIGN_USER = "oidc-group-membership-mapping-assign-user",
|
||||||
|
OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER = "oidc-group-membership-mapping-remove-user"
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserActorMetadata {
|
interface UserActorMetadata {
|
||||||
@@ -314,6 +317,8 @@ interface GetSecretsEvent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TSecretMetadata = { key: string; value: string }[];
|
||||||
|
|
||||||
interface GetSecretEvent {
|
interface GetSecretEvent {
|
||||||
type: EventType.GET_SECRET;
|
type: EventType.GET_SECRET;
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -322,6 +327,7 @@ interface GetSecretEvent {
|
|||||||
secretId: string;
|
secretId: string;
|
||||||
secretKey: string;
|
secretKey: string;
|
||||||
secretVersion: number;
|
secretVersion: number;
|
||||||
|
secretMetadata?: TSecretMetadata;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,6 +339,7 @@ interface CreateSecretEvent {
|
|||||||
secretId: string;
|
secretId: string;
|
||||||
secretKey: string;
|
secretKey: string;
|
||||||
secretVersion: number;
|
secretVersion: number;
|
||||||
|
secretMetadata?: TSecretMetadata;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,7 +348,12 @@ interface CreateSecretBatchEvent {
|
|||||||
metadata: {
|
metadata: {
|
||||||
environment: string;
|
environment: string;
|
||||||
secretPath: string;
|
secretPath: string;
|
||||||
secrets: Array<{ secretId: string; secretKey: string; secretVersion: number }>;
|
secrets: Array<{
|
||||||
|
secretId: string;
|
||||||
|
secretKey: string;
|
||||||
|
secretVersion: number;
|
||||||
|
secretMetadata?: TSecretMetadata;
|
||||||
|
}>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,6 +365,7 @@ interface UpdateSecretEvent {
|
|||||||
secretId: string;
|
secretId: string;
|
||||||
secretKey: string;
|
secretKey: string;
|
||||||
secretVersion: number;
|
secretVersion: number;
|
||||||
|
secretMetadata?: TSecretMetadata;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -361,7 +374,7 @@ interface UpdateSecretBatchEvent {
|
|||||||
metadata: {
|
metadata: {
|
||||||
environment: string;
|
environment: string;
|
||||||
secretPath: string;
|
secretPath: string;
|
||||||
secrets: Array<{ secretId: string; secretKey: string; secretVersion: number }>;
|
secrets: Array<{ secretId: string; secretKey: string; secretVersion: number; secretMetadata?: TSecretMetadata }>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -759,9 +772,9 @@ interface AddIdentityGcpAuthEvent {
|
|||||||
metadata: {
|
metadata: {
|
||||||
identityId: string;
|
identityId: string;
|
||||||
type: string;
|
type: string;
|
||||||
allowedServiceAccounts: string;
|
allowedServiceAccounts?: string | null;
|
||||||
allowedProjects: string;
|
allowedProjects?: string | null;
|
||||||
allowedZones: string;
|
allowedZones?: string | null;
|
||||||
accessTokenTTL: number;
|
accessTokenTTL: number;
|
||||||
accessTokenMaxTTL: number;
|
accessTokenMaxTTL: number;
|
||||||
accessTokenNumUsesLimit: number;
|
accessTokenNumUsesLimit: number;
|
||||||
@@ -781,9 +794,9 @@ interface UpdateIdentityGcpAuthEvent {
|
|||||||
metadata: {
|
metadata: {
|
||||||
identityId: string;
|
identityId: string;
|
||||||
type?: string;
|
type?: string;
|
||||||
allowedServiceAccounts?: string;
|
allowedServiceAccounts?: string | null;
|
||||||
allowedProjects?: string;
|
allowedProjects?: string | null;
|
||||||
allowedZones?: string;
|
allowedZones?: string | null;
|
||||||
accessTokenTTL?: number;
|
accessTokenTTL?: number;
|
||||||
accessTokenMaxTTL?: number;
|
accessTokenMaxTTL?: number;
|
||||||
accessTokenNumUsesLimit?: number;
|
accessTokenNumUsesLimit?: number;
|
||||||
@@ -2043,6 +2056,26 @@ interface SecretSyncRemoveSecretsEvent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface OidcGroupMembershipMappingAssignUserEvent {
|
||||||
|
type: EventType.OIDC_GROUP_MEMBERSHIP_MAPPING_ASSIGN_USER;
|
||||||
|
metadata: {
|
||||||
|
assignedToGroups: { id: string; name: string }[];
|
||||||
|
userId: string;
|
||||||
|
userEmail: string;
|
||||||
|
userGroupsClaim: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OidcGroupMembershipMappingRemoveUserEvent {
|
||||||
|
type: EventType.OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER;
|
||||||
|
metadata: {
|
||||||
|
removedFromGroups: { id: string; name: string }[];
|
||||||
|
userId: string;
|
||||||
|
userEmail: string;
|
||||||
|
userGroupsClaim: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export type Event =
|
export type Event =
|
||||||
| GetSecretsEvent
|
| GetSecretsEvent
|
||||||
| GetSecretEvent
|
| GetSecretEvent
|
||||||
@@ -2231,4 +2264,6 @@ export type Event =
|
|||||||
| DeleteSecretSyncEvent
|
| DeleteSecretSyncEvent
|
||||||
| SecretSyncSyncSecretsEvent
|
| SecretSyncSyncSecretsEvent
|
||||||
| SecretSyncImportSecretsEvent
|
| SecretSyncImportSecretsEvent
|
||||||
| SecretSyncRemoveSecretsEvent;
|
| SecretSyncRemoveSecretsEvent
|
||||||
|
| OidcGroupMembershipMappingAssignUserEvent
|
||||||
|
| OidcGroupMembershipMappingRemoveUserEvent;
|
||||||
|
@@ -2,6 +2,7 @@ import { ForbiddenError } from "@casl/ability";
|
|||||||
import slugify from "@sindresorhus/slugify";
|
import slugify from "@sindresorhus/slugify";
|
||||||
|
|
||||||
import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas";
|
import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas";
|
||||||
|
import { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal";
|
||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
@@ -45,6 +46,7 @@ type TGroupServiceFactoryDep = {
|
|||||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "findLatestProjectKey" | "insertMany">;
|
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "findLatestProjectKey" | "insertMany">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">;
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
|
oidcConfigDAL: Pick<TOidcConfigDALFactory, "findOne">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TGroupServiceFactory = ReturnType<typeof groupServiceFactory>;
|
export type TGroupServiceFactory = ReturnType<typeof groupServiceFactory>;
|
||||||
@@ -59,7 +61,8 @@ export const groupServiceFactory = ({
|
|||||||
projectBotDAL,
|
projectBotDAL,
|
||||||
projectKeyDAL,
|
projectKeyDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
licenseService
|
licenseService,
|
||||||
|
oidcConfigDAL
|
||||||
}: TGroupServiceFactoryDep) => {
|
}: TGroupServiceFactoryDep) => {
|
||||||
const createGroup = async ({ name, slug, role, actor, actorId, actorAuthMethod, actorOrgId }: TCreateGroupDTO) => {
|
const createGroup = async ({ name, slug, role, actor, actorId, actorAuthMethod, actorOrgId }: TCreateGroupDTO) => {
|
||||||
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
|
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
|
||||||
@@ -311,6 +314,18 @@ export const groupServiceFactory = ({
|
|||||||
message: `Failed to find group with ID ${id}`
|
message: `Failed to find group with ID ${id}`
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const oidcConfig = await oidcConfigDAL.findOne({
|
||||||
|
orgId: group.orgId,
|
||||||
|
isActive: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (oidcConfig?.manageGroupMemberships) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"Cannot add user to group: OIDC group membership mapping is enabled - user must be assigned to this group in your OIDC provider."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
|
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
|
||||||
|
|
||||||
// check if user has broader or equal to privileges than group
|
// check if user has broader or equal to privileges than group
|
||||||
@@ -366,6 +381,18 @@ export const groupServiceFactory = ({
|
|||||||
message: `Failed to find group with ID ${id}`
|
message: `Failed to find group with ID ${id}`
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const oidcConfig = await oidcConfigDAL.findOne({
|
||||||
|
orgId: group.orgId,
|
||||||
|
isActive: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (oidcConfig?.manageGroupMemberships) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"Cannot remove user from group: OIDC group membership mapping is enabled - user must be removed from this group in your OIDC provider."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
|
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
|
||||||
|
|
||||||
// check if user has broader or equal to privileges than group
|
// check if user has broader or equal to privileges than group
|
||||||
|
@@ -5,6 +5,11 @@ import { Issuer, Issuer as OpenIdIssuer, Strategy as OpenIdStrategy, TokenSet }
|
|||||||
|
|
||||||
import { OrgMembershipStatus, SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas";
|
import { OrgMembershipStatus, SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas";
|
||||||
import { TOidcConfigsUpdate } from "@app/db/schemas/oidc-configs";
|
import { TOidcConfigsUpdate } from "@app/db/schemas/oidc-configs";
|
||||||
|
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
|
||||||
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
|
||||||
|
import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns";
|
||||||
|
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
@@ -18,13 +23,18 @@ import {
|
|||||||
infisicalSymmetricEncypt
|
infisicalSymmetricEncypt
|
||||||
} from "@app/lib/crypto/encryption";
|
} from "@app/lib/crypto/encryption";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, OidcAuthError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError, OidcAuthError } from "@app/lib/errors";
|
||||||
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
|
import { OrgServiceActor } from "@app/lib/types";
|
||||||
|
import { ActorType, AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
|
||||||
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||||
import { TokenType } from "@app/services/auth-token/auth-token-types";
|
import { TokenType } from "@app/services/auth-token/auth-token-types";
|
||||||
|
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
||||||
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
|
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
|
||||||
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||||
import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns";
|
import { getDefaultOrgMembershipRole } from "@app/services/org/org-role-fns";
|
||||||
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
||||||
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
|
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
|
||||||
|
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
|
||||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||||
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
||||||
import { LoginMethod } from "@app/services/super-admin/super-admin-types";
|
import { LoginMethod } from "@app/services/super-admin/super-admin-types";
|
||||||
@@ -45,7 +55,14 @@ import {
|
|||||||
type TOidcConfigServiceFactoryDep = {
|
type TOidcConfigServiceFactoryDep = {
|
||||||
userDAL: Pick<
|
userDAL: Pick<
|
||||||
TUserDALFactory,
|
TUserDALFactory,
|
||||||
"create" | "findOne" | "transaction" | "updateById" | "findById" | "findUserEncKeyByUserId"
|
| "create"
|
||||||
|
| "findOne"
|
||||||
|
| "updateById"
|
||||||
|
| "findById"
|
||||||
|
| "findUserEncKeyByUserId"
|
||||||
|
| "findUserEncKeyByUserIdsBatch"
|
||||||
|
| "find"
|
||||||
|
| "transaction"
|
||||||
>;
|
>;
|
||||||
userAliasDAL: Pick<TUserAliasDALFactory, "create" | "findOne">;
|
userAliasDAL: Pick<TUserAliasDALFactory, "create" | "findOne">;
|
||||||
orgDAL: Pick<
|
orgDAL: Pick<
|
||||||
@@ -57,8 +74,23 @@ type TOidcConfigServiceFactoryDep = {
|
|||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgMemberCount">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgMemberCount">;
|
||||||
tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser">;
|
tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser">;
|
||||||
smtpService: Pick<TSmtpService, "sendMail" | "verify">;
|
smtpService: Pick<TSmtpService, "sendMail" | "verify">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getUserOrgPermission">;
|
||||||
oidcConfigDAL: Pick<TOidcConfigDALFactory, "findOne" | "update" | "create">;
|
oidcConfigDAL: Pick<TOidcConfigDALFactory, "findOne" | "update" | "create">;
|
||||||
|
groupDAL: Pick<TGroupDALFactory, "findByOrgId">;
|
||||||
|
userGroupMembershipDAL: Pick<
|
||||||
|
TUserGroupMembershipDALFactory,
|
||||||
|
| "find"
|
||||||
|
| "transaction"
|
||||||
|
| "insertMany"
|
||||||
|
| "findGroupMembershipsByUserIdInOrg"
|
||||||
|
| "delete"
|
||||||
|
| "filterProjectsByUserMembership"
|
||||||
|
>;
|
||||||
|
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
||||||
|
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany" | "delete">;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
|
||||||
|
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||||
|
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TOidcConfigServiceFactory = ReturnType<typeof oidcConfigServiceFactory>;
|
export type TOidcConfigServiceFactory = ReturnType<typeof oidcConfigServiceFactory>;
|
||||||
@@ -73,7 +105,14 @@ export const oidcConfigServiceFactory = ({
|
|||||||
tokenService,
|
tokenService,
|
||||||
orgBotDAL,
|
orgBotDAL,
|
||||||
smtpService,
|
smtpService,
|
||||||
oidcConfigDAL
|
oidcConfigDAL,
|
||||||
|
userGroupMembershipDAL,
|
||||||
|
groupDAL,
|
||||||
|
groupProjectDAL,
|
||||||
|
projectKeyDAL,
|
||||||
|
projectDAL,
|
||||||
|
projectBotDAL,
|
||||||
|
auditLogService
|
||||||
}: TOidcConfigServiceFactoryDep) => {
|
}: TOidcConfigServiceFactoryDep) => {
|
||||||
const getOidc = async (dto: TGetOidcCfgDTO) => {
|
const getOidc = async (dto: TGetOidcCfgDTO) => {
|
||||||
const org = await orgDAL.findOne({ slug: dto.orgSlug });
|
const org = await orgDAL.findOne({ slug: dto.orgSlug });
|
||||||
@@ -156,11 +195,21 @@ export const oidcConfigServiceFactory = ({
|
|||||||
isActive: oidcCfg.isActive,
|
isActive: oidcCfg.isActive,
|
||||||
allowedEmailDomains: oidcCfg.allowedEmailDomains,
|
allowedEmailDomains: oidcCfg.allowedEmailDomains,
|
||||||
clientId,
|
clientId,
|
||||||
clientSecret
|
clientSecret,
|
||||||
|
manageGroupMemberships: oidcCfg.manageGroupMemberships
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const oidcLogin = async ({ externalId, email, firstName, lastName, orgId, callbackPort }: TOidcLoginDTO) => {
|
const oidcLogin = async ({
|
||||||
|
externalId,
|
||||||
|
email,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
orgId,
|
||||||
|
callbackPort,
|
||||||
|
groups = [],
|
||||||
|
manageGroupMemberships
|
||||||
|
}: TOidcLoginDTO) => {
|
||||||
const serverCfg = await getServerCfg();
|
const serverCfg = await getServerCfg();
|
||||||
|
|
||||||
if (serverCfg.enabledLoginMethods && !serverCfg.enabledLoginMethods.includes(LoginMethod.OIDC)) {
|
if (serverCfg.enabledLoginMethods && !serverCfg.enabledLoginMethods.includes(LoginMethod.OIDC)) {
|
||||||
@@ -315,6 +364,83 @@ export const oidcConfigServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (manageGroupMemberships) {
|
||||||
|
const userGroups = await userGroupMembershipDAL.findGroupMembershipsByUserIdInOrg(user.id, orgId);
|
||||||
|
const orgGroups = await groupDAL.findByOrgId(orgId);
|
||||||
|
|
||||||
|
const userGroupsNames = userGroups.map((membership) => membership.groupName);
|
||||||
|
const missingGroupsMemberships = groups.filter((groupName) => !userGroupsNames.includes(groupName));
|
||||||
|
const groupsToAddUserTo = orgGroups.filter((group) => missingGroupsMemberships.includes(group.name));
|
||||||
|
|
||||||
|
for await (const group of groupsToAddUserTo) {
|
||||||
|
await addUsersToGroupByUserIds({
|
||||||
|
userIds: [user.id],
|
||||||
|
group,
|
||||||
|
userDAL,
|
||||||
|
userGroupMembershipDAL,
|
||||||
|
orgDAL,
|
||||||
|
groupProjectDAL,
|
||||||
|
projectKeyDAL,
|
||||||
|
projectDAL,
|
||||||
|
projectBotDAL
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (groupsToAddUserTo.length) {
|
||||||
|
await auditLogService.createAuditLog({
|
||||||
|
actor: {
|
||||||
|
type: ActorType.PLATFORM,
|
||||||
|
metadata: {}
|
||||||
|
},
|
||||||
|
orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.OIDC_GROUP_MEMBERSHIP_MAPPING_ASSIGN_USER,
|
||||||
|
metadata: {
|
||||||
|
userId: user.id,
|
||||||
|
userEmail: user.email ?? user.username,
|
||||||
|
assignedToGroups: groupsToAddUserTo.map(({ id, name }) => ({ id, name })),
|
||||||
|
userGroupsClaim: groups
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const membershipsToRemove = userGroups
|
||||||
|
.filter((membership) => !groups.includes(membership.groupName))
|
||||||
|
.map((membership) => membership.groupId);
|
||||||
|
const groupsToRemoveUserFrom = orgGroups.filter((group) => membershipsToRemove.includes(group.id));
|
||||||
|
|
||||||
|
for await (const group of groupsToRemoveUserFrom) {
|
||||||
|
await removeUsersFromGroupByUserIds({
|
||||||
|
userIds: [user.id],
|
||||||
|
group,
|
||||||
|
userDAL,
|
||||||
|
userGroupMembershipDAL,
|
||||||
|
groupProjectDAL,
|
||||||
|
projectKeyDAL
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (groupsToRemoveUserFrom.length) {
|
||||||
|
await auditLogService.createAuditLog({
|
||||||
|
actor: {
|
||||||
|
type: ActorType.PLATFORM,
|
||||||
|
metadata: {}
|
||||||
|
},
|
||||||
|
orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER,
|
||||||
|
metadata: {
|
||||||
|
userId: user.id,
|
||||||
|
userEmail: user.email ?? user.username,
|
||||||
|
removedFromGroups: groupsToRemoveUserFrom.map(({ id, name }) => ({ id, name })),
|
||||||
|
userGroupsClaim: groups
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await licenseService.updateSubscriptionOrgMemberCount(organization.id);
|
await licenseService.updateSubscriptionOrgMemberCount(organization.id);
|
||||||
|
|
||||||
const userEnc = await userDAL.findUserEncKeyByUserId(user.id);
|
const userEnc = await userDAL.findUserEncKeyByUserId(user.id);
|
||||||
@@ -385,7 +511,8 @@ export const oidcConfigServiceFactory = ({
|
|||||||
tokenEndpoint,
|
tokenEndpoint,
|
||||||
userinfoEndpoint,
|
userinfoEndpoint,
|
||||||
clientId,
|
clientId,
|
||||||
clientSecret
|
clientSecret,
|
||||||
|
manageGroupMemberships
|
||||||
}: TUpdateOidcCfgDTO) => {
|
}: TUpdateOidcCfgDTO) => {
|
||||||
const org = await orgDAL.findOne({
|
const org = await orgDAL.findOne({
|
||||||
slug: orgSlug
|
slug: orgSlug
|
||||||
@@ -448,7 +575,8 @@ export const oidcConfigServiceFactory = ({
|
|||||||
userinfoEndpoint,
|
userinfoEndpoint,
|
||||||
jwksUri,
|
jwksUri,
|
||||||
isActive,
|
isActive,
|
||||||
lastUsed: null
|
lastUsed: null,
|
||||||
|
manageGroupMemberships
|
||||||
};
|
};
|
||||||
|
|
||||||
if (clientId !== undefined) {
|
if (clientId !== undefined) {
|
||||||
@@ -491,7 +619,8 @@ export const oidcConfigServiceFactory = ({
|
|||||||
tokenEndpoint,
|
tokenEndpoint,
|
||||||
userinfoEndpoint,
|
userinfoEndpoint,
|
||||||
clientId,
|
clientId,
|
||||||
clientSecret
|
clientSecret,
|
||||||
|
manageGroupMemberships
|
||||||
}: TCreateOidcCfgDTO) => {
|
}: TCreateOidcCfgDTO) => {
|
||||||
const org = await orgDAL.findOne({
|
const org = await orgDAL.findOne({
|
||||||
slug: orgSlug
|
slug: orgSlug
|
||||||
@@ -589,7 +718,8 @@ export const oidcConfigServiceFactory = ({
|
|||||||
clientIdTag,
|
clientIdTag,
|
||||||
encryptedClientSecret,
|
encryptedClientSecret,
|
||||||
clientSecretIV,
|
clientSecretIV,
|
||||||
clientSecretTag
|
clientSecretTag,
|
||||||
|
manageGroupMemberships
|
||||||
});
|
});
|
||||||
|
|
||||||
return oidcCfg;
|
return oidcCfg;
|
||||||
@@ -683,7 +813,9 @@ export const oidcConfigServiceFactory = ({
|
|||||||
firstName: claims.given_name ?? "",
|
firstName: claims.given_name ?? "",
|
||||||
lastName: claims.family_name ?? "",
|
lastName: claims.family_name ?? "",
|
||||||
orgId: org.id,
|
orgId: org.id,
|
||||||
callbackPort
|
groups: claims.groups as string[] | undefined,
|
||||||
|
callbackPort,
|
||||||
|
manageGroupMemberships: oidcCfg.manageGroupMemberships
|
||||||
})
|
})
|
||||||
.then(({ isUserCompleted, providerAuthToken }) => {
|
.then(({ isUserCompleted, providerAuthToken }) => {
|
||||||
cb(null, { isUserCompleted, providerAuthToken });
|
cb(null, { isUserCompleted, providerAuthToken });
|
||||||
@@ -697,5 +829,16 @@ export const oidcConfigServiceFactory = ({
|
|||||||
return strategy;
|
return strategy;
|
||||||
};
|
};
|
||||||
|
|
||||||
return { oidcLogin, getOrgAuthStrategy, getOidc, updateOidcCfg, createOidcCfg };
|
const isOidcManageGroupMembershipsEnabled = async (orgId: string, actor: OrgServiceActor) => {
|
||||||
|
await permissionService.getUserOrgPermission(actor.id, orgId, actor.authMethod, actor.orgId);
|
||||||
|
|
||||||
|
const oidcConfig = await oidcConfigDAL.findOne({
|
||||||
|
orgId,
|
||||||
|
isActive: true
|
||||||
|
});
|
||||||
|
|
||||||
|
return Boolean(oidcConfig?.manageGroupMemberships);
|
||||||
|
};
|
||||||
|
|
||||||
|
return { oidcLogin, getOrgAuthStrategy, getOidc, updateOidcCfg, createOidcCfg, isOidcManageGroupMembershipsEnabled };
|
||||||
};
|
};
|
||||||
|
@@ -12,6 +12,8 @@ export type TOidcLoginDTO = {
|
|||||||
lastName?: string;
|
lastName?: string;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
callbackPort?: string;
|
callbackPort?: string;
|
||||||
|
groups?: string[];
|
||||||
|
manageGroupMemberships?: boolean | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TGetOidcCfgDTO =
|
export type TGetOidcCfgDTO =
|
||||||
@@ -37,6 +39,7 @@ export type TCreateOidcCfgDTO = {
|
|||||||
clientSecret: string;
|
clientSecret: string;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
orgSlug: string;
|
orgSlug: string;
|
||||||
|
manageGroupMemberships: boolean;
|
||||||
} & TGenericPermission;
|
} & TGenericPermission;
|
||||||
|
|
||||||
export type TUpdateOidcCfgDTO = Partial<{
|
export type TUpdateOidcCfgDTO = Partial<{
|
||||||
@@ -52,5 +55,6 @@ export type TUpdateOidcCfgDTO = Partial<{
|
|||||||
clientSecret: string;
|
clientSecret: string;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
orgSlug: string;
|
orgSlug: string;
|
||||||
|
manageGroupMemberships: boolean;
|
||||||
}> &
|
}> &
|
||||||
TGenericPermission;
|
TGenericPermission;
|
||||||
|
@@ -163,6 +163,27 @@ export type ProjectPermissionSet =
|
|||||||
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]
|
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]
|
||||||
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms];
|
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms];
|
||||||
|
|
||||||
|
const SECRET_PATH_MISSING_SLASH_ERR_MSG = "Invalid Secret Path; it must start with a '/'";
|
||||||
|
const SECRET_PATH_PERMISSION_OPERATOR_SCHEMA = z.union([
|
||||||
|
z.string().refine((val) => val.startsWith("/"), SECRET_PATH_MISSING_SLASH_ERR_MSG),
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ].refine(
|
||||||
|
(val) => val.startsWith("/"),
|
||||||
|
SECRET_PATH_MISSING_SLASH_ERR_MSG
|
||||||
|
),
|
||||||
|
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ].refine(
|
||||||
|
(val) => val.startsWith("/"),
|
||||||
|
SECRET_PATH_MISSING_SLASH_ERR_MSG
|
||||||
|
),
|
||||||
|
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN].refine(
|
||||||
|
(val) => val.every((el) => el.startsWith("/")),
|
||||||
|
SECRET_PATH_MISSING_SLASH_ERR_MSG
|
||||||
|
),
|
||||||
|
[PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB]
|
||||||
|
})
|
||||||
|
.partial()
|
||||||
|
]);
|
||||||
// akhilmhdh: don't modify this for v2
|
// akhilmhdh: don't modify this for v2
|
||||||
// if you want to update create a new schema
|
// if you want to update create a new schema
|
||||||
const SecretConditionV1Schema = z
|
const SecretConditionV1Schema = z
|
||||||
@@ -177,17 +198,7 @@ const SecretConditionV1Schema = z
|
|||||||
})
|
})
|
||||||
.partial()
|
.partial()
|
||||||
]),
|
]),
|
||||||
secretPath: z.union([
|
secretPath: SECRET_PATH_PERMISSION_OPERATOR_SCHEMA
|
||||||
z.string(),
|
|
||||||
z
|
|
||||||
.object({
|
|
||||||
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
|
|
||||||
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
|
|
||||||
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN],
|
|
||||||
[PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB]
|
|
||||||
})
|
|
||||||
.partial()
|
|
||||||
])
|
|
||||||
})
|
})
|
||||||
.partial();
|
.partial();
|
||||||
|
|
||||||
@@ -204,17 +215,7 @@ const SecretConditionV2Schema = z
|
|||||||
})
|
})
|
||||||
.partial()
|
.partial()
|
||||||
]),
|
]),
|
||||||
secretPath: z.union([
|
secretPath: SECRET_PATH_PERMISSION_OPERATOR_SCHEMA,
|
||||||
z.string(),
|
|
||||||
z
|
|
||||||
.object({
|
|
||||||
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
|
|
||||||
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
|
|
||||||
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN],
|
|
||||||
[PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB]
|
|
||||||
})
|
|
||||||
.partial()
|
|
||||||
]),
|
|
||||||
secretName: z.union([
|
secretName: z.union([
|
||||||
z.string(),
|
z.string(),
|
||||||
z
|
z
|
||||||
|
@@ -688,7 +688,9 @@ export const RAW_SECRETS = {
|
|||||||
environment: "The slug of the environment to list secrets from.",
|
environment: "The slug of the environment to list secrets from.",
|
||||||
secretPath: "The secret path to list secrets from.",
|
secretPath: "The secret path to list secrets from.",
|
||||||
includeImports: "Weather to include imported secrets or not.",
|
includeImports: "Weather to include imported secrets or not.",
|
||||||
tagSlugs: "The comma separated tag slugs to filter secrets."
|
tagSlugs: "The comma separated tag slugs to filter secrets.",
|
||||||
|
metadataFilter:
|
||||||
|
"The secret metadata key-value pairs to filter secrets by. When querying for multiple metadata pairs, the query is treated as an AND operation. Secret metadata format is key=value1,value=value2|key=value3,value=value4."
|
||||||
},
|
},
|
||||||
CREATE: {
|
CREATE: {
|
||||||
secretName: "The name of the secret to create.",
|
secretName: "The name of the secret to create.",
|
||||||
@@ -828,6 +830,8 @@ export const AUDIT_LOGS = {
|
|||||||
projectId:
|
projectId:
|
||||||
"Optionally filter logs by project ID. If not provided, logs from the entire organization will be returned.",
|
"Optionally filter logs by project ID. If not provided, logs from the entire organization will be returned.",
|
||||||
eventType: "The type of the event to export.",
|
eventType: "The type of the event to export.",
|
||||||
|
secretPath:
|
||||||
|
"The path of the secret to query audit logs for. Note that the projectId parameter must also be provided.",
|
||||||
userAgentType: "Choose which consuming application to export audit logs for.",
|
userAgentType: "Choose which consuming application to export audit logs for.",
|
||||||
eventMetadata:
|
eventMetadata:
|
||||||
"Filter by event metadata key-value pairs. Formatted as `key1=value1,key2=value2`, with comma-separation.",
|
"Filter by event metadata key-value pairs. Formatted as `key1=value1,key2=value2`, with comma-separation.",
|
||||||
@@ -1717,6 +1721,12 @@ export const SecretSyncs = {
|
|||||||
REGION: "The AWS region to sync secrets to.",
|
REGION: "The AWS region to sync secrets to.",
|
||||||
PATH: "The Parameter Store path to sync secrets to."
|
PATH: "The Parameter Store path to sync secrets to."
|
||||||
},
|
},
|
||||||
|
AWS_SECRETS_MANAGER: {
|
||||||
|
REGION: "The AWS region to sync secrets to.",
|
||||||
|
MAPPING_BEHAVIOR:
|
||||||
|
"How secrets from Infisical should be mapped to AWS Secrets Manager; one-to-one or many-to-one.",
|
||||||
|
SECRET_NAME: "The secret name in AWS Secrets Manager to sync to when using mapping behavior many-to-one."
|
||||||
|
},
|
||||||
GITHUB: {
|
GITHUB: {
|
||||||
ORG: "The name of the GitHub organization.",
|
ORG: "The name of the GitHub organization.",
|
||||||
OWNER: "The name of the GitHub account owner of the repository.",
|
OWNER: "The name of the GitHub account owner of the repository.",
|
||||||
|
@@ -201,6 +201,9 @@ const envSchema = z
|
|||||||
INF_APP_CONNECTION_GITHUB_APP_SLUG: zpStr(z.string().optional()),
|
INF_APP_CONNECTION_GITHUB_APP_SLUG: zpStr(z.string().optional()),
|
||||||
INF_APP_CONNECTION_GITHUB_APP_ID: zpStr(z.string().optional()),
|
INF_APP_CONNECTION_GITHUB_APP_ID: zpStr(z.string().optional()),
|
||||||
|
|
||||||
|
// gcp app
|
||||||
|
INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL: zpStr(z.string().optional()),
|
||||||
|
|
||||||
/* CORS ----------------------------------------------------------------------------- */
|
/* CORS ----------------------------------------------------------------------------- */
|
||||||
|
|
||||||
CORS_ALLOWED_ORIGINS: zpStr(
|
CORS_ALLOWED_ORIGINS: zpStr(
|
||||||
|
@@ -116,7 +116,7 @@ export const decryptAsymmetric = ({ ciphertext, nonce, publicKey, privateKey }:
|
|||||||
|
|
||||||
export const generateSymmetricKey = (size = 32) => crypto.randomBytes(size).toString("base64");
|
export const generateSymmetricKey = (size = 32) => crypto.randomBytes(size).toString("base64");
|
||||||
|
|
||||||
export const generateHash = (value: string) => crypto.createHash("sha256").update(value).digest("hex");
|
export const generateHash = (value: string | Buffer) => crypto.createHash("sha256").update(value).digest("hex");
|
||||||
|
|
||||||
export const generateAsymmetricKeyPair = () => {
|
export const generateAsymmetricKeyPair = () => {
|
||||||
const pair = nacl.box.keyPair();
|
const pair = nacl.box.keyPair();
|
||||||
|
4
backend/src/lib/error-codes/database.ts
Normal file
4
backend/src/lib/error-codes/database.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export enum DatabaseErrorCode {
|
||||||
|
ForeignKeyViolation = "23503",
|
||||||
|
UniqueViolation = "23505"
|
||||||
|
}
|
1
backend/src/lib/error-codes/index.ts
Normal file
1
backend/src/lib/error-codes/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./database";
|
@@ -467,7 +467,8 @@ export const registerRoutes = async (
|
|||||||
projectBotDAL,
|
projectBotDAL,
|
||||||
projectKeyDAL,
|
projectKeyDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
licenseService
|
licenseService,
|
||||||
|
oidcConfigDAL
|
||||||
});
|
});
|
||||||
const groupProjectService = groupProjectServiceFactory({
|
const groupProjectService = groupProjectServiceFactory({
|
||||||
groupDAL,
|
groupDAL,
|
||||||
@@ -1337,7 +1338,14 @@ export const registerRoutes = async (
|
|||||||
smtpService,
|
smtpService,
|
||||||
orgBotDAL,
|
orgBotDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
oidcConfigDAL
|
oidcConfigDAL,
|
||||||
|
projectBotDAL,
|
||||||
|
projectKeyDAL,
|
||||||
|
projectDAL,
|
||||||
|
userGroupMembershipDAL,
|
||||||
|
groupProjectDAL,
|
||||||
|
groupDAL,
|
||||||
|
auditLogService
|
||||||
});
|
});
|
||||||
|
|
||||||
const userEngagementService = userEngagementServiceFactory({
|
const userEngagementService = userEngagementServiceFactory({
|
||||||
|
@@ -110,7 +110,6 @@ export const secretRawSchema = z.object({
|
|||||||
secretReminderNote: z.string().nullable().optional(),
|
secretReminderNote: z.string().nullable().optional(),
|
||||||
secretReminderRepeatDays: z.number().nullable().optional(),
|
secretReminderRepeatDays: z.number().nullable().optional(),
|
||||||
skipMultilineEncoding: z.boolean().default(false).nullable().optional(),
|
skipMultilineEncoding: z.boolean().default(false).nullable().optional(),
|
||||||
metadata: z.unknown().nullable().optional(),
|
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date()
|
||||||
});
|
});
|
||||||
|
@@ -4,18 +4,21 @@ import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
|||||||
import { readLimit } from "@app/server/config/rateLimiter";
|
import { readLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AwsConnectionListItemSchema, SanitizedAwsConnectionSchema } from "@app/services/app-connection/aws";
|
import { AwsConnectionListItemSchema, SanitizedAwsConnectionSchema } from "@app/services/app-connection/aws";
|
||||||
|
import { GcpConnectionListItemSchema, SanitizedGcpConnectionSchema } from "@app/services/app-connection/gcp";
|
||||||
import { GitHubConnectionListItemSchema, SanitizedGitHubConnectionSchema } from "@app/services/app-connection/github";
|
import { GitHubConnectionListItemSchema, SanitizedGitHubConnectionSchema } from "@app/services/app-connection/github";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
// can't use discriminated due to multiple schemas for certain apps
|
// can't use discriminated due to multiple schemas for certain apps
|
||||||
const SanitizedAppConnectionSchema = z.union([
|
const SanitizedAppConnectionSchema = z.union([
|
||||||
...SanitizedAwsConnectionSchema.options,
|
...SanitizedAwsConnectionSchema.options,
|
||||||
...SanitizedGitHubConnectionSchema.options
|
...SanitizedGitHubConnectionSchema.options,
|
||||||
|
...SanitizedGcpConnectionSchema.options
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||||
AwsConnectionListItemSchema,
|
AwsConnectionListItemSchema,
|
||||||
GitHubConnectionListItemSchema
|
GitHubConnectionListItemSchema,
|
||||||
|
GcpConnectionListItemSchema
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
|
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
|
||||||
|
@@ -0,0 +1,48 @@
|
|||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
import { readLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import {
|
||||||
|
CreateGcpConnectionSchema,
|
||||||
|
SanitizedGcpConnectionSchema,
|
||||||
|
UpdateGcpConnectionSchema
|
||||||
|
} from "@app/services/app-connection/gcp";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||||
|
|
||||||
|
export const registerGcpConnectionRouter = async (server: FastifyZodProvider) => {
|
||||||
|
registerAppConnectionEndpoints({
|
||||||
|
app: AppConnection.GCP,
|
||||||
|
server,
|
||||||
|
sanitizedResponseSchema: SanitizedGcpConnectionSchema,
|
||||||
|
createSchema: CreateGcpConnectionSchema,
|
||||||
|
updateSchema: UpdateGcpConnectionSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
// The below endpoints are not exposed and for Infisical App use
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: `/:connectionId/secret-manager-projects`,
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
connectionId: z.string().uuid()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({ id: z.string(), name: z.string() }).array()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { connectionId } = req.params;
|
||||||
|
|
||||||
|
const projects = await server.services.appConnection.gcp.listSecretManagerProjects(connectionId, req.permission);
|
||||||
|
|
||||||
|
return projects;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@@ -1,6 +1,7 @@
|
|||||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
|
||||||
import { registerAwsConnectionRouter } from "./aws-connection-router";
|
import { registerAwsConnectionRouter } from "./aws-connection-router";
|
||||||
|
import { registerGcpConnectionRouter } from "./gcp-connection-router";
|
||||||
import { registerGitHubConnectionRouter } from "./github-connection-router";
|
import { registerGitHubConnectionRouter } from "./github-connection-router";
|
||||||
|
|
||||||
export * from "./app-connection-router";
|
export * from "./app-connection-router";
|
||||||
@@ -8,5 +9,6 @@ export * from "./app-connection-router";
|
|||||||
export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server: FastifyZodProvider) => Promise<void>> =
|
export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server: FastifyZodProvider) => Promise<void>> =
|
||||||
{
|
{
|
||||||
[AppConnection.AWS]: registerAwsConnectionRouter,
|
[AppConnection.AWS]: registerAwsConnectionRouter,
|
||||||
[AppConnection.GitHub]: registerGitHubConnectionRouter
|
[AppConnection.GitHub]: registerGitHubConnectionRouter,
|
||||||
|
[AppConnection.GCP]: registerGcpConnectionRouter
|
||||||
};
|
};
|
||||||
|
@@ -79,44 +79,44 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider)
|
|||||||
params: z.object({
|
params: z.object({
|
||||||
identityId: z.string().trim().describe(AWS_AUTH.ATTACH.identityId)
|
identityId: z.string().trim().describe(AWS_AUTH.ATTACH.identityId)
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z
|
||||||
stsEndpoint: z
|
.object({
|
||||||
.string()
|
stsEndpoint: z
|
||||||
.trim()
|
.string()
|
||||||
.min(1)
|
.trim()
|
||||||
.default("https://sts.amazonaws.com/")
|
.min(1)
|
||||||
.describe(AWS_AUTH.ATTACH.stsEndpoint),
|
.default("https://sts.amazonaws.com/")
|
||||||
allowedPrincipalArns: validatePrincipalArns.describe(AWS_AUTH.ATTACH.allowedPrincipalArns),
|
.describe(AWS_AUTH.ATTACH.stsEndpoint),
|
||||||
allowedAccountIds: validateAccountIds.describe(AWS_AUTH.ATTACH.allowedAccountIds),
|
allowedPrincipalArns: validatePrincipalArns.describe(AWS_AUTH.ATTACH.allowedPrincipalArns),
|
||||||
accessTokenTrustedIps: z
|
allowedAccountIds: validateAccountIds.describe(AWS_AUTH.ATTACH.allowedAccountIds),
|
||||||
.object({
|
accessTokenTrustedIps: z
|
||||||
ipAddress: z.string().trim()
|
.object({
|
||||||
})
|
ipAddress: z.string().trim()
|
||||||
.array()
|
})
|
||||||
.min(1)
|
.array()
|
||||||
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
|
.min(1)
|
||||||
.describe(AWS_AUTH.ATTACH.accessTokenTrustedIps),
|
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
|
||||||
accessTokenTTL: z
|
.describe(AWS_AUTH.ATTACH.accessTokenTrustedIps),
|
||||||
.number()
|
accessTokenTTL: z
|
||||||
.int()
|
.number()
|
||||||
.min(1)
|
.int()
|
||||||
.max(315360000)
|
.min(0)
|
||||||
.refine((value) => value !== 0, {
|
.max(315360000)
|
||||||
message: "accessTokenTTL must have a non zero number"
|
.default(2592000)
|
||||||
})
|
.describe(AWS_AUTH.ATTACH.accessTokenTTL),
|
||||||
.default(2592000)
|
accessTokenMaxTTL: z
|
||||||
.describe(AWS_AUTH.ATTACH.accessTokenTTL),
|
.number()
|
||||||
accessTokenMaxTTL: z
|
.int()
|
||||||
.number()
|
.min(1)
|
||||||
.int()
|
.max(315360000)
|
||||||
.max(315360000)
|
.default(2592000)
|
||||||
.refine((value) => value !== 0, {
|
.describe(AWS_AUTH.ATTACH.accessTokenMaxTTL),
|
||||||
message: "accessTokenMaxTTL must have a non zero number"
|
accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(AWS_AUTH.ATTACH.accessTokenNumUsesLimit)
|
||||||
})
|
})
|
||||||
.default(2592000)
|
.refine(
|
||||||
.describe(AWS_AUTH.ATTACH.accessTokenMaxTTL),
|
(val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
|
||||||
accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(AWS_AUTH.ATTACH.accessTokenNumUsesLimit)
|
"Access Token TTL cannot be greater than Access Token Max TTL."
|
||||||
}),
|
),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
identityAwsAuth: IdentityAwsAuthsSchema
|
identityAwsAuth: IdentityAwsAuthsSchema
|
||||||
@@ -172,30 +172,33 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider)
|
|||||||
params: z.object({
|
params: z.object({
|
||||||
identityId: z.string().describe(AWS_AUTH.UPDATE.identityId)
|
identityId: z.string().describe(AWS_AUTH.UPDATE.identityId)
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z
|
||||||
stsEndpoint: z.string().trim().min(1).optional().describe(AWS_AUTH.UPDATE.stsEndpoint),
|
.object({
|
||||||
allowedPrincipalArns: validatePrincipalArns.describe(AWS_AUTH.UPDATE.allowedPrincipalArns),
|
stsEndpoint: z.string().trim().min(1).optional().describe(AWS_AUTH.UPDATE.stsEndpoint),
|
||||||
allowedAccountIds: validateAccountIds.describe(AWS_AUTH.UPDATE.allowedAccountIds),
|
allowedPrincipalArns: validatePrincipalArns.describe(AWS_AUTH.UPDATE.allowedPrincipalArns),
|
||||||
accessTokenTrustedIps: z
|
allowedAccountIds: validateAccountIds.describe(AWS_AUTH.UPDATE.allowedAccountIds),
|
||||||
.object({
|
accessTokenTrustedIps: z
|
||||||
ipAddress: z.string().trim()
|
.object({
|
||||||
})
|
ipAddress: z.string().trim()
|
||||||
.array()
|
})
|
||||||
.min(1)
|
.array()
|
||||||
.optional()
|
.min(1)
|
||||||
.describe(AWS_AUTH.UPDATE.accessTokenTrustedIps),
|
.optional()
|
||||||
accessTokenTTL: z.number().int().min(0).max(315360000).optional().describe(AWS_AUTH.UPDATE.accessTokenTTL),
|
.describe(AWS_AUTH.UPDATE.accessTokenTrustedIps),
|
||||||
accessTokenNumUsesLimit: z.number().int().min(0).optional().describe(AWS_AUTH.UPDATE.accessTokenNumUsesLimit),
|
accessTokenTTL: z.number().int().min(0).max(315360000).optional().describe(AWS_AUTH.UPDATE.accessTokenTTL),
|
||||||
accessTokenMaxTTL: z
|
accessTokenNumUsesLimit: z.number().int().min(0).optional().describe(AWS_AUTH.UPDATE.accessTokenNumUsesLimit),
|
||||||
.number()
|
accessTokenMaxTTL: z
|
||||||
.int()
|
.number()
|
||||||
.max(315360000)
|
.int()
|
||||||
.refine((value) => value !== 0, {
|
.max(315360000)
|
||||||
message: "accessTokenMaxTTL must have a non zero number"
|
.min(0)
|
||||||
})
|
.optional()
|
||||||
.optional()
|
.describe(AWS_AUTH.UPDATE.accessTokenMaxTTL)
|
||||||
.describe(AWS_AUTH.UPDATE.accessTokenMaxTTL)
|
})
|
||||||
}),
|
.refine(
|
||||||
|
(val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true),
|
||||||
|
"Access Token TTL cannot be greater than Access Token Max TTL."
|
||||||
|
),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
identityAwsAuth: IdentityAwsAuthsSchema
|
identityAwsAuth: IdentityAwsAuthsSchema
|
||||||
|
@@ -76,39 +76,44 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider
|
|||||||
params: z.object({
|
params: z.object({
|
||||||
identityId: z.string().trim().describe(AZURE_AUTH.LOGIN.identityId)
|
identityId: z.string().trim().describe(AZURE_AUTH.LOGIN.identityId)
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z
|
||||||
tenantId: z.string().trim().describe(AZURE_AUTH.ATTACH.tenantId),
|
.object({
|
||||||
resource: z.string().trim().describe(AZURE_AUTH.ATTACH.resource),
|
tenantId: z.string().trim().describe(AZURE_AUTH.ATTACH.tenantId),
|
||||||
allowedServicePrincipalIds: validateAzureAuthField.describe(AZURE_AUTH.ATTACH.allowedServicePrincipalIds),
|
resource: z.string().trim().describe(AZURE_AUTH.ATTACH.resource),
|
||||||
accessTokenTrustedIps: z
|
allowedServicePrincipalIds: validateAzureAuthField.describe(AZURE_AUTH.ATTACH.allowedServicePrincipalIds),
|
||||||
.object({
|
accessTokenTrustedIps: z
|
||||||
ipAddress: z.string().trim()
|
.object({
|
||||||
})
|
ipAddress: z.string().trim()
|
||||||
.array()
|
})
|
||||||
.min(1)
|
.array()
|
||||||
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
|
.min(1)
|
||||||
.describe(AZURE_AUTH.ATTACH.accessTokenTrustedIps),
|
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
|
||||||
accessTokenTTL: z
|
.describe(AZURE_AUTH.ATTACH.accessTokenTrustedIps),
|
||||||
.number()
|
accessTokenTTL: z
|
||||||
.int()
|
.number()
|
||||||
.min(1)
|
.int()
|
||||||
.max(315360000)
|
.min(0)
|
||||||
.refine((value) => value !== 0, {
|
.max(315360000)
|
||||||
message: "accessTokenTTL must have a non zero number"
|
.default(2592000)
|
||||||
})
|
.describe(AZURE_AUTH.ATTACH.accessTokenTTL),
|
||||||
.default(2592000)
|
accessTokenMaxTTL: z
|
||||||
.describe(AZURE_AUTH.ATTACH.accessTokenTTL),
|
.number()
|
||||||
accessTokenMaxTTL: z
|
.int()
|
||||||
.number()
|
.min(0)
|
||||||
.int()
|
.max(315360000)
|
||||||
.max(315360000)
|
.default(2592000)
|
||||||
.refine((value) => value !== 0, {
|
.describe(AZURE_AUTH.ATTACH.accessTokenMaxTTL),
|
||||||
message: "accessTokenMaxTTL must have a non zero number"
|
accessTokenNumUsesLimit: z
|
||||||
})
|
.number()
|
||||||
.default(2592000)
|
.int()
|
||||||
.describe(AZURE_AUTH.ATTACH.accessTokenMaxTTL),
|
.min(0)
|
||||||
accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(AZURE_AUTH.ATTACH.accessTokenNumUsesLimit)
|
.default(0)
|
||||||
}),
|
.describe(AZURE_AUTH.ATTACH.accessTokenNumUsesLimit)
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
|
||||||
|
"Access Token TTL cannot be greater than Access Token Max TTL."
|
||||||
|
),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
identityAzureAuth: IdentityAzureAuthsSchema
|
identityAzureAuth: IdentityAzureAuthsSchema
|
||||||
@@ -163,32 +168,40 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider
|
|||||||
params: z.object({
|
params: z.object({
|
||||||
identityId: z.string().trim().describe(AZURE_AUTH.UPDATE.identityId)
|
identityId: z.string().trim().describe(AZURE_AUTH.UPDATE.identityId)
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z
|
||||||
tenantId: z.string().trim().optional().describe(AZURE_AUTH.UPDATE.tenantId),
|
.object({
|
||||||
resource: z.string().trim().optional().describe(AZURE_AUTH.UPDATE.resource),
|
tenantId: z.string().trim().optional().describe(AZURE_AUTH.UPDATE.tenantId),
|
||||||
allowedServicePrincipalIds: validateAzureAuthField
|
resource: z.string().trim().optional().describe(AZURE_AUTH.UPDATE.resource),
|
||||||
.optional()
|
allowedServicePrincipalIds: validateAzureAuthField
|
||||||
.describe(AZURE_AUTH.UPDATE.allowedServicePrincipalIds),
|
.optional()
|
||||||
accessTokenTrustedIps: z
|
.describe(AZURE_AUTH.UPDATE.allowedServicePrincipalIds),
|
||||||
.object({
|
accessTokenTrustedIps: z
|
||||||
ipAddress: z.string().trim()
|
.object({
|
||||||
})
|
ipAddress: z.string().trim()
|
||||||
.array()
|
})
|
||||||
.min(1)
|
.array()
|
||||||
.optional()
|
.min(1)
|
||||||
.describe(AZURE_AUTH.UPDATE.accessTokenTrustedIps),
|
.optional()
|
||||||
accessTokenTTL: z.number().int().min(0).max(315360000).optional().describe(AZURE_AUTH.UPDATE.accessTokenTTL),
|
.describe(AZURE_AUTH.UPDATE.accessTokenTrustedIps),
|
||||||
accessTokenNumUsesLimit: z.number().int().min(0).optional().describe(AZURE_AUTH.UPDATE.accessTokenNumUsesLimit),
|
accessTokenTTL: z.number().int().min(0).max(315360000).optional().describe(AZURE_AUTH.UPDATE.accessTokenTTL),
|
||||||
accessTokenMaxTTL: z
|
accessTokenNumUsesLimit: z
|
||||||
.number()
|
.number()
|
||||||
.int()
|
.int()
|
||||||
.max(315360000)
|
.min(0)
|
||||||
.refine((value) => value !== 0, {
|
.optional()
|
||||||
message: "accessTokenMaxTTL must have a non zero number"
|
.describe(AZURE_AUTH.UPDATE.accessTokenNumUsesLimit),
|
||||||
})
|
accessTokenMaxTTL: z
|
||||||
.optional()
|
.number()
|
||||||
.describe(AZURE_AUTH.UPDATE.accessTokenMaxTTL)
|
.int()
|
||||||
}),
|
.max(315360000)
|
||||||
|
.min(0)
|
||||||
|
.optional()
|
||||||
|
.describe(AZURE_AUTH.UPDATE.accessTokenMaxTTL)
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true),
|
||||||
|
"Access Token TTL cannot be greater than Access Token Max TTL."
|
||||||
|
),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
identityAzureAuth: IdentityAzureAuthsSchema
|
identityAzureAuth: IdentityAzureAuthsSchema
|
||||||
|
@@ -74,40 +74,40 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider)
|
|||||||
params: z.object({
|
params: z.object({
|
||||||
identityId: z.string().trim().describe(GCP_AUTH.ATTACH.identityId)
|
identityId: z.string().trim().describe(GCP_AUTH.ATTACH.identityId)
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z
|
||||||
type: z.enum(["iam", "gce"]),
|
.object({
|
||||||
allowedServiceAccounts: validateGcpAuthField.describe(GCP_AUTH.ATTACH.allowedServiceAccounts),
|
type: z.enum(["iam", "gce"]),
|
||||||
allowedProjects: validateGcpAuthField.describe(GCP_AUTH.ATTACH.allowedProjects),
|
allowedServiceAccounts: validateGcpAuthField.describe(GCP_AUTH.ATTACH.allowedServiceAccounts),
|
||||||
allowedZones: validateGcpAuthField.describe(GCP_AUTH.ATTACH.allowedZones),
|
allowedProjects: validateGcpAuthField.describe(GCP_AUTH.ATTACH.allowedProjects),
|
||||||
accessTokenTrustedIps: z
|
allowedZones: validateGcpAuthField.describe(GCP_AUTH.ATTACH.allowedZones),
|
||||||
.object({
|
accessTokenTrustedIps: z
|
||||||
ipAddress: z.string().trim()
|
.object({
|
||||||
})
|
ipAddress: z.string().trim()
|
||||||
.array()
|
})
|
||||||
.min(1)
|
.array()
|
||||||
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
|
.min(1)
|
||||||
.describe(GCP_AUTH.ATTACH.accessTokenTrustedIps),
|
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
|
||||||
accessTokenTTL: z
|
.describe(GCP_AUTH.ATTACH.accessTokenTrustedIps),
|
||||||
.number()
|
accessTokenTTL: z
|
||||||
.int()
|
.number()
|
||||||
.min(1)
|
.int()
|
||||||
.max(315360000)
|
.min(0)
|
||||||
.refine((value) => value !== 0, {
|
.max(315360000)
|
||||||
message: "accessTokenTTL must have a non zero number"
|
.default(2592000)
|
||||||
})
|
.describe(GCP_AUTH.ATTACH.accessTokenTTL),
|
||||||
.default(2592000)
|
accessTokenMaxTTL: z
|
||||||
.describe(GCP_AUTH.ATTACH.accessTokenTTL),
|
.number()
|
||||||
accessTokenMaxTTL: z
|
.int()
|
||||||
.number()
|
.min(0)
|
||||||
.int()
|
.max(315360000)
|
||||||
.max(315360000)
|
.default(2592000)
|
||||||
.refine((value) => value !== 0, {
|
.describe(GCP_AUTH.ATTACH.accessTokenMaxTTL),
|
||||||
message: "accessTokenMaxTTL must have a non zero number"
|
accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(GCP_AUTH.ATTACH.accessTokenNumUsesLimit)
|
||||||
})
|
})
|
||||||
.default(2592000)
|
.refine(
|
||||||
.describe(GCP_AUTH.ATTACH.accessTokenMaxTTL),
|
(val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
|
||||||
accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(GCP_AUTH.ATTACH.accessTokenNumUsesLimit)
|
"Access Token TTL cannot be greater than Access Token Max TTL."
|
||||||
}),
|
),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
identityGcpAuth: IdentityGcpAuthsSchema
|
identityGcpAuth: IdentityGcpAuthsSchema
|
||||||
@@ -164,31 +164,34 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider)
|
|||||||
params: z.object({
|
params: z.object({
|
||||||
identityId: z.string().trim().describe(GCP_AUTH.UPDATE.identityId)
|
identityId: z.string().trim().describe(GCP_AUTH.UPDATE.identityId)
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z
|
||||||
type: z.enum(["iam", "gce"]).optional(),
|
.object({
|
||||||
allowedServiceAccounts: validateGcpAuthField.optional().describe(GCP_AUTH.UPDATE.allowedServiceAccounts),
|
type: z.enum(["iam", "gce"]).optional(),
|
||||||
allowedProjects: validateGcpAuthField.optional().describe(GCP_AUTH.UPDATE.allowedProjects),
|
allowedServiceAccounts: validateGcpAuthField.optional().describe(GCP_AUTH.UPDATE.allowedServiceAccounts),
|
||||||
allowedZones: validateGcpAuthField.optional().describe(GCP_AUTH.UPDATE.allowedZones),
|
allowedProjects: validateGcpAuthField.optional().describe(GCP_AUTH.UPDATE.allowedProjects),
|
||||||
accessTokenTrustedIps: z
|
allowedZones: validateGcpAuthField.optional().describe(GCP_AUTH.UPDATE.allowedZones),
|
||||||
.object({
|
accessTokenTrustedIps: z
|
||||||
ipAddress: z.string().trim()
|
.object({
|
||||||
})
|
ipAddress: z.string().trim()
|
||||||
.array()
|
})
|
||||||
.min(1)
|
.array()
|
||||||
.optional()
|
.min(1)
|
||||||
.describe(GCP_AUTH.UPDATE.accessTokenTrustedIps),
|
.optional()
|
||||||
accessTokenTTL: z.number().int().min(0).max(315360000).optional().describe(GCP_AUTH.UPDATE.accessTokenTTL),
|
.describe(GCP_AUTH.UPDATE.accessTokenTrustedIps),
|
||||||
accessTokenNumUsesLimit: z.number().int().min(0).optional().describe(GCP_AUTH.UPDATE.accessTokenNumUsesLimit),
|
accessTokenTTL: z.number().int().min(0).max(315360000).optional().describe(GCP_AUTH.UPDATE.accessTokenTTL),
|
||||||
accessTokenMaxTTL: z
|
accessTokenNumUsesLimit: z.number().int().min(0).optional().describe(GCP_AUTH.UPDATE.accessTokenNumUsesLimit),
|
||||||
.number()
|
accessTokenMaxTTL: z
|
||||||
.int()
|
.number()
|
||||||
.max(315360000)
|
.int()
|
||||||
.refine((value) => value !== 0, {
|
.min(0)
|
||||||
message: "accessTokenMaxTTL must have a non zero number"
|
.max(315360000)
|
||||||
})
|
.optional()
|
||||||
.optional()
|
.describe(GCP_AUTH.UPDATE.accessTokenMaxTTL)
|
||||||
.describe(GCP_AUTH.UPDATE.accessTokenMaxTTL)
|
})
|
||||||
}),
|
.refine(
|
||||||
|
(val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true),
|
||||||
|
"Access Token TTL cannot be greater than Access Token Max TTL."
|
||||||
|
),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
identityGcpAuth: IdentityGcpAuthsSchema
|
identityGcpAuth: IdentityGcpAuthsSchema
|
||||||
|
@@ -34,23 +34,12 @@ const CreateBaseSchema = z.object({
|
|||||||
.min(1)
|
.min(1)
|
||||||
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
|
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
|
||||||
.describe(JWT_AUTH.ATTACH.accessTokenTrustedIps),
|
.describe(JWT_AUTH.ATTACH.accessTokenTrustedIps),
|
||||||
accessTokenTTL: z
|
accessTokenTTL: z.number().int().min(0).max(315360000).default(2592000).describe(JWT_AUTH.ATTACH.accessTokenTTL),
|
||||||
.number()
|
|
||||||
.int()
|
|
||||||
.min(1)
|
|
||||||
.max(315360000)
|
|
||||||
.refine((value) => value !== 0, {
|
|
||||||
message: "accessTokenTTL must have a non zero number"
|
|
||||||
})
|
|
||||||
.default(2592000)
|
|
||||||
.describe(JWT_AUTH.ATTACH.accessTokenTTL),
|
|
||||||
accessTokenMaxTTL: z
|
accessTokenMaxTTL: z
|
||||||
.number()
|
.number()
|
||||||
.int()
|
.int()
|
||||||
|
.min(0)
|
||||||
.max(315360000)
|
.max(315360000)
|
||||||
.refine((value) => value !== 0, {
|
|
||||||
message: "accessTokenMaxTTL must have a non zero number"
|
|
||||||
})
|
|
||||||
.default(2592000)
|
.default(2592000)
|
||||||
.describe(JWT_AUTH.ATTACH.accessTokenMaxTTL),
|
.describe(JWT_AUTH.ATTACH.accessTokenMaxTTL),
|
||||||
accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(JWT_AUTH.ATTACH.accessTokenNumUsesLimit)
|
accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(JWT_AUTH.ATTACH.accessTokenNumUsesLimit)
|
||||||
@@ -70,23 +59,12 @@ const UpdateBaseSchema = z
|
|||||||
.min(1)
|
.min(1)
|
||||||
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
|
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
|
||||||
.describe(JWT_AUTH.UPDATE.accessTokenTrustedIps),
|
.describe(JWT_AUTH.UPDATE.accessTokenTrustedIps),
|
||||||
accessTokenTTL: z
|
accessTokenTTL: z.number().int().min(0).max(315360000).default(2592000).describe(JWT_AUTH.UPDATE.accessTokenTTL),
|
||||||
.number()
|
|
||||||
.int()
|
|
||||||
.min(1)
|
|
||||||
.max(315360000)
|
|
||||||
.refine((value) => value !== 0, {
|
|
||||||
message: "accessTokenTTL must have a non zero number"
|
|
||||||
})
|
|
||||||
.default(2592000)
|
|
||||||
.describe(JWT_AUTH.UPDATE.accessTokenTTL),
|
|
||||||
accessTokenMaxTTL: z
|
accessTokenMaxTTL: z
|
||||||
.number()
|
.number()
|
||||||
.int()
|
.int()
|
||||||
|
.min(0)
|
||||||
.max(315360000)
|
.max(315360000)
|
||||||
.refine((value) => value !== 0, {
|
|
||||||
message: "accessTokenMaxTTL must have a non zero number"
|
|
||||||
})
|
|
||||||
.default(2592000)
|
.default(2592000)
|
||||||
.describe(JWT_AUTH.UPDATE.accessTokenMaxTTL),
|
.describe(JWT_AUTH.UPDATE.accessTokenMaxTTL),
|
||||||
accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(JWT_AUTH.UPDATE.accessTokenNumUsesLimit)
|
accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(JWT_AUTH.UPDATE.accessTokenNumUsesLimit)
|
||||||
|
@@ -87,47 +87,47 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
|
|||||||
params: z.object({
|
params: z.object({
|
||||||
identityId: z.string().trim().describe(KUBERNETES_AUTH.ATTACH.identityId)
|
identityId: z.string().trim().describe(KUBERNETES_AUTH.ATTACH.identityId)
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z
|
||||||
kubernetesHost: z.string().trim().min(1).describe(KUBERNETES_AUTH.ATTACH.kubernetesHost),
|
.object({
|
||||||
caCert: z.string().trim().default("").describe(KUBERNETES_AUTH.ATTACH.caCert),
|
kubernetesHost: z.string().trim().min(1).describe(KUBERNETES_AUTH.ATTACH.kubernetesHost),
|
||||||
tokenReviewerJwt: z.string().trim().min(1).describe(KUBERNETES_AUTH.ATTACH.tokenReviewerJwt),
|
caCert: z.string().trim().default("").describe(KUBERNETES_AUTH.ATTACH.caCert),
|
||||||
allowedNamespaces: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedNamespaces), // TODO: validation
|
tokenReviewerJwt: z.string().trim().min(1).describe(KUBERNETES_AUTH.ATTACH.tokenReviewerJwt),
|
||||||
allowedNames: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedNames),
|
allowedNamespaces: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedNamespaces), // TODO: validation
|
||||||
allowedAudience: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedAudience),
|
allowedNames: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedNames),
|
||||||
accessTokenTrustedIps: z
|
allowedAudience: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedAudience),
|
||||||
.object({
|
accessTokenTrustedIps: z
|
||||||
ipAddress: z.string().trim()
|
.object({
|
||||||
})
|
ipAddress: z.string().trim()
|
||||||
.array()
|
})
|
||||||
.min(1)
|
.array()
|
||||||
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
|
.min(1)
|
||||||
.describe(KUBERNETES_AUTH.ATTACH.accessTokenTrustedIps),
|
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
|
||||||
accessTokenTTL: z
|
.describe(KUBERNETES_AUTH.ATTACH.accessTokenTrustedIps),
|
||||||
.number()
|
accessTokenTTL: z
|
||||||
.int()
|
.number()
|
||||||
.min(1)
|
.int()
|
||||||
.max(315360000)
|
.min(0)
|
||||||
.refine((value) => value !== 0, {
|
.max(315360000)
|
||||||
message: "accessTokenTTL must have a non zero number"
|
.default(2592000)
|
||||||
})
|
.describe(KUBERNETES_AUTH.ATTACH.accessTokenTTL),
|
||||||
.default(2592000)
|
accessTokenMaxTTL: z
|
||||||
.describe(KUBERNETES_AUTH.ATTACH.accessTokenTTL),
|
.number()
|
||||||
accessTokenMaxTTL: z
|
.int()
|
||||||
.number()
|
.min(0)
|
||||||
.int()
|
.max(315360000)
|
||||||
.max(315360000)
|
.default(2592000)
|
||||||
.refine((value) => value !== 0, {
|
.describe(KUBERNETES_AUTH.ATTACH.accessTokenMaxTTL),
|
||||||
message: "accessTokenMaxTTL must have a non zero number"
|
accessTokenNumUsesLimit: z
|
||||||
})
|
.number()
|
||||||
.default(2592000)
|
.int()
|
||||||
.describe(KUBERNETES_AUTH.ATTACH.accessTokenMaxTTL),
|
.min(0)
|
||||||
accessTokenNumUsesLimit: z
|
.default(0)
|
||||||
.number()
|
.describe(KUBERNETES_AUTH.ATTACH.accessTokenNumUsesLimit)
|
||||||
.int()
|
})
|
||||||
.min(0)
|
.refine(
|
||||||
.default(0)
|
(val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
|
||||||
.describe(KUBERNETES_AUTH.ATTACH.accessTokenNumUsesLimit)
|
"Access Token TTL cannot be greater than Access Token Max TTL."
|
||||||
}),
|
),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
identityKubernetesAuth: IdentityKubernetesAuthResponseSchema
|
identityKubernetesAuth: IdentityKubernetesAuthResponseSchema
|
||||||
@@ -183,44 +183,47 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
|
|||||||
params: z.object({
|
params: z.object({
|
||||||
identityId: z.string().describe(KUBERNETES_AUTH.UPDATE.identityId)
|
identityId: z.string().describe(KUBERNETES_AUTH.UPDATE.identityId)
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z
|
||||||
kubernetesHost: z.string().trim().min(1).optional().describe(KUBERNETES_AUTH.UPDATE.kubernetesHost),
|
.object({
|
||||||
caCert: z.string().trim().optional().describe(KUBERNETES_AUTH.UPDATE.caCert),
|
kubernetesHost: z.string().trim().min(1).optional().describe(KUBERNETES_AUTH.UPDATE.kubernetesHost),
|
||||||
tokenReviewerJwt: z.string().trim().min(1).optional().describe(KUBERNETES_AUTH.UPDATE.tokenReviewerJwt),
|
caCert: z.string().trim().optional().describe(KUBERNETES_AUTH.UPDATE.caCert),
|
||||||
allowedNamespaces: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedNamespaces), // TODO: validation
|
tokenReviewerJwt: z.string().trim().min(1).optional().describe(KUBERNETES_AUTH.UPDATE.tokenReviewerJwt),
|
||||||
allowedNames: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedNames),
|
allowedNamespaces: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedNamespaces), // TODO: validation
|
||||||
allowedAudience: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedAudience),
|
allowedNames: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedNames),
|
||||||
accessTokenTrustedIps: z
|
allowedAudience: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedAudience),
|
||||||
.object({
|
accessTokenTrustedIps: z
|
||||||
ipAddress: z.string().trim()
|
.object({
|
||||||
})
|
ipAddress: z.string().trim()
|
||||||
.array()
|
})
|
||||||
.min(1)
|
.array()
|
||||||
.optional()
|
.min(1)
|
||||||
.describe(KUBERNETES_AUTH.UPDATE.accessTokenTrustedIps),
|
.optional()
|
||||||
accessTokenTTL: z
|
.describe(KUBERNETES_AUTH.UPDATE.accessTokenTrustedIps),
|
||||||
.number()
|
accessTokenTTL: z
|
||||||
.int()
|
.number()
|
||||||
.min(0)
|
.int()
|
||||||
.max(315360000)
|
.min(0)
|
||||||
.optional()
|
.max(315360000)
|
||||||
.describe(KUBERNETES_AUTH.UPDATE.accessTokenTTL),
|
.optional()
|
||||||
accessTokenNumUsesLimit: z
|
.describe(KUBERNETES_AUTH.UPDATE.accessTokenTTL),
|
||||||
.number()
|
accessTokenNumUsesLimit: z
|
||||||
.int()
|
.number()
|
||||||
.min(0)
|
.int()
|
||||||
.optional()
|
.min(0)
|
||||||
.describe(KUBERNETES_AUTH.UPDATE.accessTokenNumUsesLimit),
|
.optional()
|
||||||
accessTokenMaxTTL: z
|
.describe(KUBERNETES_AUTH.UPDATE.accessTokenNumUsesLimit),
|
||||||
.number()
|
accessTokenMaxTTL: z
|
||||||
.int()
|
.number()
|
||||||
.max(315360000)
|
.int()
|
||||||
.refine((value) => value !== 0, {
|
.min(0)
|
||||||
message: "accessTokenMaxTTL must have a non zero number"
|
.max(315360000)
|
||||||
})
|
.optional()
|
||||||
.optional()
|
.describe(KUBERNETES_AUTH.UPDATE.accessTokenMaxTTL)
|
||||||
.describe(KUBERNETES_AUTH.UPDATE.accessTokenMaxTTL)
|
})
|
||||||
}),
|
.refine(
|
||||||
|
(val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true),
|
||||||
|
"Access Token TTL cannot be greater than Access Token Max TTL."
|
||||||
|
),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
identityKubernetesAuth: IdentityKubernetesAuthResponseSchema
|
identityKubernetesAuth: IdentityKubernetesAuthResponseSchema
|
||||||
|
@@ -87,42 +87,42 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider)
|
|||||||
params: z.object({
|
params: z.object({
|
||||||
identityId: z.string().trim().describe(OIDC_AUTH.ATTACH.identityId)
|
identityId: z.string().trim().describe(OIDC_AUTH.ATTACH.identityId)
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z
|
||||||
oidcDiscoveryUrl: z.string().url().min(1).describe(OIDC_AUTH.ATTACH.oidcDiscoveryUrl),
|
.object({
|
||||||
caCert: z.string().trim().default("").describe(OIDC_AUTH.ATTACH.caCert),
|
oidcDiscoveryUrl: z.string().url().min(1).describe(OIDC_AUTH.ATTACH.oidcDiscoveryUrl),
|
||||||
boundIssuer: z.string().min(1).describe(OIDC_AUTH.ATTACH.boundIssuer),
|
caCert: z.string().trim().default("").describe(OIDC_AUTH.ATTACH.caCert),
|
||||||
boundAudiences: validateOidcAuthAudiencesField.describe(OIDC_AUTH.ATTACH.boundAudiences),
|
boundIssuer: z.string().min(1).describe(OIDC_AUTH.ATTACH.boundIssuer),
|
||||||
boundClaims: validateOidcBoundClaimsField.describe(OIDC_AUTH.ATTACH.boundClaims),
|
boundAudiences: validateOidcAuthAudiencesField.describe(OIDC_AUTH.ATTACH.boundAudiences),
|
||||||
boundSubject: z.string().optional().default("").describe(OIDC_AUTH.ATTACH.boundSubject),
|
boundClaims: validateOidcBoundClaimsField.describe(OIDC_AUTH.ATTACH.boundClaims),
|
||||||
accessTokenTrustedIps: z
|
boundSubject: z.string().optional().default("").describe(OIDC_AUTH.ATTACH.boundSubject),
|
||||||
.object({
|
accessTokenTrustedIps: z
|
||||||
ipAddress: z.string().trim()
|
.object({
|
||||||
})
|
ipAddress: z.string().trim()
|
||||||
.array()
|
})
|
||||||
.min(1)
|
.array()
|
||||||
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
|
.min(1)
|
||||||
.describe(OIDC_AUTH.ATTACH.accessTokenTrustedIps),
|
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
|
||||||
accessTokenTTL: z
|
.describe(OIDC_AUTH.ATTACH.accessTokenTrustedIps),
|
||||||
.number()
|
accessTokenTTL: z
|
||||||
.int()
|
.number()
|
||||||
.min(1)
|
.int()
|
||||||
.max(315360000)
|
.min(0)
|
||||||
.refine((value) => value !== 0, {
|
.max(315360000)
|
||||||
message: "accessTokenTTL must have a non zero number"
|
.default(2592000)
|
||||||
})
|
.describe(OIDC_AUTH.ATTACH.accessTokenTTL),
|
||||||
.default(2592000)
|
accessTokenMaxTTL: z
|
||||||
.describe(OIDC_AUTH.ATTACH.accessTokenTTL),
|
.number()
|
||||||
accessTokenMaxTTL: z
|
.int()
|
||||||
.number()
|
.min(0)
|
||||||
.int()
|
.max(315360000)
|
||||||
.max(315360000)
|
.default(2592000)
|
||||||
.refine((value) => value !== 0, {
|
.describe(OIDC_AUTH.ATTACH.accessTokenMaxTTL),
|
||||||
message: "accessTokenMaxTTL must have a non zero number"
|
accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(OIDC_AUTH.ATTACH.accessTokenNumUsesLimit)
|
||||||
})
|
})
|
||||||
.default(2592000)
|
.refine(
|
||||||
.describe(OIDC_AUTH.ATTACH.accessTokenMaxTTL),
|
(val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
|
||||||
accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(OIDC_AUTH.ATTACH.accessTokenNumUsesLimit)
|
"Access Token TTL cannot be greater than Access Token Max TTL."
|
||||||
}),
|
),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
identityOidcAuth: IdentityOidcAuthResponseSchema
|
identityOidcAuth: IdentityOidcAuthResponseSchema
|
||||||
@@ -202,26 +202,24 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider)
|
|||||||
accessTokenTTL: z
|
accessTokenTTL: z
|
||||||
.number()
|
.number()
|
||||||
.int()
|
.int()
|
||||||
.min(1)
|
.min(0)
|
||||||
.max(315360000)
|
.max(315360000)
|
||||||
.refine((value) => value !== 0, {
|
|
||||||
message: "accessTokenTTL must have a non zero number"
|
|
||||||
})
|
|
||||||
.default(2592000)
|
.default(2592000)
|
||||||
.describe(OIDC_AUTH.UPDATE.accessTokenTTL),
|
.describe(OIDC_AUTH.UPDATE.accessTokenTTL),
|
||||||
accessTokenMaxTTL: z
|
accessTokenMaxTTL: z
|
||||||
.number()
|
.number()
|
||||||
.int()
|
.int()
|
||||||
|
.min(0)
|
||||||
.max(315360000)
|
.max(315360000)
|
||||||
.refine((value) => value !== 0, {
|
|
||||||
message: "accessTokenMaxTTL must have a non zero number"
|
|
||||||
})
|
|
||||||
.default(2592000)
|
.default(2592000)
|
||||||
.describe(OIDC_AUTH.UPDATE.accessTokenMaxTTL),
|
.describe(OIDC_AUTH.UPDATE.accessTokenMaxTTL),
|
||||||
|
|
||||||
accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(OIDC_AUTH.UPDATE.accessTokenNumUsesLimit)
|
accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(OIDC_AUTH.UPDATE.accessTokenNumUsesLimit)
|
||||||
})
|
})
|
||||||
.partial(),
|
.partial()
|
||||||
|
.refine(
|
||||||
|
(val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true),
|
||||||
|
"Access Token TTL cannot be greater than Access Token Max TTL."
|
||||||
|
),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
identityOidcAuth: IdentityOidcAuthResponseSchema
|
identityOidcAuth: IdentityOidcAuthResponseSchema
|
||||||
|
@@ -26,36 +26,41 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider
|
|||||||
params: z.object({
|
params: z.object({
|
||||||
identityId: z.string().trim().describe(TOKEN_AUTH.ATTACH.identityId)
|
identityId: z.string().trim().describe(TOKEN_AUTH.ATTACH.identityId)
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z
|
||||||
accessTokenTrustedIps: z
|
.object({
|
||||||
.object({
|
accessTokenTrustedIps: z
|
||||||
ipAddress: z.string().trim()
|
.object({
|
||||||
})
|
ipAddress: z.string().trim()
|
||||||
.array()
|
})
|
||||||
.min(1)
|
.array()
|
||||||
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
|
.min(1)
|
||||||
.describe(TOKEN_AUTH.ATTACH.accessTokenTrustedIps),
|
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
|
||||||
accessTokenTTL: z
|
.describe(TOKEN_AUTH.ATTACH.accessTokenTrustedIps),
|
||||||
.number()
|
accessTokenTTL: z
|
||||||
.int()
|
.number()
|
||||||
.min(1)
|
.int()
|
||||||
.max(315360000)
|
.min(0)
|
||||||
.refine((value) => value !== 0, {
|
.max(315360000)
|
||||||
message: "accessTokenTTL must have a non zero number"
|
.default(2592000)
|
||||||
})
|
.describe(TOKEN_AUTH.ATTACH.accessTokenTTL),
|
||||||
.default(2592000)
|
accessTokenMaxTTL: z
|
||||||
.describe(TOKEN_AUTH.ATTACH.accessTokenTTL),
|
.number()
|
||||||
accessTokenMaxTTL: z
|
.int()
|
||||||
.number()
|
.min(0)
|
||||||
.int()
|
.max(315360000)
|
||||||
.max(315360000)
|
.default(2592000)
|
||||||
.refine((value) => value !== 0, {
|
.describe(TOKEN_AUTH.ATTACH.accessTokenMaxTTL),
|
||||||
message: "accessTokenMaxTTL must have a non zero number"
|
accessTokenNumUsesLimit: z
|
||||||
})
|
.number()
|
||||||
.default(2592000)
|
.int()
|
||||||
.describe(TOKEN_AUTH.ATTACH.accessTokenMaxTTL),
|
.min(0)
|
||||||
accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(TOKEN_AUTH.ATTACH.accessTokenNumUsesLimit)
|
.default(0)
|
||||||
}),
|
.describe(TOKEN_AUTH.ATTACH.accessTokenNumUsesLimit)
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
|
||||||
|
"Access Token TTL cannot be greater than Access Token Max TTL."
|
||||||
|
),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
identityTokenAuth: IdentityTokenAuthsSchema
|
identityTokenAuth: IdentityTokenAuthsSchema
|
||||||
@@ -110,27 +115,35 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider
|
|||||||
params: z.object({
|
params: z.object({
|
||||||
identityId: z.string().trim().describe(TOKEN_AUTH.UPDATE.identityId)
|
identityId: z.string().trim().describe(TOKEN_AUTH.UPDATE.identityId)
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z
|
||||||
accessTokenTrustedIps: z
|
.object({
|
||||||
.object({
|
accessTokenTrustedIps: z
|
||||||
ipAddress: z.string().trim()
|
.object({
|
||||||
})
|
ipAddress: z.string().trim()
|
||||||
.array()
|
})
|
||||||
.min(1)
|
.array()
|
||||||
.optional()
|
.min(1)
|
||||||
.describe(TOKEN_AUTH.UPDATE.accessTokenTrustedIps),
|
.optional()
|
||||||
accessTokenTTL: z.number().int().min(0).max(315360000).optional().describe(TOKEN_AUTH.UPDATE.accessTokenTTL),
|
.describe(TOKEN_AUTH.UPDATE.accessTokenTrustedIps),
|
||||||
accessTokenNumUsesLimit: z.number().int().min(0).optional().describe(TOKEN_AUTH.UPDATE.accessTokenNumUsesLimit),
|
accessTokenTTL: z.number().int().min(0).max(315360000).optional().describe(TOKEN_AUTH.UPDATE.accessTokenTTL),
|
||||||
accessTokenMaxTTL: z
|
accessTokenNumUsesLimit: z
|
||||||
.number()
|
.number()
|
||||||
.int()
|
.int()
|
||||||
.max(315360000)
|
.min(0)
|
||||||
.refine((value) => value !== 0, {
|
.optional()
|
||||||
message: "accessTokenMaxTTL must have a non zero number"
|
.describe(TOKEN_AUTH.UPDATE.accessTokenNumUsesLimit),
|
||||||
})
|
accessTokenMaxTTL: z
|
||||||
.optional()
|
.number()
|
||||||
.describe(TOKEN_AUTH.UPDATE.accessTokenMaxTTL)
|
.int()
|
||||||
}),
|
.min(0)
|
||||||
|
.max(315360000)
|
||||||
|
.optional()
|
||||||
|
.describe(TOKEN_AUTH.UPDATE.accessTokenMaxTTL)
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true),
|
||||||
|
"Access Token TTL cannot be greater than Access Token Max TTL."
|
||||||
|
),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
identityTokenAuth: IdentityTokenAuthsSchema
|
identityTokenAuth: IdentityTokenAuthsSchema
|
||||||
|
@@ -86,49 +86,49 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
|
|||||||
params: z.object({
|
params: z.object({
|
||||||
identityId: z.string().trim().describe(UNIVERSAL_AUTH.ATTACH.identityId)
|
identityId: z.string().trim().describe(UNIVERSAL_AUTH.ATTACH.identityId)
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z
|
||||||
clientSecretTrustedIps: z
|
.object({
|
||||||
.object({
|
clientSecretTrustedIps: z
|
||||||
ipAddress: z.string().trim()
|
.object({
|
||||||
})
|
ipAddress: z.string().trim()
|
||||||
.array()
|
})
|
||||||
.min(1)
|
.array()
|
||||||
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
|
.min(1)
|
||||||
.describe(UNIVERSAL_AUTH.ATTACH.clientSecretTrustedIps),
|
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
|
||||||
accessTokenTrustedIps: z
|
.describe(UNIVERSAL_AUTH.ATTACH.clientSecretTrustedIps),
|
||||||
.object({
|
accessTokenTrustedIps: z
|
||||||
ipAddress: z.string().trim()
|
.object({
|
||||||
})
|
ipAddress: z.string().trim()
|
||||||
.array()
|
})
|
||||||
.min(1)
|
.array()
|
||||||
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
|
.min(1)
|
||||||
.describe(UNIVERSAL_AUTH.ATTACH.accessTokenTrustedIps),
|
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
|
||||||
accessTokenTTL: z
|
.describe(UNIVERSAL_AUTH.ATTACH.accessTokenTrustedIps),
|
||||||
.number()
|
accessTokenTTL: z
|
||||||
.int()
|
.number()
|
||||||
.min(1)
|
.int()
|
||||||
.max(315360000)
|
.min(0)
|
||||||
.refine((value) => value !== 0, {
|
.max(315360000)
|
||||||
message: "accessTokenTTL must have a non zero number"
|
.default(2592000)
|
||||||
})
|
.describe(UNIVERSAL_AUTH.ATTACH.accessTokenTTL), // 30 days
|
||||||
.default(2592000)
|
accessTokenMaxTTL: z
|
||||||
.describe(UNIVERSAL_AUTH.ATTACH.accessTokenTTL), // 30 days
|
.number()
|
||||||
accessTokenMaxTTL: z
|
.int()
|
||||||
.number()
|
.min(0)
|
||||||
.int()
|
.max(315360000)
|
||||||
.max(315360000)
|
.default(2592000)
|
||||||
.refine((value) => value !== 0, {
|
.describe(UNIVERSAL_AUTH.ATTACH.accessTokenMaxTTL), // 30 days
|
||||||
message: "accessTokenMaxTTL must have a non zero number"
|
accessTokenNumUsesLimit: z
|
||||||
})
|
.number()
|
||||||
.default(2592000)
|
.int()
|
||||||
.describe(UNIVERSAL_AUTH.ATTACH.accessTokenMaxTTL), // 30 days
|
.min(0)
|
||||||
accessTokenNumUsesLimit: z
|
.default(0)
|
||||||
.number()
|
.describe(UNIVERSAL_AUTH.ATTACH.accessTokenNumUsesLimit)
|
||||||
.int()
|
})
|
||||||
.min(0)
|
.refine(
|
||||||
.default(0)
|
(val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
|
||||||
.describe(UNIVERSAL_AUTH.ATTACH.accessTokenNumUsesLimit)
|
"Access Token TTL cannot be greater than Access Token Max TTL."
|
||||||
}),
|
),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
identityUniversalAuth: IdentityUniversalAuthsSchema
|
identityUniversalAuth: IdentityUniversalAuthsSchema
|
||||||
@@ -181,46 +181,49 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
|
|||||||
params: z.object({
|
params: z.object({
|
||||||
identityId: z.string().describe(UNIVERSAL_AUTH.UPDATE.identityId)
|
identityId: z.string().describe(UNIVERSAL_AUTH.UPDATE.identityId)
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z
|
||||||
clientSecretTrustedIps: z
|
.object({
|
||||||
.object({
|
clientSecretTrustedIps: z
|
||||||
ipAddress: z.string().trim()
|
.object({
|
||||||
})
|
ipAddress: z.string().trim()
|
||||||
.array()
|
})
|
||||||
.min(1)
|
.array()
|
||||||
.optional()
|
.min(1)
|
||||||
.describe(UNIVERSAL_AUTH.UPDATE.clientSecretTrustedIps),
|
.optional()
|
||||||
accessTokenTrustedIps: z
|
.describe(UNIVERSAL_AUTH.UPDATE.clientSecretTrustedIps),
|
||||||
.object({
|
accessTokenTrustedIps: z
|
||||||
ipAddress: z.string().trim()
|
.object({
|
||||||
})
|
ipAddress: z.string().trim()
|
||||||
.array()
|
})
|
||||||
.min(1)
|
.array()
|
||||||
.optional()
|
.min(1)
|
||||||
.describe(UNIVERSAL_AUTH.UPDATE.accessTokenTrustedIps),
|
.optional()
|
||||||
accessTokenTTL: z
|
.describe(UNIVERSAL_AUTH.UPDATE.accessTokenTrustedIps),
|
||||||
.number()
|
accessTokenTTL: z
|
||||||
.int()
|
.number()
|
||||||
.min(0)
|
.int()
|
||||||
.max(315360000)
|
.min(0)
|
||||||
.optional()
|
.max(315360000)
|
||||||
.describe(UNIVERSAL_AUTH.UPDATE.accessTokenTTL),
|
.optional()
|
||||||
accessTokenNumUsesLimit: z
|
.describe(UNIVERSAL_AUTH.UPDATE.accessTokenTTL),
|
||||||
.number()
|
accessTokenNumUsesLimit: z
|
||||||
.int()
|
.number()
|
||||||
.min(0)
|
.int()
|
||||||
.optional()
|
.min(0)
|
||||||
.describe(UNIVERSAL_AUTH.UPDATE.accessTokenNumUsesLimit),
|
.optional()
|
||||||
accessTokenMaxTTL: z
|
.describe(UNIVERSAL_AUTH.UPDATE.accessTokenNumUsesLimit),
|
||||||
.number()
|
accessTokenMaxTTL: z
|
||||||
.int()
|
.number()
|
||||||
.max(315360000)
|
.int()
|
||||||
.refine((value) => value !== 0, {
|
.min(0)
|
||||||
message: "accessTokenMaxTTL must have a non zero number"
|
.max(315360000)
|
||||||
})
|
.optional()
|
||||||
.optional()
|
.describe(UNIVERSAL_AUTH.UPDATE.accessTokenMaxTTL)
|
||||||
.describe(UNIVERSAL_AUTH.UPDATE.accessTokenMaxTTL)
|
})
|
||||||
}),
|
.refine(
|
||||||
|
(val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true),
|
||||||
|
"Access Token TTL cannot be greater than Access Token Max TTL."
|
||||||
|
),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
identityUniversalAuth: IdentityUniversalAuthsSchema
|
identityUniversalAuth: IdentityUniversalAuthsSchema
|
||||||
|
@@ -1151,6 +1151,50 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:integrationAuthId/vercel/custom-environments",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
schema: {
|
||||||
|
querystring: z.object({
|
||||||
|
teamId: z.string().trim()
|
||||||
|
}),
|
||||||
|
params: z.object({
|
||||||
|
integrationAuthId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
environments: z
|
||||||
|
.object({
|
||||||
|
appId: z.string(),
|
||||||
|
customEnvironments: z
|
||||||
|
.object({
|
||||||
|
id: z.string(),
|
||||||
|
slug: z.string()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const environments = await server.services.integrationAuth.getVercelCustomEnvironments({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
id: req.params.integrationAuthId,
|
||||||
|
teamId: req.query.teamId
|
||||||
|
});
|
||||||
|
|
||||||
|
return { environments };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/:integrationAuthId/octopus-deploy/spaces",
|
url: "/:integrationAuthId/octopus-deploy/spaces",
|
||||||
|
@@ -11,7 +11,7 @@ import {
|
|||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { AUDIT_LOGS, ORGANIZATIONS } from "@app/lib/api-docs";
|
import { AUDIT_LOGS, ORGANIZATIONS } from "@app/lib/api-docs";
|
||||||
import { getLastMidnightDateISO } from "@app/lib/fn";
|
import { getLastMidnightDateISO, removeTrailingSlash } from "@app/lib/fn";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { slugSchema } from "@app/server/lib/schemas";
|
import { slugSchema } from "@app/server/lib/schemas";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
@@ -113,6 +113,12 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
projectId: z.string().optional().describe(AUDIT_LOGS.EXPORT.projectId),
|
projectId: z.string().optional().describe(AUDIT_LOGS.EXPORT.projectId),
|
||||||
actorType: z.nativeEnum(ActorType).optional(),
|
actorType: z.nativeEnum(ActorType).optional(),
|
||||||
|
secretPath: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.transform((val) => (!val ? val : removeTrailingSlash(val)))
|
||||||
|
.describe(AUDIT_LOGS.EXPORT.secretPath),
|
||||||
|
|
||||||
// eventType is split with , for multiple values, we need to transform it to array
|
// eventType is split with , for multiple values, we need to transform it to array
|
||||||
eventType: z
|
eventType: z
|
||||||
.string()
|
.string()
|
||||||
|
@@ -203,7 +203,8 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
|
|||||||
encryptedPrivateKeyIV: z.string().trim(),
|
encryptedPrivateKeyIV: z.string().trim(),
|
||||||
encryptedPrivateKeyTag: z.string().trim(),
|
encryptedPrivateKeyTag: z.string().trim(),
|
||||||
salt: z.string().trim(),
|
salt: z.string().trim(),
|
||||||
verifier: z.string().trim()
|
verifier: z.string().trim(),
|
||||||
|
password: z.string().trim()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -218,7 +219,69 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
|
|||||||
userId: token.userId
|
userId: token.userId
|
||||||
});
|
});
|
||||||
|
|
||||||
return { message: "Successfully updated backup private key" };
|
return { message: "Successfully reset password" };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/email/password-setup",
|
||||||
|
config: {
|
||||||
|
rateLimit: authRateLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
message: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
await server.services.password.sendPasswordSetupEmail(req.permission);
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: "A password setup link has been sent"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/password-setup",
|
||||||
|
config: {
|
||||||
|
rateLimit: authRateLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
protectedKey: z.string().trim(),
|
||||||
|
protectedKeyIV: z.string().trim(),
|
||||||
|
protectedKeyTag: z.string().trim(),
|
||||||
|
encryptedPrivateKey: z.string().trim(),
|
||||||
|
encryptedPrivateKeyIV: z.string().trim(),
|
||||||
|
encryptedPrivateKeyTag: z.string().trim(),
|
||||||
|
salt: z.string().trim(),
|
||||||
|
verifier: z.string().trim(),
|
||||||
|
password: z.string().trim(),
|
||||||
|
token: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
message: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req, res) => {
|
||||||
|
await server.services.password.setupPassword(req.body, req.permission);
|
||||||
|
|
||||||
|
const appCfg = getConfig();
|
||||||
|
void res.cookie("jid", "", {
|
||||||
|
httpOnly: true,
|
||||||
|
path: "/",
|
||||||
|
sameSite: "strict",
|
||||||
|
secure: appCfg.HTTPS_ENABLED
|
||||||
|
});
|
||||||
|
|
||||||
|
return { message: "Successfully setup password" };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@@ -0,0 +1,17 @@
|
|||||||
|
import {
|
||||||
|
AwsSecretsManagerSyncSchema,
|
||||||
|
CreateAwsSecretsManagerSyncSchema,
|
||||||
|
UpdateAwsSecretsManagerSyncSchema
|
||||||
|
} from "@app/services/secret-sync/aws-secrets-manager";
|
||||||
|
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||||
|
|
||||||
|
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
|
||||||
|
|
||||||
|
export const registerAwsSecretsManagerSyncRouter = async (server: FastifyZodProvider) =>
|
||||||
|
registerSyncSecretsEndpoints({
|
||||||
|
destination: SecretSync.AWSSecretsManager,
|
||||||
|
server,
|
||||||
|
responseSchema: AwsSecretsManagerSyncSchema,
|
||||||
|
createSchema: CreateAwsSecretsManagerSyncSchema,
|
||||||
|
updateSchema: UpdateAwsSecretsManagerSyncSchema
|
||||||
|
});
|
@@ -0,0 +1,13 @@
|
|||||||
|
import { CreateGcpSyncSchema, GcpSyncSchema, UpdateGcpSyncSchema } from "@app/services/secret-sync/gcp";
|
||||||
|
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||||
|
|
||||||
|
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
|
||||||
|
|
||||||
|
export const registerGcpSyncRouter = async (server: FastifyZodProvider) =>
|
||||||
|
registerSyncSecretsEndpoints({
|
||||||
|
destination: SecretSync.GCPSecretManager,
|
||||||
|
server,
|
||||||
|
responseSchema: GcpSyncSchema,
|
||||||
|
createSchema: CreateGcpSyncSchema,
|
||||||
|
updateSchema: UpdateGcpSyncSchema
|
||||||
|
});
|
@@ -1,11 +1,15 @@
|
|||||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||||
|
|
||||||
import { registerAwsParameterStoreSyncRouter } from "./aws-parameter-store-sync-router";
|
import { registerAwsParameterStoreSyncRouter } from "./aws-parameter-store-sync-router";
|
||||||
|
import { registerAwsSecretsManagerSyncRouter } from "./aws-secrets-manager-sync-router";
|
||||||
|
import { registerGcpSyncRouter } from "./gcp-sync-router";
|
||||||
import { registerGitHubSyncRouter } from "./github-sync-router";
|
import { registerGitHubSyncRouter } from "./github-sync-router";
|
||||||
|
|
||||||
export * from "./secret-sync-router";
|
export * from "./secret-sync-router";
|
||||||
|
|
||||||
export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: FastifyZodProvider) => Promise<void>> = {
|
export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: FastifyZodProvider) => Promise<void>> = {
|
||||||
[SecretSync.AWSParameterStore]: registerAwsParameterStoreSyncRouter,
|
[SecretSync.AWSParameterStore]: registerAwsParameterStoreSyncRouter,
|
||||||
[SecretSync.GitHub]: registerGitHubSyncRouter
|
[SecretSync.AWSSecretsManager]: registerAwsSecretsManagerSyncRouter,
|
||||||
|
[SecretSync.GitHub]: registerGitHubSyncRouter,
|
||||||
|
[SecretSync.GCPSecretManager]: registerGcpSyncRouter
|
||||||
};
|
};
|
||||||
|
@@ -9,13 +9,25 @@ import {
|
|||||||
AwsParameterStoreSyncListItemSchema,
|
AwsParameterStoreSyncListItemSchema,
|
||||||
AwsParameterStoreSyncSchema
|
AwsParameterStoreSyncSchema
|
||||||
} from "@app/services/secret-sync/aws-parameter-store";
|
} from "@app/services/secret-sync/aws-parameter-store";
|
||||||
|
import {
|
||||||
|
AwsSecretsManagerSyncListItemSchema,
|
||||||
|
AwsSecretsManagerSyncSchema
|
||||||
|
} from "@app/services/secret-sync/aws-secrets-manager";
|
||||||
|
import { GcpSyncListItemSchema, GcpSyncSchema } from "@app/services/secret-sync/gcp";
|
||||||
import { GitHubSyncListItemSchema, GitHubSyncSchema } from "@app/services/secret-sync/github";
|
import { GitHubSyncListItemSchema, GitHubSyncSchema } from "@app/services/secret-sync/github";
|
||||||
|
|
||||||
const SecretSyncSchema = z.discriminatedUnion("destination", [AwsParameterStoreSyncSchema, GitHubSyncSchema]);
|
const SecretSyncSchema = z.discriminatedUnion("destination", [
|
||||||
|
AwsParameterStoreSyncSchema,
|
||||||
|
AwsSecretsManagerSyncSchema,
|
||||||
|
GitHubSyncSchema,
|
||||||
|
GcpSyncSchema
|
||||||
|
]);
|
||||||
|
|
||||||
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
||||||
AwsParameterStoreSyncListItemSchema,
|
AwsParameterStoreSyncListItemSchema,
|
||||||
GitHubSyncListItemSchema
|
AwsSecretsManagerSyncListItemSchema,
|
||||||
|
GitHubSyncListItemSchema,
|
||||||
|
GcpSyncListItemSchema
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {
|
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {
|
||||||
|
@@ -181,6 +181,66 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
|
metadataFilter: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.transform((val) => {
|
||||||
|
if (!val) return undefined;
|
||||||
|
|
||||||
|
const result: { key?: string; value?: string }[] = [];
|
||||||
|
const pairs = val.split("|");
|
||||||
|
|
||||||
|
for (const pair of pairs) {
|
||||||
|
const keyValuePair: { key?: string; value?: string } = {};
|
||||||
|
const parts = pair.split(/[,=]/);
|
||||||
|
|
||||||
|
for (let i = 0; i < parts.length; i += 2) {
|
||||||
|
const identifier = parts[i].trim().toLowerCase();
|
||||||
|
const value = parts[i + 1]?.trim();
|
||||||
|
|
||||||
|
if (identifier === "key" && value) {
|
||||||
|
keyValuePair.key = value;
|
||||||
|
} else if (identifier === "value" && value) {
|
||||||
|
keyValuePair.value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyValuePair.key && keyValuePair.value) {
|
||||||
|
result.push(keyValuePair);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.length ? result : undefined;
|
||||||
|
})
|
||||||
|
.superRefine((metadata, ctx) => {
|
||||||
|
if (metadata && !Array.isArray(metadata)) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message:
|
||||||
|
"Invalid secretMetadata format. Correct format is key=value1,value=value2|key=value3,value=value4."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metadata) {
|
||||||
|
if (metadata.length > 10) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "You can only filter by up to 10 metadata fields"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const item of metadata) {
|
||||||
|
if (!item.key && !item.value) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message:
|
||||||
|
"Invalid secretMetadata format, key or value must be provided. Correct format is key=value1,value=value2|key=value3,value=value4."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.describe(RAW_SECRETS.LIST.metadataFilter),
|
||||||
workspaceId: z.string().trim().optional().describe(RAW_SECRETS.LIST.workspaceId),
|
workspaceId: z.string().trim().optional().describe(RAW_SECRETS.LIST.workspaceId),
|
||||||
workspaceSlug: z.string().trim().optional().describe(RAW_SECRETS.LIST.workspaceSlug),
|
workspaceSlug: z.string().trim().optional().describe(RAW_SECRETS.LIST.workspaceSlug),
|
||||||
environment: z.string().trim().optional().describe(RAW_SECRETS.LIST.environment),
|
environment: z.string().trim().optional().describe(RAW_SECRETS.LIST.environment),
|
||||||
@@ -281,6 +341,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
projectId: workspaceId,
|
projectId: workspaceId,
|
||||||
path: secretPath,
|
path: secretPath,
|
||||||
|
metadataFilter: req.query.metadataFilter,
|
||||||
includeImports: req.query.include_imports,
|
includeImports: req.query.include_imports,
|
||||||
recursive: req.query.recursive,
|
recursive: req.query.recursive,
|
||||||
tagSlugs: req.query.tagSlugs
|
tagSlugs: req.query.tagSlugs
|
||||||
@@ -411,7 +472,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
secretPath: req.query.secretPath,
|
secretPath: req.query.secretPath,
|
||||||
secretId: secret.id,
|
secretId: secret.id,
|
||||||
secretKey: req.params.secretName,
|
secretKey: req.params.secretName,
|
||||||
secretVersion: secret.version
|
secretVersion: secret.version,
|
||||||
|
secretMetadata: secret.secretMetadata
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -519,7 +581,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
secretPath: req.body.secretPath,
|
secretPath: req.body.secretPath,
|
||||||
secretId: secret.id,
|
secretId: secret.id,
|
||||||
secretKey: req.params.secretName,
|
secretKey: req.params.secretName,
|
||||||
secretVersion: secret.version
|
secretVersion: secret.version,
|
||||||
|
secretMetadata: req.body.secretMetadata
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -631,7 +694,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
secretPath: req.body.secretPath,
|
secretPath: req.body.secretPath,
|
||||||
secretId: secret.id,
|
secretId: secret.id,
|
||||||
secretKey: req.params.secretName,
|
secretKey: req.params.secretName,
|
||||||
secretVersion: secret.version
|
secretVersion: secret.version,
|
||||||
|
secretMetadata: req.body.secretMetadata
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1904,6 +1968,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
const { secrets } = secretOperation;
|
const { secrets } = secretOperation;
|
||||||
|
|
||||||
|
const secretMetadataMap = new Map(
|
||||||
|
inputSecrets.map(({ secretKey, secretMetadata }) => [secretKey, secretMetadata])
|
||||||
|
);
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
projectId: secrets[0].workspace,
|
projectId: secrets[0].workspace,
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
@@ -1915,7 +1983,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
secrets: secrets.map((secret) => ({
|
secrets: secrets.map((secret) => ({
|
||||||
secretId: secret.id,
|
secretId: secret.id,
|
||||||
secretKey: secret.secretKey,
|
secretKey: secret.secretKey,
|
||||||
secretVersion: secret.version
|
secretVersion: secret.version,
|
||||||
|
secretMetadata: secretMetadataMap.get(secret.secretKey)
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2010,6 +2079,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
const { secrets } = secretOperation;
|
const { secrets } = secretOperation;
|
||||||
|
|
||||||
|
const secretMetadataMap = new Map(
|
||||||
|
inputSecrets.map(({ secretKey, secretMetadata }) => [secretKey, secretMetadata])
|
||||||
|
);
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
projectId: secrets[0].workspace,
|
projectId: secrets[0].workspace,
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
@@ -2021,7 +2094,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
secrets: secrets.map((secret) => ({
|
secrets: secrets.map((secret) => ({
|
||||||
secretId: secret.id,
|
secretId: secret.id,
|
||||||
secretKey: secret.secretKey,
|
secretKey: secret.secretKey,
|
||||||
secretVersion: secret.version
|
secretVersion: secret.version,
|
||||||
|
secretMetadata: secretMetadataMap.get(secret.secretKey)
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
export enum AppConnection {
|
export enum AppConnection {
|
||||||
GitHub = "github",
|
GitHub = "github",
|
||||||
AWS = "aws"
|
AWS = "aws",
|
||||||
|
GCP = "gcp"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AWSRegion {
|
export enum AWSRegion {
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import { TAppConnections } from "@app/db/schemas/app-connections";
|
import { TAppConnections } from "@app/db/schemas/app-connections";
|
||||||
|
import { generateHash } from "@app/lib/crypto/encryption";
|
||||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
import { TAppConnectionServiceFactoryDep } from "@app/services/app-connection/app-connection-service";
|
import { TAppConnectionServiceFactoryDep } from "@app/services/app-connection/app-connection-service";
|
||||||
import { TAppConnection, TAppConnectionConfig } from "@app/services/app-connection/app-connection-types";
|
import { TAppConnection, TAppConnectionConfig } from "@app/services/app-connection/app-connection-types";
|
||||||
@@ -7,6 +8,11 @@ import {
|
|||||||
getAwsAppConnectionListItem,
|
getAwsAppConnectionListItem,
|
||||||
validateAwsConnectionCredentials
|
validateAwsConnectionCredentials
|
||||||
} from "@app/services/app-connection/aws";
|
} from "@app/services/app-connection/aws";
|
||||||
|
import {
|
||||||
|
GcpConnectionMethod,
|
||||||
|
getGcpAppConnectionListItem,
|
||||||
|
validateGcpConnectionCredentials
|
||||||
|
} from "@app/services/app-connection/gcp";
|
||||||
import {
|
import {
|
||||||
getGitHubConnectionListItem,
|
getGitHubConnectionListItem,
|
||||||
GitHubConnectionMethod,
|
GitHubConnectionMethod,
|
||||||
@@ -15,7 +21,9 @@ import {
|
|||||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
|
|
||||||
export const listAppConnectionOptions = () => {
|
export const listAppConnectionOptions = () => {
|
||||||
return [getAwsAppConnectionListItem(), getGitHubConnectionListItem()].sort((a, b) => a.name.localeCompare(b.name));
|
return [getAwsAppConnectionListItem(), getGitHubConnectionListItem(), getGcpAppConnectionListItem()].sort((a, b) =>
|
||||||
|
a.name.localeCompare(b.name)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const encryptAppConnectionCredentials = async ({
|
export const encryptAppConnectionCredentials = async ({
|
||||||
@@ -69,6 +77,8 @@ export const validateAppConnectionCredentials = async (
|
|||||||
return validateAwsConnectionCredentials(appConnection);
|
return validateAwsConnectionCredentials(appConnection);
|
||||||
case AppConnection.GitHub:
|
case AppConnection.GitHub:
|
||||||
return validateGitHubConnectionCredentials(appConnection);
|
return validateGitHubConnectionCredentials(appConnection);
|
||||||
|
case AppConnection.GCP:
|
||||||
|
return validateGcpConnectionCredentials(appConnection);
|
||||||
default:
|
default:
|
||||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||||
throw new Error(`Unhandled App Connection ${app}`);
|
throw new Error(`Unhandled App Connection ${app}`);
|
||||||
@@ -85,6 +95,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
|||||||
return "Access Key";
|
return "Access Key";
|
||||||
case AwsConnectionMethod.AssumeRole:
|
case AwsConnectionMethod.AssumeRole:
|
||||||
return "Assume Role";
|
return "Assume Role";
|
||||||
|
case GcpConnectionMethod.ServiceAccountImpersonation:
|
||||||
|
return "Service Account Impersonation";
|
||||||
default:
|
default:
|
||||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||||
throw new Error(`Unhandled App Connection Method: ${method}`);
|
throw new Error(`Unhandled App Connection Method: ${method}`);
|
||||||
@@ -101,6 +113,7 @@ export const decryptAppConnection = async (
|
|||||||
encryptedCredentials: appConnection.encryptedCredentials,
|
encryptedCredentials: appConnection.encryptedCredentials,
|
||||||
orgId: appConnection.orgId,
|
orgId: appConnection.orgId,
|
||||||
kmsService
|
kmsService
|
||||||
})
|
}),
|
||||||
|
credentialsHash: generateHash(appConnection.encryptedCredentials)
|
||||||
} as TAppConnection;
|
} as TAppConnection;
|
||||||
};
|
};
|
||||||
|
@@ -2,5 +2,6 @@ import { AppConnection } from "./app-connection-enums";
|
|||||||
|
|
||||||
export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
||||||
[AppConnection.AWS]: "AWS",
|
[AppConnection.AWS]: "AWS",
|
||||||
[AppConnection.GitHub]: "GitHub"
|
[AppConnection.GitHub]: "GitHub",
|
||||||
|
[AppConnection.GCP]: "GCP"
|
||||||
};
|
};
|
||||||
|
@@ -10,6 +10,8 @@ export const BaseAppConnectionSchema = AppConnectionsSchema.omit({
|
|||||||
encryptedCredentials: true,
|
encryptedCredentials: true,
|
||||||
app: true,
|
app: true,
|
||||||
method: true
|
method: true
|
||||||
|
}).extend({
|
||||||
|
credentialsHash: z.string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const GenericCreateAppConnectionFieldsSchema = (app: AppConnection) =>
|
export const GenericCreateAppConnectionFieldsSchema = (app: AppConnection) =>
|
||||||
|
@@ -2,6 +2,8 @@ import { ForbiddenError, subject } from "@casl/ability";
|
|||||||
|
|
||||||
import { OrgPermissionAppConnectionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
import { OrgPermissionAppConnectionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
|
import { generateHash } from "@app/lib/crypto/encryption";
|
||||||
|
import { DatabaseErrorCode } from "@app/lib/error-codes";
|
||||||
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
|
||||||
import { DiscriminativePick, OrgServiceActor } from "@app/lib/types";
|
import { DiscriminativePick, OrgServiceActor } from "@app/lib/types";
|
||||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
@@ -26,6 +28,8 @@ import { githubConnectionService } from "@app/services/app-connection/github/git
|
|||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
|
||||||
import { TAppConnectionDALFactory } from "./app-connection-dal";
|
import { TAppConnectionDALFactory } from "./app-connection-dal";
|
||||||
|
import { ValidateGcpConnectionCredentialsSchema } from "./gcp";
|
||||||
|
import { gcpConnectionService } from "./gcp/gcp-connection-service";
|
||||||
|
|
||||||
export type TAppConnectionServiceFactoryDep = {
|
export type TAppConnectionServiceFactoryDep = {
|
||||||
appConnectionDAL: TAppConnectionDALFactory;
|
appConnectionDAL: TAppConnectionDALFactory;
|
||||||
@@ -37,7 +41,8 @@ export type TAppConnectionServiceFactory = ReturnType<typeof appConnectionServic
|
|||||||
|
|
||||||
const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAppConnectionCredentials> = {
|
const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAppConnectionCredentials> = {
|
||||||
[AppConnection.AWS]: ValidateAwsConnectionCredentialsSchema,
|
[AppConnection.AWS]: ValidateAwsConnectionCredentialsSchema,
|
||||||
[AppConnection.GitHub]: ValidateGitHubConnectionCredentialsSchema
|
[AppConnection.GitHub]: ValidateGitHubConnectionCredentialsSchema,
|
||||||
|
[AppConnection.GCP]: ValidateGcpConnectionCredentialsSchema
|
||||||
};
|
};
|
||||||
|
|
||||||
export const appConnectionServiceFactory = ({
|
export const appConnectionServiceFactory = ({
|
||||||
@@ -140,53 +145,40 @@ export const appConnectionServiceFactory = ({
|
|||||||
OrgPermissionSubjects.AppConnections
|
OrgPermissionSubjects.AppConnections
|
||||||
);
|
);
|
||||||
|
|
||||||
const appConnection = await appConnectionDAL.transaction(async (tx) => {
|
const validatedCredentials = await validateAppConnectionCredentials({
|
||||||
const isConflictingName = Boolean(
|
app,
|
||||||
await appConnectionDAL.findOne(
|
credentials,
|
||||||
{
|
method,
|
||||||
name: params.name,
|
orgId: actor.orgId
|
||||||
orgId: actor.orgId
|
} as TAppConnectionConfig);
|
||||||
},
|
|
||||||
tx
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isConflictingName)
|
const encryptedCredentials = await encryptAppConnectionCredentials({
|
||||||
throw new BadRequestError({
|
credentials: validatedCredentials,
|
||||||
message: `An App Connection with the name "${params.name}" already exists`
|
orgId: actor.orgId,
|
||||||
});
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
const validatedCredentials = await validateAppConnectionCredentials({
|
try {
|
||||||
app,
|
const connection = await appConnectionDAL.create({
|
||||||
credentials,
|
|
||||||
method,
|
|
||||||
orgId: actor.orgId
|
|
||||||
} as TAppConnectionConfig);
|
|
||||||
|
|
||||||
const encryptedCredentials = await encryptAppConnectionCredentials({
|
|
||||||
credentials: validatedCredentials,
|
|
||||||
orgId: actor.orgId,
|
orgId: actor.orgId,
|
||||||
kmsService
|
encryptedCredentials,
|
||||||
|
method,
|
||||||
|
app,
|
||||||
|
...params
|
||||||
});
|
});
|
||||||
|
|
||||||
const connection = await appConnectionDAL.create(
|
|
||||||
{
|
|
||||||
orgId: actor.orgId,
|
|
||||||
encryptedCredentials,
|
|
||||||
method,
|
|
||||||
app,
|
|
||||||
...params
|
|
||||||
},
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...connection,
|
...connection,
|
||||||
|
credentialsHash: generateHash(connection.encryptedCredentials),
|
||||||
credentials: validatedCredentials
|
credentials: validatedCredentials
|
||||||
};
|
} as TAppConnection;
|
||||||
});
|
} catch (err) {
|
||||||
|
if (err instanceof DatabaseError && (err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation) {
|
||||||
|
throw new BadRequestError({ message: `An App Connection with the name "${params.name}" already exists` });
|
||||||
|
}
|
||||||
|
|
||||||
return appConnection as TAppConnection;
|
throw err;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateAppConnection = async (
|
const updateAppConnection = async (
|
||||||
@@ -210,72 +202,55 @@ export const appConnectionServiceFactory = ({
|
|||||||
OrgPermissionSubjects.AppConnections
|
OrgPermissionSubjects.AppConnections
|
||||||
);
|
);
|
||||||
|
|
||||||
const updatedAppConnection = await appConnectionDAL.transaction(async (tx) => {
|
let encryptedCredentials: undefined | Buffer;
|
||||||
if (params.name && appConnection.name !== params.name) {
|
|
||||||
const isConflictingName = Boolean(
|
|
||||||
await appConnectionDAL.findOne(
|
|
||||||
{
|
|
||||||
name: params.name,
|
|
||||||
orgId: appConnection.orgId
|
|
||||||
},
|
|
||||||
tx
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isConflictingName)
|
if (credentials) {
|
||||||
throw new BadRequestError({
|
const { app, method } = appConnection as DiscriminativePick<TAppConnectionConfig, "app" | "method">;
|
||||||
message: `An App Connection with the name "${params.name}" already exists`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let encryptedCredentials: undefined | Buffer;
|
if (
|
||||||
|
!VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[app].safeParse({
|
||||||
if (credentials) {
|
method,
|
||||||
const { app, method } = appConnection as DiscriminativePick<TAppConnectionConfig, "app" | "method">;
|
credentials
|
||||||
|
}).success
|
||||||
if (
|
)
|
||||||
!VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[app].safeParse({
|
throw new BadRequestError({
|
||||||
method,
|
message: `Invalid credential format for ${
|
||||||
credentials
|
APP_CONNECTION_NAME_MAP[app]
|
||||||
}).success
|
} Connection with method ${getAppConnectionMethodName(method)}`
|
||||||
)
|
|
||||||
throw new BadRequestError({
|
|
||||||
message: `Invalid credential format for ${
|
|
||||||
APP_CONNECTION_NAME_MAP[app]
|
|
||||||
} Connection with method ${getAppConnectionMethodName(method)}`
|
|
||||||
});
|
|
||||||
|
|
||||||
const validatedCredentials = await validateAppConnectionCredentials({
|
|
||||||
app,
|
|
||||||
orgId: actor.orgId,
|
|
||||||
credentials,
|
|
||||||
method
|
|
||||||
} as TAppConnectionConfig);
|
|
||||||
|
|
||||||
if (!validatedCredentials)
|
|
||||||
throw new BadRequestError({ message: "Unable to validate connection - check credentials" });
|
|
||||||
|
|
||||||
encryptedCredentials = await encryptAppConnectionCredentials({
|
|
||||||
credentials: validatedCredentials,
|
|
||||||
orgId: actor.orgId,
|
|
||||||
kmsService
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const validatedCredentials = await validateAppConnectionCredentials({
|
||||||
|
app,
|
||||||
|
orgId: actor.orgId,
|
||||||
|
credentials,
|
||||||
|
method
|
||||||
|
} as TAppConnectionConfig);
|
||||||
|
|
||||||
|
if (!validatedCredentials)
|
||||||
|
throw new BadRequestError({ message: "Unable to validate connection - check credentials" });
|
||||||
|
|
||||||
|
encryptedCredentials = await encryptAppConnectionCredentials({
|
||||||
|
credentials: validatedCredentials,
|
||||||
|
orgId: actor.orgId,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updatedConnection = await appConnectionDAL.updateById(connectionId, {
|
||||||
|
orgId: actor.orgId,
|
||||||
|
encryptedCredentials,
|
||||||
|
...params
|
||||||
|
});
|
||||||
|
|
||||||
|
return await decryptAppConnection(updatedConnection, kmsService);
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof DatabaseError && (err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation) {
|
||||||
|
throw new BadRequestError({ message: `An App Connection with the name "${params.name}" already exists` });
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedConnection = await appConnectionDAL.updateById(
|
throw err;
|
||||||
connectionId,
|
}
|
||||||
{
|
|
||||||
orgId: actor.orgId,
|
|
||||||
encryptedCredentials,
|
|
||||||
...params
|
|
||||||
},
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
|
|
||||||
return updatedConnection;
|
|
||||||
});
|
|
||||||
|
|
||||||
return decryptAppConnection(updatedAppConnection, kmsService);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteAppConnection = async (app: AppConnection, connectionId: string, actor: OrgServiceActor) => {
|
const deleteAppConnection = async (app: AppConnection, connectionId: string, actor: OrgServiceActor) => {
|
||||||
@@ -306,7 +281,10 @@ export const appConnectionServiceFactory = ({
|
|||||||
|
|
||||||
return await decryptAppConnection(deletedAppConnection, kmsService);
|
return await decryptAppConnection(deletedAppConnection, kmsService);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof DatabaseError && (err.error as { code: string })?.code === "23503") {
|
if (
|
||||||
|
err instanceof DatabaseError &&
|
||||||
|
(err.error as { code: string })?.code === DatabaseErrorCode.ForeignKeyViolation
|
||||||
|
) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message:
|
message:
|
||||||
"Cannot delete App Connection with existing connections. Remove all existing connections and try again."
|
"Cannot delete App Connection with existing connections. Remove all existing connections and try again."
|
||||||
@@ -382,6 +360,7 @@ export const appConnectionServiceFactory = ({
|
|||||||
deleteAppConnection,
|
deleteAppConnection,
|
||||||
connectAppConnectionById,
|
connectAppConnectionById,
|
||||||
listAvailableAppConnectionsForUser,
|
listAvailableAppConnectionsForUser,
|
||||||
github: githubConnectionService(connectAppConnectionById)
|
github: githubConnectionService(connectAppConnectionById),
|
||||||
|
gcp: gcpConnectionService(connectAppConnectionById)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@@ -11,9 +11,11 @@ import {
|
|||||||
TValidateGitHubConnectionCredentials
|
TValidateGitHubConnectionCredentials
|
||||||
} from "@app/services/app-connection/github";
|
} from "@app/services/app-connection/github";
|
||||||
|
|
||||||
export type TAppConnection = { id: string } & (TAwsConnection | TGitHubConnection);
|
import { TGcpConnection, TGcpConnectionConfig, TGcpConnectionInput, TValidateGcpConnectionCredentials } from "./gcp";
|
||||||
|
|
||||||
export type TAppConnectionInput = { id: string } & (TAwsConnectionInput | TGitHubConnectionInput);
|
export type TAppConnection = { id: string } & (TAwsConnection | TGitHubConnection | TGcpConnection);
|
||||||
|
|
||||||
|
export type TAppConnectionInput = { id: string } & (TAwsConnectionInput | TGitHubConnectionInput | TGcpConnectionInput);
|
||||||
|
|
||||||
export type TCreateAppConnectionDTO = Pick<
|
export type TCreateAppConnectionDTO = Pick<
|
||||||
TAppConnectionInput,
|
TAppConnectionInput,
|
||||||
@@ -24,8 +26,9 @@ export type TUpdateAppConnectionDTO = Partial<Omit<TCreateAppConnectionDTO, "met
|
|||||||
connectionId: string;
|
connectionId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TAppConnectionConfig = TAwsConnectionConfig | TGitHubConnectionConfig;
|
export type TAppConnectionConfig = TAwsConnectionConfig | TGitHubConnectionConfig | TGcpConnectionConfig;
|
||||||
|
|
||||||
export type TValidateAppConnectionCredentials =
|
export type TValidateAppConnectionCredentials =
|
||||||
| TValidateAwsConnectionCredentials
|
| TValidateAwsConnectionCredentials
|
||||||
| TValidateGitHubConnectionCredentials;
|
| TValidateGitHubConnectionCredentials
|
||||||
|
| TValidateGcpConnectionCredentials;
|
||||||
|
@@ -81,11 +81,14 @@ export const getAwsConnectionConfig = async (appConnection: TAwsConnectionConfig
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const validateAwsConnectionCredentials = async (appConnection: TAwsConnectionConfig) => {
|
export const validateAwsConnectionCredentials = async (appConnection: TAwsConnectionConfig) => {
|
||||||
const awsConfig = await getAwsConnectionConfig(appConnection);
|
let resp: AWS.STS.GetCallerIdentityResponse & {
|
||||||
const sts = new AWS.STS(awsConfig);
|
$response: AWS.Response<AWS.STS.GetCallerIdentityResponse, AWS.AWSError>;
|
||||||
let resp: Awaited<ReturnType<ReturnType<typeof sts.getCallerIdentity>["promise"]>>;
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const awsConfig = await getAwsConnectionConfig(appConnection);
|
||||||
|
const sts = new AWS.STS(awsConfig);
|
||||||
|
|
||||||
resp = await sts.getCallerIdentity().promise();
|
resp = await sts.getCallerIdentity().promise();
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
@@ -93,7 +96,7 @@ export const validateAwsConnectionCredentials = async (appConnection: TAwsConnec
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resp.$response.httpResponse.statusCode !== 200)
|
if (resp?.$response.httpResponse.statusCode !== 200)
|
||||||
throw new InternalServerError({
|
throw new InternalServerError({
|
||||||
message: `Unable to validate credentials: ${
|
message: `Unable to validate credentials: ${
|
||||||
resp.$response.error?.message ??
|
resp.$response.error?.message ??
|
||||||
|
@@ -0,0 +1,3 @@
|
|||||||
|
export enum GcpConnectionMethod {
|
||||||
|
ServiceAccountImpersonation = "service-account-impersonation"
|
||||||
|
}
|
164
backend/src/services/app-connection/gcp/gcp-connection-fns.ts
Normal file
164
backend/src/services/app-connection/gcp/gcp-connection-fns.ts
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
import { gaxios, Impersonated, JWT } from "google-auth-library";
|
||||||
|
import { GetAccessTokenResponse } from "google-auth-library/build/src/auth/oauth2client";
|
||||||
|
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { request } from "@app/lib/config/request";
|
||||||
|
import { BadRequestError, InternalServerError } from "@app/lib/errors";
|
||||||
|
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||||
|
|
||||||
|
import { AppConnection } from "../app-connection-enums";
|
||||||
|
import { getAppConnectionMethodName } from "../app-connection-fns";
|
||||||
|
import { GcpConnectionMethod } from "./gcp-connection-enums";
|
||||||
|
import {
|
||||||
|
GCPApp,
|
||||||
|
GCPGetProjectsRes,
|
||||||
|
GCPGetServiceRes,
|
||||||
|
TGcpConnection,
|
||||||
|
TGcpConnectionConfig
|
||||||
|
} from "./gcp-connection-types";
|
||||||
|
|
||||||
|
export const getGcpAppConnectionListItem = () => {
|
||||||
|
return {
|
||||||
|
name: "GCP" as const,
|
||||||
|
app: AppConnection.GCP as const,
|
||||||
|
methods: Object.values(GcpConnectionMethod) as [GcpConnectionMethod.ServiceAccountImpersonation]
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getGcpConnectionAuthToken = async (appConnection: TGcpConnectionConfig) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
if (!appCfg.INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL) {
|
||||||
|
throw new InternalServerError({
|
||||||
|
message: `Environment variables have not been configured for GCP ${getAppConnectionMethodName(
|
||||||
|
GcpConnectionMethod.ServiceAccountImpersonation
|
||||||
|
)}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const credJson = JSON.parse(appCfg.INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL) as {
|
||||||
|
client_email: string;
|
||||||
|
private_key: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const sourceClient = new JWT({
|
||||||
|
email: credJson.client_email,
|
||||||
|
key: credJson.private_key,
|
||||||
|
scopes: ["https://www.googleapis.com/auth/cloud-platform"]
|
||||||
|
});
|
||||||
|
|
||||||
|
const impersonatedCredentials = new Impersonated({
|
||||||
|
sourceClient,
|
||||||
|
targetPrincipal: appConnection.credentials.serviceAccountEmail,
|
||||||
|
lifetime: 3600,
|
||||||
|
delegates: [],
|
||||||
|
targetScopes: ["https://www.googleapis.com/auth/cloud-platform"]
|
||||||
|
});
|
||||||
|
|
||||||
|
let tokenResponse: GetAccessTokenResponse | undefined;
|
||||||
|
try {
|
||||||
|
tokenResponse = await impersonatedCredentials.getAccessToken();
|
||||||
|
} catch (error) {
|
||||||
|
let message = "Unable to validate connection";
|
||||||
|
if (error instanceof gaxios.GaxiosError) {
|
||||||
|
message = error.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BadRequestError({
|
||||||
|
message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tokenResponse || !tokenResponse.token) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Unable to validate connection`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenResponse.token;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getGcpSecretManagerProjects = async (appConnection: TGcpConnection) => {
|
||||||
|
const accessToken = await getGcpConnectionAuthToken(appConnection);
|
||||||
|
|
||||||
|
let gcpApps: GCPApp[] = [];
|
||||||
|
|
||||||
|
const pageSize = 100;
|
||||||
|
let pageToken: string | undefined;
|
||||||
|
let hasMorePages = true;
|
||||||
|
|
||||||
|
const projects: {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
|
while (hasMorePages) {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
pageSize: String(pageSize),
|
||||||
|
...(pageToken ? { pageToken } : {})
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const { data } = await request.get<GCPGetProjectsRes>(`${IntegrationUrls.GCP_API_URL}/v1/projects`, {
|
||||||
|
params,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Accept-Encoding": "application/json"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
gcpApps = gcpApps.concat(data.projects);
|
||||||
|
|
||||||
|
if (!data.nextPageToken) {
|
||||||
|
hasMorePages = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pageToken = data.nextPageToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
for await (const gcpApp of gcpApps) {
|
||||||
|
try {
|
||||||
|
const res = (
|
||||||
|
await request.get<GCPGetServiceRes>(
|
||||||
|
`${IntegrationUrls.GCP_SERVICE_USAGE_URL}/v1/projects/${gcpApp.projectId}/services/${IntegrationUrls.GCP_SECRET_MANAGER_SERVICE_NAME}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Accept-Encoding": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).data;
|
||||||
|
|
||||||
|
if (res.state === "ENABLED") {
|
||||||
|
projects.push({
|
||||||
|
name: gcpApp.name,
|
||||||
|
id: gcpApp.projectId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return projects;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const validateGcpConnectionCredentials = async (appConnection: TGcpConnectionConfig) => {
|
||||||
|
// Check if provided service account email suffix matches organization ID.
|
||||||
|
// We do this to mitigate confused deputy attacks in multi-tenant instances
|
||||||
|
if (appConnection.credentials.serviceAccountEmail) {
|
||||||
|
const expectedAccountIdSuffix = appConnection.orgId.split("-").slice(0, 2).join("-");
|
||||||
|
const serviceAccountId = appConnection.credentials.serviceAccountEmail.split("@")[0];
|
||||||
|
if (!serviceAccountId.endsWith(expectedAccountIdSuffix)) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `GCP service account ID must have a suffix of "${expectedAccountIdSuffix}" e.g. service-account-${expectedAccountIdSuffix}@my-project.iam.gserviceaccount.com"`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await getGcpConnectionAuthToken(appConnection);
|
||||||
|
|
||||||
|
return appConnection.credentials;
|
||||||
|
};
|
@@ -0,0 +1,65 @@
|
|||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
import { AppConnections } from "@app/lib/api-docs";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import {
|
||||||
|
BaseAppConnectionSchema,
|
||||||
|
GenericCreateAppConnectionFieldsSchema,
|
||||||
|
GenericUpdateAppConnectionFieldsSchema
|
||||||
|
} from "@app/services/app-connection/app-connection-schemas";
|
||||||
|
|
||||||
|
import { GcpConnectionMethod } from "./gcp-connection-enums";
|
||||||
|
|
||||||
|
export const GcpConnectionServiceAccountImpersonationCredentialsSchema = z.object({
|
||||||
|
serviceAccountEmail: z.string().email().trim().min(1, "Service account email required")
|
||||||
|
});
|
||||||
|
|
||||||
|
const BaseGcpConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.GCP) });
|
||||||
|
|
||||||
|
export const GcpConnectionSchema = z.intersection(
|
||||||
|
BaseGcpConnectionSchema,
|
||||||
|
z.discriminatedUnion("method", [
|
||||||
|
z.object({
|
||||||
|
method: z.literal(GcpConnectionMethod.ServiceAccountImpersonation),
|
||||||
|
credentials: GcpConnectionServiceAccountImpersonationCredentialsSchema
|
||||||
|
})
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
export const SanitizedGcpConnectionSchema = z.discriminatedUnion("method", [
|
||||||
|
BaseGcpConnectionSchema.extend({
|
||||||
|
method: z.literal(GcpConnectionMethod.ServiceAccountImpersonation),
|
||||||
|
credentials: GcpConnectionServiceAccountImpersonationCredentialsSchema.pick({})
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const ValidateGcpConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||||
|
z.object({
|
||||||
|
method: z
|
||||||
|
.literal(GcpConnectionMethod.ServiceAccountImpersonation)
|
||||||
|
.describe(AppConnections?.CREATE(AppConnection.GCP).method),
|
||||||
|
credentials: GcpConnectionServiceAccountImpersonationCredentialsSchema.describe(
|
||||||
|
AppConnections.CREATE(AppConnection.GCP).credentials
|
||||||
|
)
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const CreateGcpConnectionSchema = ValidateGcpConnectionCredentialsSchema.and(
|
||||||
|
GenericCreateAppConnectionFieldsSchema(AppConnection.GCP)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const UpdateGcpConnectionSchema = z
|
||||||
|
.object({
|
||||||
|
credentials: GcpConnectionServiceAccountImpersonationCredentialsSchema.optional().describe(
|
||||||
|
AppConnections.UPDATE(AppConnection.GCP).credentials
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.GCP));
|
||||||
|
|
||||||
|
export const GcpConnectionListItemSchema = z.object({
|
||||||
|
name: z.literal("GCP"),
|
||||||
|
app: z.literal(AppConnection.GCP),
|
||||||
|
// the below is preferable but currently breaks with our zod to json schema parser
|
||||||
|
// methods: z.tuple([z.literal(GitHubConnectionMethod.App), z.literal(GitHubConnectionMethod.OAuth)]),
|
||||||
|
methods: z.nativeEnum(GcpConnectionMethod).array()
|
||||||
|
});
|
@@ -0,0 +1,29 @@
|
|||||||
|
import { OrgServiceActor } from "@app/lib/types";
|
||||||
|
|
||||||
|
import { AppConnection } from "../app-connection-enums";
|
||||||
|
import { getGcpSecretManagerProjects } from "./gcp-connection-fns";
|
||||||
|
import { TGcpConnection } from "./gcp-connection-types";
|
||||||
|
|
||||||
|
type TGetAppConnectionFunc = (
|
||||||
|
app: AppConnection,
|
||||||
|
connectionId: string,
|
||||||
|
actor: OrgServiceActor
|
||||||
|
) => Promise<TGcpConnection>;
|
||||||
|
|
||||||
|
export const gcpConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
|
||||||
|
const listSecretManagerProjects = async (connectionId: string, actor: OrgServiceActor) => {
|
||||||
|
const appConnection = await getAppConnection(AppConnection.GCP, connectionId, actor);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const projects = await getGcpSecretManagerProjects(appConnection);
|
||||||
|
|
||||||
|
return projects;
|
||||||
|
} catch (error) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
listSecretManagerProjects
|
||||||
|
};
|
||||||
|
};
|
@@ -0,0 +1,45 @@
|
|||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
import { DiscriminativePick } from "@app/lib/types";
|
||||||
|
|
||||||
|
import { AppConnection } from "../app-connection-enums";
|
||||||
|
import {
|
||||||
|
CreateGcpConnectionSchema,
|
||||||
|
GcpConnectionSchema,
|
||||||
|
ValidateGcpConnectionCredentialsSchema
|
||||||
|
} from "./gcp-connection-schemas";
|
||||||
|
|
||||||
|
export type TGcpConnection = z.infer<typeof GcpConnectionSchema>;
|
||||||
|
|
||||||
|
export type TGcpConnectionInput = z.infer<typeof CreateGcpConnectionSchema> & {
|
||||||
|
app: AppConnection.GCP;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TValidateGcpConnectionCredentials = typeof ValidateGcpConnectionCredentialsSchema;
|
||||||
|
|
||||||
|
export type TGcpConnectionConfig = DiscriminativePick<TGcpConnectionInput, "method" | "app" | "credentials"> & {
|
||||||
|
orgId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GCPApp = {
|
||||||
|
projectNumber: string;
|
||||||
|
projectId: string;
|
||||||
|
lifecycleState: "ACTIVE" | "LIFECYCLE_STATE_UNSPECIFIED" | "DELETE_REQUESTED" | "DELETE_IN_PROGRESS";
|
||||||
|
name: string;
|
||||||
|
createTime: string;
|
||||||
|
parent: {
|
||||||
|
type: "organization" | "folder" | "project";
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GCPGetProjectsRes = {
|
||||||
|
projects: GCPApp[];
|
||||||
|
nextPageToken?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GCPGetServiceRes = {
|
||||||
|
name: string;
|
||||||
|
parent: string;
|
||||||
|
state: "ENABLED" | "DISABLED" | "STATE_UNSPECIFIED";
|
||||||
|
};
|
4
backend/src/services/app-connection/gcp/index.ts
Normal file
4
backend/src/services/app-connection/gcp/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export * from "./gcp-connection-enums";
|
||||||
|
export * from "./gcp-connection-fns";
|
||||||
|
export * from "./gcp-connection-schemas";
|
||||||
|
export * from "./gcp-connection-types";
|
@@ -57,6 +57,12 @@ export const getTokenConfig = (tokenType: TokenType) => {
|
|||||||
const expiresAt = new Date(new Date().getTime() + 86400000);
|
const expiresAt = new Date(new Date().getTime() + 86400000);
|
||||||
return { token, expiresAt };
|
return { token, expiresAt };
|
||||||
}
|
}
|
||||||
|
case TokenType.TOKEN_EMAIL_PASSWORD_SETUP: {
|
||||||
|
// generate random hex
|
||||||
|
const token = crypto.randomBytes(16).toString("hex");
|
||||||
|
const expiresAt = new Date(new Date().getTime() + 86400000);
|
||||||
|
return { token, expiresAt };
|
||||||
|
}
|
||||||
case TokenType.TOKEN_USER_UNLOCK: {
|
case TokenType.TOKEN_USER_UNLOCK: {
|
||||||
const token = crypto.randomBytes(16).toString("hex");
|
const token = crypto.randomBytes(16).toString("hex");
|
||||||
const expiresAt = new Date(new Date().getTime() + 259200000);
|
const expiresAt = new Date(new Date().getTime() + 259200000);
|
||||||
|
@@ -6,6 +6,7 @@ export enum TokenType {
|
|||||||
TOKEN_EMAIL_MFA = "emailMfa",
|
TOKEN_EMAIL_MFA = "emailMfa",
|
||||||
TOKEN_EMAIL_ORG_INVITATION = "organizationInvitation",
|
TOKEN_EMAIL_ORG_INVITATION = "organizationInvitation",
|
||||||
TOKEN_EMAIL_PASSWORD_RESET = "passwordReset",
|
TOKEN_EMAIL_PASSWORD_RESET = "passwordReset",
|
||||||
|
TOKEN_EMAIL_PASSWORD_SETUP = "passwordSetup",
|
||||||
TOKEN_USER_UNLOCK = "userUnlock"
|
TOKEN_USER_UNLOCK = "userUnlock"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -4,6 +4,8 @@ import jwt from "jsonwebtoken";
|
|||||||
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
|
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
|
import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { OrgServiceActor } from "@app/lib/types";
|
||||||
|
|
||||||
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
||||||
import { TokenType } from "../auth-token/auth-token-types";
|
import { TokenType } from "../auth-token/auth-token-types";
|
||||||
@@ -11,8 +13,13 @@ import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
|||||||
import { TTotpConfigDALFactory } from "../totp/totp-config-dal";
|
import { TTotpConfigDALFactory } from "../totp/totp-config-dal";
|
||||||
import { TUserDALFactory } from "../user/user-dal";
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
import { TAuthDALFactory } from "./auth-dal";
|
import { TAuthDALFactory } from "./auth-dal";
|
||||||
import { TChangePasswordDTO, TCreateBackupPrivateKeyDTO, TResetPasswordViaBackupKeyDTO } from "./auth-password-type";
|
import {
|
||||||
import { AuthTokenType } from "./auth-type";
|
TChangePasswordDTO,
|
||||||
|
TCreateBackupPrivateKeyDTO,
|
||||||
|
TResetPasswordViaBackupKeyDTO,
|
||||||
|
TSetupPasswordViaBackupKeyDTO
|
||||||
|
} from "./auth-password-type";
|
||||||
|
import { ActorType, AuthMethod, AuthTokenType } from "./auth-type";
|
||||||
|
|
||||||
type TAuthPasswordServiceFactoryDep = {
|
type TAuthPasswordServiceFactoryDep = {
|
||||||
authDAL: TAuthDALFactory;
|
authDAL: TAuthDALFactory;
|
||||||
@@ -169,8 +176,13 @@ export const authPaswordServiceFactory = ({
|
|||||||
verifier,
|
verifier,
|
||||||
encryptedPrivateKeyIV,
|
encryptedPrivateKeyIV,
|
||||||
encryptedPrivateKeyTag,
|
encryptedPrivateKeyTag,
|
||||||
userId
|
userId,
|
||||||
|
password
|
||||||
}: TResetPasswordViaBackupKeyDTO) => {
|
}: TResetPasswordViaBackupKeyDTO) => {
|
||||||
|
const cfg = getConfig();
|
||||||
|
|
||||||
|
const hashedPassword = await bcrypt.hash(password, cfg.BCRYPT_SALT_ROUND);
|
||||||
|
|
||||||
await userDAL.updateUserEncryptionByUserId(userId, {
|
await userDAL.updateUserEncryptionByUserId(userId, {
|
||||||
encryptionVersion: 2,
|
encryptionVersion: 2,
|
||||||
protectedKey,
|
protectedKey,
|
||||||
@@ -180,7 +192,8 @@ export const authPaswordServiceFactory = ({
|
|||||||
iv: encryptedPrivateKeyIV,
|
iv: encryptedPrivateKeyIV,
|
||||||
tag: encryptedPrivateKeyTag,
|
tag: encryptedPrivateKeyTag,
|
||||||
salt,
|
salt,
|
||||||
verifier
|
verifier,
|
||||||
|
hashedPassword
|
||||||
});
|
});
|
||||||
|
|
||||||
await userDAL.updateById(userId, {
|
await userDAL.updateById(userId, {
|
||||||
@@ -267,6 +280,108 @@ export const authPaswordServiceFactory = ({
|
|||||||
return backupKey;
|
return backupKey;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const sendPasswordSetupEmail = async (actor: OrgServiceActor) => {
|
||||||
|
if (actor.type !== ActorType.USER)
|
||||||
|
throw new BadRequestError({ message: `Actor of type ${actor.type} cannot set password` });
|
||||||
|
|
||||||
|
const user = await userDAL.findById(actor.id);
|
||||||
|
|
||||||
|
if (!user) throw new BadRequestError({ message: `Could not find user with ID ${actor.id}` });
|
||||||
|
|
||||||
|
if (!user.isAccepted || !user.authMethods)
|
||||||
|
throw new BadRequestError({ message: `You must complete signup to set a password` });
|
||||||
|
|
||||||
|
const cfg = getConfig();
|
||||||
|
|
||||||
|
const token = await tokenService.createTokenForUser({
|
||||||
|
type: TokenType.TOKEN_EMAIL_PASSWORD_SETUP,
|
||||||
|
userId: user.id
|
||||||
|
});
|
||||||
|
|
||||||
|
const email = user.email ?? user.username;
|
||||||
|
|
||||||
|
await smtpService.sendMail({
|
||||||
|
template: SmtpTemplates.SetupPassword,
|
||||||
|
recipients: [email],
|
||||||
|
subjectLine: "Infisical Password Setup",
|
||||||
|
substitutions: {
|
||||||
|
email,
|
||||||
|
token,
|
||||||
|
callback_url: cfg.SITE_URL ? `${cfg.SITE_URL}/password-setup` : ""
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const setupPassword = async (
|
||||||
|
{
|
||||||
|
encryptedPrivateKey,
|
||||||
|
protectedKeyTag,
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
salt,
|
||||||
|
verifier,
|
||||||
|
encryptedPrivateKeyIV,
|
||||||
|
encryptedPrivateKeyTag,
|
||||||
|
password,
|
||||||
|
token
|
||||||
|
}: TSetupPasswordViaBackupKeyDTO,
|
||||||
|
actor: OrgServiceActor
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
await tokenService.validateTokenForUser({
|
||||||
|
type: TokenType.TOKEN_EMAIL_PASSWORD_SETUP,
|
||||||
|
userId: actor.id,
|
||||||
|
code: token
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
throw new BadRequestError({ message: "Expired or invalid token. Please try again." });
|
||||||
|
}
|
||||||
|
|
||||||
|
await userDAL.transaction(async (tx) => {
|
||||||
|
const user = await userDAL.findById(actor.id, tx);
|
||||||
|
|
||||||
|
if (!user) throw new BadRequestError({ message: `Could not find user with ID ${actor.id}` });
|
||||||
|
|
||||||
|
if (!user.isAccepted || !user.authMethods)
|
||||||
|
throw new BadRequestError({ message: `You must complete signup to set a password` });
|
||||||
|
|
||||||
|
if (!user.authMethods.includes(AuthMethod.EMAIL)) {
|
||||||
|
await userDAL.updateById(
|
||||||
|
actor.id,
|
||||||
|
{
|
||||||
|
authMethods: [...user.authMethods, AuthMethod.EMAIL]
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cfg = getConfig();
|
||||||
|
|
||||||
|
const hashedPassword = await bcrypt.hash(password, cfg.BCRYPT_SALT_ROUND);
|
||||||
|
|
||||||
|
await userDAL.updateUserEncryptionByUserId(
|
||||||
|
actor.id,
|
||||||
|
{
|
||||||
|
encryptionVersion: 2,
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
iv: encryptedPrivateKeyIV,
|
||||||
|
tag: encryptedPrivateKeyTag,
|
||||||
|
salt,
|
||||||
|
verifier,
|
||||||
|
hashedPassword,
|
||||||
|
serverPrivateKey: null,
|
||||||
|
clientPublicKey: null
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await tokenService.revokeAllMySessions(actor.id);
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
generateServerPubKey,
|
generateServerPubKey,
|
||||||
changePassword,
|
changePassword,
|
||||||
@@ -274,6 +389,8 @@ export const authPaswordServiceFactory = ({
|
|||||||
sendPasswordResetEmail,
|
sendPasswordResetEmail,
|
||||||
verifyPasswordResetEmail,
|
verifyPasswordResetEmail,
|
||||||
createBackupPrivateKey,
|
createBackupPrivateKey,
|
||||||
getBackupPrivateKeyOfUser
|
getBackupPrivateKeyOfUser,
|
||||||
|
sendPasswordSetupEmail,
|
||||||
|
setupPassword
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@@ -23,6 +23,20 @@ export type TResetPasswordViaBackupKeyDTO = {
|
|||||||
encryptedPrivateKeyTag: string;
|
encryptedPrivateKeyTag: string;
|
||||||
salt: string;
|
salt: string;
|
||||||
verifier: string;
|
verifier: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TSetupPasswordViaBackupKeyDTO = {
|
||||||
|
protectedKey: string;
|
||||||
|
protectedKeyIV: string;
|
||||||
|
protectedKeyTag: string;
|
||||||
|
encryptedPrivateKey: string;
|
||||||
|
encryptedPrivateKeyIV: string;
|
||||||
|
encryptedPrivateKeyTag: string;
|
||||||
|
salt: string;
|
||||||
|
verifier: string;
|
||||||
|
password: string;
|
||||||
|
token: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCreateBackupPrivateKeyDTO = {
|
export type TCreateBackupPrivateKeyDTO = {
|
||||||
|
@@ -126,12 +126,12 @@ export const identityAwsAuthServiceFactory = ({
|
|||||||
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
||||||
} as TIdentityAccessTokenJwtPayload,
|
} as TIdentityAccessTokenJwtPayload,
|
||||||
appCfg.AUTH_SECRET,
|
appCfg.AUTH_SECRET,
|
||||||
{
|
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
|
||||||
expiresIn:
|
Number(identityAccessToken.accessTokenTTL) === 0
|
||||||
Number(identityAccessToken.accessTokenMaxTTL) === 0
|
? undefined
|
||||||
? undefined
|
: {
|
||||||
: Number(identityAccessToken.accessTokenMaxTTL)
|
expiresIn: Number(identityAccessToken.accessTokenTTL)
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return { accessToken, identityAwsAuth, identityAccessToken, identityMembershipOrg };
|
return { accessToken, identityAwsAuth, identityAccessToken, identityMembershipOrg };
|
||||||
|
@@ -99,12 +99,12 @@ export const identityAzureAuthServiceFactory = ({
|
|||||||
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
||||||
} as TIdentityAccessTokenJwtPayload,
|
} as TIdentityAccessTokenJwtPayload,
|
||||||
appCfg.AUTH_SECRET,
|
appCfg.AUTH_SECRET,
|
||||||
{
|
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
|
||||||
expiresIn:
|
Number(identityAccessToken.accessTokenTTL) === 0
|
||||||
Number(identityAccessToken.accessTokenMaxTTL) === 0
|
? undefined
|
||||||
? undefined
|
: {
|
||||||
: Number(identityAccessToken.accessTokenMaxTTL)
|
expiresIn: Number(identityAccessToken.accessTokenTTL)
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return { accessToken, identityAzureAuth, identityAccessToken, identityMembershipOrg };
|
return { accessToken, identityAzureAuth, identityAccessToken, identityMembershipOrg };
|
||||||
|
@@ -138,12 +138,12 @@ export const identityGcpAuthServiceFactory = ({
|
|||||||
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
||||||
} as TIdentityAccessTokenJwtPayload,
|
} as TIdentityAccessTokenJwtPayload,
|
||||||
appCfg.AUTH_SECRET,
|
appCfg.AUTH_SECRET,
|
||||||
{
|
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
|
||||||
expiresIn:
|
Number(identityAccessToken.accessTokenTTL) === 0
|
||||||
Number(identityAccessToken.accessTokenMaxTTL) === 0
|
? undefined
|
||||||
? undefined
|
: {
|
||||||
: Number(identityAccessToken.accessTokenMaxTTL)
|
expiresIn: Number(identityAccessToken.accessTokenTTL)
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return { accessToken, identityGcpAuth, identityAccessToken, identityMembershipOrg };
|
return { accessToken, identityGcpAuth, identityAccessToken, identityMembershipOrg };
|
||||||
|
@@ -212,12 +212,12 @@ export const identityJwtAuthServiceFactory = ({
|
|||||||
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
||||||
} as TIdentityAccessTokenJwtPayload,
|
} as TIdentityAccessTokenJwtPayload,
|
||||||
appCfg.AUTH_SECRET,
|
appCfg.AUTH_SECRET,
|
||||||
{
|
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
|
||||||
expiresIn:
|
Number(identityAccessToken.accessTokenTTL) === 0
|
||||||
Number(identityAccessToken.accessTokenMaxTTL) === 0
|
? undefined
|
||||||
? undefined
|
: {
|
||||||
: Number(identityAccessToken.accessTokenMaxTTL)
|
expiresIn: Number(identityAccessToken.accessTokenTTL)
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return { accessToken, identityJwtAuth, identityAccessToken, identityMembershipOrg };
|
return { accessToken, identityJwtAuth, identityAccessToken, identityMembershipOrg };
|
||||||
|
@@ -229,12 +229,12 @@ export const identityKubernetesAuthServiceFactory = ({
|
|||||||
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
||||||
} as TIdentityAccessTokenJwtPayload,
|
} as TIdentityAccessTokenJwtPayload,
|
||||||
appCfg.AUTH_SECRET,
|
appCfg.AUTH_SECRET,
|
||||||
{
|
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
|
||||||
expiresIn:
|
Number(identityAccessToken.accessTokenTTL) === 0
|
||||||
Number(identityAccessToken.accessTokenMaxTTL) === 0
|
? undefined
|
||||||
? undefined
|
: {
|
||||||
: Number(identityAccessToken.accessTokenMaxTTL)
|
expiresIn: Number(identityAccessToken.accessTokenTTL)
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return { accessToken, identityKubernetesAuth, identityAccessToken, identityMembershipOrg };
|
return { accessToken, identityKubernetesAuth, identityAccessToken, identityMembershipOrg };
|
||||||
|
@@ -194,12 +194,12 @@ export const identityOidcAuthServiceFactory = ({
|
|||||||
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
||||||
} as TIdentityAccessTokenJwtPayload,
|
} as TIdentityAccessTokenJwtPayload,
|
||||||
appCfg.AUTH_SECRET,
|
appCfg.AUTH_SECRET,
|
||||||
{
|
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
|
||||||
expiresIn:
|
Number(identityAccessToken.accessTokenTTL) === 0
|
||||||
Number(identityAccessToken.accessTokenMaxTTL) === 0
|
? undefined
|
||||||
? undefined
|
: {
|
||||||
: Number(identityAccessToken.accessTokenMaxTTL)
|
expiresIn: Number(identityAccessToken.accessTokenTTL)
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return { accessToken, identityOidcAuth, identityAccessToken, identityMembershipOrg };
|
return { accessToken, identityOidcAuth, identityAccessToken, identityMembershipOrg };
|
||||||
|
@@ -328,12 +328,12 @@ export const identityTokenAuthServiceFactory = ({
|
|||||||
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
||||||
} as TIdentityAccessTokenJwtPayload,
|
} as TIdentityAccessTokenJwtPayload,
|
||||||
appCfg.AUTH_SECRET,
|
appCfg.AUTH_SECRET,
|
||||||
{
|
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
|
||||||
expiresIn:
|
Number(identityAccessToken.accessTokenTTL) === 0
|
||||||
Number(identityAccessToken.accessTokenMaxTTL) === 0
|
? undefined
|
||||||
? undefined
|
: {
|
||||||
: Number(identityAccessToken.accessTokenMaxTTL)
|
expiresIn: Number(identityAccessToken.accessTokenTTL)
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return { accessToken, identityTokenAuth, identityAccessToken, identityMembershipOrg };
|
return { accessToken, identityTokenAuth, identityAccessToken, identityMembershipOrg };
|
||||||
|
@@ -129,12 +129,12 @@ export const identityUaServiceFactory = ({
|
|||||||
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
||||||
} as TIdentityAccessTokenJwtPayload,
|
} as TIdentityAccessTokenJwtPayload,
|
||||||
appCfg.AUTH_SECRET,
|
appCfg.AUTH_SECRET,
|
||||||
{
|
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
|
||||||
expiresIn:
|
Number(identityAccessToken.accessTokenTTL) === 0
|
||||||
Number(identityAccessToken.accessTokenMaxTTL) === 0
|
? undefined
|
||||||
? undefined
|
: {
|
||||||
: Number(identityAccessToken.accessTokenMaxTTL)
|
expiresIn: Number(identityAccessToken.accessTokenTTL)
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return { accessToken, identityUa, validClientSecretInfo, identityAccessToken, identityMembershipOrg };
|
return { accessToken, identityUa, validClientSecretInfo, identityAccessToken, identityMembershipOrg };
|
||||||
|
@@ -132,16 +132,26 @@ const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Return list of names of apps for Vercel integration
|
* Return list of names of apps for Vercel integration
|
||||||
|
* This is re-used for getting custom environments for Vercel
|
||||||
*/
|
*/
|
||||||
const getAppsVercel = async ({ accessToken, teamId }: { teamId?: string | null; accessToken: string }) => {
|
export const getAppsVercel = async ({ accessToken, teamId }: { teamId?: string | null; accessToken: string }) => {
|
||||||
const apps: Array<{ name: string; appId: string }> = [];
|
const apps: Array<{ name: string; appId: string; customEnvironments: Array<{ slug: string; id: string }> }> = [];
|
||||||
|
|
||||||
const limit = "20";
|
const limit = "20";
|
||||||
let hasMorePages = true;
|
let hasMorePages = true;
|
||||||
let next: number | null = null;
|
let next: number | null = null;
|
||||||
|
|
||||||
interface Response {
|
interface Response {
|
||||||
projects: { name: string; id: string }[];
|
projects: {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
customEnvironments?: {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
description: string;
|
||||||
|
slug: string;
|
||||||
|
}[];
|
||||||
|
}[];
|
||||||
pagination: {
|
pagination: {
|
||||||
count: number;
|
count: number;
|
||||||
next: number | null;
|
next: number | null;
|
||||||
@@ -173,7 +183,12 @@ const getAppsVercel = async ({ accessToken, teamId }: { teamId?: string | null;
|
|||||||
data.projects.forEach((a) => {
|
data.projects.forEach((a) => {
|
||||||
apps.push({
|
apps.push({
|
||||||
name: a.name,
|
name: a.name,
|
||||||
appId: a.id
|
appId: a.id,
|
||||||
|
customEnvironments:
|
||||||
|
a.customEnvironments?.map((env) => ({
|
||||||
|
slug: env.slug,
|
||||||
|
id: env.id
|
||||||
|
})) ?? []
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -25,11 +25,12 @@ import { TIntegrationDALFactory } from "../integration/integration-dal";
|
|||||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||||
import { KmsDataKey } from "../kms/kms-types";
|
import { KmsDataKey } from "../kms/kms-types";
|
||||||
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
|
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
|
||||||
import { getApps } from "./integration-app-list";
|
import { getApps, getAppsVercel } from "./integration-app-list";
|
||||||
import { TCircleCIContext } from "./integration-app-types";
|
import { TCircleCIContext } from "./integration-app-types";
|
||||||
import { TIntegrationAuthDALFactory } from "./integration-auth-dal";
|
import { TIntegrationAuthDALFactory } from "./integration-auth-dal";
|
||||||
import { IntegrationAuthMetadataSchema, TIntegrationAuthMetadata } from "./integration-auth-schema";
|
import { IntegrationAuthMetadataSchema, TIntegrationAuthMetadata } from "./integration-auth-schema";
|
||||||
import {
|
import {
|
||||||
|
GetVercelCustomEnvironmentsDTO,
|
||||||
OctopusDeployScope,
|
OctopusDeployScope,
|
||||||
TBitbucketEnvironment,
|
TBitbucketEnvironment,
|
||||||
TBitbucketWorkspace,
|
TBitbucketWorkspace,
|
||||||
@@ -1825,6 +1826,41 @@ export const integrationAuthServiceFactory = ({
|
|||||||
return integrationAuthDAL.create(newIntegrationAuth);
|
return integrationAuthDAL.create(newIntegrationAuth);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getVercelCustomEnvironments = async ({
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
teamId,
|
||||||
|
id
|
||||||
|
}: GetVercelCustomEnvironmentsDTO) => {
|
||||||
|
const integrationAuth = await integrationAuthDAL.findById(id);
|
||||||
|
if (!integrationAuth) throw new NotFoundError({ message: `Integration auth with ID '${id}' not found` });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId: integrationAuth.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
|
});
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
|
||||||
|
|
||||||
|
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(integrationAuth.projectId);
|
||||||
|
const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey);
|
||||||
|
|
||||||
|
const vercelApps = await getAppsVercel({
|
||||||
|
accessToken,
|
||||||
|
teamId
|
||||||
|
});
|
||||||
|
|
||||||
|
return vercelApps.map((app) => ({
|
||||||
|
customEnvironments: app.customEnvironments,
|
||||||
|
appId: app.appId
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
const getOctopusDeploySpaces = async ({
|
const getOctopusDeploySpaces = async ({
|
||||||
actorId,
|
actorId,
|
||||||
actor,
|
actor,
|
||||||
@@ -1944,6 +1980,7 @@ export const integrationAuthServiceFactory = ({
|
|||||||
getIntegrationAccessToken,
|
getIntegrationAccessToken,
|
||||||
duplicateIntegrationAuth,
|
duplicateIntegrationAuth,
|
||||||
getOctopusDeploySpaces,
|
getOctopusDeploySpaces,
|
||||||
getOctopusDeployScopeValues
|
getOctopusDeployScopeValues,
|
||||||
|
getVercelCustomEnvironments
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@@ -284,3 +284,8 @@ export type TOctopusDeployVariableSet = {
|
|||||||
Self: string;
|
Self: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type GetVercelCustomEnvironmentsDTO = {
|
||||||
|
teamId: string;
|
||||||
|
id: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
@@ -1450,9 +1450,13 @@ const syncSecretsVercel = async ({
|
|||||||
secrets: Record<string, { value: string; comment?: string } | null>;
|
secrets: Record<string, { value: string; comment?: string } | null>;
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
}) => {
|
}) => {
|
||||||
|
const isCustomEnvironment = !["development", "preview", "production"].includes(
|
||||||
|
integration.targetEnvironment as string
|
||||||
|
);
|
||||||
interface VercelSecret {
|
interface VercelSecret {
|
||||||
id?: string;
|
id?: string;
|
||||||
type: string;
|
type: string;
|
||||||
|
customEnvironmentIds?: string[];
|
||||||
key: string;
|
key: string;
|
||||||
value: string;
|
value: string;
|
||||||
target: string[];
|
target: string[];
|
||||||
@@ -1486,6 +1490,16 @@ const syncSecretsVercel = async ({
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
).data.envs.filter((secret) => {
|
).data.envs.filter((secret) => {
|
||||||
|
if (isCustomEnvironment) {
|
||||||
|
if (!secret.customEnvironmentIds?.includes(integration.targetEnvironment as string)) {
|
||||||
|
// case: secret does not have the same custom environment
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// no need to check for preview environment, as custom environments are not available in preview
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (!secret.target.includes(integration.targetEnvironment as string)) {
|
if (!secret.target.includes(integration.targetEnvironment as string)) {
|
||||||
// case: secret does not have the same target environment
|
// case: secret does not have the same target environment
|
||||||
return false;
|
return false;
|
||||||
@@ -1583,7 +1597,13 @@ const syncSecretsVercel = async ({
|
|||||||
key,
|
key,
|
||||||
value: infisicalSecrets[key]?.value,
|
value: infisicalSecrets[key]?.value,
|
||||||
type: "encrypted",
|
type: "encrypted",
|
||||||
target: [integration.targetEnvironment as string],
|
...(isCustomEnvironment
|
||||||
|
? {
|
||||||
|
customEnvironmentIds: [integration.targetEnvironment as string]
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
target: [integration.targetEnvironment as string]
|
||||||
|
}),
|
||||||
...(integration.path
|
...(integration.path
|
||||||
? {
|
? {
|
||||||
gitBranch: integration.path
|
gitBranch: integration.path
|
||||||
@@ -1607,9 +1627,19 @@ const syncSecretsVercel = async ({
|
|||||||
key,
|
key,
|
||||||
value: infisicalSecrets[key]?.value,
|
value: infisicalSecrets[key]?.value,
|
||||||
type: res[key].type,
|
type: res[key].type,
|
||||||
target: res[key].target.includes(integration.targetEnvironment as string)
|
|
||||||
? [...res[key].target]
|
...(!isCustomEnvironment
|
||||||
: [...res[key].target, integration.targetEnvironment as string],
|
? {
|
||||||
|
target: res[key].target.includes(integration.targetEnvironment as string)
|
||||||
|
? [...res[key].target]
|
||||||
|
: [...res[key].target, integration.targetEnvironment as string]
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
customEnvironmentIds: res[key].customEnvironmentIds?.includes(integration.targetEnvironment as string)
|
||||||
|
? [...(res[key].customEnvironmentIds || [])]
|
||||||
|
: [...(res[key]?.customEnvironmentIds || []), integration.targetEnvironment as string]
|
||||||
|
}),
|
||||||
|
|
||||||
...(integration.path
|
...(integration.path
|
||||||
? {
|
? {
|
||||||
gitBranch: integration.path
|
gitBranch: integration.path
|
||||||
|
@@ -493,6 +493,7 @@ export const secretFolderDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("parents.environment")
|
db.ref("parents.environment")
|
||||||
)
|
)
|
||||||
.from(TableName.SecretFolder)
|
.from(TableName.SecretFolder)
|
||||||
|
.where(`${TableName.SecretFolder}.isReserved`, false)
|
||||||
.join("parents", `${TableName.SecretFolder}.parentId`, "parents.id");
|
.join("parents", `${TableName.SecretFolder}.parentId`, "parents.id");
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
@@ -69,6 +69,8 @@ const getParametersByPath = async (ssm: AWS.SSM, path: string): Promise<TAWSPara
|
|||||||
attempt += 1;
|
attempt += 1;
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
await sleep();
|
await sleep();
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw e;
|
throw e;
|
||||||
|
@@ -0,0 +1,10 @@
|
|||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||||
|
import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types";
|
||||||
|
|
||||||
|
export const AWS_SECRETS_MANAGER_SYNC_LIST_OPTION: TSecretSyncListItem = {
|
||||||
|
name: "AWS Secrets Manager",
|
||||||
|
destination: SecretSync.AWSSecretsManager,
|
||||||
|
connection: AppConnection.AWS,
|
||||||
|
canImportSecrets: true
|
||||||
|
};
|
@@ -0,0 +1,4 @@
|
|||||||
|
export enum AwsSecretsManagerSyncMappingBehavior {
|
||||||
|
OneToOne = "one-to-one",
|
||||||
|
ManyToOne = "many-to-one"
|
||||||
|
}
|
@@ -0,0 +1,352 @@
|
|||||||
|
import {
|
||||||
|
BatchGetSecretValueCommand,
|
||||||
|
CreateSecretCommand,
|
||||||
|
CreateSecretCommandInput,
|
||||||
|
DeleteSecretCommand,
|
||||||
|
DeleteSecretResponse,
|
||||||
|
ListSecretsCommand,
|
||||||
|
SecretsManagerClient,
|
||||||
|
UpdateSecretCommand,
|
||||||
|
UpdateSecretCommandInput
|
||||||
|
} from "@aws-sdk/client-secrets-manager";
|
||||||
|
import { AWSError } from "aws-sdk";
|
||||||
|
import { CreateSecretResponse, SecretListEntry, SecretValueEntry } from "aws-sdk/clients/secretsmanager";
|
||||||
|
|
||||||
|
import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns";
|
||||||
|
import { AwsSecretsManagerSyncMappingBehavior } from "@app/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-enums";
|
||||||
|
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
|
||||||
|
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
|
||||||
|
|
||||||
|
import { TAwsSecretsManagerSyncWithCredentials } from "./aws-secrets-manager-sync-types";
|
||||||
|
|
||||||
|
type TAwsSecretsRecord = Record<string, SecretListEntry>;
|
||||||
|
type TAwsSecretValuesRecord = Record<string, SecretValueEntry>;
|
||||||
|
|
||||||
|
const MAX_RETRIES = 5;
|
||||||
|
const BATCH_SIZE = 20;
|
||||||
|
|
||||||
|
const getSecretsManagerClient = async (secretSync: TAwsSecretsManagerSyncWithCredentials) => {
|
||||||
|
const { destinationConfig, connection } = secretSync;
|
||||||
|
|
||||||
|
const config = await getAwsConnectionConfig(connection, destinationConfig.region);
|
||||||
|
|
||||||
|
const secretsManagerClient = new SecretsManagerClient({
|
||||||
|
region: config.region,
|
||||||
|
credentials: config.credentials!
|
||||||
|
});
|
||||||
|
|
||||||
|
return secretsManagerClient;
|
||||||
|
};
|
||||||
|
|
||||||
|
const sleep = async () =>
|
||||||
|
new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
const getSecretsRecord = async (client: SecretsManagerClient): Promise<TAwsSecretsRecord> => {
|
||||||
|
const awsSecretsRecord: TAwsSecretsRecord = {};
|
||||||
|
let hasNext = true;
|
||||||
|
let nextToken: string | undefined;
|
||||||
|
let attempt = 0;
|
||||||
|
|
||||||
|
while (hasNext) {
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const output = await client.send(new ListSecretsCommand({ NextToken: nextToken }));
|
||||||
|
|
||||||
|
attempt = 0;
|
||||||
|
|
||||||
|
if (output.SecretList) {
|
||||||
|
output.SecretList.forEach((secretEntry) => {
|
||||||
|
if (secretEntry.Name) {
|
||||||
|
awsSecretsRecord[secretEntry.Name] = secretEntry;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
hasNext = Boolean(output.NextToken);
|
||||||
|
nextToken = output.NextToken;
|
||||||
|
} catch (e) {
|
||||||
|
if ((e as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) {
|
||||||
|
attempt += 1;
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await sleep();
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return awsSecretsRecord;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSecretValuesRecord = async (
|
||||||
|
client: SecretsManagerClient,
|
||||||
|
awsSecretsRecord: TAwsSecretsRecord
|
||||||
|
): Promise<TAwsSecretValuesRecord> => {
|
||||||
|
const awsSecretValuesRecord: TAwsSecretValuesRecord = {};
|
||||||
|
let attempt = 0;
|
||||||
|
|
||||||
|
const secretIdList = Object.keys(awsSecretsRecord);
|
||||||
|
|
||||||
|
for (let i = 0; i < secretIdList.length; i += BATCH_SIZE) {
|
||||||
|
const batchSecretIds = secretIdList.slice(i, i + BATCH_SIZE);
|
||||||
|
let hasNext = true;
|
||||||
|
let nextToken: string | undefined;
|
||||||
|
|
||||||
|
while (hasNext) {
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const output = await client.send(
|
||||||
|
new BatchGetSecretValueCommand({
|
||||||
|
SecretIdList: batchSecretIds,
|
||||||
|
NextToken: nextToken
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
attempt = 0;
|
||||||
|
|
||||||
|
if (output.SecretValues) {
|
||||||
|
output.SecretValues.forEach((secretValueEntry) => {
|
||||||
|
if (secretValueEntry.Name) {
|
||||||
|
awsSecretValuesRecord[secretValueEntry.Name] = secretValueEntry;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
hasNext = Boolean(output.NextToken);
|
||||||
|
nextToken = output.NextToken;
|
||||||
|
} catch (e) {
|
||||||
|
if ((e as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) {
|
||||||
|
attempt += 1;
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await sleep();
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return awsSecretValuesRecord;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createSecret = async (
|
||||||
|
client: SecretsManagerClient,
|
||||||
|
input: CreateSecretCommandInput,
|
||||||
|
attempt = 0
|
||||||
|
): Promise<CreateSecretResponse> => {
|
||||||
|
try {
|
||||||
|
return await client.send(new CreateSecretCommand(input));
|
||||||
|
} catch (error) {
|
||||||
|
if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) {
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
// retry
|
||||||
|
return createSecret(client, input, attempt + 1);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateSecret = async (
|
||||||
|
client: SecretsManagerClient,
|
||||||
|
input: UpdateSecretCommandInput,
|
||||||
|
attempt = 0
|
||||||
|
): Promise<CreateSecretResponse> => {
|
||||||
|
try {
|
||||||
|
return await client.send(new UpdateSecretCommand(input));
|
||||||
|
} catch (error) {
|
||||||
|
if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) {
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
// retry
|
||||||
|
return updateSecret(client, input, attempt + 1);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteSecret = async (
|
||||||
|
client: SecretsManagerClient,
|
||||||
|
secretKey: string,
|
||||||
|
attempt = 0
|
||||||
|
): Promise<DeleteSecretResponse> => {
|
||||||
|
try {
|
||||||
|
return await client.send(new DeleteSecretCommand({ SecretId: secretKey, ForceDeleteWithoutRecovery: true }));
|
||||||
|
} catch (error) {
|
||||||
|
if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) {
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
// retry
|
||||||
|
return deleteSecret(client, secretKey, attempt + 1);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AwsSecretsManagerSyncFns = {
|
||||||
|
syncSecrets: async (secretSync: TAwsSecretsManagerSyncWithCredentials, secretMap: TSecretMap) => {
|
||||||
|
const { destinationConfig } = secretSync;
|
||||||
|
|
||||||
|
const client = await getSecretsManagerClient(secretSync);
|
||||||
|
|
||||||
|
const awsSecretsRecord = await getSecretsRecord(client);
|
||||||
|
|
||||||
|
const awsValuesRecord = await getSecretValuesRecord(client, awsSecretsRecord);
|
||||||
|
|
||||||
|
if (destinationConfig.mappingBehavior === AwsSecretsManagerSyncMappingBehavior.OneToOne) {
|
||||||
|
for await (const entry of Object.entries(secretMap)) {
|
||||||
|
const [key, { value }] = entry;
|
||||||
|
|
||||||
|
// skip secrets that don't have a value set
|
||||||
|
if (!value) {
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (awsSecretsRecord[key]) {
|
||||||
|
// skip secrets that haven't changed
|
||||||
|
if (awsValuesRecord[key]?.SecretString === value) {
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateSecret(client, {
|
||||||
|
SecretId: key,
|
||||||
|
SecretString: value
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new SecretSyncError({
|
||||||
|
error,
|
||||||
|
secretKey: key
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
await createSecret(client, {
|
||||||
|
Name: key,
|
||||||
|
SecretString: value
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new SecretSyncError({
|
||||||
|
error,
|
||||||
|
secretKey: key
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for await (const secretKey of Object.keys(awsSecretsRecord)) {
|
||||||
|
if (!(secretKey in secretMap) || !secretMap[secretKey].value) {
|
||||||
|
try {
|
||||||
|
await deleteSecret(client, secretKey);
|
||||||
|
} catch (error) {
|
||||||
|
throw new SecretSyncError({
|
||||||
|
error,
|
||||||
|
secretKey
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Many-To-One Mapping
|
||||||
|
|
||||||
|
const secretValue = JSON.stringify(
|
||||||
|
Object.fromEntries(Object.entries(secretMap).map(([key, secretData]) => [key, secretData.value]))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (awsValuesRecord[destinationConfig.secretName]) {
|
||||||
|
await updateSecret(client, {
|
||||||
|
SecretId: destinationConfig.secretName,
|
||||||
|
SecretString: secretValue
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await createSecret(client, {
|
||||||
|
Name: destinationConfig.secretName,
|
||||||
|
SecretString: secretValue
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for await (const secretKey of Object.keys(awsSecretsRecord)) {
|
||||||
|
if (secretKey === destinationConfig.secretName) {
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deleteSecret(client, secretKey);
|
||||||
|
} catch (error) {
|
||||||
|
throw new SecretSyncError({
|
||||||
|
error,
|
||||||
|
secretKey
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getSecrets: async (secretSync: TAwsSecretsManagerSyncWithCredentials): Promise<TSecretMap> => {
|
||||||
|
const client = await getSecretsManagerClient(secretSync);
|
||||||
|
|
||||||
|
const awsSecretsRecord = await getSecretsRecord(client);
|
||||||
|
const awsValuesRecord = await getSecretValuesRecord(client, awsSecretsRecord);
|
||||||
|
|
||||||
|
const { destinationConfig } = secretSync;
|
||||||
|
|
||||||
|
if (destinationConfig.mappingBehavior === AwsSecretsManagerSyncMappingBehavior.OneToOne) {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.keys(awsSecretsRecord).map((key) => [key, { value: awsValuesRecord[key].SecretString ?? "" }])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Many-To-One Mapping
|
||||||
|
|
||||||
|
const secretValueEntry = awsValuesRecord[destinationConfig.secretName];
|
||||||
|
|
||||||
|
if (!secretValueEntry) return {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsedValue = (secretValueEntry.SecretString ? JSON.parse(secretValueEntry.SecretString) : {}) as Record<
|
||||||
|
string,
|
||||||
|
string
|
||||||
|
>;
|
||||||
|
|
||||||
|
return Object.fromEntries(Object.entries(parsedValue).map(([key, value]) => [key, { value }]));
|
||||||
|
} catch {
|
||||||
|
throw new SecretSyncError({
|
||||||
|
message:
|
||||||
|
"Failed to import secrets. Invalid format for Many-To-One mapping behavior: requires key/value configuration.",
|
||||||
|
shouldRetry: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeSecrets: async (secretSync: TAwsSecretsManagerSyncWithCredentials, secretMap: TSecretMap) => {
|
||||||
|
const { destinationConfig } = secretSync;
|
||||||
|
|
||||||
|
const client = await getSecretsManagerClient(secretSync);
|
||||||
|
|
||||||
|
const awsSecretsRecord = await getSecretsRecord(client);
|
||||||
|
|
||||||
|
if (destinationConfig.mappingBehavior === AwsSecretsManagerSyncMappingBehavior.OneToOne) {
|
||||||
|
for await (const secretKey of Object.keys(awsSecretsRecord)) {
|
||||||
|
if (secretKey in secretMap) {
|
||||||
|
try {
|
||||||
|
await deleteSecret(client, secretKey);
|
||||||
|
} catch (error) {
|
||||||
|
throw new SecretSyncError({
|
||||||
|
error,
|
||||||
|
secretKey
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await deleteSecret(client, destinationConfig.secretName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
@@ -0,0 +1,63 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { SecretSyncs } from "@app/lib/api-docs";
|
||||||
|
import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import { AwsSecretsManagerSyncMappingBehavior } from "@app/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-enums";
|
||||||
|
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||||
|
import {
|
||||||
|
BaseSecretSyncSchema,
|
||||||
|
GenericCreateSecretSyncFieldsSchema,
|
||||||
|
GenericUpdateSecretSyncFieldsSchema
|
||||||
|
} from "@app/services/secret-sync/secret-sync-schemas";
|
||||||
|
|
||||||
|
const AwsSecretsManagerSyncDestinationConfigSchema = z
|
||||||
|
.discriminatedUnion("mappingBehavior", [
|
||||||
|
z.object({
|
||||||
|
mappingBehavior: z
|
||||||
|
.literal(AwsSecretsManagerSyncMappingBehavior.OneToOne)
|
||||||
|
.describe(SecretSyncs.DESTINATION_CONFIG.AWS_SECRETS_MANAGER.MAPPING_BEHAVIOR)
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
mappingBehavior: z
|
||||||
|
.literal(AwsSecretsManagerSyncMappingBehavior.ManyToOne)
|
||||||
|
.describe(SecretSyncs.DESTINATION_CONFIG.AWS_SECRETS_MANAGER.MAPPING_BEHAVIOR),
|
||||||
|
secretName: z
|
||||||
|
.string()
|
||||||
|
.regex(
|
||||||
|
/^[a-zA-Z0-9/_+=.@-]+$/,
|
||||||
|
"Secret name must contain only alphanumeric characters and the characters /_+=.@-"
|
||||||
|
)
|
||||||
|
.min(1, "Secret name is required")
|
||||||
|
.max(256, "Secret name cannot exceed 256 characters")
|
||||||
|
.describe(SecretSyncs.DESTINATION_CONFIG.AWS_SECRETS_MANAGER.SECRET_NAME)
|
||||||
|
})
|
||||||
|
])
|
||||||
|
.and(
|
||||||
|
z.object({
|
||||||
|
region: z.nativeEnum(AWSRegion).describe(SecretSyncs.DESTINATION_CONFIG.AWS_SECRETS_MANAGER.REGION)
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
export const AwsSecretsManagerSyncSchema = BaseSecretSyncSchema(SecretSync.AWSSecretsManager).extend({
|
||||||
|
destination: z.literal(SecretSync.AWSSecretsManager),
|
||||||
|
destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CreateAwsSecretsManagerSyncSchema = GenericCreateSecretSyncFieldsSchema(
|
||||||
|
SecretSync.AWSSecretsManager
|
||||||
|
).extend({
|
||||||
|
destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UpdateAwsSecretsManagerSyncSchema = GenericUpdateSecretSyncFieldsSchema(
|
||||||
|
SecretSync.AWSSecretsManager
|
||||||
|
).extend({
|
||||||
|
destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema.optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AwsSecretsManagerSyncListItemSchema = z.object({
|
||||||
|
name: z.literal("AWS Secrets Manager"),
|
||||||
|
connection: z.literal(AppConnection.AWS),
|
||||||
|
destination: z.literal(SecretSync.AWSSecretsManager),
|
||||||
|
canImportSecrets: z.literal(true)
|
||||||
|
});
|
@@ -0,0 +1,19 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TAwsConnection } from "@app/services/app-connection/aws";
|
||||||
|
|
||||||
|
import {
|
||||||
|
AwsSecretsManagerSyncListItemSchema,
|
||||||
|
AwsSecretsManagerSyncSchema,
|
||||||
|
CreateAwsSecretsManagerSyncSchema
|
||||||
|
} from "./aws-secrets-manager-sync-schemas";
|
||||||
|
|
||||||
|
export type TAwsSecretsManagerSync = z.infer<typeof AwsSecretsManagerSyncSchema>;
|
||||||
|
|
||||||
|
export type TAwsSecretsManagerSyncInput = z.infer<typeof CreateAwsSecretsManagerSyncSchema>;
|
||||||
|
|
||||||
|
export type TAwsSecretsManagerSyncListItem = z.infer<typeof AwsSecretsManagerSyncListItemSchema>;
|
||||||
|
|
||||||
|
export type TAwsSecretsManagerSyncWithCredentials = TAwsSecretsManagerSync & {
|
||||||
|
connection: TAwsConnection;
|
||||||
|
};
|
@@ -0,0 +1,4 @@
|
|||||||
|
export * from "./aws-secrets-manager-sync-constants";
|
||||||
|
export * from "./aws-secrets-manager-sync-fns";
|
||||||
|
export * from "./aws-secrets-manager-sync-schemas";
|
||||||
|
export * from "./aws-secrets-manager-sync-types";
|
10
backend/src/services/secret-sync/gcp/gcp-sync-constants.ts
Normal file
10
backend/src/services/secret-sync/gcp/gcp-sync-constants.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||||
|
import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types";
|
||||||
|
|
||||||
|
export const GCP_SYNC_LIST_OPTION: TSecretSyncListItem = {
|
||||||
|
name: "GCP Secret Manager",
|
||||||
|
destination: SecretSync.GCPSecretManager,
|
||||||
|
connection: AppConnection.GCP,
|
||||||
|
canImportSecrets: true
|
||||||
|
};
|
3
backend/src/services/secret-sync/gcp/gcp-sync-enums.ts
Normal file
3
backend/src/services/secret-sync/gcp/gcp-sync-enums.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export enum GcpSyncScope {
|
||||||
|
Global = "global"
|
||||||
|
}
|
218
backend/src/services/secret-sync/gcp/gcp-sync-fns.ts
Normal file
218
backend/src/services/secret-sync/gcp/gcp-sync-fns.ts
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
import { AxiosError } from "axios";
|
||||||
|
|
||||||
|
import { request } from "@app/lib/config/request";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { getGcpConnectionAuthToken } from "@app/services/app-connection/gcp";
|
||||||
|
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||||
|
|
||||||
|
import { SecretSyncError } from "../secret-sync-errors";
|
||||||
|
import { TSecretMap } from "../secret-sync-types";
|
||||||
|
import {
|
||||||
|
GCPLatestSecretVersionAccess,
|
||||||
|
GCPSecret,
|
||||||
|
GCPSMListSecretsRes,
|
||||||
|
TGcpSyncWithCredentials
|
||||||
|
} from "./gcp-sync-types";
|
||||||
|
|
||||||
|
const getGcpSecrets = async (accessToken: string, secretSync: TGcpSyncWithCredentials) => {
|
||||||
|
const { destinationConfig } = secretSync;
|
||||||
|
|
||||||
|
let gcpSecrets: GCPSecret[] = [];
|
||||||
|
|
||||||
|
const pageSize = 100;
|
||||||
|
let pageToken: string | undefined;
|
||||||
|
let hasMorePages = true;
|
||||||
|
|
||||||
|
while (hasMorePages) {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
pageSize: String(pageSize),
|
||||||
|
...(pageToken ? { pageToken } : {})
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const { data: secretsRes } = await request.get<GCPSMListSecretsRes>(
|
||||||
|
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${secretSync.destinationConfig.projectId}/secrets`,
|
||||||
|
{
|
||||||
|
params,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Accept-Encoding": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (secretsRes.secrets) {
|
||||||
|
gcpSecrets = gcpSecrets.concat(secretsRes.secrets);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!secretsRes.nextPageToken) {
|
||||||
|
hasMorePages = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pageToken = secretsRes.nextPageToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res: { [key: string]: string } = {};
|
||||||
|
|
||||||
|
for await (const gcpSecret of gcpSecrets) {
|
||||||
|
const arr = gcpSecret.name.split("/");
|
||||||
|
const key = arr[arr.length - 1];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data: secretLatest } = await request.get<GCPLatestSecretVersionAccess>(
|
||||||
|
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}/versions/latest:access`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Accept-Encoding": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
res[key] = Buffer.from(secretLatest.payload.data, "base64").toString("utf-8");
|
||||||
|
} catch (error) {
|
||||||
|
// when a secret in GCP has no versions, we treat it as if it's a blank value
|
||||||
|
if (error instanceof AxiosError && error.response?.status === 404) {
|
||||||
|
res[key] = "";
|
||||||
|
} else {
|
||||||
|
throw new SecretSyncError({
|
||||||
|
error,
|
||||||
|
secretKey: key
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GcpSyncFns = {
|
||||||
|
syncSecrets: async (secretSync: TGcpSyncWithCredentials, secretMap: TSecretMap) => {
|
||||||
|
const { destinationConfig, connection } = secretSync;
|
||||||
|
const accessToken = await getGcpConnectionAuthToken(connection);
|
||||||
|
|
||||||
|
const gcpSecrets = await getGcpSecrets(accessToken, secretSync);
|
||||||
|
|
||||||
|
for await (const key of Object.keys(secretMap)) {
|
||||||
|
try {
|
||||||
|
// we do not process secrets with no value because GCP secret manager does not allow it
|
||||||
|
if (!secretMap[key].value) {
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(key in gcpSecrets)) {
|
||||||
|
// case: create secret
|
||||||
|
await request.post(
|
||||||
|
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets`,
|
||||||
|
{
|
||||||
|
replication: {
|
||||||
|
automatic: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
secretId: key
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Accept-Encoding": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
await request.post(
|
||||||
|
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}:addVersion`,
|
||||||
|
{
|
||||||
|
payload: {
|
||||||
|
data: Buffer.from(secretMap[key].value).toString("base64")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Accept-Encoding": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw new SecretSyncError({
|
||||||
|
error,
|
||||||
|
secretKey: key
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for await (const key of Object.keys(gcpSecrets)) {
|
||||||
|
try {
|
||||||
|
if (!(key in secretMap) || !secretMap[key].value) {
|
||||||
|
// case: delete secret
|
||||||
|
await request.delete(
|
||||||
|
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Accept-Encoding": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else if (secretMap[key].value !== gcpSecrets[key]) {
|
||||||
|
if (!secretMap[key].value) {
|
||||||
|
logger.warn(
|
||||||
|
`syncSecretsGcpsecretManager: update secret value in gcp where [key=${key}] and [projectId=${destinationConfig.projectId}]`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await request.post(
|
||||||
|
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}:addVersion`,
|
||||||
|
{
|
||||||
|
payload: {
|
||||||
|
data: Buffer.from(secretMap[key].value).toString("base64")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Accept-Encoding": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw new SecretSyncError({
|
||||||
|
error,
|
||||||
|
secretKey: key
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getSecrets: async (secretSync: TGcpSyncWithCredentials): Promise<TSecretMap> => {
|
||||||
|
const { connection } = secretSync;
|
||||||
|
const accessToken = await getGcpConnectionAuthToken(connection);
|
||||||
|
|
||||||
|
const gcpSecrets = await getGcpSecrets(accessToken, secretSync);
|
||||||
|
return Object.fromEntries(Object.entries(gcpSecrets).map(([key, value]) => [key, { value: value ?? "" }]));
|
||||||
|
},
|
||||||
|
|
||||||
|
removeSecrets: async (secretSync: TGcpSyncWithCredentials, secretMap: TSecretMap) => {
|
||||||
|
const { destinationConfig, connection } = secretSync;
|
||||||
|
const accessToken = await getGcpConnectionAuthToken(connection);
|
||||||
|
|
||||||
|
const gcpSecrets = await getGcpSecrets(accessToken, secretSync);
|
||||||
|
for await (const [key] of Object.entries(gcpSecrets)) {
|
||||||
|
if (key in secretMap) {
|
||||||
|
await request.delete(
|
||||||
|
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${destinationConfig.projectId}/secrets/${key}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Accept-Encoding": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
45
backend/src/services/secret-sync/gcp/gcp-sync-schemas.ts
Normal file
45
backend/src/services/secret-sync/gcp/gcp-sync-schemas.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import {
|
||||||
|
BaseSecretSyncSchema,
|
||||||
|
GenericCreateSecretSyncFieldsSchema,
|
||||||
|
GenericUpdateSecretSyncFieldsSchema
|
||||||
|
} from "@app/services/secret-sync/secret-sync-schemas";
|
||||||
|
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
|
||||||
|
|
||||||
|
import { SecretSync } from "../secret-sync-enums";
|
||||||
|
import { GcpSyncScope } from "./gcp-sync-enums";
|
||||||
|
|
||||||
|
const GcpSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
|
||||||
|
|
||||||
|
const GcpSyncDestinationConfigSchema = z.object({
|
||||||
|
scope: z.literal(GcpSyncScope.Global),
|
||||||
|
projectId: z.string().min(1, "Project ID is required")
|
||||||
|
});
|
||||||
|
|
||||||
|
export const GcpSyncSchema = BaseSecretSyncSchema(SecretSync.GCPSecretManager, GcpSyncOptionsConfig).extend({
|
||||||
|
destination: z.literal(SecretSync.GCPSecretManager),
|
||||||
|
destinationConfig: GcpSyncDestinationConfigSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CreateGcpSyncSchema = GenericCreateSecretSyncFieldsSchema(
|
||||||
|
SecretSync.GCPSecretManager,
|
||||||
|
GcpSyncOptionsConfig
|
||||||
|
).extend({
|
||||||
|
destinationConfig: GcpSyncDestinationConfigSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UpdateGcpSyncSchema = GenericUpdateSecretSyncFieldsSchema(
|
||||||
|
SecretSync.GCPSecretManager,
|
||||||
|
GcpSyncOptionsConfig
|
||||||
|
).extend({
|
||||||
|
destinationConfig: GcpSyncDestinationConfigSchema.optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const GcpSyncListItemSchema = z.object({
|
||||||
|
name: z.literal("GCP Secret Manager"),
|
||||||
|
connection: z.literal(AppConnection.GCP),
|
||||||
|
destination: z.literal(SecretSync.GCPSecretManager),
|
||||||
|
canImportSecrets: z.literal(true)
|
||||||
|
});
|
33
backend/src/services/secret-sync/gcp/gcp-sync-types.ts
Normal file
33
backend/src/services/secret-sync/gcp/gcp-sync-types.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
import { TGcpConnection } from "@app/services/app-connection/gcp";
|
||||||
|
|
||||||
|
import { CreateGcpSyncSchema, GcpSyncListItemSchema, GcpSyncSchema } from "./gcp-sync-schemas";
|
||||||
|
|
||||||
|
export type TGcpSyncListItem = z.infer<typeof GcpSyncListItemSchema>;
|
||||||
|
|
||||||
|
export type TGcpSync = z.infer<typeof GcpSyncSchema>;
|
||||||
|
|
||||||
|
export type TGcpSyncInput = z.infer<typeof CreateGcpSyncSchema>;
|
||||||
|
|
||||||
|
export type TGcpSyncWithCredentials = TGcpSync & {
|
||||||
|
connection: TGcpConnection;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GCPSecret = {
|
||||||
|
name: string;
|
||||||
|
createTime: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GCPSMListSecretsRes = {
|
||||||
|
secrets?: GCPSecret[];
|
||||||
|
totalSize?: number;
|
||||||
|
nextPageToken?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GCPLatestSecretVersionAccess = {
|
||||||
|
name: string;
|
||||||
|
payload: {
|
||||||
|
data: string;
|
||||||
|
};
|
||||||
|
};
|
4
backend/src/services/secret-sync/gcp/index.ts
Normal file
4
backend/src/services/secret-sync/gcp/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export * from "./gcp-sync-constants";
|
||||||
|
export * from "./gcp-sync-enums";
|
||||||
|
export * from "./gcp-sync-schemas";
|
||||||
|
export * from "./gcp-sync-types";
|
@@ -123,47 +123,39 @@ export const secretSyncDALFactory = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const create = async (data: Parameters<(typeof secretSyncOrm)["create"]>[0]) => {
|
const create = async (data: Parameters<(typeof secretSyncOrm)["create"]>[0]) => {
|
||||||
try {
|
const secretSync = (await secretSyncOrm.transaction(async (tx) => {
|
||||||
const secretSync = (await secretSyncOrm.transaction(async (tx) => {
|
const sync = await secretSyncOrm.create(data, tx);
|
||||||
const sync = await secretSyncOrm.create(data, tx);
|
|
||||||
|
|
||||||
return baseSecretSyncQuery({
|
return baseSecretSyncQuery({
|
||||||
filter: { id: sync.id },
|
filter: { id: sync.id },
|
||||||
db,
|
db,
|
||||||
tx
|
tx
|
||||||
}).first();
|
}).first();
|
||||||
}))!;
|
}))!;
|
||||||
|
|
||||||
// TODO (scott): replace with cached folder path once implemented
|
// TODO (scott): replace with cached folder path once implemented
|
||||||
const [folderWithPath] = secretSync.folderId
|
const [folderWithPath] = secretSync.folderId
|
||||||
? await folderDAL.findSecretPathByFolderIds(secretSync.projectId, [secretSync.folderId])
|
? await folderDAL.findSecretPathByFolderIds(secretSync.projectId, [secretSync.folderId])
|
||||||
: [];
|
: [];
|
||||||
return expandSecretSync(secretSync, folderWithPath);
|
return expandSecretSync(secretSync, folderWithPath);
|
||||||
} catch (error) {
|
|
||||||
throw new DatabaseError({ error, name: "Create - Secret Sync" });
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateById = async (syncId: string, data: Parameters<(typeof secretSyncOrm)["updateById"]>[1]) => {
|
const updateById = async (syncId: string, data: Parameters<(typeof secretSyncOrm)["updateById"]>[1]) => {
|
||||||
try {
|
const secretSync = (await secretSyncOrm.transaction(async (tx) => {
|
||||||
const secretSync = (await secretSyncOrm.transaction(async (tx) => {
|
const sync = await secretSyncOrm.updateById(syncId, data, tx);
|
||||||
const sync = await secretSyncOrm.updateById(syncId, data, tx);
|
|
||||||
|
|
||||||
return baseSecretSyncQuery({
|
return baseSecretSyncQuery({
|
||||||
filter: { id: sync.id },
|
filter: { id: sync.id },
|
||||||
db,
|
db,
|
||||||
tx
|
tx
|
||||||
}).first();
|
}).first();
|
||||||
}))!;
|
}))!;
|
||||||
|
|
||||||
// TODO (scott): replace with cached folder path once implemented
|
// TODO (scott): replace with cached folder path once implemented
|
||||||
const [folderWithPath] = secretSync.folderId
|
const [folderWithPath] = secretSync.folderId
|
||||||
? await folderDAL.findSecretPathByFolderIds(secretSync.projectId, [secretSync.folderId])
|
? await folderDAL.findSecretPathByFolderIds(secretSync.projectId, [secretSync.folderId])
|
||||||
: [];
|
: [];
|
||||||
return expandSecretSync(secretSync, folderWithPath);
|
return expandSecretSync(secretSync, folderWithPath);
|
||||||
} catch (error) {
|
|
||||||
throw new DatabaseError({ error, name: "Update by ID - Secret Sync" });
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const findOne = async (filter: Parameters<(typeof secretSyncOrm)["findOne"]>[0], tx?: Knex) => {
|
const findOne = async (filter: Parameters<(typeof secretSyncOrm)["findOne"]>[0], tx?: Knex) => {
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
export enum SecretSync {
|
export enum SecretSync {
|
||||||
AWSParameterStore = "aws-parameter-store",
|
AWSParameterStore = "aws-parameter-store",
|
||||||
GitHub = "github"
|
AWSSecretsManager = "aws-secrets-manager",
|
||||||
|
GitHub = "github",
|
||||||
|
GCPSecretManager = "gcp-secret-manager"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SecretSyncInitialSyncBehavior {
|
export enum SecretSyncInitialSyncBehavior {
|
||||||
|
@@ -4,6 +4,10 @@ import {
|
|||||||
AWS_PARAMETER_STORE_SYNC_LIST_OPTION,
|
AWS_PARAMETER_STORE_SYNC_LIST_OPTION,
|
||||||
AwsParameterStoreSyncFns
|
AwsParameterStoreSyncFns
|
||||||
} from "@app/services/secret-sync/aws-parameter-store";
|
} from "@app/services/secret-sync/aws-parameter-store";
|
||||||
|
import {
|
||||||
|
AWS_SECRETS_MANAGER_SYNC_LIST_OPTION,
|
||||||
|
AwsSecretsManagerSyncFns
|
||||||
|
} from "@app/services/secret-sync/aws-secrets-manager";
|
||||||
import { GITHUB_SYNC_LIST_OPTION, GithubSyncFns } from "@app/services/secret-sync/github";
|
import { GITHUB_SYNC_LIST_OPTION, GithubSyncFns } from "@app/services/secret-sync/github";
|
||||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||||
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
|
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
|
||||||
@@ -13,9 +17,14 @@ import {
|
|||||||
TSecretSyncWithCredentials
|
TSecretSyncWithCredentials
|
||||||
} from "@app/services/secret-sync/secret-sync-types";
|
} from "@app/services/secret-sync/secret-sync-types";
|
||||||
|
|
||||||
|
import { GCP_SYNC_LIST_OPTION } from "./gcp";
|
||||||
|
import { GcpSyncFns } from "./gcp/gcp-sync-fns";
|
||||||
|
|
||||||
const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
|
const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
|
||||||
[SecretSync.AWSParameterStore]: AWS_PARAMETER_STORE_SYNC_LIST_OPTION,
|
[SecretSync.AWSParameterStore]: AWS_PARAMETER_STORE_SYNC_LIST_OPTION,
|
||||||
[SecretSync.GitHub]: GITHUB_SYNC_LIST_OPTION
|
[SecretSync.AWSSecretsManager]: AWS_SECRETS_MANAGER_SYNC_LIST_OPTION,
|
||||||
|
[SecretSync.GitHub]: GITHUB_SYNC_LIST_OPTION,
|
||||||
|
[SecretSync.GCPSecretManager]: GCP_SYNC_LIST_OPTION
|
||||||
};
|
};
|
||||||
|
|
||||||
export const listSecretSyncOptions = () => {
|
export const listSecretSyncOptions = () => {
|
||||||
@@ -69,8 +78,12 @@ export const SecretSyncFns = {
|
|||||||
switch (secretSync.destination) {
|
switch (secretSync.destination) {
|
||||||
case SecretSync.AWSParameterStore:
|
case SecretSync.AWSParameterStore:
|
||||||
return AwsParameterStoreSyncFns.syncSecrets(secretSync, secretMap);
|
return AwsParameterStoreSyncFns.syncSecrets(secretSync, secretMap);
|
||||||
|
case SecretSync.AWSSecretsManager:
|
||||||
|
return AwsSecretsManagerSyncFns.syncSecrets(secretSync, secretMap);
|
||||||
case SecretSync.GitHub:
|
case SecretSync.GitHub:
|
||||||
return GithubSyncFns.syncSecrets(secretSync, secretMap);
|
return GithubSyncFns.syncSecrets(secretSync, secretMap);
|
||||||
|
case SecretSync.GCPSecretManager:
|
||||||
|
return GcpSyncFns.syncSecrets(secretSync, secretMap);
|
||||||
default:
|
default:
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
`Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||||
@@ -83,9 +96,15 @@ export const SecretSyncFns = {
|
|||||||
case SecretSync.AWSParameterStore:
|
case SecretSync.AWSParameterStore:
|
||||||
secretMap = await AwsParameterStoreSyncFns.getSecrets(secretSync);
|
secretMap = await AwsParameterStoreSyncFns.getSecrets(secretSync);
|
||||||
break;
|
break;
|
||||||
|
case SecretSync.AWSSecretsManager:
|
||||||
|
secretMap = await AwsSecretsManagerSyncFns.getSecrets(secretSync);
|
||||||
|
break;
|
||||||
case SecretSync.GitHub:
|
case SecretSync.GitHub:
|
||||||
secretMap = await GithubSyncFns.getSecrets(secretSync);
|
secretMap = await GithubSyncFns.getSecrets(secretSync);
|
||||||
break;
|
break;
|
||||||
|
case SecretSync.GCPSecretManager:
|
||||||
|
secretMap = await GcpSyncFns.getSecrets(secretSync);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
`Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||||
@@ -101,8 +120,12 @@ export const SecretSyncFns = {
|
|||||||
switch (secretSync.destination) {
|
switch (secretSync.destination) {
|
||||||
case SecretSync.AWSParameterStore:
|
case SecretSync.AWSParameterStore:
|
||||||
return AwsParameterStoreSyncFns.removeSecrets(secretSync, secretMap);
|
return AwsParameterStoreSyncFns.removeSecrets(secretSync, secretMap);
|
||||||
|
case SecretSync.AWSSecretsManager:
|
||||||
|
return AwsSecretsManagerSyncFns.removeSecrets(secretSync, secretMap);
|
||||||
case SecretSync.GitHub:
|
case SecretSync.GitHub:
|
||||||
return GithubSyncFns.removeSecrets(secretSync, secretMap);
|
return GithubSyncFns.removeSecrets(secretSync, secretMap);
|
||||||
|
case SecretSync.GCPSecretManager:
|
||||||
|
return GcpSyncFns.removeSecrets(secretSync, secretMap);
|
||||||
default:
|
default:
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
`Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||||
@@ -115,7 +138,7 @@ export const parseSyncErrorMessage = (err: unknown): string => {
|
|||||||
if (err instanceof SecretSyncError) {
|
if (err instanceof SecretSyncError) {
|
||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
secretKey: err.secretKey,
|
secretKey: err.secretKey,
|
||||||
error: err.message ?? parseSyncErrorMessage(err.error)
|
error: err.message || parseSyncErrorMessage(err.error)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -3,10 +3,14 @@ import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
|||||||
|
|
||||||
export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
|
export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
|
||||||
[SecretSync.AWSParameterStore]: "AWS Parameter Store",
|
[SecretSync.AWSParameterStore]: "AWS Parameter Store",
|
||||||
[SecretSync.GitHub]: "GitHub"
|
[SecretSync.AWSSecretsManager]: "AWS Secrets Manager",
|
||||||
|
[SecretSync.GitHub]: "GitHub",
|
||||||
|
[SecretSync.GCPSecretManager]: "GCP Secret Manager"
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
|
export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
|
||||||
[SecretSync.AWSParameterStore]: AppConnection.AWS,
|
[SecretSync.AWSParameterStore]: AppConnection.AWS,
|
||||||
[SecretSync.GitHub]: AppConnection.GitHub
|
[SecretSync.AWSSecretsManager]: AppConnection.AWS,
|
||||||
|
[SecretSync.GitHub]: AppConnection.GitHub,
|
||||||
|
[SecretSync.GCPSecretManager]: AppConnection.GCP
|
||||||
};
|
};
|
||||||
|
@@ -8,7 +8,8 @@ import {
|
|||||||
ProjectPermissionSub
|
ProjectPermissionSub
|
||||||
} from "@app/ee/services/permission/project-permission";
|
} from "@app/ee/services/permission/project-permission";
|
||||||
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
|
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
|
||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { DatabaseErrorCode } from "@app/lib/error-codes";
|
||||||
|
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
|
||||||
import { OrgServiceActor } from "@app/lib/types";
|
import { OrgServiceActor } from "@app/lib/types";
|
||||||
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
||||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||||
@@ -197,37 +198,26 @@ export const secretSyncServiceFactory = ({
|
|||||||
// validates permission to connect and app is valid for sync destination
|
// validates permission to connect and app is valid for sync destination
|
||||||
await appConnectionService.connectAppConnectionById(destinationApp, params.connectionId, actor);
|
await appConnectionService.connectAppConnectionById(destinationApp, params.connectionId, actor);
|
||||||
|
|
||||||
const secretSync = await secretSyncDAL.transaction(async (tx) => {
|
try {
|
||||||
const isConflictingName = Boolean(
|
const secretSync = await secretSyncDAL.create({
|
||||||
(
|
|
||||||
await secretSyncDAL.find(
|
|
||||||
{
|
|
||||||
name: params.name,
|
|
||||||
projectId
|
|
||||||
},
|
|
||||||
tx
|
|
||||||
)
|
|
||||||
).length
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isConflictingName)
|
|
||||||
throw new BadRequestError({
|
|
||||||
message: `A Secret Sync with the name "${params.name}" already exists for the project with ID "${folder.projectId}"`
|
|
||||||
});
|
|
||||||
|
|
||||||
const sync = await secretSyncDAL.create({
|
|
||||||
folderId: folder.id,
|
folderId: folder.id,
|
||||||
...params,
|
...params,
|
||||||
...(params.isAutoSyncEnabled && { syncStatus: SecretSyncStatus.Pending }),
|
...(params.isAutoSyncEnabled && { syncStatus: SecretSyncStatus.Pending }),
|
||||||
projectId
|
projectId
|
||||||
});
|
});
|
||||||
|
|
||||||
return sync;
|
if (secretSync.isAutoSyncEnabled) await secretSyncQueue.queueSecretSyncSyncSecretsById({ syncId: secretSync.id });
|
||||||
});
|
|
||||||
|
|
||||||
if (secretSync.isAutoSyncEnabled) await secretSyncQueue.queueSecretSyncSyncSecretsById({ syncId: secretSync.id });
|
return secretSync as TSecretSync;
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof DatabaseError && (err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `A Secret Sync with the name "${params.name}" already exists for the project with ID "${folder.projectId}"`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return secretSync as TSecretSync;
|
throw err;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateSecretSync = async (
|
const updateSecretSync = async (
|
||||||
@@ -260,78 +250,65 @@ export const secretSyncServiceFactory = ({
|
|||||||
message: `Secret sync with ID "${secretSync.id}" is not configured for ${SECRET_SYNC_NAME_MAP[destination]}`
|
message: `Secret sync with ID "${secretSync.id}" is not configured for ${SECRET_SYNC_NAME_MAP[destination]}`
|
||||||
});
|
});
|
||||||
|
|
||||||
const updatedSecretSync = await secretSyncDAL.transaction(async (tx) => {
|
let { folderId } = secretSync;
|
||||||
let { folderId } = secretSync;
|
|
||||||
|
|
||||||
if (params.connectionId) {
|
if (params.connectionId) {
|
||||||
const destinationApp = SECRET_SYNC_CONNECTION_MAP[secretSync.destination as SecretSync];
|
const destinationApp = SECRET_SYNC_CONNECTION_MAP[secretSync.destination as SecretSync];
|
||||||
|
|
||||||
// validates permission to connect and app is valid for sync destination
|
// validates permission to connect and app is valid for sync destination
|
||||||
await appConnectionService.connectAppConnectionById(destinationApp, params.connectionId, actor);
|
await appConnectionService.connectAppConnectionById(destinationApp, params.connectionId, actor);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(secretPath && secretPath !== secretSync.folder?.path) ||
|
(secretPath && secretPath !== secretSync.folder?.path) ||
|
||||||
(environment && environment !== secretSync.environment?.slug)
|
(environment && environment !== secretSync.environment?.slug)
|
||||||
) {
|
) {
|
||||||
const updatedEnvironment = environment ?? secretSync.environment?.slug;
|
const updatedEnvironment = environment ?? secretSync.environment?.slug;
|
||||||
const updatedSecretPath = secretPath ?? secretSync.folder?.path;
|
const updatedSecretPath = secretPath ?? secretSync.folder?.path;
|
||||||
|
|
||||||
if (!updatedEnvironment || !updatedSecretPath)
|
if (!updatedEnvironment || !updatedSecretPath)
|
||||||
throw new BadRequestError({ message: "Must specify both source environment and secret path" });
|
throw new BadRequestError({ message: "Must specify both source environment and secret path" });
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionActions.Read,
|
ProjectPermissionActions.Read,
|
||||||
subject(ProjectPermissionSub.Secrets, {
|
subject(ProjectPermissionSub.Secrets, {
|
||||||
environment: updatedEnvironment,
|
environment: updatedEnvironment,
|
||||||
secretPath: updatedSecretPath
|
secretPath: updatedSecretPath
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const newFolder = await folderDAL.findBySecretPath(secretSync.projectId, updatedEnvironment, updatedSecretPath);
|
const newFolder = await folderDAL.findBySecretPath(secretSync.projectId, updatedEnvironment, updatedSecretPath);
|
||||||
|
|
||||||
if (!newFolder)
|
if (!newFolder)
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message: `Could not find folder with path "${secretPath}" in environment "${environment}" for project with ID "${secretSync.projectId}"`
|
message: `Could not find folder with path "${secretPath}" in environment "${environment}" for project with ID "${secretSync.projectId}"`
|
||||||
});
|
});
|
||||||
|
|
||||||
folderId = newFolder.id;
|
folderId = newFolder.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.name && secretSync.name !== params.name) {
|
const isAutoSyncEnabled = params.isAutoSyncEnabled ?? secretSync.isAutoSyncEnabled;
|
||||||
const isConflictingName = Boolean(
|
|
||||||
(
|
|
||||||
await secretSyncDAL.find(
|
|
||||||
{
|
|
||||||
name: params.name,
|
|
||||||
projectId: secretSync.projectId
|
|
||||||
},
|
|
||||||
tx
|
|
||||||
)
|
|
||||||
).length
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isConflictingName)
|
try {
|
||||||
throw new BadRequestError({
|
const updatedSecretSync = await secretSyncDAL.updateById(syncId, {
|
||||||
message: `A Secret Sync with the name "${params.name}" already exists for project with ID "${secretSync.projectId}"`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const isAutoSyncEnabled = params.isAutoSyncEnabled ?? secretSync.isAutoSyncEnabled;
|
|
||||||
|
|
||||||
const updatedSync = await secretSyncDAL.updateById(syncId, {
|
|
||||||
...params,
|
...params,
|
||||||
...(isAutoSyncEnabled && folderId && { syncStatus: SecretSyncStatus.Pending }),
|
...(isAutoSyncEnabled && folderId && { syncStatus: SecretSyncStatus.Pending }),
|
||||||
folderId
|
folderId
|
||||||
});
|
});
|
||||||
|
|
||||||
return updatedSync;
|
if (updatedSecretSync.isAutoSyncEnabled)
|
||||||
});
|
await secretSyncQueue.queueSecretSyncSyncSecretsById({ syncId: secretSync.id });
|
||||||
|
|
||||||
if (updatedSecretSync.isAutoSyncEnabled)
|
return updatedSecretSync as TSecretSync;
|
||||||
await secretSyncQueue.queueSecretSyncSyncSecretsById({ syncId: secretSync.id });
|
} catch (err) {
|
||||||
|
if (err instanceof DatabaseError && (err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `A Secret Sync with the name "${params.name}" already exists for the project with ID "${secretSync.projectId}"`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return updatedSecretSync as TSecretSync;
|
throw err;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteSecretSync = async (
|
const deleteSecretSync = async (
|
||||||
|
@@ -2,6 +2,12 @@ import { Job } from "bullmq";
|
|||||||
|
|
||||||
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
|
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { QueueJobs } from "@app/queue";
|
import { QueueJobs } from "@app/queue";
|
||||||
|
import {
|
||||||
|
TAwsSecretsManagerSync,
|
||||||
|
TAwsSecretsManagerSyncInput,
|
||||||
|
TAwsSecretsManagerSyncListItem,
|
||||||
|
TAwsSecretsManagerSyncWithCredentials
|
||||||
|
} from "@app/services/secret-sync/aws-secrets-manager";
|
||||||
import {
|
import {
|
||||||
TGitHubSync,
|
TGitHubSync,
|
||||||
TGitHubSyncInput,
|
TGitHubSyncInput,
|
||||||
@@ -17,14 +23,27 @@ import {
|
|||||||
TAwsParameterStoreSyncListItem,
|
TAwsParameterStoreSyncListItem,
|
||||||
TAwsParameterStoreSyncWithCredentials
|
TAwsParameterStoreSyncWithCredentials
|
||||||
} from "./aws-parameter-store";
|
} from "./aws-parameter-store";
|
||||||
|
import { TGcpSync, TGcpSyncInput, TGcpSyncListItem, TGcpSyncWithCredentials } from "./gcp";
|
||||||
|
|
||||||
export type TSecretSync = TAwsParameterStoreSync | TGitHubSync;
|
export type TSecretSync = TAwsParameterStoreSync | TAwsSecretsManagerSync | TGitHubSync | TGcpSync;
|
||||||
|
|
||||||
export type TSecretSyncWithCredentials = TAwsParameterStoreSyncWithCredentials | TGitHubSyncWithCredentials;
|
export type TSecretSyncWithCredentials =
|
||||||
|
| TAwsParameterStoreSyncWithCredentials
|
||||||
|
| TAwsSecretsManagerSyncWithCredentials
|
||||||
|
| TGitHubSyncWithCredentials
|
||||||
|
| TGcpSyncWithCredentials;
|
||||||
|
|
||||||
export type TSecretSyncInput = TAwsParameterStoreSyncInput | TGitHubSyncInput;
|
export type TSecretSyncInput =
|
||||||
|
| TAwsParameterStoreSyncInput
|
||||||
|
| TAwsSecretsManagerSyncInput
|
||||||
|
| TGitHubSyncInput
|
||||||
|
| TGcpSyncInput;
|
||||||
|
|
||||||
export type TSecretSyncListItem = TAwsParameterStoreSyncListItem | TGitHubSyncListItem;
|
export type TSecretSyncListItem =
|
||||||
|
| TAwsParameterStoreSyncListItem
|
||||||
|
| TAwsSecretsManagerSyncListItem
|
||||||
|
| TGitHubSyncListItem
|
||||||
|
| TGcpSyncListItem;
|
||||||
|
|
||||||
export type TSyncOptionsConfig = {
|
export type TSyncOptionsConfig = {
|
||||||
canImportSecrets: boolean;
|
canImportSecrets: boolean;
|
||||||
|
@@ -414,6 +414,20 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.SecretTag}.id`
|
`${TableName.SecretTag}.id`
|
||||||
)
|
)
|
||||||
.leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`)
|
.leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`)
|
||||||
|
.where((qb) => {
|
||||||
|
if (filters?.metadataFilter && filters.metadataFilter.length > 0) {
|
||||||
|
filters.metadataFilter.forEach((meta) => {
|
||||||
|
void qb.whereExists((subQuery) => {
|
||||||
|
void subQuery
|
||||||
|
.select("secretId")
|
||||||
|
.from(TableName.ResourceMetadata)
|
||||||
|
.whereRaw(`"${TableName.ResourceMetadata}"."secretId" = "${TableName.SecretV2}"."id"`)
|
||||||
|
.where(`${TableName.ResourceMetadata}.key`, meta.key)
|
||||||
|
.where(`${TableName.ResourceMetadata}.value`, meta.value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
.select(
|
.select(
|
||||||
selectAllTableCols(TableName.SecretV2),
|
selectAllTableCols(TableName.SecretV2),
|
||||||
db.raw(
|
db.raw(
|
||||||
@@ -481,6 +495,7 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "get all secret" });
|
throw new DatabaseError({ error, name: "get all secret" });
|
||||||
|
@@ -1291,8 +1291,13 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (secretsToUpdate.length !== inputSecrets.length)
|
if (secretsToUpdate.length !== inputSecrets.length) {
|
||||||
throw new NotFoundError({ message: `Secret does not exist: ${secretsToUpdate.map((el) => el.key).join(",")}` });
|
const secretsToUpdateNames = secretsToUpdate.map((secret) => secret.key);
|
||||||
|
const invalidSecrets = inputSecrets.filter((secret) => !secretsToUpdateNames.includes(secret.secretKey));
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Secret does not exist: ${invalidSecrets.map((el) => el.secretKey).join(",")}`
|
||||||
|
});
|
||||||
|
}
|
||||||
const secretsToUpdateInDBGroupedByKey = groupBy(secretsToUpdate, (i) => i.key);
|
const secretsToUpdateInDBGroupedByKey = groupBy(secretsToUpdate, (i) => i.key);
|
||||||
|
|
||||||
secretsToUpdate.forEach((el) => {
|
secretsToUpdate.forEach((el) => {
|
||||||
|
@@ -30,6 +30,10 @@ export type TGetSecretsDTO = {
|
|||||||
includeImports?: boolean;
|
includeImports?: boolean;
|
||||||
recursive?: boolean;
|
recursive?: boolean;
|
||||||
tagSlugs?: string[];
|
tagSlugs?: string[];
|
||||||
|
metadataFilter?: {
|
||||||
|
key?: string;
|
||||||
|
value?: string;
|
||||||
|
}[];
|
||||||
orderBy?: SecretsOrderBy;
|
orderBy?: SecretsOrderBy;
|
||||||
orderDirection?: OrderByDirection;
|
orderDirection?: OrderByDirection;
|
||||||
offset?: number;
|
offset?: number;
|
||||||
@@ -310,6 +314,7 @@ export type TFindSecretsByFolderIdsFilter = {
|
|||||||
orderDirection?: OrderByDirection;
|
orderDirection?: OrderByDirection;
|
||||||
search?: string;
|
search?: string;
|
||||||
tagSlugs?: string[];
|
tagSlugs?: string[];
|
||||||
|
metadataFilter?: { key?: string; value?: string }[];
|
||||||
includeTagsInSearch?: boolean;
|
includeTagsInSearch?: boolean;
|
||||||
keys?: string[];
|
keys?: string[];
|
||||||
};
|
};
|
||||||
|
@@ -1263,6 +1263,13 @@ export const secretServiceFactory = ({
|
|||||||
name: "bot_not_found_error"
|
name: "bot_not_found_error"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (paramsV2.metadataFilter) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Please upgrade your project to filter secrets by metadata",
|
||||||
|
name: "SecretMetadataNotSupported"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const { secrets, imports } = await getSecrets({
|
const { secrets, imports } = await getSecrets({
|
||||||
actorId,
|
actorId,
|
||||||
projectId,
|
projectId,
|
||||||
@@ -1444,7 +1451,7 @@ export const secretServiceFactory = ({
|
|||||||
decryptedSecret.secretValue = expandedSecretValue || "";
|
decryptedSecret.secretValue = expandedSecretValue || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
return decryptedSecret;
|
return { secretMetadata: undefined, ...decryptedSecret };
|
||||||
};
|
};
|
||||||
|
|
||||||
const createSecretRaw = async ({
|
const createSecretRaw = async ({
|
||||||
|
@@ -182,6 +182,10 @@ export type TGetSecretsRawDTO = {
|
|||||||
includeImports?: boolean;
|
includeImports?: boolean;
|
||||||
recursive?: boolean;
|
recursive?: boolean;
|
||||||
tagSlugs?: string[];
|
tagSlugs?: string[];
|
||||||
|
metadataFilter?: {
|
||||||
|
key?: string;
|
||||||
|
value?: string;
|
||||||
|
}[];
|
||||||
orderBy?: SecretsOrderBy;
|
orderBy?: SecretsOrderBy;
|
||||||
orderDirection?: OrderByDirection;
|
orderDirection?: OrderByDirection;
|
||||||
offset?: number;
|
offset?: number;
|
||||||
|
@@ -30,6 +30,7 @@ export enum SmtpTemplates {
|
|||||||
NewDeviceJoin = "newDevice.handlebars",
|
NewDeviceJoin = "newDevice.handlebars",
|
||||||
OrgInvite = "organizationInvitation.handlebars",
|
OrgInvite = "organizationInvitation.handlebars",
|
||||||
ResetPassword = "passwordReset.handlebars",
|
ResetPassword = "passwordReset.handlebars",
|
||||||
|
SetupPassword = "passwordSetup.handlebars",
|
||||||
SecretLeakIncident = "secretLeakIncident.handlebars",
|
SecretLeakIncident = "secretLeakIncident.handlebars",
|
||||||
WorkspaceInvite = "workspaceInvitation.handlebars",
|
WorkspaceInvite = "workspaceInvitation.handlebars",
|
||||||
ScimUserProvisioned = "scimUserProvisioned.handlebars",
|
ScimUserProvisioned = "scimUserProvisioned.handlebars",
|
||||||
|
17
backend/src/services/smtp/templates/passwordSetup.handlebars
Normal file
17
backend/src/services/smtp/templates/passwordSetup.handlebars
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||||
|
<title>Password Setup</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>Setup your password</h2>
|
||||||
|
<p>Someone requested to set up a password for your account.</p>
|
||||||
|
<p><strong>Make sure you are already logged in to Infisical in the current browser before clicking the link below.</strong></p>
|
||||||
|
<a href="{{callback_url}}?token={{token}}&to={{email}}">Setup password</a>
|
||||||
|
<p>If you didn't initiate this request, please contact
|
||||||
|
{{#if isCloud}}us immediately at team@infisical.com.{{else}}your administrator immediately.{{/if}}</p>
|
||||||
|
|
||||||
|
{{emailFooter}}
|
||||||
|
</body>
|
||||||
|
</html>
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user