mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-21 08:28:22 +00:00
feat(infisical-pg): completed secret rotation with queue service
This commit is contained in:
@@ -1,10 +1,20 @@
|
||||
import { registerOrgRoleRouter } from "./org-role-router";
|
||||
import { registerProjectRoleRouter } from "./project-role-router";
|
||||
import { registerSecretApprovalPolicyRouter } from "./secret-approval-policy-router";
|
||||
import { registerSecretApprovalRequestRouter } from "./secret-approval-request-router";
|
||||
import { registerSecretRotationProviderRouter } from "./secret-rotation-provider-router";
|
||||
import { registerSecretRotationRouter } from "./secret-rotation-router";
|
||||
|
||||
export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
||||
// org role starts with organization
|
||||
await server.register(registerOrgRoleRouter, { prefix: "/organization" });
|
||||
await server.register(registerProjectRoleRouter, { prefix: "/workspace" });
|
||||
await server.register(registerSecretApprovalPolicyRouter, { prefix: "/secret-approvals" });
|
||||
await server.register(registerSecretApprovalRequestRouter, {
|
||||
prefix: "/secret-approval-requests"
|
||||
});
|
||||
await server.register(registerSecretRotationProviderRouter, {
|
||||
prefix: "/secret-rotation-providers"
|
||||
});
|
||||
await server.register(registerSecretRotationRouter, { prefix: "/secret-rotations" });
|
||||
};
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { z } from "zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { sapPubSchema } from "@app/server/routes/sanitizedSchemas";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
SaRequestSecretsSchema,
|
||||
SarReviewersSchema,
|
||||
SecretApprovalRequestsSchema,
|
||||
SecretsSchema,
|
||||
SecretVersionsSchema
|
||||
} from "@app/db/schemas";
|
||||
import {
|
||||
ApprovalStatus,
|
||||
RequestState
|
||||
} from "@app/ee/services/secret-approval-request/secret-approval-request-types";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerSecretApprovalRequestRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
url: "/",
|
||||
method: "GET",
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
environment: z.string().trim().optional(),
|
||||
committer: z.string().trim().optional(),
|
||||
status: z.nativeEnum(RequestState).optional(),
|
||||
limit: z.coerce.number().default(20),
|
||||
offset: z.coerce.number().default(0)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
approvals: SecretApprovalRequestsSchema.merge(
|
||||
z.object({
|
||||
// secretPath: z.string(),
|
||||
policy: z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
approvals: z.number(),
|
||||
approvers: z.string().array(),
|
||||
secretPath: z.string().optional().nullable()
|
||||
}),
|
||||
reviewers: z.object({ member: z.string(), status: z.string() }).array(),
|
||||
approvers: z.string().array()
|
||||
})
|
||||
).array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const approvals = await server.services.secretApprovalRequest.getSecretApprovals({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
...req.query,
|
||||
projectId: req.query.workspaceId
|
||||
});
|
||||
return { approvals };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/count",
|
||||
method: "GET",
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
approvals: z.object({
|
||||
open: z.number().default(0),
|
||||
closed: z.number().default(0)
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const approvals = await server.services.secretApprovalRequest.requestCount({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
projectId: req.query.workspaceId
|
||||
});
|
||||
return { approvals };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:id",
|
||||
method: "GET",
|
||||
schema: {
|
||||
params: z.object({
|
||||
id: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
approval: SecretApprovalRequestsSchema.merge(
|
||||
z.object({
|
||||
// secretPath: z.string(),
|
||||
policy: z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
approvals: z.number(),
|
||||
approvers: z.string().array(),
|
||||
secretPath: z.string().optional().nullable()
|
||||
}),
|
||||
reviewers: z.object({ member: z.string(), status: z.string() }).array(),
|
||||
approvers: z.string().array(),
|
||||
commits: SaRequestSecretsSchema.omit({ secretBlindIndex: true })
|
||||
.merge(
|
||||
z.object({
|
||||
secret: SecretsSchema.pick({
|
||||
id: true,
|
||||
secretKeyIV: true,
|
||||
secretKeyTag: true,
|
||||
secretKeyCiphertext: true,
|
||||
secretValueIV: true,
|
||||
secretValueTag: true,
|
||||
secretValueCiphertext: true,
|
||||
secretCommentIV: true,
|
||||
secretCommentTag: true,
|
||||
secretCommentCiphertext: true
|
||||
})
|
||||
.optional()
|
||||
.nullable(),
|
||||
secretVersion: SecretVersionsSchema.pick({
|
||||
id: true,
|
||||
secretKeyIV: true,
|
||||
secretKeyTag: true,
|
||||
secretKeyCiphertext: true,
|
||||
secretValueIV: true,
|
||||
secretValueTag: true,
|
||||
secretValueCiphertext: true,
|
||||
secretCommentIV: true,
|
||||
secretCommentTag: true,
|
||||
secretCommentCiphertext: true
|
||||
}).optional()
|
||||
})
|
||||
)
|
||||
.array()
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const approval = await server.services.secretApprovalRequest.getSecretApprovalDetails({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
id: req.params.id
|
||||
});
|
||||
return { approval };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:id/merge",
|
||||
method: "POST",
|
||||
schema: {
|
||||
params: z.object({
|
||||
id: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
approval: SecretApprovalRequestsSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { approval } = await server.services.secretApprovalRequest.mergeSecretApprovalRequest({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
approvalId: req.params.id
|
||||
});
|
||||
return { approval };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:id/review",
|
||||
method: "POST",
|
||||
schema: {
|
||||
params: z.object({
|
||||
id: z.string()
|
||||
}),
|
||||
body: z.object({
|
||||
status: z.enum([ApprovalStatus.APPROVED, ApprovalStatus.REJECTED])
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
review: SarReviewersSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const review = await server.services.secretApprovalRequest.reviewApproval({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
approvalId: req.params.id,
|
||||
status: req.body.status
|
||||
});
|
||||
return { review };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:id/status",
|
||||
method: "POST",
|
||||
schema: {
|
||||
params: z.object({
|
||||
id: z.string()
|
||||
}),
|
||||
body: z.object({
|
||||
status: z.nativeEnum(RequestState)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
approval: SecretApprovalRequestsSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const approval = await server.services.secretApprovalRequest.updateApprovalStatus({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
approvalId: req.params.id,
|
||||
status: req.body.status
|
||||
});
|
||||
return { approval };
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerSecretRotationProviderRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
url: "/:workspaceId",
|
||||
method: "GET",
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
providers: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
title: z.string(),
|
||||
image: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
template: z.any()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const providers = await server.services.secretRotation.getProviderTemplates({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
projectId: req.params.workspaceId
|
||||
});
|
||||
return providers;
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,162 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretRotationOutputsSchema, SecretRotationsSchema, SecretsSchema } from "@app/db/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerSecretRotationRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
url: "/",
|
||||
method: "POST",
|
||||
schema: {
|
||||
body: z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
secretPath: z.string().trim(),
|
||||
environment: z.string().trim(),
|
||||
interval: z.number().min(1),
|
||||
provider: z.string().trim(),
|
||||
customProvider: z.string().trim().optional(),
|
||||
inputs: z.record(z.unknown()),
|
||||
outputs: z.record(z.string())
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
secretRotation: SecretRotationsSchema.merge(
|
||||
z.object({
|
||||
environment: z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
slug: z.string()
|
||||
}),
|
||||
outputs: SecretRotationOutputsSchema.array()
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const secretRotation = await server.services.secretRotation.createRotation({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
...req.body,
|
||||
projectId: req.body.workspaceId
|
||||
});
|
||||
return { secretRotation };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/restart",
|
||||
method: "POST",
|
||||
schema: {
|
||||
body: z.object({
|
||||
id: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
secretRotation: SecretRotationsSchema.merge(
|
||||
z.object({
|
||||
environment: z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
slug: z.string()
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const secretRotation = await server.services.secretRotation.restartById({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
rotationId: req.body.id
|
||||
});
|
||||
return { secretRotation };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/",
|
||||
method: "GET",
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
secretRotations: SecretRotationsSchema.merge(
|
||||
z.object({
|
||||
environment: z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
slug: z.string()
|
||||
}),
|
||||
outputs: z
|
||||
.object({
|
||||
key: z.string(),
|
||||
secret: SecretsSchema.pick({
|
||||
id: true,
|
||||
version: true,
|
||||
secretKeyIV: true,
|
||||
secretKeyTag: true,
|
||||
secretKeyCiphertext: true,
|
||||
secretValueIV: true,
|
||||
secretValueTag: true,
|
||||
secretValueCiphertext: true,
|
||||
secretCommentIV: true,
|
||||
secretCommentTag: true,
|
||||
secretCommentCiphertext: true
|
||||
})
|
||||
})
|
||||
.array()
|
||||
})
|
||||
).array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const secretRotations = await server.services.secretRotation.getByProjectId({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
projectId: req.query.workspaceId
|
||||
});
|
||||
return { secretRotations };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/:id",
|
||||
method: "DELETE",
|
||||
schema: {
|
||||
params: z.object({
|
||||
id: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
secretRotation: SecretRotationsSchema.merge(
|
||||
z.object({
|
||||
environment: z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
slug: z.string()
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const secretRotation = await server.services.secretRotation.deleteById({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
rotationId: req.params.id
|
||||
});
|
||||
return { secretRotation };
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -1,14 +1,14 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TSecretApprovalPolicies, TableName } from "@app/db/schemas";
|
||||
import { TableName,TSecretApprovalPolicies } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import {
|
||||
TFindFilter,
|
||||
buildFindFilter,
|
||||
mergeOneToManyRelation,
|
||||
ormify,
|
||||
selectAllTableCols
|
||||
} from "@app/lib/knex";
|
||||
import { Knex } from "knex";
|
||||
selectAllTableCols,
|
||||
TFindFilter} from "@app/lib/knex";
|
||||
|
||||
export type TSecretApprovalPolicyDalFactory = ReturnType<typeof secretApprovalPolicyDalFactory>;
|
||||
|
||||
|
||||
+13
-11
@@ -1,6 +1,18 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import picomatch from "picomatch";
|
||||
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { TSecretApprovalPolicyDalFactory } from "./secret-approval-policy-dal";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { containsGlobPatterns } from "@app/lib/picomatch";
|
||||
import { TProjectEnvDalFactory } from "@app/services/project-env/project-env-dal";
|
||||
import { TProjectMembershipDalFactory } from "@app/services/project-membership/project-membership-dal";
|
||||
|
||||
import { TSapApproverDalFactory } from "./sap-approver-dal";
|
||||
import { TSecretApprovalPolicyDalFactory } from "./secret-approval-policy-dal";
|
||||
import {
|
||||
TCreateSapDTO,
|
||||
TDeleteSapDTO,
|
||||
@@ -8,16 +20,6 @@ import {
|
||||
TListSapDTO,
|
||||
TUpdateSapDTO
|
||||
} from "./secret-approval-policy-types";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import picomatch from "picomatch";
|
||||
import { containsGlobPatterns } from "@app/lib/picomatch";
|
||||
import { TProjectMembershipDalFactory } from "@app/services/project-membership/project-membership-dal";
|
||||
import { TProjectEnvDalFactory } from "@app/services/project-env/project-env-dal";
|
||||
|
||||
const getPolicyScore = (policy: { secretPath?: string | null }) =>
|
||||
// if glob pattern score is 1, if not exist score is 0 and if its not both then its exact path meaning score 2
|
||||
|
||||
@@ -1,25 +1,121 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
import { Knex } from "knex";
|
||||
|
||||
export type TSarSecretDalFactory = ReturnType<typeof sarSecretDalFactory>;
|
||||
|
||||
export const sarSecretDalFactory = (db: TDbClient) => {
|
||||
const sarSecretOrm = ormify(db, TableName.SarSecret);
|
||||
|
||||
const findByRequestId = (requestId: string, tx?: Knex) => {
|
||||
const findByRequestId = async (requestId: string, tx?: Knex) => {
|
||||
try {
|
||||
const doc = (tx || db)(TableName.SarSecret)
|
||||
const doc = await (tx || db)(TableName.SarSecret)
|
||||
.where({ requestId })
|
||||
.leftJoin(TableName.Secret, `${TableName.SarSecret}.secretId`, `${TableName.Secret}.id`)
|
||||
.leftJoin(
|
||||
TableName.SecretVersion,
|
||||
`${TableName.SecretVersion}.id`,
|
||||
`${TableName.SarSecret}.secretVersion`
|
||||
)
|
||||
.select(selectAllTableCols(TableName.SarSecret))
|
||||
.select(
|
||||
db.ref("secretBlindIndex").withSchema(TableName.Secret).as("latestSecretBlindIndex"),
|
||||
db.ref("version").withSchema(TableName.Secret).as("latestSecretVersion")
|
||||
db.ref("secretBlindIndex").withSchema(TableName.Secret).as("orgSecBlindIndex"),
|
||||
db.ref("version").withSchema(TableName.Secret).as("orgSecVersion"),
|
||||
db.ref("secretKeyIV").withSchema(TableName.Secret).as("orgSecKeyIV"),
|
||||
db.ref("secretKeyTag").withSchema(TableName.Secret).as("orgSecKeyTag"),
|
||||
db.ref("secretKeyCiphertext").withSchema(TableName.Secret).as("orgSecKeyCiphertext"),
|
||||
db.ref("secretValueIV").withSchema(TableName.Secret).as("orgSecValueIV"),
|
||||
db.ref("secretValueTag").withSchema(TableName.Secret).as("orgSecValueTag"),
|
||||
db.ref("secretValueCiphertext").withSchema(TableName.Secret).as("orgSecValueCiphertext"),
|
||||
db.ref("secretCommentIV").withSchema(TableName.Secret).as("orgSecCommentIV"),
|
||||
db.ref("secretCommentTag").withSchema(TableName.Secret).as("orgSecCommentTag"),
|
||||
db
|
||||
.ref("secretCommentCiphertext")
|
||||
.withSchema(TableName.Secret)
|
||||
.as("orgSecCommentCiphertext")
|
||||
)
|
||||
.select(
|
||||
// db.ref("secretBlindIndex").withSchema(TableName.Secret).as("orgSecBlindInex"),
|
||||
db.ref("version").withSchema(TableName.SecretVersion).as("secVerVersion"),
|
||||
db.ref("secretKeyIV").withSchema(TableName.SecretVersion).as("secVerKeyIV"),
|
||||
db.ref("secretKeyTag").withSchema(TableName.SecretVersion).as("secVerKeyTag"),
|
||||
db
|
||||
.ref("secretKeyCiphertext")
|
||||
.withSchema(TableName.SecretVersion)
|
||||
.as("secVerKeyCiphertext"),
|
||||
db.ref("secretValueIV").withSchema(TableName.SecretVersion).as("secVerValueIV"),
|
||||
db.ref("secretValueTag").withSchema(TableName.SecretVersion).as("secVerValueTag"),
|
||||
db
|
||||
.ref("secretValueCiphertext")
|
||||
.withSchema(TableName.SecretVersion)
|
||||
.as("secVerValueCiphertext"),
|
||||
db.ref("secretCommentIV").withSchema(TableName.SecretVersion).as("secVerCommentIV"),
|
||||
db.ref("secretCommentTag").withSchema(TableName.SecretVersion).as("secVerCommentTag"),
|
||||
db
|
||||
.ref("secretCommentCiphertext")
|
||||
.withSchema(TableName.SecretVersion)
|
||||
.as("secVerCommentCiphertext")
|
||||
);
|
||||
return doc;
|
||||
return doc.map(
|
||||
({
|
||||
orgSecKeyIV,
|
||||
orgSecKeyTag,
|
||||
orgSecValueIV,
|
||||
orgSecVersion,
|
||||
orgSecValueTag,
|
||||
orgSecCommentIV,
|
||||
orgSecBlindIndex,
|
||||
orgSecCommentTag,
|
||||
orgSecKeyCiphertext,
|
||||
orgSecValueCiphertext,
|
||||
orgSecCommentCiphertext,
|
||||
secVerCommentIV,
|
||||
secVerCommentCiphertext,
|
||||
secVerCommentTag,
|
||||
secVerValueCiphertext,
|
||||
secVerKeyIV,
|
||||
secVerKeyTag,
|
||||
secVerValueIV,
|
||||
secVerVersion,
|
||||
secVerValueTag,
|
||||
secVerKeyCiphertext,
|
||||
...el
|
||||
}) => ({
|
||||
...el,
|
||||
secret: el.secretId
|
||||
? {
|
||||
id: el.secretId,
|
||||
secretBlindIndex: orgSecBlindIndex,
|
||||
secretKeyIV: orgSecKeyIV,
|
||||
secretKeyTag: orgSecKeyTag,
|
||||
secretKeyCiphertext: orgSecKeyCiphertext,
|
||||
secretValueIV: orgSecValueIV,
|
||||
secretValueTag: orgSecValueTag,
|
||||
secretValueCiphertext: orgSecValueCiphertext,
|
||||
secretCommentIV: orgSecCommentIV,
|
||||
secretCommentTag: orgSecCommentTag,
|
||||
secretCommentCiphertext: orgSecCommentCiphertext
|
||||
}
|
||||
: undefined,
|
||||
secretVersion: el.secretVersion
|
||||
? {
|
||||
id: el.secretVersion,
|
||||
secretKeyIV: secVerKeyIV,
|
||||
secretKeyTag: secVerKeyTag,
|
||||
secretKeyCiphertext: secVerKeyCiphertext,
|
||||
secretValueIV: secVerValueIV,
|
||||
secretValueTag: secVerValueTag,
|
||||
secretValueCiphertext: secVerValueCiphertext,
|
||||
secretCommentIV: secVerCommentIV,
|
||||
secretCommentTag: secVerCommentTag,
|
||||
secretCommentCiphertext: secVerCommentCiphertext
|
||||
}
|
||||
: undefined
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindByRequestId" });
|
||||
}
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TSecretApprovalRequests, TableName } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { TFindFilter, ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { SecretApprovalRequestsSchema, TableName, TSecretApprovalRequests } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import {
|
||||
ormify,
|
||||
selectAllTableCols,
|
||||
sqlNestRelationships,
|
||||
stripUndefinedInWhere,
|
||||
TFindFilter
|
||||
} from "@app/lib/knex";
|
||||
|
||||
import { RequestState } from "./secret-approval-request-types";
|
||||
|
||||
export type TSecretApprovalRequestDalFactory = ReturnType<typeof secretApprovalRequestDalFactory>;
|
||||
@@ -23,27 +31,33 @@ export const secretApprovalRequestDalFactory = (db: TDbClient) => {
|
||||
const findQuery = (filter: TFindFilter<TSecretApprovalRequests>, tx: Knex) =>
|
||||
tx(TableName.SecretApprovalRequest)
|
||||
.where(filter)
|
||||
.join(
|
||||
TableName.SecretFolder,
|
||||
`${TableName.SecretApprovalRequest}.folderId`,
|
||||
`${TableName.SecretFolder}.id`
|
||||
)
|
||||
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
||||
.join(
|
||||
TableName.SecretApprovalPolicy,
|
||||
`${TableName.SecretApprovalRequest}.policyId`,
|
||||
`${TableName.SecretApprovalPolicy}.id`
|
||||
)
|
||||
.join(
|
||||
TableName.SarReviewer,
|
||||
`${TableName.SecretApprovalRequest}.id`,
|
||||
`${TableName.SarReviewer}.requestId`
|
||||
)
|
||||
.join(
|
||||
TableName.SapApprover,
|
||||
`${TableName.SecretApprovalPolicy}.id`,
|
||||
`${TableName.SapApprover}.policyId`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.SarReviewer,
|
||||
`${TableName.SecretApprovalRequest}.id`,
|
||||
`${TableName.SarReviewer}.requestId`
|
||||
)
|
||||
.select(selectAllTableCols(TableName.SecretApprovalRequest))
|
||||
.select(tx.ref("id").withSchema(TableName.SarReviewer).as("reviewerMemberId"))
|
||||
.select(tx.ref("statue").withSchema(TableName.SarReviewer).as("reviewerStatus"))
|
||||
.select(tx.ref("member").withSchema(TableName.SarReviewer).as("reviewerMemberId"))
|
||||
.select(tx.ref("status").withSchema(TableName.SarReviewer).as("reviewerStatus"))
|
||||
.select(tx.ref("id").withSchema(TableName.SecretApprovalPolicy).as("policyId"))
|
||||
.select(tx.ref("name").withSchema(TableName.SecretApprovalPolicy).as("policyName"))
|
||||
.select(tx.ref("projectId").withSchema(TableName.SecretApprovalPolicy))
|
||||
.select(tx.ref("projectId").withSchema(TableName.Environment))
|
||||
.select(
|
||||
tx.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath")
|
||||
)
|
||||
@@ -52,51 +66,27 @@ export const secretApprovalRequestDalFactory = (db: TDbClient) => {
|
||||
|
||||
const findById = async (id: string, tx?: Knex) => {
|
||||
try {
|
||||
const docs = await findQuery({ id }, tx || db);
|
||||
const sql = findQuery({ [`${TableName.SecretApprovalRequest}.id` as "id"]: id }, tx || db);
|
||||
const docs = await sql;
|
||||
const formatedDoc = sqlNestRelationships({
|
||||
data: docs,
|
||||
key: "id",
|
||||
parentMapper: ({
|
||||
id: pk,
|
||||
projectId,
|
||||
hasMerged,
|
||||
status,
|
||||
conflicts,
|
||||
slug,
|
||||
folderId,
|
||||
statusChangeBy,
|
||||
committerId,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
policyId,
|
||||
policyName,
|
||||
policyApprovals,
|
||||
policySecretPath
|
||||
}) => ({
|
||||
id: pk,
|
||||
hasMerged,
|
||||
projectId,
|
||||
status,
|
||||
conflicts,
|
||||
slug,
|
||||
folderId,
|
||||
statusChangeBy,
|
||||
committerId,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
policyId,
|
||||
parentMapper: (el) => ({
|
||||
...SecretApprovalRequestsSchema.parse(el),
|
||||
projectId: el.projectId,
|
||||
policy: {
|
||||
id: policyId,
|
||||
name: policyName,
|
||||
approvals: policyApprovals,
|
||||
secretPath: policySecretPath
|
||||
id: el.policyId,
|
||||
name: el.policyName,
|
||||
approvals: el.policyApprovals,
|
||||
secretPath: el.policySecretPath
|
||||
}
|
||||
}),
|
||||
childrenMapper: [
|
||||
{
|
||||
key: "reviewerMemberId",
|
||||
label: "reviewers",
|
||||
mapper: ({ reviewerMemberId: member, reviewerStatus: status }) => ({ member, status })
|
||||
mapper: ({ reviewerMemberId: member, reviewerStatus: status }) =>
|
||||
member ? { member, status } : undefined
|
||||
},
|
||||
{ key: "approverId", label: "approvers", mapper: ({ approverId }) => approverId }
|
||||
] as const
|
||||
@@ -124,18 +114,27 @@ export const secretApprovalRequestDalFactory = (db: TDbClient) => {
|
||||
`${TableName.SecretFolder}.envId`,
|
||||
`${TableName.Environment}.id`
|
||||
)
|
||||
.where({ projectId })
|
||||
.join(
|
||||
TableName.SapApprover,
|
||||
`${TableName.SecretApprovalRequest}.policyId`,
|
||||
`${TableName.SapApprover}.policyId`
|
||||
)
|
||||
.where({ projectId })
|
||||
.where(`${TableName.SapApprover}.approverId`, membershipId)
|
||||
.orWhere(`${TableName.SecretApprovalRequest}.committerId`, membershipId)
|
||||
.groupBy("status")
|
||||
.count("status");
|
||||
console.log(JSON.stringify(doc, null, 4));
|
||||
return { open: 0, closed: 0 };
|
||||
.count("status")
|
||||
.select("status");
|
||||
return {
|
||||
open: parseInt(
|
||||
(doc.find(({ status }) => status === RequestState.Open)?.count as string) || "0",
|
||||
10
|
||||
),
|
||||
closed: parseInt(
|
||||
(doc.find(({ status }) => status === RequestState.Closed)?.count as string) || "0",
|
||||
10
|
||||
)
|
||||
};
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindRequestCount" });
|
||||
}
|
||||
@@ -157,7 +156,6 @@ export const secretApprovalRequestDalFactory = (db: TDbClient) => {
|
||||
`${TableName.SecretFolder}.envId`,
|
||||
`${TableName.Environment}.id`
|
||||
)
|
||||
.where({ projectId, slug: environment, status, committerId: committer })
|
||||
.join(
|
||||
TableName.SecretApprovalPolicy,
|
||||
`${TableName.SecretApprovalRequest}.policyId`,
|
||||
@@ -168,11 +166,28 @@ export const secretApprovalRequestDalFactory = (db: TDbClient) => {
|
||||
`${TableName.SecretApprovalPolicy}.id`,
|
||||
`${TableName.SapApprover}.policyId`
|
||||
)
|
||||
.where(`${TableName.SapApprover}.approverId`, membershipId)
|
||||
.orWhere(`${TableName.SecretApprovalRequest}.committerId`, membershipId)
|
||||
.leftJoin(
|
||||
TableName.SarReviewer,
|
||||
`${TableName.SecretApprovalRequest}.id`,
|
||||
`${TableName.SarReviewer}.requestId`
|
||||
)
|
||||
.where(
|
||||
stripUndefinedInWhere({
|
||||
projectId,
|
||||
slug: environment,
|
||||
[`${TableName.SecretApprovalRequest}.status`]: status,
|
||||
committerId: committer
|
||||
})
|
||||
)
|
||||
.andWhere((bd) =>
|
||||
bd
|
||||
.where(`${TableName.SapApprover}.approverId`, membershipId)
|
||||
.orWhere(`${TableName.SecretApprovalRequest}.committerId`, membershipId)
|
||||
)
|
||||
.select(selectAllTableCols(TableName.SecretApprovalRequest))
|
||||
.select(db.ref("projectId").withSchema(TableName.Environment))
|
||||
.select(db.ref("id").withSchema(TableName.SarReviewer).as("reviewerMemberId"))
|
||||
.select(db.ref("statue").withSchema(TableName.SarReviewer).as("reviewerStatus"))
|
||||
.select(db.ref("status").withSchema(TableName.SarReviewer).as("reviewerStatus"))
|
||||
.select(db.ref("id").withSchema(TableName.SecretApprovalPolicy).as("policyId"))
|
||||
.select(db.ref("name").withSchema(TableName.SecretApprovalPolicy).as("policyName"))
|
||||
.select(
|
||||
@@ -185,46 +200,22 @@ export const secretApprovalRequestDalFactory = (db: TDbClient) => {
|
||||
const formatedDoc = sqlNestRelationships({
|
||||
data: docs,
|
||||
key: "id",
|
||||
parentMapper: ({
|
||||
id: pk,
|
||||
hasMerged,
|
||||
status: pStatus,
|
||||
conflicts,
|
||||
slug,
|
||||
folderId,
|
||||
statusChangeBy,
|
||||
committerId,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
policyId,
|
||||
policyName,
|
||||
policyApprovals,
|
||||
policySecretPath
|
||||
}) => ({
|
||||
id: pk,
|
||||
hasMerged,
|
||||
projectId,
|
||||
status: pStatus,
|
||||
conflicts,
|
||||
slug,
|
||||
folderId,
|
||||
statusChangeBy,
|
||||
committerId,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
policyId,
|
||||
parentMapper: (el) => ({
|
||||
...SecretApprovalRequestsSchema.parse(el),
|
||||
projectId: el.projectId,
|
||||
policy: {
|
||||
id: policyId,
|
||||
name: policyName,
|
||||
approvals: policyApprovals,
|
||||
secretPath: policySecretPath
|
||||
id: el.policyId,
|
||||
name: el.policyName,
|
||||
approvals: el.policyApprovals,
|
||||
secretPath: el.policySecretPath
|
||||
}
|
||||
}),
|
||||
childrenMapper: [
|
||||
{
|
||||
key: "reviewerMemberId",
|
||||
label: "reviewers",
|
||||
mapper: ({ reviewerMemberId: member, reviewerStatus: s }) => ({ member, status: s })
|
||||
mapper: ({ reviewerMemberId: member, reviewerStatus: s }) =>
|
||||
member ? { member, status: s } : undefined
|
||||
},
|
||||
{ key: "approverId", label: "approvers", mapper: ({ approverId }) => approverId }
|
||||
] as const
|
||||
|
||||
+168
-134
@@ -1,19 +1,5 @@
|
||||
import { TSecretFolderDalFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||
import { TSecretApprovalRequestDalFactory } from "./secret-approval-request-dal";
|
||||
import {
|
||||
ApprovalStatus,
|
||||
CommitType,
|
||||
TApprovalRequestCountDTO,
|
||||
TGenerateSecretApprovalRequestDTO,
|
||||
TListApprovalsDTO,
|
||||
TMergeSecretApprovalRequestDTO,
|
||||
TReviewRequestDTO,
|
||||
TSecretApprovalDetailsDTO,
|
||||
TStatusChangeDTO
|
||||
} from "./secret-approval-request-types";
|
||||
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { TSecretBlindIndexDalFactory } from "@app/services/secret/secret-blind-index-dal";
|
||||
import { generateSecretBlindIndexBySalt } from "@app/services/secret/secret-service";
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
|
||||
import {
|
||||
ProjectMembershipRole,
|
||||
SecretEncryptionAlgo,
|
||||
@@ -22,13 +8,32 @@ import {
|
||||
TSaRequestSecretsInsert,
|
||||
TSecrets
|
||||
} from "@app/db/schemas";
|
||||
import { TSecretDalFactory } from "@app/services/secret/secret-dal";
|
||||
import { TSecretVersionDalFactory } from "@app/services/secret/secret-version-dal";
|
||||
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { TSarSecretDalFactory } from "./sar-secret-dal";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { TSecretBlindIndexDalFactory } from "@app/services/secret/secret-blind-index-dal";
|
||||
import { TSecretDalFactory } from "@app/services/secret/secret-dal";
|
||||
import { generateSecretBlindIndexBySalt } from "@app/services/secret/secret-service";
|
||||
import { TSecretVersionDalFactory } from "@app/services/secret/secret-version-dal";
|
||||
import { TSecretFolderDalFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
|
||||
import { TSarReviewerDalFactory } from "./sar-reviewer-dal";
|
||||
import { TSarSecretDalFactory } from "./sar-secret-dal";
|
||||
import { TSecretApprovalRequestDalFactory } from "./secret-approval-request-dal";
|
||||
import {
|
||||
ApprovalStatus,
|
||||
CommitType,
|
||||
RequestState,
|
||||
TApprovalRequestCountDTO,
|
||||
TGenerateSecretApprovalRequestDTO,
|
||||
TListApprovalsDTO,
|
||||
TMergeSecretApprovalRequestDTO,
|
||||
TReviewRequestDTO,
|
||||
TSecretApprovalDetailsDTO,
|
||||
TStatusChangeDTO
|
||||
} from "./secret-approval-request-types";
|
||||
|
||||
type TSecretApprovalRequestServiceFactoryDep = {
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
@@ -167,9 +172,9 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
|
||||
if (secretApprovalRequest.hasMerged)
|
||||
throw new BadRequestError({ message: "Approval request has been merged" });
|
||||
if (secretApprovalRequest.status === "close" && status === "close")
|
||||
if (secretApprovalRequest.status === RequestState.Closed && status === RequestState.Closed)
|
||||
throw new BadRequestError({ message: "Approval request is already closed" });
|
||||
if (secretApprovalRequest.status === "open" && status === "open")
|
||||
if (secretApprovalRequest.status === RequestState.Open && status === RequestState.Open)
|
||||
throw new BadRequestError({ message: "Approval request is already open" });
|
||||
|
||||
const updatedRequest = await secretApprovalRequestDal.updateById(secretApprovalRequest.id, {
|
||||
@@ -203,7 +208,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
throw new UnauthorizedError({ message: "User has no access" });
|
||||
}
|
||||
const reviewers = secretApprovalRequest.reviewers.reduce<Record<string, ApprovalStatus>>(
|
||||
(prev, curr) => ({ ...prev, [curr.member.toString()]: curr.status }),
|
||||
(prev, curr) => ({ ...prev, [curr.member.toString()]: curr.status as ApprovalStatus }),
|
||||
{}
|
||||
);
|
||||
const hasMinApproval =
|
||||
@@ -245,10 +250,14 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
if (secretUpdationCommits.length) {
|
||||
const conflictedByNewBlindIndex = await secretDal.findByBlindIndexes(
|
||||
folderId,
|
||||
secretUpdationCommits.map(({ secretBlindIndex }) => ({
|
||||
type: SecretType.Shared,
|
||||
blindIndex: secretBlindIndex
|
||||
}))
|
||||
secretUpdationCommits
|
||||
.filter(
|
||||
({ secretBlindIndex, secret }) => secret && secret.secretBlindIndex !== secretBlindIndex
|
||||
)
|
||||
.map(({ secretBlindIndex }) => ({
|
||||
type: SecretType.Shared,
|
||||
blindIndex: secretBlindIndex
|
||||
}))
|
||||
);
|
||||
const conflictGroupByBlindIndex = conflictedByNewBlindIndex.reduce<Record<string, boolean>>(
|
||||
(prev, curr) =>
|
||||
@@ -276,118 +285,128 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
);
|
||||
|
||||
const mergeStatus = await secretDal.transaction(async (tx) => {
|
||||
const newSecrets = await secretDal.insertMany(
|
||||
secretCreationCommits.map(
|
||||
({
|
||||
secretBlindIndex,
|
||||
metadata,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretCommentCiphertext,
|
||||
skipMultilineEncoding,
|
||||
secretReminderNotice,
|
||||
secretReminderRepeatDays
|
||||
}) => ({
|
||||
secretBlindIndex,
|
||||
metadata,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretCommentCiphertext,
|
||||
skipMultilineEncoding,
|
||||
secretReminderNotice,
|
||||
secretReminderRepeatDays,
|
||||
version: 1,
|
||||
const newSecrets = secretCreationCommits.length
|
||||
? await secretDal.insertMany(
|
||||
secretCreationCommits.map(
|
||||
({
|
||||
secretBlindIndex,
|
||||
metadata,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretCommentCiphertext,
|
||||
skipMultilineEncoding,
|
||||
secretReminderNotice,
|
||||
secretReminderRepeatDays
|
||||
}) => ({
|
||||
secretBlindIndex,
|
||||
metadata,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretCommentCiphertext,
|
||||
skipMultilineEncoding,
|
||||
secretReminderNotice,
|
||||
secretReminderRepeatDays,
|
||||
version: 1,
|
||||
folderId,
|
||||
type: SecretType.Shared,
|
||||
algorithm: SecretEncryptionAlgo.AES_256_GCM,
|
||||
keyEncoding: SecretKeyEncoding.UTF8
|
||||
})
|
||||
),
|
||||
tx
|
||||
)
|
||||
: [];
|
||||
const updatedSecrets = secretUpdationCommits.length
|
||||
? await secretDal.bulkUpdate(
|
||||
secretUpdationCommits.map(
|
||||
({
|
||||
version,
|
||||
secretId,
|
||||
secretBlindIndex,
|
||||
metadata,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretCommentCiphertext,
|
||||
skipMultilineEncoding,
|
||||
secretReminderNotice,
|
||||
secretReminderRepeatDays
|
||||
}) => ({
|
||||
folderId,
|
||||
version: (version || 0) + 1,
|
||||
id: secretId as string,
|
||||
type: SecretType.Shared,
|
||||
secretBlindIndex,
|
||||
metadata,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretCommentCiphertext,
|
||||
skipMultilineEncoding,
|
||||
secretReminderNotice,
|
||||
secretReminderRepeatDays
|
||||
})
|
||||
),
|
||||
tx
|
||||
)
|
||||
: [];
|
||||
const deletedSecret = secretDeletionCommits.length
|
||||
? await secretDal.deleteMany(
|
||||
secretDeletionCommits.map(({ secretBlindIndex }) => ({
|
||||
blindIndex: secretBlindIndex,
|
||||
type: SecretType.Shared
|
||||
})),
|
||||
folderId,
|
||||
type: SecretType.Shared,
|
||||
algorithm: SecretEncryptionAlgo.AES_256_GCM,
|
||||
keyEncoding: SecretKeyEncoding.UTF8
|
||||
})
|
||||
),
|
||||
tx
|
||||
);
|
||||
const updatedSecrets = await secretDal.bulkUpdate(
|
||||
secretUpdationCommits.map(
|
||||
({
|
||||
secretId,
|
||||
secretBlindIndex,
|
||||
metadata,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretCommentCiphertext,
|
||||
skipMultilineEncoding,
|
||||
secretReminderNotice,
|
||||
secretReminderRepeatDays
|
||||
}) => ({
|
||||
folderId,
|
||||
id: secretId as string,
|
||||
type: SecretType.Shared,
|
||||
secretBlindIndex,
|
||||
metadata,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretValueCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretCommentCiphertext,
|
||||
skipMultilineEncoding,
|
||||
secretReminderNotice,
|
||||
secretReminderRepeatDays
|
||||
})
|
||||
),
|
||||
tx
|
||||
);
|
||||
const deletedSecret = await secretDal.deleteMany(
|
||||
secretDeletionCommits.map(({ secretBlindIndex }) => ({
|
||||
blindIndex: secretBlindIndex,
|
||||
type: SecretType.Shared
|
||||
})),
|
||||
folderId,
|
||||
actorId,
|
||||
tx
|
||||
);
|
||||
await secretVersionDal.insertMany(
|
||||
newSecrets
|
||||
.map(({ id, updatedAt, createdAt, ...el }) => ({
|
||||
...el,
|
||||
secretId: id
|
||||
}))
|
||||
.concat(
|
||||
updatedSecrets.map(({ id, updatedAt, createdAt, ...el }) => ({
|
||||
actorId,
|
||||
tx
|
||||
)
|
||||
: [];
|
||||
if (newSecrets.length || updatedSecrets.length) {
|
||||
await secretVersionDal.insertMany(
|
||||
newSecrets
|
||||
.map(({ id, updatedAt, createdAt, ...el }) => ({
|
||||
...el,
|
||||
secretId: id
|
||||
}))
|
||||
),
|
||||
tx
|
||||
);
|
||||
.concat(
|
||||
updatedSecrets.map(({ id, updatedAt, createdAt, ...el }) => ({
|
||||
...el,
|
||||
secretId: id
|
||||
}))
|
||||
),
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
const updatedSecretApproval = await secretApprovalRequestDal.updateById(
|
||||
secretApprovalRequest.id,
|
||||
{
|
||||
conflicts,
|
||||
conflicts: JSON.stringify(conflicts),
|
||||
hasMerged: true,
|
||||
status: "close",
|
||||
statusChangeBy: actorId
|
||||
status: RequestState.Closed,
|
||||
statusChangeBy: membership.id
|
||||
},
|
||||
tx
|
||||
);
|
||||
@@ -403,12 +422,23 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
// this will keep a copy to do merge later when accepting
|
||||
const generateSecretApprovalRequest = async ({
|
||||
data,
|
||||
actorId,
|
||||
actor,
|
||||
policy,
|
||||
projectId,
|
||||
secretPath,
|
||||
environment,
|
||||
commiterMembershipId
|
||||
environment
|
||||
}: TGenerateSecretApprovalRequestDTO) => {
|
||||
const { permission, membership } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
);
|
||||
|
||||
const folder = await folderDal.findBySecretPath(projectId, environment, secretPath);
|
||||
if (!folder)
|
||||
throw new BadRequestError({ message: "Folder not found", name: "GenSecretApproval" });
|
||||
@@ -449,7 +479,9 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
...el,
|
||||
op: CommitType.Create as const,
|
||||
version: 0,
|
||||
secretBlindIndex: secretBlindIndexes[el.secretName]
|
||||
secretBlindIndex: secretBlindIndexes[el.secretName],
|
||||
algorithm: SecretEncryptionAlgo.AES_256_GCM,
|
||||
keyEncoding: SecretKeyEncoding.BASE64
|
||||
}))
|
||||
);
|
||||
}
|
||||
@@ -533,7 +565,8 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
secret: secretId,
|
||||
secretVersion: latestSecretVersions[secretId].id,
|
||||
...el,
|
||||
secretBlindIndex: newSecretBlindIndexes?.[el.secretName],
|
||||
secretBlindIndex:
|
||||
newSecretBlindIndexes?.[el.secretName] || secretBlindIndexes[el.secretName],
|
||||
version: secretsGroupedByBlindIndex[secretBlindIndexes[el.secretName]].version || 1
|
||||
};
|
||||
})
|
||||
@@ -596,6 +629,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (!commits.length) throw new BadRequestError({ message: "Empty commits" });
|
||||
const secretApprovalRequest = await secretApprovalRequestDal.transaction(async (tx) => {
|
||||
const doc = await secretApprovalRequestDal.create(
|
||||
{
|
||||
@@ -604,7 +638,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
policyId: policy.id,
|
||||
status: "open",
|
||||
hasMerged: false,
|
||||
committerId: commiterMembershipId
|
||||
committerId: membership.id
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
+20
-12
@@ -9,7 +9,7 @@ export enum CommitType {
|
||||
|
||||
export enum RequestState {
|
||||
Open = "open",
|
||||
Closed = "closed"
|
||||
Closed = "close"
|
||||
}
|
||||
|
||||
export enum ApprovalStatus {
|
||||
@@ -18,28 +18,36 @@ export enum ApprovalStatus {
|
||||
REJECTED = "rejected"
|
||||
}
|
||||
|
||||
type TApprovalCreateSecret = Omit<TSaRequestSecrets, TImmutableDBKeys | "version"> & {
|
||||
type TApprovalCreateSecret = Omit<
|
||||
TSaRequestSecrets,
|
||||
| TImmutableDBKeys
|
||||
| "version"
|
||||
| "algorithm"
|
||||
| "keyEncoding"
|
||||
| "requestId"
|
||||
| "op"
|
||||
| "secretVersion"
|
||||
| "secretBlindIndex"
|
||||
> & {
|
||||
secretName: string;
|
||||
tagIds?: string[];
|
||||
};
|
||||
type TApprovalUpdateSecret = Partial<Omit<TSaRequestSecrets, TImmutableDBKeys | "version">> & {
|
||||
type TApprovalUpdateSecret = Partial<TApprovalCreateSecret> & {
|
||||
secretName: string;
|
||||
newSecretName?: string;
|
||||
tagIds?: string[];
|
||||
};
|
||||
|
||||
export type TGenerateSecretApprovalRequestDTO = {
|
||||
projectId: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
policy: TSecretApprovalPolicies;
|
||||
commiterMembershipId: string;
|
||||
data: {
|
||||
[CommitType.Create]: TApprovalCreateSecret[];
|
||||
[CommitType.Update]: TApprovalUpdateSecret[];
|
||||
[CommitType.Delete]: { secretName: string }[];
|
||||
[CommitType.Create]?: TApprovalCreateSecret[];
|
||||
[CommitType.Update]?: TApprovalUpdateSecret[];
|
||||
[CommitType.Delete]?: { secretName: string }[];
|
||||
};
|
||||
};
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TMergeSecretApprovalRequestDTO = {
|
||||
approvalId: string;
|
||||
@@ -47,7 +55,7 @@ export type TMergeSecretApprovalRequestDTO = {
|
||||
|
||||
export type TStatusChangeDTO = {
|
||||
approvalId: string;
|
||||
status: "open" | "close";
|
||||
status: RequestState;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TReviewRequestDTO = {
|
||||
@@ -67,5 +75,5 @@ export type TListApprovalsDTO = {
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TSecretApprovalDetailsDTO = {
|
||||
id:string;
|
||||
} & Omit<TProjectPermission, 'projectId'>
|
||||
id: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { SecretRotationsSchema, TableName, TSecretRotations } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols, sqlNestRelationships, TFindFilter } from "@app/lib/knex";
|
||||
|
||||
export type TSecretRotationDalFactory = ReturnType<typeof secretRotationDalFactory>;
|
||||
|
||||
export const secretRotationDalFactory = (db: TDbClient) => {
|
||||
const secretRotationOrm = ormify(db, TableName.SecretRotation);
|
||||
const secretRotationOutputOrm = ormify(db, TableName.SecretRotationOutput);
|
||||
|
||||
const findQuery = (filter: TFindFilter<TSecretRotations & { projectId: string }>, tx: Knex) =>
|
||||
tx(TableName.SecretRotation)
|
||||
.where(filter)
|
||||
.join(
|
||||
TableName.Environment,
|
||||
`${TableName.SecretRotation}.envId`,
|
||||
`${TableName.Environment}.id`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.SecretRotationOutput,
|
||||
`${TableName.SecretRotation}.id`,
|
||||
`${TableName.SecretRotationOutput}.rotationId`
|
||||
)
|
||||
.join(
|
||||
TableName.Secret,
|
||||
`${TableName.SecretRotationOutput}.secretId`,
|
||||
`${TableName.Secret}.id`
|
||||
)
|
||||
.select(selectAllTableCols(TableName.SecretRotation))
|
||||
.select(tx.ref("name").withSchema(TableName.Environment).as("envName"))
|
||||
.select(tx.ref("slug").withSchema(TableName.Environment).as("envSlug"))
|
||||
.select(tx.ref("id").withSchema(TableName.Environment).as("envId"))
|
||||
.select(tx.ref("projectId").withSchema(TableName.Environment))
|
||||
.select(tx.ref("key").withSchema(TableName.SecretRotationOutput).as("outputKey"))
|
||||
.select(tx.ref("id").withSchema(TableName.Secret).as("secId"))
|
||||
.select(tx.ref("version").withSchema(TableName.Secret).as("secVersion"))
|
||||
.select(tx.ref("secretKeyIV").withSchema(TableName.Secret))
|
||||
.select(tx.ref("secretKeyTag").withSchema(TableName.Secret))
|
||||
.select(tx.ref("secretKeyCiphertext").withSchema(TableName.Secret))
|
||||
.select(tx.ref("secretValueIV").withSchema(TableName.Secret))
|
||||
.select(tx.ref("secretValueTag").withSchema(TableName.Secret))
|
||||
.select(tx.ref("secretValueCiphertext").withSchema(TableName.Secret))
|
||||
.select(tx.ref("secretCommentIV").withSchema(TableName.Secret))
|
||||
.select(tx.ref("secretCommentTag").withSchema(TableName.Secret))
|
||||
.select(tx.ref("secretCommentCiphertext").withSchema(TableName.Secret));
|
||||
|
||||
const find = async (filter: TFindFilter<TSecretRotations & { projectId: string }>, tx?: Knex) => {
|
||||
try {
|
||||
const data = await findQuery(filter, tx || db);
|
||||
return sqlNestRelationships({
|
||||
data,
|
||||
key: "id",
|
||||
parentMapper: (el) => ({
|
||||
...SecretRotationsSchema.parse(el),
|
||||
projectId: el.projectId,
|
||||
environment: { id: el.envId, name: el.envName, slug: el.envSlug }
|
||||
}),
|
||||
childrenMapper: [
|
||||
{
|
||||
key: "secId",
|
||||
label: "outputs",
|
||||
mapper: ({
|
||||
secId,
|
||||
outputKey,
|
||||
secVersion,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyCiphertext,
|
||||
secretValueTag,
|
||||
secretValueIV,
|
||||
secretValueCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretCommentCiphertext
|
||||
}) => ({
|
||||
key: outputKey,
|
||||
secret: {
|
||||
id: secId,
|
||||
version: secVersion,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyCiphertext,
|
||||
secretValueTag,
|
||||
secretValueIV,
|
||||
secretValueCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretCommentCiphertext
|
||||
}
|
||||
})
|
||||
}
|
||||
] as const
|
||||
});
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "SecretRotationFind" });
|
||||
}
|
||||
};
|
||||
|
||||
const findById = async (id: string, tx?: Knex) => {
|
||||
try {
|
||||
const doc = await (tx || db)(TableName.SecretRotation)
|
||||
.join(
|
||||
TableName.Environment,
|
||||
`${TableName.SecretRotation}.envId`,
|
||||
`${TableName.Environment}.id`
|
||||
)
|
||||
.where({ [`${TableName.SecretRotation}.id` as "id"]: id })
|
||||
.select(selectAllTableCols(TableName.SecretRotation))
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.Environment).as("envId"),
|
||||
db.ref("projectId").withSchema(TableName.Environment),
|
||||
db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
|
||||
db.ref("name").withSchema(TableName.Environment).as("envName")
|
||||
)
|
||||
.first();
|
||||
if (doc) {
|
||||
const { envName, envSlug, envId, ...el } = doc;
|
||||
return { ...el, envId, environment: { id: envId, slug: envSlug, name: envName } };
|
||||
}
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "SecretRotationFindById" });
|
||||
}
|
||||
};
|
||||
|
||||
const findRotationOutputsByRotationId = async (rotationId: string) =>
|
||||
secretRotationOutputOrm.find({ rotationId });
|
||||
|
||||
return {
|
||||
...secretRotationOrm,
|
||||
find,
|
||||
findById,
|
||||
secretOutputInsertMany: secretRotationOutputOrm.insertMany,
|
||||
findRotationOutputsByRotationId
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
export {
|
||||
DisableRotationErrors,
|
||||
secretRotationQueueFactory,
|
||||
TSecretRotationQueueFactory
|
||||
} from "./secret-rotation-queue";
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import axios from "axios";
|
||||
import jmespath from "jmespath";
|
||||
import knex from "knex";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import {
|
||||
TAssignOp,
|
||||
TDbProviderClients,
|
||||
TDirectAssignOp,
|
||||
THttpProviderFunction
|
||||
} from "../templates/types";
|
||||
import { TSecretRotationData, TSecretRotationDbFn } from "./secret-rotation-queue-types";
|
||||
|
||||
const REGEX = /\${([^}]+)}/g;
|
||||
const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
|
||||
|
||||
export const interpolate = (data: any, getValue: (key: string) => unknown) => {
|
||||
if (!data) return;
|
||||
|
||||
if (typeof data === "number") return data;
|
||||
|
||||
if (typeof data === "string") {
|
||||
return data.replace(REGEX, (_a, b) => getValue(b) as string);
|
||||
}
|
||||
|
||||
if (typeof data === "object" && Array.isArray(data)) {
|
||||
data.forEach((el, index) => {
|
||||
// eslint-disable-next-line
|
||||
data[index] = interpolate(el, getValue);
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof data === "object") {
|
||||
if ((data as { ref: string })?.ref) return getValue((data as { ref: string }).ref);
|
||||
const temp = data as Record<string, unknown>; // for converting ts object to record type
|
||||
Object.keys(temp).forEach((key) => {
|
||||
temp[key as keyof typeof temp] = interpolate(data[key as keyof typeof temp], getValue);
|
||||
});
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
const getInterpolationValue = (variables: TSecretRotationData) => (key: string) => {
|
||||
if (key.includes("|")) {
|
||||
const [keyword, ...arg] = key.split("|").map((el) => el.trim());
|
||||
switch (keyword) {
|
||||
case "random": {
|
||||
return alphaNumericNanoId(parseInt(arg[0], 10));
|
||||
}
|
||||
default: {
|
||||
throw Error(`Interpolation key not found - ${key}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
const [type, keyName] = key.split(".").map((el) => el.trim());
|
||||
return variables[type as keyof TSecretRotationData][keyName];
|
||||
};
|
||||
|
||||
export const secretRotationHttpFn = async (
|
||||
func: THttpProviderFunction,
|
||||
variables: TSecretRotationData
|
||||
) => {
|
||||
// string interpolation
|
||||
const headers = interpolate(func.header, getInterpolationValue(variables));
|
||||
const url = interpolate(func.url, getInterpolationValue(variables));
|
||||
const body = interpolate(func.body, getInterpolationValue(variables));
|
||||
// axios will automatically throw error if req status is not between 2xx range
|
||||
return axios({
|
||||
method: func.method,
|
||||
url,
|
||||
headers,
|
||||
data: body,
|
||||
timeout: EXTERNAL_REQUEST_TIMEOUT
|
||||
});
|
||||
};
|
||||
|
||||
export const secretRotationDbFn = async ({
|
||||
ca,
|
||||
host,
|
||||
port,
|
||||
query,
|
||||
database,
|
||||
password,
|
||||
username,
|
||||
client,
|
||||
variables
|
||||
}: TSecretRotationDbFn) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
const ssl = ca ? { rejectUnauthorized: false, ca } : undefined;
|
||||
if (host === "localhost" || host === "127.0.0.1" || appCfg.DB_CONNECTION_URI.includes(host))
|
||||
throw new Error("Invalid db host");
|
||||
|
||||
const db = knex({
|
||||
client,
|
||||
connection: {
|
||||
db: database,
|
||||
port,
|
||||
host,
|
||||
user: username,
|
||||
password,
|
||||
connectionTimeoutMillis: EXTERNAL_REQUEST_TIMEOUT,
|
||||
ssl,
|
||||
pool: { min: 0, max: 1 }
|
||||
}
|
||||
});
|
||||
const data = await db.raw(query, variables);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const secretRotationPreSetFn = (
|
||||
op: Record<string, TDirectAssignOp>,
|
||||
variables: TSecretRotationData
|
||||
) => {
|
||||
const getValFn = getInterpolationValue(variables);
|
||||
Object.entries(op || {}).forEach(([key, assignFn]) => {
|
||||
const [type, keyName] = key.split(".") as [keyof TSecretRotationData, string];
|
||||
variables[type][keyName] = interpolate(assignFn.value, getValFn);
|
||||
});
|
||||
};
|
||||
|
||||
export const secretRotationHttpSetFn = async (
|
||||
func: THttpProviderFunction,
|
||||
variables: TSecretRotationData
|
||||
) => {
|
||||
const getValFn = getInterpolationValue(variables);
|
||||
// http setter
|
||||
const res = await secretRotationHttpFn(func, variables);
|
||||
Object.entries(func.setter || {}).forEach(([key, assignFn]) => {
|
||||
const [type, keyName] = key.split(".") as [keyof TSecretRotationData, string];
|
||||
if (assignFn.assign === TAssignOp.JmesPath) {
|
||||
variables[type][keyName] = jmespath.search(res.data, assignFn.path);
|
||||
} else if (assignFn.value) {
|
||||
variables[type][keyName] = interpolate(assignFn.value, getValFn);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const getDbSetQuery = (
|
||||
db: TDbProviderClients,
|
||||
variable: { username: string; password: string }
|
||||
) => {
|
||||
if (db === TDbProviderClients.Pg) {
|
||||
return {
|
||||
query: "ALTER USER :username WITH PASSWORD :password",
|
||||
variable
|
||||
};
|
||||
}
|
||||
// add more based on client
|
||||
return {
|
||||
query: "ALTER USER :username IDENTIFIED BY :password",
|
||||
variable
|
||||
};
|
||||
};
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { TDbProviderClients } from "../templates/types";
|
||||
|
||||
export type TSecretRotationEncData = {
|
||||
inputs: Record<string, unknown>;
|
||||
creds: Array<{
|
||||
outputs: Record<string, unknown>;
|
||||
internal: Record<string, unknown>;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type TSecretRotationData = {
|
||||
inputs: Record<string, unknown>;
|
||||
outputs: Record<string, unknown>;
|
||||
internal: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type TSecretRotationDbFn = {
|
||||
client: TDbProviderClients;
|
||||
username: string;
|
||||
password: string;
|
||||
host: string;
|
||||
database: string;
|
||||
port: number;
|
||||
query: string;
|
||||
variables: Record<string, unknown>;
|
||||
ca?: string;
|
||||
};
|
||||
+245
@@ -0,0 +1,245 @@
|
||||
import { SecretKeyEncoding } from "@app/db/schemas";
|
||||
import {
|
||||
encryptSymmetric128BitHexKeyUTF8,
|
||||
infisicalSymmetricDecrypt,
|
||||
infisicalSymmetricEncypt
|
||||
} from "@app/lib/crypto/encryption";
|
||||
import { daysToMillisecond } from "@app/lib/dates";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||
import { TSecretDalFactory } from "@app/services/secret/secret-dal";
|
||||
import { TSecretVersionDalFactory } from "@app/services/secret/secret-version-dal";
|
||||
|
||||
import { TSecretRotationDalFactory } from "../secret-rotation-dal";
|
||||
import { rotationTemplates } from "../templates";
|
||||
import { TProviderFunctionTypes, TSecretRotationProviderTemplate } from "../templates/types";
|
||||
import {
|
||||
getDbSetQuery,
|
||||
secretRotationDbFn,
|
||||
secretRotationHttpFn,
|
||||
secretRotationHttpSetFn,
|
||||
secretRotationPreSetFn} from "./secret-rotation-queue-fn";
|
||||
import {
|
||||
TSecretRotationData,
|
||||
TSecretRotationDbFn,
|
||||
TSecretRotationEncData
|
||||
} from "./secret-rotation-queue-types";
|
||||
|
||||
export type TSecretRotationQueueFactory = ReturnType<typeof secretRotationQueueFactory>;
|
||||
|
||||
type TSecretRotationQueueFactoryDep = {
|
||||
queue: TQueueServiceFactory;
|
||||
secretRotationDal: TSecretRotationDalFactory;
|
||||
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
||||
secretDal: Pick<TSecretDalFactory, "bulkUpdate">;
|
||||
secretVersionDal: Pick<TSecretVersionDalFactory, "insertMany">;
|
||||
};
|
||||
|
||||
// These error should stop the repeatable job and ask user to reconfigure rotation
|
||||
export class DisableRotationErrors extends Error {
|
||||
name: string;
|
||||
|
||||
error: unknown;
|
||||
|
||||
constructor({ name, error, message }: { message: string; name?: string; error?: unknown }) {
|
||||
super(message);
|
||||
this.name = name || "DisableRotationErrors";
|
||||
this.error = error;
|
||||
}
|
||||
}
|
||||
|
||||
export const secretRotationQueueFactory = ({
|
||||
queue,
|
||||
secretRotationDal,
|
||||
projectBotService,
|
||||
secretDal,
|
||||
secretVersionDal
|
||||
}: TSecretRotationQueueFactoryDep) => {
|
||||
const addToQueue = async (rotationId: string, interval: number) =>
|
||||
queue.queue(
|
||||
QueueName.SecretRotation,
|
||||
QueueJobs.SecretRotation,
|
||||
{ rotationId },
|
||||
{ jobId: rotationId, repeat: { every: daysToMillisecond(interval), immediately: true } }
|
||||
);
|
||||
|
||||
const removeFromQueue = async (rotationId: string) =>
|
||||
queue.stopRepeatableJob(QueueName.SecretRotation, rotationId);
|
||||
|
||||
queue.start(QueueName.SecretRotation, async (job) => {
|
||||
const { rotationId } = job.data;
|
||||
logger.info(`secretRotationQueue.process: [rotationDocument=${rotationId}]`);
|
||||
const secretRotation = await secretRotationDal.findById(rotationId);
|
||||
const rotationProvider = rotationTemplates.find(
|
||||
({ name }) => name === secretRotation?.provider
|
||||
);
|
||||
|
||||
try {
|
||||
if (!rotationProvider || !secretRotation)
|
||||
throw new DisableRotationErrors({ message: "Provider not found" });
|
||||
|
||||
const rotationOutputs = await secretRotationDal.findRotationOutputsByRotationId(rotationId);
|
||||
if (!rotationOutputs.length)
|
||||
throw new DisableRotationErrors({ message: "Secrets not found" });
|
||||
|
||||
// deep copy
|
||||
const provider = JSON.parse(
|
||||
JSON.stringify(rotationProvider)
|
||||
) as TSecretRotationProviderTemplate;
|
||||
|
||||
// now get the encrypted variable values
|
||||
// in includes the inputs, the previous outputs
|
||||
// internal mapping variables etc
|
||||
const { encryptedDataTag, encryptedDataIV, encryptedData, keyEncoding } = secretRotation;
|
||||
if (!encryptedDataTag || !encryptedDataIV || !encryptedData || !keyEncoding) {
|
||||
throw new DisableRotationErrors({ message: "No inputs found" });
|
||||
}
|
||||
const decryptedData = infisicalSymmetricDecrypt({
|
||||
keyEncoding: keyEncoding as SecretKeyEncoding,
|
||||
ciphertext: encryptedData,
|
||||
iv: encryptedDataIV,
|
||||
tag: encryptedDataTag
|
||||
});
|
||||
|
||||
const variables = JSON.parse(decryptedData) as TSecretRotationEncData;
|
||||
// rotation set cycle
|
||||
const newCredential: TSecretRotationData = {
|
||||
inputs: variables.inputs,
|
||||
outputs: {},
|
||||
internal: {}
|
||||
};
|
||||
|
||||
// when its a database we keep cycling the variables accordingly
|
||||
if (provider.template.type === TProviderFunctionTypes.DB) {
|
||||
const lastCred = variables.creds.at(-1);
|
||||
if (lastCred && variables.creds.length === 1) {
|
||||
newCredential.internal.username =
|
||||
lastCred.internal.username === variables.inputs.username1
|
||||
? variables.inputs.username2
|
||||
: variables.inputs.username1;
|
||||
} else {
|
||||
newCredential.internal.username = lastCred
|
||||
? lastCred.internal.username
|
||||
: variables.inputs.username1;
|
||||
}
|
||||
// set a random value for new password
|
||||
newCredential.internal.rotated_password = alphaNumericNanoId(32);
|
||||
const { username, password, host, database, port, ca } = newCredential.inputs;
|
||||
const dbFunctionArg = {
|
||||
username,
|
||||
password,
|
||||
host,
|
||||
database,
|
||||
port,
|
||||
ca: ca as string,
|
||||
client: provider.template.client
|
||||
} as TSecretRotationDbFn;
|
||||
// set function
|
||||
await secretRotationDbFn({
|
||||
...dbFunctionArg,
|
||||
...getDbSetQuery(provider.template.client, {
|
||||
password: newCredential.internal.rotated_password as string,
|
||||
username: newCredential.internal.username as string
|
||||
})
|
||||
});
|
||||
// test function
|
||||
await secretRotationDbFn({
|
||||
...dbFunctionArg,
|
||||
query: "SELECT NOW()",
|
||||
variables: {}
|
||||
});
|
||||
// clean up
|
||||
if (variables.creds.length === 2) variables.creds.pop();
|
||||
}
|
||||
|
||||
if (provider.template.type === TProviderFunctionTypes.HTTP) {
|
||||
if (provider.template.functions.set?.pre) {
|
||||
secretRotationPreSetFn(provider.template.functions.set.pre, newCredential);
|
||||
}
|
||||
await secretRotationHttpSetFn(provider.template.functions.set, newCredential);
|
||||
// now test
|
||||
await secretRotationHttpFn(provider.template.functions.test, newCredential);
|
||||
if (variables.creds.length === 2) {
|
||||
const deleteCycleCred = variables.creds.pop();
|
||||
if (deleteCycleCred && provider.template.functions.remove) {
|
||||
const deleteCycleVar = { inputs: variables.inputs, ...deleteCycleCred };
|
||||
if (provider.template.type === TProviderFunctionTypes.HTTP) {
|
||||
await secretRotationHttpFn(provider.template.functions.remove, deleteCycleVar);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variables.creds.unshift({
|
||||
outputs: newCredential.outputs,
|
||||
internal: newCredential.internal
|
||||
});
|
||||
const encVarData = infisicalSymmetricEncypt(JSON.stringify(variables));
|
||||
const key = await projectBotService.getBotKey(secretRotation.projectId);
|
||||
const encryptedSecrets = rotationOutputs.map(({ key: outputKey, secretId }) => ({
|
||||
secretId,
|
||||
value: encryptSymmetric128BitHexKeyUTF8(
|
||||
typeof newCredential.outputs[outputKey] === "object"
|
||||
? JSON.stringify(newCredential.outputs[outputKey])
|
||||
: String(newCredential.outputs[outputKey]),
|
||||
key
|
||||
)
|
||||
}));
|
||||
await secretRotationDal.transaction(async (tx) => {
|
||||
await secretRotationDal.updateById(
|
||||
rotationId,
|
||||
{
|
||||
encryptedData: encVarData.ciphertext,
|
||||
encryptedDataIV: encVarData.iv,
|
||||
encryptedDataTag: encVarData.tag,
|
||||
keyEncoding: encVarData.encoding,
|
||||
algorithm: encVarData.algorithm,
|
||||
lastRotatedAt: new Date(),
|
||||
statusMessage: "Rotated successfull",
|
||||
status: "success"
|
||||
},
|
||||
tx
|
||||
);
|
||||
const updatedSecrets = await secretDal.bulkUpdate(
|
||||
encryptedSecrets.map(({ secretId, value }) => ({
|
||||
id: secretId,
|
||||
secretValueCiphertext: value.ciphertext,
|
||||
secretValueIV: value.iv,
|
||||
secretValueTag: value.tag
|
||||
})),
|
||||
tx
|
||||
);
|
||||
await secretDal.bulkUpdate(
|
||||
updatedSecrets.map(({ id, version }) => ({ id, version: (version || 0) + 1 })),
|
||||
tx
|
||||
);
|
||||
await secretVersionDal.insertMany(
|
||||
updatedSecrets.map(({ id, updatedAt, createdAt, ...el }) => ({
|
||||
...el,
|
||||
secretId: id
|
||||
})),
|
||||
tx
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof DisableRotationErrors) {
|
||||
if (job.id) {
|
||||
queue.stopRepeatableJob(QueueName.SecretRotation, job.id);
|
||||
}
|
||||
}
|
||||
|
||||
await secretRotationDal.updateById(rotationId, {
|
||||
status: "failed",
|
||||
statusMessage: (error as Error).message.slice(0, 500),
|
||||
lastRotatedAt: new Date()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
addToQueue,
|
||||
removeFromQueue
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,195 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import Ajv from "ajv";
|
||||
|
||||
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
import { TProjectEnvDalFactory } from "@app/services/project-env/project-env-dal";
|
||||
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
|
||||
import { TSecretRotationDalFactory } from "./secret-rotation-dal";
|
||||
import { TSecretRotationQueueFactory } from "./secret-rotation-queue";
|
||||
import { TSecretRotationEncData } from "./secret-rotation-queue/secret-rotation-queue-types";
|
||||
import {
|
||||
TCreateSecretRotationDTO,
|
||||
TDeleteDTO,
|
||||
TGetByIdDTO,
|
||||
TListByProjectIdDTO,
|
||||
TRestartDTO
|
||||
} from "./secret-rotation-types";
|
||||
import { rotationTemplates } from "./templates";
|
||||
|
||||
type TSecretRotationServiceFactoryDep = {
|
||||
secretRotationDal: TSecretRotationDalFactory;
|
||||
projectEnvDal: Pick<TProjectEnvDalFactory, "findOne">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
secretRotationQueue: TSecretRotationQueueFactory;
|
||||
};
|
||||
|
||||
export type TSecretRotationServiceFactory = ReturnType<typeof secretRotationServiceFactory>;
|
||||
|
||||
const ajv = new Ajv({ strict: false });
|
||||
export const secretRotationServiceFactory = ({
|
||||
secretRotationDal,
|
||||
permissionService,
|
||||
projectEnvDal,
|
||||
secretRotationQueue
|
||||
}: TSecretRotationServiceFactoryDep) => {
|
||||
const getProviderTemplates = async ({ actor, actorId, projectId }: TProjectPermission) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.SecretRotation
|
||||
);
|
||||
|
||||
return {
|
||||
custom: [],
|
||||
providers: rotationTemplates
|
||||
};
|
||||
};
|
||||
|
||||
const createRotation = async ({
|
||||
projectId,
|
||||
actorId,
|
||||
actor,
|
||||
inputs,
|
||||
outputs,
|
||||
interval,
|
||||
provider,
|
||||
secretPath,
|
||||
environment
|
||||
}: TCreateSecretRotationDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.SecretRotation
|
||||
);
|
||||
const env = await projectEnvDal.findOne({ slug: environment });
|
||||
if (!env) throw new BadRequestError({ message: "Environment not found" });
|
||||
|
||||
const selectedTemplate = rotationTemplates.find(({ name }) => name === provider);
|
||||
if (!selectedTemplate) throw new BadRequestError({ message: "Provider not found" });
|
||||
const formattedInputs: Record<string, unknown> = {};
|
||||
Object.entries(inputs).forEach(([key, value]) => {
|
||||
const { type } = selectedTemplate.template.inputs.properties[key];
|
||||
if (type === "string") {
|
||||
formattedInputs[key] = value;
|
||||
return;
|
||||
}
|
||||
if (type === "integer") {
|
||||
formattedInputs[key] = parseInt(value as string, 10);
|
||||
return;
|
||||
}
|
||||
formattedInputs[key] = JSON.parse(value as string);
|
||||
});
|
||||
// ensure input one follows the correct schema
|
||||
const valid = ajv.validate(selectedTemplate.template.inputs, formattedInputs);
|
||||
if (!valid) {
|
||||
throw new BadRequestError({ message: ajv.errors?.[0].message });
|
||||
}
|
||||
|
||||
const unencryptedData: Partial<TSecretRotationEncData> = {
|
||||
inputs: formattedInputs,
|
||||
creds: []
|
||||
};
|
||||
const encData = infisicalSymmetricEncypt(JSON.stringify(unencryptedData));
|
||||
const secretRotation = secretRotationDal.transaction(async (tx) => {
|
||||
const doc = await secretRotationDal.create(
|
||||
{
|
||||
provider,
|
||||
secretPath,
|
||||
interval,
|
||||
envId: env.id,
|
||||
encryptedDataTag: encData.tag,
|
||||
encryptedDataIV: encData.iv,
|
||||
encryptedData: encData.ciphertext,
|
||||
algorithm: encData.algorithm,
|
||||
keyEncoding: encData.encoding
|
||||
},
|
||||
tx
|
||||
);
|
||||
await secretRotationQueue.addToQueue(doc.id, doc.interval);
|
||||
const outputSecretMapping = await secretRotationDal.secretOutputInsertMany(
|
||||
Object.entries(outputs).map(([key, secretId]) => ({ key, secretId, rotationId: doc.id })),
|
||||
tx
|
||||
);
|
||||
return { ...doc, outputs: outputSecretMapping, environment: env };
|
||||
});
|
||||
return secretRotation;
|
||||
};
|
||||
|
||||
const getById = async ({ rotationId, actor, actorId }: TGetByIdDTO) => {
|
||||
const [doc] = await secretRotationDal.find({ id: rotationId });
|
||||
if (!doc) throw new BadRequestError({ message: "Rotation not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
doc.projectId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.SecretRotation
|
||||
);
|
||||
return doc;
|
||||
};
|
||||
|
||||
const getByProjectId = async ({ actorId, projectId, actor }: TListByProjectIdDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.SecretRotation
|
||||
);
|
||||
const doc = await secretRotationDal.find({ projectId });
|
||||
return doc;
|
||||
};
|
||||
|
||||
const restartById = async ({ actor, actorId, rotationId }: TRestartDTO) => {
|
||||
const doc = await secretRotationDal.findById(rotationId);
|
||||
if (!doc) throw new BadRequestError({ message: "Rotation not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
doc.projectId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionSub.SecretRotation
|
||||
);
|
||||
await secretRotationQueue.removeFromQueue(doc.id);
|
||||
await secretRotationQueue.addToQueue(doc.id, doc.interval);
|
||||
return doc;
|
||||
};
|
||||
|
||||
const deleteById = async ({ actor, actorId, rotationId }: TDeleteDTO) => {
|
||||
const doc = await secretRotationDal.findById(rotationId);
|
||||
if (!doc) throw new BadRequestError({ message: "Rotation not found" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
doc.projectId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSub.SecretRotation
|
||||
);
|
||||
const deletedDoc = await secretRotationDal.transaction(async (tx) => {
|
||||
const strat = await secretRotationDal.deleteById(rotationId, tx);
|
||||
await secretRotationQueue.removeFromQueue(strat.id);
|
||||
return strat;
|
||||
});
|
||||
return { ...doc, ...deletedDoc };
|
||||
};
|
||||
|
||||
return {
|
||||
getProviderTemplates,
|
||||
getById,
|
||||
getByProjectId,
|
||||
createRotation,
|
||||
restartById,
|
||||
deleteById
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
export type TCreateSecretRotationDTO = {
|
||||
secretPath: string;
|
||||
environment: string;
|
||||
interval: number;
|
||||
provider: string;
|
||||
inputs: Record<string, unknown>;
|
||||
outputs: Record<string, string>;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TListByProjectIdDTO = TProjectPermission;
|
||||
|
||||
export type TDeleteDTO = {
|
||||
rotationId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TRestartDTO = {
|
||||
rotationId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetByIdDTO = {
|
||||
rotationId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
@@ -0,0 +1,28 @@
|
||||
import { MYSQL_TEMPLATE } from "./mysql";
|
||||
import { POSTGRES_TEMPLATE } from "./postgres";
|
||||
import { SENDGRID_TEMPLATE } from "./sendgrid";
|
||||
import { TSecretRotationProviderTemplate } from "./types";
|
||||
|
||||
export const rotationTemplates: TSecretRotationProviderTemplate[] = [
|
||||
{
|
||||
name: "sendgrid",
|
||||
title: "Twilio Sendgrid",
|
||||
image: "sendgrid.png",
|
||||
description: "Rotate Twilio Sendgrid API keys",
|
||||
template: SENDGRID_TEMPLATE
|
||||
},
|
||||
{
|
||||
name: "postgres",
|
||||
title: "PostgreSQL",
|
||||
image: "postgres.png",
|
||||
description: "Rotate PostgreSQL/CockroachDB user credentials",
|
||||
template: POSTGRES_TEMPLATE
|
||||
},
|
||||
{
|
||||
name: "mysql",
|
||||
title: "MySQL",
|
||||
image: "mysql.png",
|
||||
description: "Rotate MySQL@7/MariaDB user credentials",
|
||||
template: MYSQL_TEMPLATE
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,41 @@
|
||||
import { TDbProviderClients, TProviderFunctionTypes } from "./types";
|
||||
|
||||
export const MYSQL_TEMPLATE = {
|
||||
type: TProviderFunctionTypes.DB as const,
|
||||
client: TDbProviderClients.MySql,
|
||||
inputs: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
admin_username: { type: "string" as const },
|
||||
admin_password: { type: "string" as const },
|
||||
host: { type: "string" as const },
|
||||
database: { type: "string" as const },
|
||||
port: { type: "integer" as const, default: "3306" },
|
||||
username1: {
|
||||
type: "string",
|
||||
default: "infisical-sql-user1",
|
||||
desc: "This user must be created in your database"
|
||||
},
|
||||
username2: {
|
||||
type: "string",
|
||||
default: "infisical-sql-user2",
|
||||
desc: "This user must be created in your database"
|
||||
},
|
||||
ca: { type: "string", desc: "SSL certificate for db auth(string)" }
|
||||
},
|
||||
required: [
|
||||
"admin_username",
|
||||
"admin_password",
|
||||
"host",
|
||||
"database",
|
||||
"username1",
|
||||
"username2",
|
||||
"port"
|
||||
],
|
||||
additionalProperties: false
|
||||
},
|
||||
outputs: {
|
||||
db_username: { type: "string" },
|
||||
db_password: { type: "string" }
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
import { TDbProviderClients, TProviderFunctionTypes } from "./types";
|
||||
|
||||
export const POSTGRES_TEMPLATE = {
|
||||
type: TProviderFunctionTypes.DB as const,
|
||||
client: TDbProviderClients.Pg as const,
|
||||
inputs: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
admin_username: { type: "string" as const },
|
||||
admin_password: { type: "string" as const },
|
||||
host: { type: "string" as const },
|
||||
database: { type: "string" as const },
|
||||
port: { type: "integer" as const, default: "5432" },
|
||||
username1: {
|
||||
type: "string",
|
||||
default: "infisical-pg-user1",
|
||||
desc: "This user must be created in your database"
|
||||
},
|
||||
username2: {
|
||||
type: "string",
|
||||
default: "infisical-pg-user2",
|
||||
desc: "This user must be created in your database"
|
||||
},
|
||||
ca: { type: "string", desc: "SSL certificate for db auth(string)" }
|
||||
},
|
||||
required: [
|
||||
"admin_username",
|
||||
"admin_password",
|
||||
"host",
|
||||
"database",
|
||||
"username1",
|
||||
"username2",
|
||||
"port"
|
||||
],
|
||||
additionalProperties: false
|
||||
},
|
||||
outputs: {
|
||||
db_username: { type: "string" },
|
||||
db_password: { type: "string" }
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
/* eslint-disable no-template-curly-in-string */
|
||||
import { TAssignOp, TProviderFunctionTypes } from "./types";
|
||||
|
||||
export const SENDGRID_TEMPLATE = {
|
||||
type: TProviderFunctionTypes.HTTP as const,
|
||||
inputs: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
admin_api_key: { type: "string" as const, desc: "Sendgrid admin api key to create new keys" },
|
||||
api_key_scopes: {
|
||||
type: "array",
|
||||
items: { type: "string" as const },
|
||||
desc: "Scopes for created tokens by rotation(Array)"
|
||||
}
|
||||
},
|
||||
required: ["admin_api_key", "api_key_scopes"],
|
||||
additionalProperties: false
|
||||
},
|
||||
outputs: {
|
||||
api_key: { type: "string" }
|
||||
},
|
||||
internal: {
|
||||
api_key_id: { type: "string" }
|
||||
},
|
||||
functions: {
|
||||
set: {
|
||||
url: "https://api.sendgrid.com/v3/api_keys",
|
||||
method: "POST",
|
||||
header: {
|
||||
Authorization: "Bearer ${inputs.admin_api_key}"
|
||||
},
|
||||
body: {
|
||||
name: "infisical-${random | 16}",
|
||||
scopes: { ref: "inputs.api_key_scopes" }
|
||||
},
|
||||
setter: {
|
||||
"outputs.api_key": {
|
||||
assign: TAssignOp.JmesPath as const,
|
||||
path: "api_key"
|
||||
},
|
||||
"internal.api_key_id": {
|
||||
assign: TAssignOp.JmesPath as const,
|
||||
path: "api_key_id"
|
||||
}
|
||||
}
|
||||
},
|
||||
remove: {
|
||||
url: "https://api.sendgrid.com/v3/api_keys/${internal.api_key_id}",
|
||||
header: {
|
||||
Authorization: "Bearer ${inputs.admin_api_key}"
|
||||
},
|
||||
method: "DELETE"
|
||||
},
|
||||
test: {
|
||||
url: "https://api.sendgrid.com/v3/api_keys/${internal.api_key_id}",
|
||||
header: {
|
||||
Authorization: "Bearer ${inputs.admin_api_key}"
|
||||
},
|
||||
method: "GET"
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
export enum TProviderFunctionTypes {
|
||||
HTTP = "http",
|
||||
DB = "database"
|
||||
}
|
||||
|
||||
export enum TDbProviderClients {
|
||||
// postgres, cockroack db, amazon red shift
|
||||
Pg = "pg",
|
||||
// mysql and maria db
|
||||
MySql = "mysql"
|
||||
}
|
||||
|
||||
export enum TAssignOp {
|
||||
Direct = "direct",
|
||||
JmesPath = "jmesopath"
|
||||
}
|
||||
|
||||
export type TJmesPathAssignOp = {
|
||||
assign: TAssignOp.JmesPath;
|
||||
path: string;
|
||||
};
|
||||
|
||||
export type TDirectAssignOp = {
|
||||
assign: TAssignOp.Direct;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type TAssignFunction = TJmesPathAssignOp | TDirectAssignOp;
|
||||
|
||||
export type THttpProviderFunction = {
|
||||
url: string;
|
||||
method: string;
|
||||
header?: Record<string, string>;
|
||||
query?: Record<string, string>;
|
||||
body?: Record<string, unknown>;
|
||||
setter?: Record<string, TAssignFunction>;
|
||||
pre?: Record<string, TDirectAssignOp>;
|
||||
};
|
||||
|
||||
export type TSecretRotationProviderTemplate = {
|
||||
name: string;
|
||||
title: string;
|
||||
image?: string;
|
||||
description?: string;
|
||||
template: THttpProviderTemplate | TDbProviderTemplate;
|
||||
};
|
||||
|
||||
export type THttpProviderTemplate = {
|
||||
type: TProviderFunctionTypes.HTTP;
|
||||
inputs: {
|
||||
type: "object";
|
||||
properties: Record<string, { type: string; [x: string]: unknown; desc?: string }>;
|
||||
required?: string[];
|
||||
};
|
||||
outputs: Record<string, unknown>;
|
||||
functions: {
|
||||
set: THttpProviderFunction;
|
||||
remove?: THttpProviderFunction;
|
||||
test: THttpProviderFunction;
|
||||
};
|
||||
};
|
||||
|
||||
export type TDbProviderTemplate = {
|
||||
type: TProviderFunctionTypes.DB;
|
||||
client: TDbProviderClients;
|
||||
inputs: {
|
||||
type: "object";
|
||||
properties: Record<string, { type: string; [x: string]: unknown; desc?: string }>;
|
||||
required?: string[];
|
||||
};
|
||||
outputs: Record<string, unknown>;
|
||||
};
|
||||
Reference in New Issue
Block a user