Merge branch 'main' into feat/point-in-time-revamp

This commit is contained in:
carlosmonastyrski
2025-05-26 14:42:55 -03:00
258 changed files with 4999 additions and 1499 deletions
+1
View File
@@ -112,6 +112,7 @@ import { TWorkflowIntegrationServiceFactory } from "@app/services/workflow-integ
declare module "@fastify/request-context" { declare module "@fastify/request-context" {
interface RequestContextData { interface RequestContextData {
reqId: string; reqId: string;
orgId?: string;
identityAuthInfo?: { identityAuthInfo?: {
identityId: string; identityId: string;
oidc?: { oidc?: {
@@ -0,0 +1,47 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasEmail = await knex.schema.hasColumn(TableName.Users, "email");
const hasUsername = await knex.schema.hasColumn(TableName.Users, "username");
if (hasEmail) {
await knex(TableName.Users)
.where({ isGhost: false })
.update({
// @ts-expect-error email assume string this is expected
email: knex.raw("lower(email)")
});
}
if (hasUsername) {
await knex.schema.raw(`
CREATE INDEX IF NOT EXISTS ${TableName.Users}_lower_username_idx
ON ${TableName.Users} (LOWER(username))
`);
const duplicatesSubquery = knex(TableName.Users)
.select(knex.raw("lower(username) as lowercase_username"))
.groupBy("lowercase_username")
.having(knex.raw("count(*)"), ">", 1);
// Update usernames to lowercase where they won't create duplicates
await knex(TableName.Users)
.where({ isGhost: false })
.whereRaw("username <> lower(username)") // Only update if not already lowercase
// @ts-expect-error username assume string this is expected
.whereNotIn(knex.raw("lower(username)"), duplicatesSubquery)
.update({
// @ts-expect-error username assume string this is expected
username: knex.raw("lower(username)")
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasUsername = await knex.schema.hasColumn(TableName.Users, "username");
if (hasUsername) {
await knex.schema.raw(`
DROP INDEX IF EXISTS ${TableName.Users}_lower_username_idx
`);
}
}
@@ -0,0 +1,22 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
await knex.schema.alterTable(TableName.SecretSync, (t) => {
t.string("name", 64).notNullable().alter();
});
await knex.schema.alterTable(TableName.ProjectTemplates, (t) => {
t.string("name", 64).notNullable().alter();
});
await knex.schema.alterTable(TableName.AppConnection, (t) => {
t.string("name", 64).notNullable().alter();
});
await knex.schema.alterTable(TableName.SecretRotationV2, (t) => {
t.string("name", 64).notNullable().alter();
});
}
export async function down(): Promise<void> {
// No down migration or it will error
}
@@ -154,7 +154,8 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
requestId: z.string().trim() requestId: z.string().trim()
}), }),
body: z.object({ body: z.object({
status: z.enum([ApprovalStatus.APPROVED, ApprovalStatus.REJECTED]) status: z.enum([ApprovalStatus.APPROVED, ApprovalStatus.REJECTED]),
bypassReason: z.string().min(10).max(1000).optional()
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -170,7 +171,8 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
requestId: req.params.requestId, requestId: req.params.requestId,
status: req.body.status status: req.body.status,
bypassReason: req.body.bypassReason
}); });
return { review }; return { review };
@@ -1,16 +1,16 @@
import z from "zod"; 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 { import {
CreateOCIConnectionSchema, CreateOCIConnectionSchema,
SanitizedOCIConnectionSchema, SanitizedOCIConnectionSchema,
UpdateOCIConnectionSchema UpdateOCIConnectionSchema
} from "@app/services/app-connection/oci"; } from "@app/ee/services/app-connections/oci";
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 { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints"; import { registerAppConnectionEndpoints } from "../../../../server/routes/v1/app-connection-routers/app-connection-endpoints";
export const registerOCIConnectionRouter = async (server: FastifyZodProvider) => { export const registerOCIConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({ registerAppConnectionEndpoints({
+1 -1
View File
@@ -145,7 +145,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
const { isUserCompleted, providerAuthToken } = await server.services.saml.samlLogin({ const { isUserCompleted, providerAuthToken } = await server.services.saml.samlLogin({
externalId: profile.nameID, externalId: profile.nameID,
email, email: email.toLowerCase(),
firstName, firstName,
lastName: lastName as string, lastName: lastName as string,
relayState: (req.body as { RelayState?: string }).RelayState, relayState: (req.body as { RelayState?: string }).RelayState,
@@ -2,11 +2,10 @@ import {
CreateOCIVaultSyncSchema, CreateOCIVaultSyncSchema,
OCIVaultSyncSchema, OCIVaultSyncSchema,
UpdateOCIVaultSyncSchema UpdateOCIVaultSyncSchema
} from "@app/services/secret-sync/oci-vault"; } from "@app/ee/services/secret-sync/oci-vault";
import { registerSyncSecretsEndpoints } from "@app/server/routes/v1/secret-sync-routers/secret-sync-endpoints";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
export const registerOCIVaultSyncRouter = async (server: FastifyZodProvider) => export const registerOCIVaultSyncRouter = async (server: FastifyZodProvider) =>
registerSyncSecretsEndpoints({ registerSyncSecretsEndpoints({
destination: SecretSync.OCIVault, destination: SecretSync.OCIVault,
@@ -2,7 +2,7 @@ import { ForbiddenError } from "@casl/ability";
import { ActionProjectType } from "@app/db/schemas"; import { ActionProjectType } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionApprovalActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal"; import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
@@ -98,7 +98,7 @@ export const accessApprovalPolicyServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create, ProjectPermissionApprovalActions.Create,
ProjectPermissionSub.SecretApproval ProjectPermissionSub.SecretApproval
); );
const env = await projectEnvDAL.findOne({ slug: environment, projectId: project.id }); const env = await projectEnvDAL.findOne({ slug: environment, projectId: project.id });
@@ -256,7 +256,10 @@ export const accessApprovalPolicyServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval); ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionApprovalActions.Edit,
ProjectPermissionSub.SecretApproval
);
const updatedPolicy = await accessApprovalPolicyDAL.transaction(async (tx) => { const updatedPolicy = await accessApprovalPolicyDAL.transaction(async (tx) => {
const doc = await accessApprovalPolicyDAL.updateById( const doc = await accessApprovalPolicyDAL.updateById(
@@ -341,7 +344,7 @@ export const accessApprovalPolicyServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete, ProjectPermissionApprovalActions.Delete,
ProjectPermissionSub.SecretApproval ProjectPermissionSub.SecretApproval
); );
@@ -432,7 +435,10 @@ export const accessApprovalPolicyServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionApprovalActions.Read,
ProjectPermissionSub.SecretApproval
);
return policy; return policy;
}; };
@@ -6,6 +6,7 @@ import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { ms } from "@app/lib/ms"; import { ms } from "@app/lib/ms";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { EnforcementLevel } from "@app/lib/types";
import { triggerWorkflowIntegrationNotification } from "@app/lib/workflow-integrations/trigger-notification"; import { triggerWorkflowIntegrationNotification } from "@app/lib/workflow-integrations/trigger-notification";
import { TriggerFeature } from "@app/lib/workflow-integrations/types"; import { TriggerFeature } from "@app/lib/workflow-integrations/types";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
@@ -22,6 +23,7 @@ import { TAccessApprovalPolicyApproverDALFactory } from "../access-approval-poli
import { TAccessApprovalPolicyDALFactory } from "../access-approval-policy/access-approval-policy-dal"; import { TAccessApprovalPolicyDALFactory } from "../access-approval-policy/access-approval-policy-dal";
import { TGroupDALFactory } from "../group/group-dal"; import { TGroupDALFactory } from "../group/group-dal";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service";
import { ProjectPermissionApprovalActions, ProjectPermissionSub } from "../permission/project-permission";
import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal"; import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal";
import { ProjectUserAdditionalPrivilegeTemporaryMode } from "../project-user-additional-privilege/project-user-additional-privilege-types"; import { ProjectUserAdditionalPrivilegeTemporaryMode } from "../project-user-additional-privilege/project-user-additional-privilege-types";
import { TAccessApprovalRequestDALFactory } from "./access-approval-request-dal"; import { TAccessApprovalRequestDALFactory } from "./access-approval-request-dal";
@@ -323,26 +325,22 @@ export const accessApprovalRequestServiceFactory = ({
status, status,
actorId, actorId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
bypassReason
}: TReviewAccessRequestDTO) => { }: TReviewAccessRequestDTO) => {
const accessApprovalRequest = await accessApprovalRequestDAL.findById(requestId); const accessApprovalRequest = await accessApprovalRequestDAL.findById(requestId);
if (!accessApprovalRequest) { if (!accessApprovalRequest) {
throw new NotFoundError({ message: `Secret approval request with ID '${requestId}' not found` }); throw new NotFoundError({ message: `Secret approval request with ID '${requestId}' not found` });
} }
const { policy } = accessApprovalRequest; const { policy, environment } = accessApprovalRequest;
if (policy.deletedAt) { if (policy.deletedAt) {
throw new BadRequestError({ throw new BadRequestError({
message: "The policy associated with this access request has been deleted." message: "The policy associated with this access request has been deleted."
}); });
} }
if (!policy.allowedSelfApprovals && actorId === accessApprovalRequest.requestedByUserId) {
throw new BadRequestError({
message: "Failed to review access approval request. Users are not authorized to review their own request."
});
}
const { membership, hasRole } = await permissionService.getProjectPermission({ const { membership, hasRole, permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
projectId: accessApprovalRequest.projectId, projectId: accessApprovalRequest.projectId,
@@ -355,6 +353,20 @@ export const accessApprovalRequestServiceFactory = ({
throw new ForbiddenRequestError({ message: "You are not a member of this project" }); throw new ForbiddenRequestError({ message: "You are not a member of this project" });
} }
const isSelfApproval = actorId === accessApprovalRequest.requestedByUserId;
const isSoftEnforcement = policy.enforcementLevel === EnforcementLevel.Soft;
const canBypassApproval = permission.can(
ProjectPermissionApprovalActions.AllowAccessBypass,
ProjectPermissionSub.SecretApproval
);
const cannotBypassUnderSoftEnforcement = !(isSoftEnforcement && canBypassApproval);
if (!policy.allowedSelfApprovals && isSelfApproval && cannotBypassUnderSoftEnforcement) {
throw new BadRequestError({
message: "Failed to review access approval request. Users are not authorized to review their own request."
});
}
if ( if (
!hasRole(ProjectMembershipRole.Admin) && !hasRole(ProjectMembershipRole.Admin) &&
accessApprovalRequest.requestedByUserId !== actorId && // The request wasn't made by the current user accessApprovalRequest.requestedByUserId !== actorId && // The request wasn't made by the current user
@@ -363,21 +375,49 @@ export const accessApprovalRequestServiceFactory = ({
throw new ForbiddenRequestError({ message: "You are not authorized to approve this request" }); throw new ForbiddenRequestError({ message: "You are not authorized to approve this request" });
} }
const project = await projectDAL.findById(accessApprovalRequest.projectId);
if (!project) {
throw new NotFoundError({ message: "The project associated with this access request was not found." });
}
const existingReviews = await accessApprovalRequestReviewerDAL.find({ requestId: accessApprovalRequest.id }); const existingReviews = await accessApprovalRequestReviewerDAL.find({ requestId: accessApprovalRequest.id });
if (existingReviews.some((review) => review.status === ApprovalStatus.REJECTED)) { if (existingReviews.some((review) => review.status === ApprovalStatus.REJECTED)) {
throw new BadRequestError({ message: "The request has already been rejected by another reviewer" }); throw new BadRequestError({ message: "The request has already been rejected by another reviewer" });
} }
const reviewStatus = await accessApprovalRequestReviewerDAL.transaction(async (tx) => { const reviewStatus = await accessApprovalRequestReviewerDAL.transaction(async (tx) => {
const review = await accessApprovalRequestReviewerDAL.findOne( const isBreakGlassApprovalAttempt =
policy.enforcementLevel === EnforcementLevel.Soft &&
actorId === accessApprovalRequest.requestedByUserId &&
status === ApprovalStatus.APPROVED;
let reviewForThisActorProcessing: {
id: string;
requestId: string;
reviewerUserId: string;
status: string;
createdAt: Date;
updatedAt: Date;
};
const existingReviewByActorInTx = await accessApprovalRequestReviewerDAL.findOne(
{ {
requestId: accessApprovalRequest.id, requestId: accessApprovalRequest.id,
reviewerUserId: actorId reviewerUserId: actorId
}, },
tx tx
); );
if (!review) {
const newReview = await accessApprovalRequestReviewerDAL.create( // Check if review exists for actor
if (existingReviewByActorInTx) {
// Check if breakglass re-approval
if (isBreakGlassApprovalAttempt && existingReviewByActorInTx.status === ApprovalStatus.APPROVED) {
reviewForThisActorProcessing = existingReviewByActorInTx;
} else {
throw new BadRequestError({ message: "You have already reviewed this request" });
}
} else {
reviewForThisActorProcessing = await accessApprovalRequestReviewerDAL.create(
{ {
status, status,
requestId: accessApprovalRequest.id, requestId: accessApprovalRequest.id,
@@ -385,19 +425,26 @@ export const accessApprovalRequestServiceFactory = ({
}, },
tx tx
); );
}
const allReviews = [...existingReviews, newReview]; const otherReviews = existingReviews.filter((er) => er.reviewerUserId !== actorId);
const allUniqueReviews = [...otherReviews, reviewForThisActorProcessing];
const approvedReviews = allReviews.filter((r) => r.status === ApprovalStatus.APPROVED); const approvedReviews = allUniqueReviews.filter((r) => r.status === ApprovalStatus.APPROVED);
const meetsStandardApprovalThreshold = approvedReviews.length >= policy.approvals;
// approvals is the required number of approvals. If the number of approved reviews is equal to the number of required approvals, then the request is approved. if (
if (approvedReviews.length === policy.approvals) { reviewForThisActorProcessing.status === ApprovalStatus.APPROVED &&
(meetsStandardApprovalThreshold || isBreakGlassApprovalAttempt)
) {
const currentRequestState = await accessApprovalRequestDAL.findById(accessApprovalRequest.id, tx);
let privilegeIdToSet = currentRequestState?.privilegeId || null;
if (!privilegeIdToSet) {
if (accessApprovalRequest.isTemporary && !accessApprovalRequest.temporaryRange) { if (accessApprovalRequest.isTemporary && !accessApprovalRequest.temporaryRange) {
throw new BadRequestError({ message: "Temporary range is required for temporary access" }); throw new BadRequestError({ message: "Temporary range is required for temporary access" });
} }
let privilegeId: string | null = null;
if (!accessApprovalRequest.isTemporary && !accessApprovalRequest.temporaryRange) { if (!accessApprovalRequest.isTemporary && !accessApprovalRequest.temporaryRange) {
// Permanent access // Permanent access
const privilege = await additionalPrivilegeDAL.create( const privilege = await additionalPrivilegeDAL.create(
@@ -409,7 +456,7 @@ export const accessApprovalRequestServiceFactory = ({
}, },
tx tx
); );
privilegeId = privilege.id; privilegeIdToSet = privilege.id;
} else { } else {
// Temporary access // Temporary access
const relativeTempAllocatedTimeInMs = ms(accessApprovalRequest.temporaryRange!); const relativeTempAllocatedTimeInMs = ms(accessApprovalRequest.temporaryRange!);
@@ -421,23 +468,57 @@ export const accessApprovalRequestServiceFactory = ({
projectId: accessApprovalRequest.projectId, projectId: accessApprovalRequest.projectId,
slug: `requested-privilege-${slugify(alphaNumericNanoId(12))}`, slug: `requested-privilege-${slugify(alphaNumericNanoId(12))}`,
permissions: JSON.stringify(accessApprovalRequest.permissions), permissions: JSON.stringify(accessApprovalRequest.permissions),
isTemporary: true, isTemporary: true, // Explicitly set to true for the privilege
temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative, temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative,
temporaryRange: accessApprovalRequest.temporaryRange!, temporaryRange: accessApprovalRequest.temporaryRange!,
temporaryAccessStartTime: startTime, temporaryAccessStartTime: startTime,
temporaryAccessEndTime: new Date(new Date(startTime).getTime() + relativeTempAllocatedTimeInMs) temporaryAccessEndTime: new Date(startTime.getTime() + relativeTempAllocatedTimeInMs)
}, },
tx tx
); );
privilegeId = privilege.id; privilegeIdToSet = privilege.id;
} }
await accessApprovalRequestDAL.updateById(accessApprovalRequest.id, { privilegeId: privilegeIdToSet }, tx);
await accessApprovalRequestDAL.updateById(accessApprovalRequest.id, { privilegeId }, tx);
} }
return newReview;
} }
throw new BadRequestError({ message: "You have already reviewed this request" });
// Send notification if this was a breakglass approval
if (isBreakGlassApprovalAttempt) {
const cfg = getConfig();
const actingUser = await userDAL.findById(actorId, tx);
if (actingUser) {
const policyApproverUserIds = policy.approvers
.map((ap) => ap.userId)
.filter((id): id is string => typeof id === "string");
if (policyApproverUserIds.length > 0) {
const approverUsersForEmail = await userDAL.find({ $in: { id: policyApproverUserIds } }, { tx });
const recipientEmails = approverUsersForEmail
.map((appUser) => appUser.email)
.filter((email): email is string => !!email);
if (recipientEmails.length > 0) {
await smtpService.sendMail({
recipients: recipientEmails,
subjectLine: "Infisical Secret Access Policy Bypassed",
substitutions: {
projectName: project.name,
requesterFullName: `${actingUser.firstName} ${actingUser.lastName}`,
requesterEmail: actingUser.email,
bypassReason: bypassReason || "No reason provided",
secretPath: policy.secretPath || "/",
environment,
approvalUrl: `${cfg.SITE_URL}/secret-manager/${project.id}/approval`,
requestType: "access"
},
template: SmtpTemplates.AccessSecretRequestBypassed
});
}
}
}
}
return reviewForThisActorProcessing;
}); });
return reviewStatus; return reviewStatus;
@@ -17,6 +17,8 @@ export type TGetAccessRequestCountDTO = {
export type TReviewAccessRequestDTO = { export type TReviewAccessRequestDTO = {
requestId: string; requestId: string;
status: ApprovalStatus; status: ApprovalStatus;
envName?: string;
bypassReason?: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TCreateAccessApprovalRequestDTO = { export type TCreateAccessApprovalRequestDTO = {
@@ -1,7 +1,9 @@
import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { OrgServiceActor } from "@app/lib/types"; import { OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums"; import { AppConnection } from "../../../../services/app-connection/app-connection-enums";
import { TLicenseServiceFactory } from "../../license/license-service";
import { listOCICompartments, listOCIVaultKeys, listOCIVaults } from "./oci-connection-fns"; import { listOCICompartments, listOCIVaultKeys, listOCIVaults } from "./oci-connection-fns";
import { TOCIConnection } from "./oci-connection-types"; import { TOCIConnection } from "./oci-connection-types";
@@ -22,8 +24,23 @@ type TListOCIVaultKeysDTO = {
vaultOcid: string; vaultOcid: string;
}; };
export const ociConnectionService = (getAppConnection: TGetAppConnectionFunc) => { // Enterprise check
export const checkPlan = async (licenseService: Pick<TLicenseServiceFactory, "getPlan">, orgId: string) => {
const plan = await licenseService.getPlan(orgId);
if (!plan.enterpriseAppConnections)
throw new BadRequestError({
message:
"Failed to use app connection due to plan restriction. Upgrade plan to access enterprise app connections."
});
};
export const ociConnectionService = (
getAppConnection: TGetAppConnectionFunc,
licenseService: Pick<TLicenseServiceFactory, "getPlan">
) => {
const listCompartments = async (connectionId: string, actor: OrgServiceActor) => { const listCompartments = async (connectionId: string, actor: OrgServiceActor) => {
await checkPlan(licenseService, actor.orgId);
const appConnection = await getAppConnection(AppConnection.OCI, connectionId, actor); const appConnection = await getAppConnection(AppConnection.OCI, connectionId, actor);
try { try {
@@ -36,6 +53,8 @@ export const ociConnectionService = (getAppConnection: TGetAppConnectionFunc) =>
}; };
const listVaults = async ({ connectionId, compartmentOcid }: TListOCIVaultsDTO, actor: OrgServiceActor) => { const listVaults = async ({ connectionId, compartmentOcid }: TListOCIVaultsDTO, actor: OrgServiceActor) => {
await checkPlan(licenseService, actor.orgId);
const appConnection = await getAppConnection(AppConnection.OCI, connectionId, actor); const appConnection = await getAppConnection(AppConnection.OCI, connectionId, actor);
try { try {
@@ -51,6 +70,8 @@ export const ociConnectionService = (getAppConnection: TGetAppConnectionFunc) =>
{ connectionId, compartmentOcid, vaultOcid }: TListOCIVaultKeysDTO, { connectionId, compartmentOcid, vaultOcid }: TListOCIVaultKeysDTO,
actor: OrgServiceActor actor: OrgServiceActor
) => { ) => {
await checkPlan(licenseService, actor.orgId);
const appConnection = await getAppConnection(AppConnection.OCI, connectionId, actor); const appConnection = await getAppConnection(AppConnection.OCI, connectionId, actor);
try { try {
@@ -2,7 +2,7 @@ import z from "zod";
import { DiscriminativePick } from "@app/lib/types"; import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums"; import { AppConnection } from "../../../../services/app-connection/app-connection-enums";
import { import {
CreateOCIConnectionSchema, CreateOCIConnectionSchema,
OCIConnectionSchema, OCIConnectionSchema,
@@ -1,3 +1,4 @@
import { ProjectType } from "@app/db/schemas";
import { import {
TCreateProjectTemplateDTO, TCreateProjectTemplateDTO,
TUpdateProjectTemplateDTO TUpdateProjectTemplateDTO
@@ -315,7 +316,6 @@ export enum EventType {
CREATE_PROJECT_TEMPLATE = "create-project-template", CREATE_PROJECT_TEMPLATE = "create-project-template",
UPDATE_PROJECT_TEMPLATE = "update-project-template", UPDATE_PROJECT_TEMPLATE = "update-project-template",
DELETE_PROJECT_TEMPLATE = "delete-project-template", DELETE_PROJECT_TEMPLATE = "delete-project-template",
APPLY_PROJECT_TEMPLATE = "apply-project-template",
GET_APP_CONNECTIONS = "get-app-connections", GET_APP_CONNECTIONS = "get-app-connections",
GET_AVAILABLE_APP_CONNECTIONS_DETAILS = "get-available-app-connections-details", GET_AVAILABLE_APP_CONNECTIONS_DETAILS = "get-available-app-connections-details",
GET_APP_CONNECTION = "get-app-connection", GET_APP_CONNECTION = "get-app-connection",
@@ -383,7 +383,13 @@ export enum EventType {
PIT_ROLLBACK_COMMIT = "pit-rollback-commit", PIT_ROLLBACK_COMMIT = "pit-rollback-commit",
PIT_REVERT_COMMIT = "pit-revert-commit", PIT_REVERT_COMMIT = "pit-revert-commit",
PIT_GET_FOLDER_STATE = "pit-get-folder-state", PIT_GET_FOLDER_STATE = "pit-get-folder-state",
PIT_COMPARE_FOLDER_STATES = "pit-compare-folder-states" PIT_COMPARE_FOLDER_STATES = "pit-compare-folder-states",
UPDATE_ORG = "update-org",
CREATE_PROJECT = "create-project",
UPDATE_PROJECT = "update-project",
DELETE_PROJECT = "delete-project"
} }
export const filterableSecretEvents: EventType[] = [ export const filterableSecretEvents: EventType[] = [
@@ -2459,14 +2465,6 @@ interface DeleteProjectTemplateEvent {
}; };
} }
interface ApplyProjectTemplateEvent {
type: EventType.APPLY_PROJECT_TEMPLATE;
metadata: {
template: string;
projectId: string;
};
}
interface GetAppConnectionsEvent { interface GetAppConnectionsEvent {
type: EventType.GET_APP_CONNECTIONS; type: EventType.GET_APP_CONNECTIONS;
metadata: { metadata: {
@@ -2990,6 +2988,59 @@ interface PitCompareFolderStatesEvent {
diffsCount: string; diffsCount: string;
environment: string; environment: string;
folderPath: string; folderPath: string;
}
}
interface OrgUpdateEvent {
type: EventType.UPDATE_ORG;
metadata: {
name?: string;
slug?: string;
authEnforced?: boolean;
scimEnabled?: boolean;
defaultMembershipRoleSlug?: string;
enforceMfa?: boolean;
selectedMfaMethod?: string;
allowSecretSharingOutsideOrganization?: boolean;
bypassOrgAuthEnabled?: boolean;
userTokenExpiration?: string;
secretsProductEnabled?: boolean;
pkiProductEnabled?: boolean;
kmsProductEnabled?: boolean;
sshProductEnabled?: boolean;
scannerProductEnabled?: boolean;
shareSecretsProductEnabled?: boolean;
};
}
interface ProjectCreateEvent {
type: EventType.CREATE_PROJECT;
metadata: {
name: string;
slug?: string;
type: ProjectType;
};
}
interface ProjectUpdateEvent {
type: EventType.UPDATE_PROJECT;
metadata: {
name?: string;
description?: string;
autoCapitalization?: boolean;
hasDeleteProtection?: boolean;
slug?: string;
secretSharing?: boolean;
pitVersionLimit?: number;
auditLogsRetentionDays?: number;
};
}
interface ProjectDeleteEvent {
type: EventType.DELETE_PROJECT;
metadata: {
id: string;
name: string;
}; };
} }
@@ -3197,7 +3248,6 @@ export type Event =
| CreateProjectTemplateEvent | CreateProjectTemplateEvent
| UpdateProjectTemplateEvent | UpdateProjectTemplateEvent
| DeleteProjectTemplateEvent | DeleteProjectTemplateEvent
| ApplyProjectTemplateEvent
| GetAppConnectionsEvent | GetAppConnectionsEvent
| GetAvailableAppConnectionsDetailsEvent | GetAvailableAppConnectionsDetailsEvent
| GetAppConnectionEvent | GetAppConnectionEvent
@@ -3266,4 +3316,8 @@ export type Event =
| GetProjectPitCommitCountEvent | GetProjectPitCommitCountEvent
| PitRevertCommitEvent | PitRevertCommitEvent
| PitCompareFolderStatesEvent | PitCompareFolderStatesEvent
| PitGetFolderStateEvent; | PitGetFolderStateEvent
| OrgUpdateEvent
| ProjectCreateEvent
| ProjectUpdateEvent
| ProjectDeleteEvent;
+2 -2
View File
@@ -111,9 +111,9 @@ export const groupDALFactory = (db: TDbClient) => {
} }
if (search) { if (search) {
void query.andWhereRaw(`CONCAT_WS(' ', "firstName", "lastName", "username") ilike ?`, [`%${search}%`]); void query.andWhereRaw(`CONCAT_WS(' ', "firstName", "lastName", lower("username")) ilike ?`, [`%${search}%`]);
} else if (username) { } else if (username) {
void query.andWhere(`${TableName.Users}.username`, "ilike", `%${username}%`); void query.andWhereRaw(`lower("${TableName.Users}"."username") ilike ?`, `%${username}%`);
} }
switch (filter) { switch (filter) {
@@ -30,7 +30,7 @@ import {
import { TUserGroupMembershipDALFactory } from "./user-group-membership-dal"; import { TUserGroupMembershipDALFactory } from "./user-group-membership-dal";
type TGroupServiceFactoryDep = { type TGroupServiceFactoryDep = {
userDAL: Pick<TUserDALFactory, "find" | "findUserEncKeyByUserIdsBatch" | "transaction" | "findOne">; userDAL: Pick<TUserDALFactory, "find" | "findUserEncKeyByUserIdsBatch" | "transaction" | "findUserByUsername">;
groupDAL: Pick< groupDAL: Pick<
TGroupDALFactory, TGroupDALFactory,
"create" | "findOne" | "update" | "delete" | "findAllGroupPossibleMembers" | "findById" | "transaction" "create" | "findOne" | "update" | "delete" | "findAllGroupPossibleMembers" | "findById" | "transaction"
@@ -380,7 +380,10 @@ export const groupServiceFactory = ({
details: { missingPermissions: permissionBoundary.missingPermissions } details: { missingPermissions: permissionBoundary.missingPermissions }
}); });
const user = await userDAL.findOne({ username }); const usersWithUsername = await userDAL.findUserByUsername(username);
// akhilmhdh: case sensitive email resolution
const user =
usersWithUsername?.length > 1 ? usersWithUsername.find((el) => el.username === username) : usersWithUsername?.[0];
if (!user) throw new NotFoundError({ message: `Failed to find user with username ${username}` }); if (!user) throw new NotFoundError({ message: `Failed to find user with username ${username}` });
const users = await addUsersToGroupByUserIds({ const users = await addUsersToGroupByUserIds({
@@ -461,7 +464,10 @@ export const groupServiceFactory = ({
details: { missingPermissions: permissionBoundary.missingPermissions } details: { missingPermissions: permissionBoundary.missingPermissions }
}); });
const user = await userDAL.findOne({ username }); const usersWithUsername = await userDAL.findUserByUsername(username);
// akhilmhdh: case sensitive email resolution
const user =
usersWithUsername?.length > 1 ? usersWithUsername.find((el) => el.username === username) : usersWithUsername?.[0];
if (!user) throw new NotFoundError({ message: `Failed to find user with username ${username}` }); if (!user) throw new NotFoundError({ message: `Failed to find user with username ${username}` });
const users = await removeUsersFromGroupByUserIds({ const users = await removeUsersFromGroupByUserIds({
@@ -380,7 +380,7 @@ export const ldapConfigServiceFactory = ({
if (serverCfg.trustLdapEmails) { if (serverCfg.trustLdapEmails) {
newUser = await userDAL.findOne( newUser = await userDAL.findOne(
{ {
email, email: email.toLowerCase(),
isEmailVerified: true isEmailVerified: true
}, },
tx tx
@@ -391,8 +391,8 @@ export const ldapConfigServiceFactory = ({
const uniqueUsername = await normalizeUsername(username, userDAL); const uniqueUsername = await normalizeUsername(username, userDAL);
newUser = await userDAL.create( newUser = await userDAL.create(
{ {
username: serverCfg.trustLdapEmails ? email : uniqueUsername, username: serverCfg.trustLdapEmails ? email.toLowerCase() : uniqueUsername,
email, email: email.toLowerCase(),
isEmailVerified: serverCfg.trustLdapEmails, isEmailVerified: serverCfg.trustLdapEmails,
firstName, firstName,
lastName, lastName,
@@ -429,7 +429,7 @@ export const ldapConfigServiceFactory = ({
await orgMembershipDAL.create( await orgMembershipDAL.create(
{ {
userId: newUser.id, userId: newUser.id,
inviteEmail: email, inviteEmail: email.toLowerCase(),
orgId, orgId,
role, role,
roleId, roleId,
@@ -29,7 +29,9 @@ export const getDefaultOnPremFeatures = () => {
secretApproval: true, secretApproval: true,
secretRotation: true, secretRotation: true,
caCrl: false, caCrl: false,
sshHostGroups: false sshHostGroups: false,
enterpriseSecretSyncs: false,
enterpriseAppConnections: false
}; };
}; };
@@ -19,7 +19,7 @@ export const licenseDALFactory = (db: TDbClient) => {
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
.where(`${TableName.Users}.isGhost`, false) .where(`${TableName.Users}.isGhost`, false)
.count(); .count();
return Number(doc?.[0].count); return Number(doc?.[0]?.count ?? 0);
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "Count of Org Members" }); throw new DatabaseError({ error, name: "Count of Org Members" });
} }
@@ -2,6 +2,7 @@ import axios, { AxiosError } from "axios";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request"; import { request } from "@app/lib/config/request";
import { logger } from "@app/lib/logger";
import { TFeatureSet } from "./license-types"; import { TFeatureSet } from "./license-types";
@@ -54,7 +55,9 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
projectTemplates: false, projectTemplates: false,
kmip: false, kmip: false,
gateway: false, gateway: false,
sshHostGroups: false sshHostGroups: false,
enterpriseSecretSyncs: false,
enterpriseAppConnections: false
}); });
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => { export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
@@ -98,9 +101,10 @@ export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string
(response) => response, (response) => response,
async (err) => { async (err) => {
const originalRequest = (err as AxiosError).config; const originalRequest = (err as AxiosError).config;
const errStatusCode = Number((err as AxiosError)?.response?.status);
logger.error((err as AxiosError)?.response?.data, "License server call error");
// eslint-disable-next-line // eslint-disable-next-line
if ((err as AxiosError)?.response?.status === 401 && !(originalRequest as any)._retry) { if ((errStatusCode === 401 || errStatusCode === 403) && !(originalRequest as any)._retry) {
// eslint-disable-next-line // eslint-disable-next-line
(originalRequest as any)._retry = true; // injected (originalRequest as any)._retry = true; // injected
@@ -92,6 +92,10 @@ export const licenseServiceFactory = ({
const { const {
data: { currentPlan } data: { currentPlan }
} = await licenseServerOnPremApi.request.get<{ currentPlan: TFeatureSet }>("/api/license/v1/plan"); } = await licenseServerOnPremApi.request.get<{ currentPlan: TFeatureSet }>("/api/license/v1/plan");
const workspacesUsed = await projectDAL.countOfOrgProjects(null);
currentPlan.workspacesUsed = workspacesUsed;
onPremFeatures = currentPlan; onPremFeatures = currentPlan;
logger.info("Successfully synchronized license key features"); logger.info("Successfully synchronized license key features");
} catch (error) { } catch (error) {
@@ -185,6 +189,14 @@ export const licenseServiceFactory = ({
} = await licenseServerCloudApi.request.get<{ currentPlan: TFeatureSet }>( } = await licenseServerCloudApi.request.get<{ currentPlan: TFeatureSet }>(
`/api/license-server/v1/customers/${org.customerId}/cloud-plan` `/api/license-server/v1/customers/${org.customerId}/cloud-plan`
); );
const workspacesUsed = await projectDAL.countOfOrgProjects(orgId);
currentPlan.workspacesUsed = workspacesUsed;
const membersUsed = await licenseDAL.countOfOrgMembers(orgId);
currentPlan.membersUsed = membersUsed;
const identityUsed = await licenseDAL.countOrgUsersAndIdentities(orgId);
currentPlan.identitiesUsed = identityUsed;
await keyStore.setItemWithExpiry( await keyStore.setItemWithExpiry(
FEATURE_CACHE_KEY(org.id), FEATURE_CACHE_KEY(org.id),
LICENSE_SERVER_CLOUD_PLAN_TTL, LICENSE_SERVER_CLOUD_PLAN_TTL,
@@ -348,8 +360,8 @@ export const licenseServiceFactory = ({
} = await licenseServerCloudApi.request.post( } = await licenseServerCloudApi.request.post(
`/api/license-server/v1/customers/${organization.customerId}/billing-details/payment-methods`, `/api/license-server/v1/customers/${organization.customerId}/billing-details/payment-methods`,
{ {
success_url: `${appCfg.SITE_URL}/dashboard`, success_url: `${appCfg.SITE_URL}/organization/billing`,
cancel_url: `${appCfg.SITE_URL}/dashboard` cancel_url: `${appCfg.SITE_URL}/organization/billing`
} }
); );
@@ -362,7 +374,7 @@ export const licenseServiceFactory = ({
} = await licenseServerCloudApi.request.post( } = await licenseServerCloudApi.request.post(
`/api/license-server/v1/customers/${organization.customerId}/billing-details/billing-portal`, `/api/license-server/v1/customers/${organization.customerId}/billing-details/billing-portal`,
{ {
return_url: `${appCfg.SITE_URL}/dashboard` return_url: `${appCfg.SITE_URL}/organization/billing`
} }
); );
@@ -379,7 +391,7 @@ export const licenseServiceFactory = ({
message: `Organization with ID '${orgId}' not found` message: `Organization with ID '${orgId}' not found`
}); });
} }
if (instanceType !== InstanceType.OnPrem && instanceType !== InstanceType.EnterpriseOnPremOffline) { if (instanceType === InstanceType.Cloud) {
const { data } = await licenseServerCloudApi.request.get( const { data } = await licenseServerCloudApi.request.get(
`/api/license-server/v1/customers/${organization.customerId}/cloud-plan/billing` `/api/license-server/v1/customers/${organization.customerId}/cloud-plan/billing`
); );
@@ -407,11 +419,38 @@ export const licenseServiceFactory = ({
message: `Organization with ID '${orgId}' not found` message: `Organization with ID '${orgId}' not found`
}); });
} }
if (instanceType !== InstanceType.OnPrem && instanceType !== InstanceType.EnterpriseOnPremOffline) {
const { data } = await licenseServerCloudApi.request.get( const orgMembersUsed = await orgDAL.countAllOrgMembers(orgId);
`/api/license-server/v1/customers/${organization.customerId}/cloud-plan/table` const identityUsed = await identityOrgMembershipDAL.countAllOrgIdentities({ orgId });
); const projects = await projectDAL.find({ orgId });
return data; const projectCount = projects.length;
if (instanceType === InstanceType.Cloud) {
const { data } = await licenseServerCloudApi.request.get<{
head: { name: string }[];
rows: { name: string; allowed: boolean }[];
}>(`/api/license-server/v1/customers/${organization.customerId}/cloud-plan/table`);
const formattedData = {
head: data.head,
rows: data.rows.map((el) => {
let used = "-";
if (el.name === BillingPlanRows.MemberLimit.name) {
used = orgMembersUsed.toString();
} else if (el.name === BillingPlanRows.WorkspaceLimit.name) {
used = projectCount.toString();
} else if (el.name === BillingPlanRows.IdentityLimit.name) {
used = (identityUsed + orgMembersUsed).toString();
}
return {
...el,
used
};
})
};
return formattedData;
} }
const mappedRows = await Promise.all( const mappedRows = await Promise.all(
@@ -420,14 +459,11 @@ export const licenseServiceFactory = ({
let used = "-"; let used = "-";
if (field === BillingPlanRows.MemberLimit.field) { if (field === BillingPlanRows.MemberLimit.field) {
const orgMemberships = await orgDAL.countAllOrgMembers(orgId); used = orgMembersUsed.toString();
used = orgMemberships.toString();
} else if (field === BillingPlanRows.WorkspaceLimit.field) { } else if (field === BillingPlanRows.WorkspaceLimit.field) {
const projects = await projectDAL.find({ orgId }); used = projectCount.toString();
used = projects.length.toString();
} else if (field === BillingPlanRows.IdentityLimit.field) { } else if (field === BillingPlanRows.IdentityLimit.field) {
const identities = await identityOrgMembershipDAL.countAllOrgIdentities({ orgId }); used = identityUsed.toString();
used = identities.toString();
} }
return { return {
@@ -27,7 +27,7 @@ export type TFeatureSet = {
slug: null; slug: null;
tier: -1; tier: -1;
workspaceLimit: null; workspaceLimit: null;
workspacesUsed: 0; workspacesUsed: number;
dynamicSecret: false; dynamicSecret: false;
memberLimit: null; memberLimit: null;
membersUsed: number; membersUsed: number;
@@ -72,6 +72,8 @@ export type TFeatureSet = {
kmip: false; kmip: false;
gateway: false; gateway: false;
sshHostGroups: false; sshHostGroups: false;
enterpriseSecretSyncs: false;
enterpriseAppConnections: false;
}; };
export type TOrgPlansTableDTO = { export type TOrgPlansTableDTO = {
@@ -171,8 +171,8 @@ export const oidcConfigServiceFactory = ({
}; };
const oidcLogin = async ({ const oidcLogin = async ({
externalId,
email, email,
externalId,
firstName, firstName,
lastName, lastName,
orgId, orgId,
@@ -717,7 +717,7 @@ export const oidcConfigServiceFactory = ({
const groups = typeof claims.groups === "string" ? [claims.groups] : (claims.groups as string[] | undefined); const groups = typeof claims.groups === "string" ? [claims.groups] : (claims.groups as string[] | undefined);
oidcLogin({ oidcLogin({
email: claims.email, email: claims.email.toLowerCase(),
externalId: claims.sub, externalId: claims.sub,
firstName: claims.given_name ?? "", firstName: claims.given_name ?? "",
lastName: claims.family_name ?? "", lastName: claims.family_name ?? "",
@@ -2,6 +2,7 @@ import { AbilityBuilder, createMongoAbility, MongoAbility } from "@casl/ability"
import { import {
ProjectPermissionActions, ProjectPermissionActions,
ProjectPermissionApprovalActions,
ProjectPermissionCertificateActions, ProjectPermissionCertificateActions,
ProjectPermissionCmekActions, ProjectPermissionCmekActions,
ProjectPermissionCommitsActions, ProjectPermissionCommitsActions,
@@ -26,7 +27,6 @@ const buildAdminPermissionRules = () => {
[ [
ProjectPermissionSub.SecretFolders, ProjectPermissionSub.SecretFolders,
ProjectPermissionSub.SecretImports, ProjectPermissionSub.SecretImports,
ProjectPermissionSub.SecretApproval,
ProjectPermissionSub.Role, ProjectPermissionSub.Role,
ProjectPermissionSub.Integrations, ProjectPermissionSub.Integrations,
ProjectPermissionSub.Webhooks, ProjectPermissionSub.Webhooks,
@@ -56,6 +56,18 @@ const buildAdminPermissionRules = () => {
); );
}); });
can(
[
ProjectPermissionApprovalActions.Read,
ProjectPermissionApprovalActions.Edit,
ProjectPermissionApprovalActions.Create,
ProjectPermissionApprovalActions.Delete,
ProjectPermissionApprovalActions.AllowChangeBypass,
ProjectPermissionApprovalActions.AllowAccessBypass
],
ProjectPermissionSub.SecretApproval
);
can( can(
[ [
ProjectPermissionCertificateActions.Read, ProjectPermissionCertificateActions.Read,
@@ -254,7 +266,7 @@ const buildMemberPermissionRules = () => {
ProjectPermissionSub.Commits ProjectPermissionSub.Commits
); );
can([ProjectPermissionActions.Read], ProjectPermissionSub.SecretApproval); can([ProjectPermissionApprovalActions.Read], ProjectPermissionSub.SecretApproval);
can([ProjectPermissionSecretRotationActions.Read], ProjectPermissionSub.SecretRotation); can([ProjectPermissionSecretRotationActions.Read], ProjectPermissionSub.SecretRotation);
can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback); can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback);
@@ -402,7 +414,7 @@ const buildViewerPermissionRules = () => {
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders);
can(ProjectPermissionDynamicSecretActions.ReadRootCredential, ProjectPermissionSub.DynamicSecrets); can(ProjectPermissionDynamicSecretActions.ReadRootCredential, ProjectPermissionSub.DynamicSecrets);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); can(ProjectPermissionApprovalActions.Read, ProjectPermissionSub.SecretApproval);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
can(ProjectPermissionSecretRotationActions.Read, ProjectPermissionSub.SecretRotation); can(ProjectPermissionSecretRotationActions.Read, ProjectPermissionSub.SecretRotation);
can(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member); can(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member);
@@ -39,6 +39,15 @@ export enum ProjectPermissionSecretActions {
Delete = "delete" Delete = "delete"
} }
export enum ProjectPermissionApprovalActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete",
AllowChangeBypass = "allow-change-bypass",
AllowAccessBypass = "allow-access-bypass"
}
export enum ProjectPermissionCmekActions { export enum ProjectPermissionCmekActions {
Read = "read", Read = "read",
Create = "create", Create = "create",
@@ -248,7 +257,7 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions, ProjectPermissionSub.IpAllowList] | [ProjectPermissionActions, ProjectPermissionSub.IpAllowList]
| [ProjectPermissionActions, ProjectPermissionSub.Settings] | [ProjectPermissionActions, ProjectPermissionSub.Settings]
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens] | [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval] | [ProjectPermissionApprovalActions, ProjectPermissionSub.SecretApproval]
| [ | [
ProjectPermissionSecretRotationActions, ProjectPermissionSecretRotationActions,
( (
@@ -446,7 +455,7 @@ const PkiSubscriberConditionSchema = z
const GeneralPermissionSchema = [ const GeneralPermissionSchema = [
z.object({ z.object({
subject: z.literal(ProjectPermissionSub.SecretApproval).describe("The entity this permission pertains to."), subject: z.literal(ProjectPermissionSub.SecretApproval).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionApprovalActions).describe(
"Describe what action an entity can take." "Describe what action an entity can take."
) )
}), }),
@@ -618,7 +627,7 @@ const GeneralPermissionSchema = [
}) })
]; ];
// Do not update this schema anymore, as it's kept purely for backwards compatability. Update V2 schema only. // Do not update this schema anymore, as it's kept purely for backwards compatibility. Update V2 schema only.
export const ProjectPermissionV1Schema = z.discriminatedUnion("subject", [ export const ProjectPermissionV1Schema = z.discriminatedUnion("subject", [
z.object({ z.object({
subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."), subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."),
+8 -8
View File
@@ -342,7 +342,7 @@ export const scimServiceFactory = ({
orgMembership = await orgMembershipDAL.create( orgMembership = await orgMembershipDAL.create(
{ {
userId: userAlias.userId, userId: userAlias.userId,
inviteEmail: email, inviteEmail: email.toLowerCase(),
orgId, orgId,
role, role,
roleId, roleId,
@@ -364,7 +364,7 @@ export const scimServiceFactory = ({
if (trustScimEmails) { if (trustScimEmails) {
user = await userDAL.findOne( user = await userDAL.findOne(
{ {
email, email: email.toLowerCase(),
isEmailVerified: true isEmailVerified: true
}, },
tx tx
@@ -379,8 +379,8 @@ export const scimServiceFactory = ({
); );
user = await userDAL.create( user = await userDAL.create(
{ {
username: trustScimEmails ? email : uniqueUsername, username: trustScimEmails ? email.toLowerCase() : uniqueUsername,
email, email: email.toLowerCase(),
isEmailVerified: trustScimEmails, isEmailVerified: trustScimEmails,
firstName, firstName,
lastName, lastName,
@@ -396,7 +396,7 @@ export const scimServiceFactory = ({
userId: user.id, userId: user.id,
aliasType, aliasType,
externalId, externalId,
emails: email ? [email] : [], emails: email ? [email.toLowerCase()] : [],
orgId orgId
}, },
tx tx
@@ -418,7 +418,7 @@ export const scimServiceFactory = ({
orgMembership = await orgMembershipDAL.create( orgMembership = await orgMembershipDAL.create(
{ {
userId: user.id, userId: user.id,
inviteEmail: email, inviteEmail: email.toLowerCase(),
orgId, orgId,
role, role,
roleId, roleId,
@@ -529,7 +529,7 @@ export const scimServiceFactory = ({
membership.userId, membership.userId,
{ {
firstName: scimUser.name.givenName, firstName: scimUser.name.givenName,
email: scimUser.emails[0].value, email: scimUser.emails[0].value.toLowerCase(),
lastName: scimUser.name.familyName, lastName: scimUser.name.familyName,
isEmailVerified: hasEmailChanged ? trustScimEmails : undefined isEmailVerified: hasEmailChanged ? trustScimEmails : undefined
}, },
@@ -606,7 +606,7 @@ export const scimServiceFactory = ({
membership.userId, membership.userId,
{ {
firstName, firstName,
email, email: email?.toLowerCase(),
lastName, lastName,
isEmailVerified: isEmailVerified:
org.orgAuthMethod === OrgAuthMethod.OIDC ? serverCfg.trustOidcEmails : serverCfg.trustSamlEmails org.orgAuthMethod === OrgAuthMethod.OIDC ? serverCfg.trustOidcEmails : serverCfg.trustSamlEmails
@@ -3,7 +3,7 @@ import picomatch from "picomatch";
import { ActionProjectType } from "@app/db/schemas"; import { ActionProjectType } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionApprovalActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { removeTrailingSlash } from "@app/lib/fn"; import { removeTrailingSlash } from "@app/lib/fn";
import { containsGlobPatterns } from "@app/lib/picomatch"; import { containsGlobPatterns } from "@app/lib/picomatch";
@@ -89,7 +89,7 @@ export const secretApprovalPolicyServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create, ProjectPermissionApprovalActions.Create,
ProjectPermissionSub.SecretApproval ProjectPermissionSub.SecretApproval
); );
@@ -204,7 +204,10 @@ export const secretApprovalPolicyServiceFactory = ({
actorOrgId, actorOrgId,
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval); ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionApprovalActions.Edit,
ProjectPermissionSub.SecretApproval
);
const plan = await licenseService.getPlan(actorOrgId); const plan = await licenseService.getPlan(actorOrgId);
if (!plan.secretApproval) { if (!plan.secretApproval) {
@@ -301,7 +304,7 @@ export const secretApprovalPolicyServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete, ProjectPermissionApprovalActions.Delete,
ProjectPermissionSub.SecretApproval ProjectPermissionSub.SecretApproval
); );
@@ -340,7 +343,10 @@ export const secretApprovalPolicyServiceFactory = ({
actorOrgId, actorOrgId,
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionApprovalActions.Read,
ProjectPermissionSub.SecretApproval
);
const sapPolicies = await secretApprovalPolicyDAL.find({ projectId, deletedAt: null }); const sapPolicies = await secretApprovalPolicyDAL.find({ projectId, deletedAt: null });
return sapPolicies; return sapPolicies;
@@ -413,7 +419,10 @@ export const secretApprovalPolicyServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionApprovalActions.Read,
ProjectPermissionSub.SecretApproval
);
return sapPolicy; return sapPolicy;
}; };
@@ -63,7 +63,11 @@ import { TUserDALFactory } from "@app/services/user/user-dal";
import { TLicenseServiceFactory } from "../license/license-service"; import { TLicenseServiceFactory } from "../license/license-service";
import { throwIfMissingSecretReadValueOrDescribePermission } from "../permission/permission-fns"; import { throwIfMissingSecretReadValueOrDescribePermission } from "../permission/permission-fns";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service";
import { ProjectPermissionSecretActions, ProjectPermissionSub } from "../permission/project-permission"; import {
ProjectPermissionApprovalActions,
ProjectPermissionSecretActions,
ProjectPermissionSub
} from "../permission/project-permission";
import { TSecretApprovalPolicyDALFactory } from "../secret-approval-policy/secret-approval-policy-dal"; import { TSecretApprovalPolicyDALFactory } from "../secret-approval-policy/secret-approval-policy-dal";
import { TSecretSnapshotServiceFactory } from "../secret-snapshot/secret-snapshot-service"; import { TSecretSnapshotServiceFactory } from "../secret-snapshot/secret-snapshot-service";
import { TSecretApprovalRequestDALFactory } from "./secret-approval-request-dal"; import { TSecretApprovalRequestDALFactory } from "./secret-approval-request-dal";
@@ -507,7 +511,7 @@ export const secretApprovalRequestServiceFactory = ({
}); });
} }
const { hasRole } = await permissionService.getProjectPermission({ const { hasRole, permission } = await permissionService.getProjectPermission({
actor: ActorType.USER, actor: ActorType.USER,
actorId, actorId,
projectId, projectId,
@@ -534,7 +538,13 @@ export const secretApprovalRequestServiceFactory = ({
).length; ).length;
const isSoftEnforcement = secretApprovalRequest.policy.enforcementLevel === EnforcementLevel.Soft; const isSoftEnforcement = secretApprovalRequest.policy.enforcementLevel === EnforcementLevel.Soft;
if (!hasMinApproval && !isSoftEnforcement) if (
!hasMinApproval &&
!(
isSoftEnforcement &&
permission.can(ProjectPermissionApprovalActions.AllowChangeBypass, ProjectPermissionSub.SecretApproval)
)
)
throw new BadRequestError({ message: "Doesn't have minimum approvals needed" }); throw new BadRequestError({ message: "Doesn't have minimum approvals needed" });
const { botKey, shouldUseSecretV2Bridge, project } = await projectBotService.getBotKey(projectId); const { botKey, shouldUseSecretV2Bridge, project } = await projectBotService.getBotKey(projectId);
@@ -1,4 +1,4 @@
import ldap from "ldapjs"; import ldap, { Client, SearchOptions } from "ldapjs";
import { import {
TRotationFactory, TRotationFactory,
@@ -8,26 +8,73 @@ import {
TRotationFactoryRotateCredentials TRotationFactoryRotateCredentials
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types"; } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { DistinguishedNameRegex } from "@app/lib/regex";
import { encryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns"; import { encryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns";
import { getLdapConnectionClient, LdapProvider, TLdapConnection } from "@app/services/app-connection/ldap"; import { getLdapConnectionClient, LdapProvider, TLdapConnection } from "@app/services/app-connection/ldap";
import { generatePassword } from "../shared/utils"; import { generatePassword } from "../shared/utils";
import { import {
LdapPasswordRotationMethod,
TLdapPasswordRotationGeneratedCredentials, TLdapPasswordRotationGeneratedCredentials,
TLdapPasswordRotationInput,
TLdapPasswordRotationWithConnection TLdapPasswordRotationWithConnection
} from "./ldap-password-rotation-types"; } from "./ldap-password-rotation-types";
const getEncodedPassword = (password: string) => Buffer.from(`"${password}"`, "utf16le"); const getEncodedPassword = (password: string) => Buffer.from(`"${password}"`, "utf16le");
const getDN = async (dn: string, client: Client): Promise<string> => {
if (DistinguishedNameRegex.test(dn)) return dn;
const opts: SearchOptions = {
filter: `(userPrincipalName=${dn})`,
scope: "sub",
attributes: ["dn"]
};
const base = dn
.split("@")[1]
.split(".")
.map((dc) => `dc=${dc}`)
.join(",");
return new Promise((resolve, reject) => {
// Perform the search
client.search(base, opts, (err, res) => {
if (err) {
logger.error(err, "LDAP Failed to get DN");
reject(new Error(`Provider Resolve DN Error: ${err.message}`));
}
let userDn: string | null;
res.on("searchEntry", (entry) => {
userDn = entry.objectName;
});
res.on("error", (error) => {
logger.error(error, "LDAP Failed to get DN");
reject(new Error(`Provider Resolve DN Error: ${error.message}`));
});
res.on("end", () => {
if (userDn) {
resolve(userDn);
} else {
reject(new Error(`Unable to resolve DN for ${dn}.`));
}
});
});
});
};
export const ldapPasswordRotationFactory: TRotationFactory< export const ldapPasswordRotationFactory: TRotationFactory<
TLdapPasswordRotationWithConnection, TLdapPasswordRotationWithConnection,
TLdapPasswordRotationGeneratedCredentials TLdapPasswordRotationGeneratedCredentials,
TLdapPasswordRotationInput["temporaryParameters"]
> = (secretRotation, appConnectionDAL, kmsService) => { > = (secretRotation, appConnectionDAL, kmsService) => {
const { const { connection, parameters, secretsMapping, activeIndex } = secretRotation;
connection,
parameters: { dn, passwordRequirements }, const { dn, passwordRequirements } = parameters;
secretsMapping
} = secretRotation;
const $verifyCredentials = async (credentials: Pick<TLdapConnection["credentials"], "dn" | "password">) => { const $verifyCredentials = async (credentials: Pick<TLdapConnection["credentials"], "dn" | "password">) => {
try { try {
@@ -40,13 +87,21 @@ export const ldapPasswordRotationFactory: TRotationFactory<
} }
}; };
const $rotatePassword = async () => { const $rotatePassword = async (currentPassword?: string) => {
const { credentials, orgId } = connection; const { credentials, orgId } = connection;
if (!credentials.url.startsWith("ldaps")) throw new Error("Password Rotation requires an LDAPS connection"); if (!credentials.url.startsWith("ldaps")) throw new Error("Password Rotation requires an LDAPS connection");
const client = await getLdapConnectionClient(credentials); const client = await getLdapConnectionClient(
const isPersonalRotation = credentials.dn === dn; currentPassword
? {
...credentials,
password: currentPassword,
dn
}
: credentials
);
const isConnectionRotation = credentials.dn === dn;
const password = generatePassword(passwordRequirements); const password = generatePassword(passwordRequirements);
@@ -58,8 +113,8 @@ export const ldapPasswordRotationFactory: TRotationFactory<
const encodedPassword = getEncodedPassword(password); const encodedPassword = getEncodedPassword(password);
// service account vs personal password rotation require different changes // service account vs personal password rotation require different changes
if (isPersonalRotation) { if (isConnectionRotation || currentPassword) {
const currentEncodedPassword = getEncodedPassword(credentials.password); const currentEncodedPassword = getEncodedPassword(currentPassword || credentials.password);
changes = [ changes = [
new ldap.Change({ new ldap.Change({
@@ -93,8 +148,9 @@ export const ldapPasswordRotationFactory: TRotationFactory<
} }
try { try {
const userDn = await getDN(dn, client);
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
client.modify(dn, changes, (err) => { client.modify(userDn, changes, (err) => {
if (err) { if (err) {
logger.error(err, "LDAP Password Rotation Failed"); logger.error(err, "LDAP Password Rotation Failed");
reject(new Error(`Provider Modify Error: ${err.message}`)); reject(new Error(`Provider Modify Error: ${err.message}`));
@@ -110,7 +166,7 @@ export const ldapPasswordRotationFactory: TRotationFactory<
await $verifyCredentials({ dn, password }); await $verifyCredentials({ dn, password });
if (isPersonalRotation) { if (isConnectionRotation) {
const updatedCredentials: TLdapConnection["credentials"] = { const updatedCredentials: TLdapConnection["credentials"] = {
...credentials, ...credentials,
password password
@@ -128,29 +184,41 @@ export const ldapPasswordRotationFactory: TRotationFactory<
return { dn, password }; return { dn, password };
}; };
const issueCredentials: TRotationFactoryIssueCredentials<TLdapPasswordRotationGeneratedCredentials> = async ( const issueCredentials: TRotationFactoryIssueCredentials<
callback TLdapPasswordRotationGeneratedCredentials,
) => { TLdapPasswordRotationInput["temporaryParameters"]
const credentials = await $rotatePassword(); > = async (callback, temporaryParameters) => {
const credentials = await $rotatePassword(
parameters.rotationMethod === LdapPasswordRotationMethod.TargetPrincipal
? temporaryParameters?.password
: undefined
);
return callback(credentials); return callback(credentials);
}; };
const revokeCredentials: TRotationFactoryRevokeCredentials<TLdapPasswordRotationGeneratedCredentials> = async ( const revokeCredentials: TRotationFactoryRevokeCredentials<TLdapPasswordRotationGeneratedCredentials> = async (
_, credentialsToRevoke,
callback callback
) => { ) => {
const currentPassword = credentialsToRevoke[activeIndex].password;
// we just rotate to a new password, essentially revoking old credentials // we just rotate to a new password, essentially revoking old credentials
await $rotatePassword(); await $rotatePassword(
parameters.rotationMethod === LdapPasswordRotationMethod.TargetPrincipal ? currentPassword : undefined
);
return callback(); return callback();
}; };
const rotateCredentials: TRotationFactoryRotateCredentials<TLdapPasswordRotationGeneratedCredentials> = async ( const rotateCredentials: TRotationFactoryRotateCredentials<TLdapPasswordRotationGeneratedCredentials> = async (
_, _,
callback callback,
activeCredentials
) => { ) => {
const credentials = await $rotatePassword(); const credentials = await $rotatePassword(
parameters.rotationMethod === LdapPasswordRotationMethod.TargetPrincipal ? activeCredentials.password : undefined
);
return callback(credentials); return callback(credentials);
}; };
@@ -1,6 +1,6 @@
import RE2 from "re2";
import { z } from "zod"; import { z } from "zod";
import { LdapPasswordRotationMethod } from "@app/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-types";
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
import { import {
BaseCreateSecretRotationSchema, BaseCreateSecretRotationSchema,
@@ -9,7 +9,7 @@ import {
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-schemas"; } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-schemas";
import { PasswordRequirementsSchema } from "@app/ee/services/secret-rotation-v2/shared/general"; import { PasswordRequirementsSchema } from "@app/ee/services/secret-rotation-v2/shared/general";
import { SecretRotations } from "@app/lib/api-docs"; import { SecretRotations } from "@app/lib/api-docs";
import { DistinguishedNameRegex } from "@app/lib/regex"; import { DistinguishedNameRegex, UserPrincipalNameRegex } from "@app/lib/regex";
import { SecretNameSchema } from "@app/server/lib/schemas"; import { SecretNameSchema } from "@app/server/lib/schemas";
import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { AppConnection } from "@app/services/app-connection/app-connection-enums";
@@ -26,10 +26,16 @@ const LdapPasswordRotationParametersSchema = z.object({
dn: z dn: z
.string() .string()
.trim() .trim()
.regex(new RE2(DistinguishedNameRegex), "Invalid DN format, ie; CN=user,OU=users,DC=example,DC=com") .min(1, "DN/UPN required")
.min(1, "Distinguished Name (DN) Required") .refine((value) => DistinguishedNameRegex.test(value) || UserPrincipalNameRegex.test(value), {
message: "Invalid DN/UPN format"
})
.describe(SecretRotations.PARAMETERS.LDAP_PASSWORD.dn), .describe(SecretRotations.PARAMETERS.LDAP_PASSWORD.dn),
passwordRequirements: PasswordRequirementsSchema.optional() passwordRequirements: PasswordRequirementsSchema.optional(),
rotationMethod: z
.nativeEnum(LdapPasswordRotationMethod)
.optional()
.describe(SecretRotations.PARAMETERS.LDAP_PASSWORD.rotationMethod)
}); });
const LdapPasswordRotationSecretsMappingSchema = z.object({ const LdapPasswordRotationSecretsMappingSchema = z.object({
@@ -50,10 +56,28 @@ export const LdapPasswordRotationSchema = BaseSecretRotationSchema(SecretRotatio
secretsMapping: LdapPasswordRotationSecretsMappingSchema secretsMapping: LdapPasswordRotationSecretsMappingSchema
}); });
export const CreateLdapPasswordRotationSchema = BaseCreateSecretRotationSchema(SecretRotation.LdapPassword).extend({ export const CreateLdapPasswordRotationSchema = BaseCreateSecretRotationSchema(SecretRotation.LdapPassword)
parameters: LdapPasswordRotationParametersSchema, .extend({
secretsMapping: LdapPasswordRotationSecretsMappingSchema parameters: LdapPasswordRotationParametersSchema,
}); secretsMapping: LdapPasswordRotationSecretsMappingSchema,
temporaryParameters: z
.object({
password: z.string().min(1, "Password required").describe(SecretRotations.PARAMETERS.LDAP_PASSWORD.password)
})
.optional()
})
.superRefine((val, ctx) => {
if (
val.parameters.rotationMethod === LdapPasswordRotationMethod.TargetPrincipal &&
!val.temporaryParameters?.password
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Password required",
path: ["temporaryParameters", "password"]
});
}
});
export const UpdateLdapPasswordRotationSchema = BaseUpdateSecretRotationSchema(SecretRotation.LdapPassword).extend({ export const UpdateLdapPasswordRotationSchema = BaseUpdateSecretRotationSchema(SecretRotation.LdapPassword).extend({
parameters: LdapPasswordRotationParametersSchema.optional(), parameters: LdapPasswordRotationParametersSchema.optional(),
@@ -9,6 +9,11 @@ import {
LdapPasswordRotationSchema LdapPasswordRotationSchema
} from "./ldap-password-rotation-schemas"; } from "./ldap-password-rotation-schemas";
export enum LdapPasswordRotationMethod {
ConnectionPrincipal = "connection-principal",
TargetPrincipal = "target-principal"
}
export type TLdapPasswordRotation = z.infer<typeof LdapPasswordRotationSchema>; export type TLdapPasswordRotation = z.infer<typeof LdapPasswordRotationSchema>;
export type TLdapPasswordRotationInput = z.infer<typeof CreateLdapPasswordRotationSchema>; export type TLdapPasswordRotationInput = z.infer<typeof CreateLdapPasswordRotationSchema>;
@@ -1,12 +1,13 @@
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { KmsDataKey } from "@app/services/kms/kms-types"; import { KmsDataKey } from "@app/services/kms/kms-types";
import { AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./auth0-client-secret"; import { AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./auth0-client-secret";
import { AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION } from "./aws-iam-user-secret"; import { AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION } from "./aws-iam-user-secret";
import { AZURE_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./azure-client-secret"; import { AZURE_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./azure-client-secret";
import { LDAP_PASSWORD_ROTATION_LIST_OPTION } from "./ldap-password"; import { LDAP_PASSWORD_ROTATION_LIST_OPTION, TLdapPasswordRotation } from "./ldap-password";
import { MSSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mssql-credentials"; import { MSSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mssql-credentials";
import { POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION } from "./postgres-credentials"; import { POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION } from "./postgres-credentials";
import { SecretRotation, SecretRotationStatus } from "./secret-rotation-v2-enums"; import { SecretRotation, SecretRotationStatus } from "./secret-rotation-v2-enums";
@@ -15,7 +16,8 @@ import {
TSecretRotationV2, TSecretRotationV2,
TSecretRotationV2GeneratedCredentials, TSecretRotationV2GeneratedCredentials,
TSecretRotationV2ListItem, TSecretRotationV2ListItem,
TSecretRotationV2Raw TSecretRotationV2Raw,
TUpdateSecretRotationV2DTO
} from "./secret-rotation-v2-types"; } from "./secret-rotation-v2-types";
const SECRET_ROTATION_LIST_OPTIONS: Record<SecretRotation, TSecretRotationV2ListItem> = { const SECRET_ROTATION_LIST_OPTIONS: Record<SecretRotation, TSecretRotationV2ListItem> = {
@@ -228,3 +230,30 @@ export const parseRotationErrorMessage = (err: unknown): string => {
? errorMessage ? errorMessage
: `${errorMessage.substring(0, MAX_MESSAGE_LENGTH - 3)}...`; : `${errorMessage.substring(0, MAX_MESSAGE_LENGTH - 3)}...`;
}; };
function haveUnequalProperties<T>(obj1: T, obj2: T, properties: (keyof T)[]): boolean {
return properties.some((prop) => obj1[prop] !== obj2[prop]);
}
export const throwOnImmutableParameterUpdate = (
updatePayload: TUpdateSecretRotationV2DTO,
secretRotation: TSecretRotationV2Raw
) => {
if (!updatePayload.parameters) return;
switch (updatePayload.type) {
case SecretRotation.LdapPassword:
if (
haveUnequalProperties(
updatePayload.parameters as TLdapPasswordRotation["parameters"],
secretRotation.parameters as TLdapPasswordRotation["parameters"],
["rotationMethod", "dn"]
)
) {
throw new BadRequestError({ message: "Cannot update rotation method or DN" });
}
break;
default:
// do nothing
}
};
@@ -25,7 +25,8 @@ import {
getNextUtcRotationInterval, getNextUtcRotationInterval,
getSecretRotationRotateSecretJobOptions, getSecretRotationRotateSecretJobOptions,
listSecretRotationOptions, listSecretRotationOptions,
parseRotationErrorMessage parseRotationErrorMessage,
throwOnImmutableParameterUpdate
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-fns"; } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-fns";
import { import {
SECRET_ROTATION_CONNECTION_MAP, SECRET_ROTATION_CONNECTION_MAP,
@@ -46,6 +47,7 @@ import {
TSecretRotationV2, TSecretRotationV2,
TSecretRotationV2GeneratedCredentials, TSecretRotationV2GeneratedCredentials,
TSecretRotationV2Raw, TSecretRotationV2Raw,
TSecretRotationV2TemporaryParameters,
TSecretRotationV2WithConnection, TSecretRotationV2WithConnection,
TUpdateSecretRotationV2DTO TUpdateSecretRotationV2DTO
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types"; } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
@@ -114,7 +116,8 @@ const MAX_GENERATED_CREDENTIALS_LENGTH = 2;
type TRotationFactoryImplementation = TRotationFactory< type TRotationFactoryImplementation = TRotationFactory<
TSecretRotationV2WithConnection, TSecretRotationV2WithConnection,
TSecretRotationV2GeneratedCredentials TSecretRotationV2GeneratedCredentials,
TSecretRotationV2TemporaryParameters
>; >;
const SECRET_ROTATION_FACTORY_MAP: Record<SecretRotation, TRotationFactoryImplementation> = { const SECRET_ROTATION_FACTORY_MAP: Record<SecretRotation, TRotationFactoryImplementation> = {
[SecretRotation.PostgresCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation, [SecretRotation.PostgresCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation,
@@ -403,6 +406,7 @@ export const secretRotationV2ServiceFactory = ({
environment, environment,
rotateAtUtc = { hours: 0, minutes: 0 }, rotateAtUtc = { hours: 0, minutes: 0 },
secretsMapping, secretsMapping,
temporaryParameters,
...payload ...payload
}: TCreateSecretRotationV2DTO, }: TCreateSecretRotationV2DTO,
actor: OrgServiceActor actor: OrgServiceActor
@@ -554,7 +558,7 @@ export const secretRotationV2ServiceFactory = ({
return createdRotation; return createdRotation;
}); });
}); }, temporaryParameters);
await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId); await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId);
await snapshotService.performSnapshot(folder.id); await snapshotService.performSnapshot(folder.id);
@@ -593,10 +597,7 @@ export const secretRotationV2ServiceFactory = ({
} }
}; };
const updateSecretRotation = async ( const updateSecretRotation = async (dto: TUpdateSecretRotationV2DTO, actor: OrgServiceActor) => {
{ type, rotationId, ...payload }: TUpdateSecretRotationV2DTO,
actor: OrgServiceActor
) => {
const plan = await licenseService.getPlan(actor.orgId); const plan = await licenseService.getPlan(actor.orgId);
if (!plan.secretRotation) if (!plan.secretRotation)
@@ -604,6 +605,8 @@ export const secretRotationV2ServiceFactory = ({
message: "Failed to update secret rotation due to plan restriction. Upgrade plan to update secret rotations." message: "Failed to update secret rotation due to plan restriction. Upgrade plan to update secret rotations."
}); });
const { type, rotationId, ...payload } = dto;
const secretRotation = await secretRotationV2DAL.findById(rotationId); const secretRotation = await secretRotationV2DAL.findById(rotationId);
if (!secretRotation) if (!secretRotation)
@@ -611,6 +614,8 @@ export const secretRotationV2ServiceFactory = ({
message: `Could not find ${SECRET_ROTATION_NAME_MAP[type]} Rotation with ID ${rotationId}` message: `Could not find ${SECRET_ROTATION_NAME_MAP[type]} Rotation with ID ${rotationId}`
}); });
throwOnImmutableParameterUpdate(dto, secretRotation);
const { folder, environment, projectId, folderId, connection } = secretRotation; const { folder, environment, projectId, folderId, connection } = secretRotation;
const secretsMapping = secretRotation.secretsMapping as TSecretRotationV2["secretsMapping"]; const secretsMapping = secretRotation.secretsMapping as TSecretRotationV2["secretsMapping"];
@@ -893,6 +898,7 @@ export const secretRotationV2ServiceFactory = ({
const inactiveIndex = (activeIndex + 1) % MAX_GENERATED_CREDENTIALS_LENGTH; const inactiveIndex = (activeIndex + 1) % MAX_GENERATED_CREDENTIALS_LENGTH;
const inactiveCredentials = generatedCredentials[inactiveIndex]; const inactiveCredentials = generatedCredentials[inactiveIndex];
const activeCredentials = generatedCredentials[activeIndex];
const rotationFactory = SECRET_ROTATION_FACTORY_MAP[type as SecretRotation]( const rotationFactory = SECRET_ROTATION_FACTORY_MAP[type as SecretRotation](
{ {
@@ -903,77 +909,81 @@ export const secretRotationV2ServiceFactory = ({
kmsService kmsService
); );
const updatedRotation = await rotationFactory.rotateCredentials(inactiveCredentials, async (newCredentials) => { const updatedRotation = await rotationFactory.rotateCredentials(
const updatedCredentials = [...generatedCredentials]; inactiveCredentials,
updatedCredentials[inactiveIndex] = newCredentials; async (newCredentials) => {
const updatedCredentials = [...generatedCredentials];
updatedCredentials[inactiveIndex] = newCredentials;
const encryptedUpdatedCredentials = await encryptSecretRotationCredentials({ const encryptedUpdatedCredentials = await encryptSecretRotationCredentials({
projectId, projectId,
generatedCredentials: updatedCredentials as TSecretRotationV2GeneratedCredentials, generatedCredentials: updatedCredentials as TSecretRotationV2GeneratedCredentials,
kmsService kmsService
});
return secretRotationV2DAL.transaction(async (tx) => {
const secretsPayload = rotationFactory.getSecretsPayload(newCredentials);
const { encryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
}); });
// update mapped secrets with new credential values return secretRotationV2DAL.transaction(async (tx) => {
await fnSecretBulkUpdate({ const secretsPayload = rotationFactory.getSecretsPayload(newCredentials);
folderId,
orgId: connection.orgId, const { encryptor } = await kmsService.createCipherPairWithDataKey({
tx, type: KmsDataKey.SecretManager,
inputSecrets: secretsPayload.map(({ key, value }) => ({ projectId
filter: { });
key,
folderId, // update mapped secrets with new credential values
type: SecretType.Shared await fnSecretBulkUpdate({
folderId,
orgId: connection.orgId,
tx,
inputSecrets: secretsPayload.map(({ key, value }) => ({
filter: {
key,
folderId,
type: SecretType.Shared
},
data: {
encryptedValue: encryptor({
plainText: Buffer.from(value)
}).cipherTextBlob,
references: []
}
})),
secretDAL: secretV2BridgeDAL,
secretVersionDAL: secretVersionV2BridgeDAL,
secretVersionTagDAL: secretVersionTagV2BridgeDAL,
folderCommitService,
actor: {
type: ActorType.PLATFORM
}, },
data: { secretTagDAL,
encryptedValue: encryptor({ resourceMetadataDAL
plainText: Buffer.from(value) });
}).cipherTextBlob,
references: []
}
})),
secretDAL: secretV2BridgeDAL,
secretVersionDAL: secretVersionV2BridgeDAL,
secretVersionTagDAL: secretVersionTagV2BridgeDAL,
folderCommitService,
actor: {
type: ActorType.PLATFORM
},
secretTagDAL,
resourceMetadataDAL
});
const currentTime = new Date(); const currentTime = new Date();
return secretRotationV2DAL.updateById( return secretRotationV2DAL.updateById(
secretRotation.id, secretRotation.id,
{ {
encryptedGeneratedCredentials: encryptedUpdatedCredentials, encryptedGeneratedCredentials: encryptedUpdatedCredentials,
activeIndex: inactiveIndex, activeIndex: inactiveIndex,
isLastRotationManual: isManualRotation, isLastRotationManual: isManualRotation,
lastRotatedAt: currentTime,
lastRotationAttemptedAt: currentTime,
nextRotationAt: calculateNextRotationAt({
...(secretRotation as TSecretRotationV2),
rotationStatus: SecretRotationStatus.Success,
lastRotatedAt: currentTime, lastRotatedAt: currentTime,
isManualRotation lastRotationAttemptedAt: currentTime,
}), nextRotationAt: calculateNextRotationAt({
rotationStatus: SecretRotationStatus.Success, ...(secretRotation as TSecretRotationV2),
lastRotationJobId: jobId, rotationStatus: SecretRotationStatus.Success,
encryptedLastRotationMessage: null lastRotatedAt: currentTime,
}, isManualRotation
tx }),
); rotationStatus: SecretRotationStatus.Success,
}); lastRotationJobId: jobId,
}); encryptedLastRotationMessage: null
},
tx
);
});
},
activeCredentials
);
await auditLogService.createAuditLog({ await auditLogService.createAuditLog({
...(auditLogInfo ?? { ...(auditLogInfo ?? {
@@ -87,6 +87,8 @@ export type TSecretRotationV2ListItem =
| TLdapPasswordRotationListItem | TLdapPasswordRotationListItem
| TAwsIamUserSecretRotationListItem; | TAwsIamUserSecretRotationListItem;
export type TSecretRotationV2TemporaryParameters = TLdapPasswordRotationInput["temporaryParameters"] | undefined;
export type TSecretRotationV2Raw = NonNullable<Awaited<ReturnType<TSecretRotationV2DALFactory["findById"]>>>; export type TSecretRotationV2Raw = NonNullable<Awaited<ReturnType<TSecretRotationV2DALFactory["findById"]>>>;
export type TListSecretRotationsV2ByProjectId = { export type TListSecretRotationsV2ByProjectId = {
@@ -120,6 +122,7 @@ export type TCreateSecretRotationV2DTO = Pick<
environment: string; environment: string;
isAutoRotationEnabled?: boolean; isAutoRotationEnabled?: boolean;
rotateAtUtc?: TRotateAtUtc; rotateAtUtc?: TRotateAtUtc;
temporaryParameters?: TSecretRotationV2TemporaryParameters;
}; };
export type TUpdateSecretRotationV2DTO = Partial< export type TUpdateSecretRotationV2DTO = Partial<
@@ -186,8 +189,12 @@ export type TSecretRotationSendNotificationJobPayload = {
// transactional behavior. By passing in the rotation mutation, if this mutation fails we can roll back the // transactional behavior. By passing in the rotation mutation, if this mutation fails we can roll back the
// third party credential changes (when supported), preventing credentials getting out of sync // third party credential changes (when supported), preventing credentials getting out of sync
export type TRotationFactoryIssueCredentials<T extends TSecretRotationV2GeneratedCredentials> = ( export type TRotationFactoryIssueCredentials<
callback: (newCredentials: T[number]) => Promise<TSecretRotationV2Raw> T extends TSecretRotationV2GeneratedCredentials,
P extends TSecretRotationV2TemporaryParameters = undefined
> = (
callback: (newCredentials: T[number]) => Promise<TSecretRotationV2Raw>,
temporaryParameters?: P
) => Promise<TSecretRotationV2Raw>; ) => Promise<TSecretRotationV2Raw>;
export type TRotationFactoryRevokeCredentials<T extends TSecretRotationV2GeneratedCredentials> = ( export type TRotationFactoryRevokeCredentials<T extends TSecretRotationV2GeneratedCredentials> = (
@@ -197,7 +204,8 @@ export type TRotationFactoryRevokeCredentials<T extends TSecretRotationV2Generat
export type TRotationFactoryRotateCredentials<T extends TSecretRotationV2GeneratedCredentials> = ( export type TRotationFactoryRotateCredentials<T extends TSecretRotationV2GeneratedCredentials> = (
credentialsToRevoke: T[number] | undefined, credentialsToRevoke: T[number] | undefined,
callback: (newCredentials: T[number]) => Promise<TSecretRotationV2Raw> callback: (newCredentials: T[number]) => Promise<TSecretRotationV2Raw>,
activeCredentials: T[number]
) => Promise<TSecretRotationV2Raw>; ) => Promise<TSecretRotationV2Raw>;
export type TRotationFactoryGetSecretsPayload<T extends TSecretRotationV2GeneratedCredentials> = ( export type TRotationFactoryGetSecretsPayload<T extends TSecretRotationV2GeneratedCredentials> = (
@@ -206,13 +214,14 @@ export type TRotationFactoryGetSecretsPayload<T extends TSecretRotationV2Generat
export type TRotationFactory< export type TRotationFactory<
T extends TSecretRotationV2WithConnection, T extends TSecretRotationV2WithConnection,
C extends TSecretRotationV2GeneratedCredentials C extends TSecretRotationV2GeneratedCredentials,
P extends TSecretRotationV2TemporaryParameters = undefined
> = ( > = (
secretRotation: T, secretRotation: T,
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">, appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">,
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey"> kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
) => { ) => {
issueCredentials: TRotationFactoryIssueCredentials<C>; issueCredentials: TRotationFactoryIssueCredentials<C, P>;
revokeCredentials: TRotationFactoryRevokeCredentials<C>; revokeCredentials: TRotationFactoryRevokeCredentials<C>;
rotateCredentials: TRotationFactoryRotateCredentials<C>; rotateCredentials: TRotationFactoryRotateCredentials<C>;
getSecretsPayload: TRotationFactoryGetSecretsPayload<C>; getSecretsPayload: TRotationFactoryGetSecretsPayload<C>;
@@ -6,5 +6,6 @@ export const OCI_VAULT_SYNC_LIST_OPTION: TSecretSyncListItem = {
name: "OCI Vault", name: "OCI Vault",
destination: SecretSync.OCIVault, destination: SecretSync.OCIVault,
connection: AppConnection.OCI, connection: AppConnection.OCI,
canImportSecrets: true canImportSecrets: true,
enterprise: true
}; };
@@ -1,7 +1,6 @@
import { secrets, vault } from "oci-sdk"; import { secrets, vault } from "oci-sdk";
import { delay } from "@app/lib/delay"; import { getOCIProvider } from "@app/ee/services/app-connections/oci";
import { getOCIProvider } from "@app/services/app-connection/oci";
import { import {
TCreateOCIVaultVariable, TCreateOCIVaultVariable,
TDeleteOCIVaultVariable, TDeleteOCIVaultVariable,
@@ -9,7 +8,8 @@ import {
TOCIVaultSyncWithCredentials, TOCIVaultSyncWithCredentials,
TUnmarkOCIVaultVariableFromDeletion, TUnmarkOCIVaultVariableFromDeletion,
TUpdateOCIVaultVariable TUpdateOCIVaultVariable
} from "@app/services/secret-sync/oci-vault/oci-vault-sync-types"; } from "@app/ee/services/secret-sync/oci-vault/oci-vault-sync-types";
import { delay } from "@app/lib/delay";
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns"; import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
@@ -66,5 +66,6 @@ export const OCIVaultSyncListItemSchema = z.object({
name: z.literal("OCI Vault"), name: z.literal("OCI Vault"),
connection: z.literal(AppConnection.OCI), connection: z.literal(AppConnection.OCI),
destination: z.literal(SecretSync.OCIVault), destination: z.literal(SecretSync.OCIVault),
canImportSecrets: z.literal(true) canImportSecrets: z.literal(true),
enterprise: z.boolean()
}); });
@@ -1,7 +1,7 @@
import { SimpleAuthenticationDetailsProvider } from "oci-sdk"; import { SimpleAuthenticationDetailsProvider } from "oci-sdk";
import { z } from "zod"; import { z } from "zod";
import { TOCIConnection } from "@app/services/app-connection/oci"; import { TOCIConnection } from "@app/ee/services/app-connections/oci";
import { CreateOCIVaultSyncSchema, OCIVaultSyncListItemSchema, OCIVaultSyncSchema } from "./oci-vault-sync-schemas"; import { CreateOCIVaultSyncSchema, OCIVaultSyncListItemSchema, OCIVaultSyncSchema } from "./oci-vault-sync-schemas";
+13 -3
View File
@@ -2063,7 +2063,7 @@ export const AppConnections = {
LDAP: { LDAP: {
provider: "The type of LDAP provider. Determines provider-specific behaviors.", provider: "The type of LDAP provider. Determines provider-specific behaviors.",
url: "The LDAP/LDAPS URL to connect to (e.g., 'ldap://domain-or-ip:389' or 'ldaps://domain-or-ip:636').", url: "The LDAP/LDAPS URL to connect to (e.g., 'ldap://domain-or-ip:389' or 'ldaps://domain-or-ip:636').",
dn: "The Distinguished Name (DN) of the principal to bind with (e.g., 'CN=John,CN=Users,DC=example,DC=com').", dn: "The Distinguished Name (DN) or User Principal Name (UPN) of the principal to bind with (e.g., 'CN=John,CN=Users,DC=example,DC=com').",
password: "The password to bind with for authentication.", password: "The password to bind with for authentication.",
sslRejectUnauthorized: sslRejectUnauthorized:
"Whether or not to reject unauthorized SSL certificates (true/false) when using ldaps://. Set to false only in test environments.", "Whether or not to reject unauthorized SSL certificates (true/false) when using ldaps://. Set to false only in test environments.",
@@ -2084,6 +2084,10 @@ export const AppConnections = {
region: "The region identifier in Oracle Cloud Infrastructure where the vault is located.", region: "The region identifier in Oracle Cloud Infrastructure where the vault is located.",
fingerprint: "The fingerprint of the public key uploaded to the user's API keys.", fingerprint: "The fingerprint of the public key uploaded to the user's API keys.",
privateKey: "The private key content in PEM format used to sign API requests." privateKey: "The private key content in PEM format used to sign API requests."
},
ONEPASS: {
instanceUrl: "The URL of the 1Password Connect Server instance to authenticate with.",
apiToken: "The API token used to access the 1Password Connect Server."
} }
} }
}; };
@@ -2237,6 +2241,9 @@ export const SecretSyncs = {
compartmentOcid: "The OCID (Oracle Cloud Identifier) of the compartment where the vault is located.", compartmentOcid: "The OCID (Oracle Cloud Identifier) of the compartment where the vault is located.",
vaultOcid: "The OCID (Oracle Cloud Identifier) of the vault to sync secrets to.", vaultOcid: "The OCID (Oracle Cloud Identifier) of the vault to sync secrets to.",
keyOcid: "The OCID (Oracle Cloud Identifier) of the encryption key to use when creating secrets in the vault." keyOcid: "The OCID (Oracle Cloud Identifier) of the encryption key to use when creating secrets in the vault."
},
ONEPASS: {
vaultId: "The ID of the 1Password vault to sync secrets to."
} }
} }
}; };
@@ -2308,7 +2315,10 @@ export const SecretRotations = {
clientId: "The client ID of the Azure Application to rotate the client secret for." clientId: "The client ID of the Azure Application to rotate the client secret for."
}, },
LDAP_PASSWORD: { LDAP_PASSWORD: {
dn: "The Distinguished Name (DN) of the principal to rotate the password for." dn: "The Distinguished Name (DN) or User Principal Name (UPN) of the principal to rotate the password for.",
rotationMethod:
'Whether the rotation should be performed by the LDAP "connection-principal" or the "target-principal" (defaults to \'connection-principal\').',
password: 'The password of the provided principal if "parameters.rotationMethod" is set to "target-principal".'
}, },
GENERAL: { GENERAL: {
PASSWORD_REQUIREMENTS: { PASSWORD_REQUIREMENTS: {
@@ -2342,7 +2352,7 @@ export const SecretRotations = {
clientSecret: "The name of the secret that the rotated client secret will be mapped to." clientSecret: "The name of the secret that the rotated client secret will be mapped to."
}, },
LDAP_PASSWORD: { LDAP_PASSWORD: {
dn: "The name of the secret that the Distinguished Name (DN) of the principal will be mapped to.", dn: "The name of the secret that the Distinguished Name (DN) or User Principal Name (UPN) of the principal will be mapped to.",
password: "The name of the secret that the rotated password will be mapped to." password: "The name of the secret that the rotated password will be mapped to."
}, },
AWS_IAM_USER_SECRET: { AWS_IAM_USER_SECRET: {
+15 -3
View File
@@ -69,6 +69,9 @@ const envSchema = z
SMTP_PASSWORD: zpStr(z.string().optional()), SMTP_PASSWORD: zpStr(z.string().optional()),
SMTP_FROM_ADDRESS: zpStr(z.string().optional()), SMTP_FROM_ADDRESS: zpStr(z.string().optional()),
SMTP_FROM_NAME: zpStr(z.string().optional().default("Infisical")), SMTP_FROM_NAME: zpStr(z.string().optional().default("Infisical")),
SMTP_CUSTOM_CA_CERT: zpStr(
z.string().optional().describe("Base64 encoded custom CA certificate PEM(s) for the SMTP server")
),
COOKIE_SECRET_SIGN_KEY: z COOKIE_SECRET_SIGN_KEY: z
.string() .string()
.min(32) .min(32)
@@ -302,6 +305,17 @@ export const initEnvConfig = (logger?: CustomLogger) => {
}; };
export const formatSmtpConfig = () => { export const formatSmtpConfig = () => {
const tlsOptions: {
rejectUnauthorized: boolean;
ca?: string | string[];
} = {
rejectUnauthorized: envCfg.SMTP_TLS_REJECT_UNAUTHORIZED
};
if (envCfg.SMTP_CUSTOM_CA_CERT) {
tlsOptions.ca = Buffer.from(envCfg.SMTP_CUSTOM_CA_CERT, "base64").toString("utf-8");
}
return { return {
host: envCfg.SMTP_HOST, host: envCfg.SMTP_HOST,
port: envCfg.SMTP_PORT, port: envCfg.SMTP_PORT,
@@ -313,8 +327,6 @@ export const formatSmtpConfig = () => {
from: `"${envCfg.SMTP_FROM_NAME}" <${envCfg.SMTP_FROM_ADDRESS}>`, from: `"${envCfg.SMTP_FROM_NAME}" <${envCfg.SMTP_FROM_ADDRESS}>`,
ignoreTLS: envCfg.SMTP_IGNORE_TLS, ignoreTLS: envCfg.SMTP_IGNORE_TLS,
requireTLS: envCfg.SMTP_REQUIRE_TLS, requireTLS: envCfg.SMTP_REQUIRE_TLS,
tls: { tls: tlsOptions
rejectUnauthorized: envCfg.SMTP_TLS_REJECT_UNAUTHORIZED
}
}; };
}; };
+22 -4
View File
@@ -1,6 +1,8 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { Compare, Filter, parse } from "scim2-parse-filter"; import { Compare, Filter, parse } from "scim2-parse-filter";
import { TableName } from "@app/db/schemas";
const appendParentToGroupingOperator = (parentPath: string, filter: Filter) => { const appendParentToGroupingOperator = (parentPath: string, filter: Filter) => {
if (filter.op !== "[]" && filter.op !== "and" && filter.op !== "or" && filter.op !== "not") { if (filter.op !== "[]" && filter.op !== "and" && filter.op !== "or" && filter.op !== "not") {
return { ...filter, attrPath: `${parentPath}.${(filter as Compare).attrPath}` }; return { ...filter, attrPath: `${parentPath}.${(filter as Compare).attrPath}` };
@@ -27,8 +29,12 @@ const processDynamicQuery = (
const { scimFilterAst, query } = stack.pop()!; const { scimFilterAst, query } = stack.pop()!;
switch (scimFilterAst.op) { switch (scimFilterAst.op) {
case "eq": { case "eq": {
let sanitizedValue = scimFilterAst.compValue;
const attrPath = getAttributeField(scimFilterAst.attrPath); const attrPath = getAttributeField(scimFilterAst.attrPath);
if (attrPath) void query.where(attrPath, scimFilterAst.compValue); if (attrPath === `${TableName.Users}.email` && typeof sanitizedValue === "string") {
sanitizedValue = sanitizedValue.toLowerCase();
}
if (attrPath) void query.where(attrPath, sanitizedValue);
break; break;
} }
case "pr": { case "pr": {
@@ -62,18 +68,30 @@ const processDynamicQuery = (
break; break;
} }
case "ew": { case "ew": {
let sanitizedValue = scimFilterAst.compValue;
const attrPath = getAttributeField(scimFilterAst.attrPath); const attrPath = getAttributeField(scimFilterAst.attrPath);
if (attrPath) void query.whereILike(attrPath, `%${scimFilterAst.compValue}`); if (attrPath === `${TableName.Users}.email` && typeof sanitizedValue === "string") {
sanitizedValue = sanitizedValue.toLowerCase();
}
if (attrPath) void query.whereILike(attrPath, `%${sanitizedValue}`);
break; break;
} }
case "co": { case "co": {
let sanitizedValue = scimFilterAst.compValue;
const attrPath = getAttributeField(scimFilterAst.attrPath); const attrPath = getAttributeField(scimFilterAst.attrPath);
if (attrPath) void query.whereILike(attrPath, `%${scimFilterAst.compValue}%`); if (attrPath === `${TableName.Users}.email` && typeof sanitizedValue === "string") {
sanitizedValue = sanitizedValue.toLowerCase();
}
if (attrPath) void query.whereILike(attrPath, `%${sanitizedValue}%`);
break; break;
} }
case "ne": { case "ne": {
let sanitizedValue = scimFilterAst.compValue;
const attrPath = getAttributeField(scimFilterAst.attrPath); const attrPath = getAttributeField(scimFilterAst.attrPath);
if (attrPath) void query.whereNot(attrPath, "=", scimFilterAst.compValue); if (attrPath === `${TableName.Users}.email` && typeof sanitizedValue === "string") {
sanitizedValue = sanitizedValue.toLowerCase();
}
if (attrPath) void query.whereNot(attrPath, "=", sanitizedValue);
break; break;
} }
case "and": { case "and": {
+13 -4
View File
@@ -95,11 +95,20 @@ const extractReqId = () => {
try { try {
return requestContext.get("reqId") || UNKNOWN_REQUEST_ID; return requestContext.get("reqId") || UNKNOWN_REQUEST_ID;
} catch (err) { } catch (err) {
// eslint-disable-next-line no-console
console.log("failed to get request context", err); console.log("failed to get request context", err);
return UNKNOWN_REQUEST_ID; return UNKNOWN_REQUEST_ID;
} }
}; };
const extractOrgId = () => {
try {
return requestContext.get("orgId");
} catch {
return "";
}
};
export const initLogger = () => { export const initLogger = () => {
const cfg = loggerConfig.parse(process.env); const cfg = loggerConfig.parse(process.env);
const targets: pino.TransportMultiOptions["targets"][number][] = [ const targets: pino.TransportMultiOptions["targets"][number][] = [
@@ -135,22 +144,22 @@ export const initLogger = () => {
const wrapLogger = (originalLogger: Logger): CustomLogger => { const wrapLogger = (originalLogger: Logger): CustomLogger => {
// eslint-disable-next-line no-param-reassign, @typescript-eslint/no-explicit-any // eslint-disable-next-line no-param-reassign, @typescript-eslint/no-explicit-any
originalLogger.info = (obj: unknown, msg?: string, ...args: any[]) => { originalLogger.info = (obj: unknown, msg?: string, ...args: any[]) => {
return originalLogger.child({ reqId: extractReqId() }).info(obj, msg, ...args); return originalLogger.child({ reqId: extractReqId(), orgId: extractOrgId() }).info(obj, msg, ...args);
}; };
// eslint-disable-next-line no-param-reassign, @typescript-eslint/no-explicit-any // eslint-disable-next-line no-param-reassign, @typescript-eslint/no-explicit-any
originalLogger.error = (obj: unknown, msg?: string, ...args: any[]) => { originalLogger.error = (obj: unknown, msg?: string, ...args: any[]) => {
return originalLogger.child({ reqId: extractReqId() }).error(obj, msg, ...args); return originalLogger.child({ reqId: extractReqId(), orgId: extractOrgId() }).error(obj, msg, ...args);
}; };
// eslint-disable-next-line no-param-reassign, @typescript-eslint/no-explicit-any // eslint-disable-next-line no-param-reassign, @typescript-eslint/no-explicit-any
originalLogger.warn = (obj: unknown, msg?: string, ...args: any[]) => { originalLogger.warn = (obj: unknown, msg?: string, ...args: any[]) => {
return originalLogger.child({ reqId: extractReqId() }).warn(obj, msg, ...args); return originalLogger.child({ reqId: extractReqId(), orgId: extractOrgId() }).warn(obj, msg, ...args);
}; };
// eslint-disable-next-line no-param-reassign, @typescript-eslint/no-explicit-any // eslint-disable-next-line no-param-reassign, @typescript-eslint/no-explicit-any
originalLogger.debug = (obj: unknown, msg?: string, ...args: any[]) => { originalLogger.debug = (obj: unknown, msg?: string, ...args: any[]) => {
return originalLogger.child({ reqId: extractReqId() }).debug(obj, msg, ...args); return originalLogger.child({ reqId: extractReqId(), orgId: extractOrgId() }).debug(obj, msg, ...args);
}; };
return originalLogger; return originalLogger;
+9 -1
View File
@@ -1,3 +1,11 @@
import RE2 from "re2";
export const DistinguishedNameRegex = export const DistinguishedNameRegex =
// DN format, ie; CN=user,OU=users,DC=example,DC=com // DN format, ie; CN=user,OU=users,DC=example,DC=com
/^(?:(?:[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)(?:(?:\\+[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)*)(?:,(?:[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)(?:(?:\\+[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)*))*)?$/; new RE2(
/^(?:(?:[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)(?:(?:\\+[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)*)(?:,(?:[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)(?:(?:\\+[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)*))*)?$/
);
export const UserPrincipalNameRegex = new RE2(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9._-]+\.[a-zA-Z]{2,}$/);
export const LdapUrlRegex = new RE2(/^ldaps?:\/\//);
+1 -1
View File
@@ -9,7 +9,7 @@ interface SlugSchemaInputs {
field?: string; field?: string;
} }
export const slugSchema = ({ min = 1, max = 32, field = "Slug" }: SlugSchemaInputs = {}) => { export const slugSchema = ({ min = 1, max = 64, field = "Slug" }: SlugSchemaInputs = {}) => {
return z return z
.string() .string()
.trim() .trim()
@@ -123,6 +123,7 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
switch (authMode) { switch (authMode) {
case AuthMode.JWT: { case AuthMode.JWT: {
const { user, tokenVersionId, orgId } = await server.services.authToken.fnValidateJwtIdentity(token); const { user, tokenVersionId, orgId } = await server.services.authToken.fnValidateJwtIdentity(token);
requestContext.set("orgId", orgId);
req.auth = { req.auth = {
authMode: AuthMode.JWT, authMode: AuthMode.JWT,
user, user,
@@ -138,6 +139,7 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
case AuthMode.IDENTITY_ACCESS_TOKEN: { case AuthMode.IDENTITY_ACCESS_TOKEN: {
const identity = await server.services.identityAccessToken.fnValidateIdentityAccessToken(token, req.realIp); const identity = await server.services.identityAccessToken.fnValidateIdentityAccessToken(token, req.realIp);
const serverCfg = await getServerCfg(); const serverCfg = await getServerCfg();
requestContext.set("orgId", identity.orgId);
req.auth = { req.auth = {
authMode: AuthMode.IDENTITY_ACCESS_TOKEN, authMode: AuthMode.IDENTITY_ACCESS_TOKEN,
actor, actor,
@@ -157,6 +159,7 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
} }
case AuthMode.SERVICE_TOKEN: { case AuthMode.SERVICE_TOKEN: {
const serviceToken = await server.services.serviceToken.fnValidateServiceToken(token); const serviceToken = await server.services.serviceToken.fnValidateServiceToken(token);
requestContext.set("orgId", serviceToken.orgId);
req.auth = { req.auth = {
orgId: serviceToken.orgId, orgId: serviceToken.orgId,
authMode: AuthMode.SERVICE_TOKEN as const, authMode: AuthMode.SERVICE_TOKEN as const,
@@ -181,6 +184,7 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
} }
case AuthMode.SCIM_TOKEN: { case AuthMode.SCIM_TOKEN: {
const { orgId, scimTokenId } = await server.services.scim.fnValidateScimToken(token); const { orgId, scimTokenId } = await server.services.scim.fnValidateScimToken(token);
requestContext.set("orgId", orgId);
req.auth = { authMode: AuthMode.SCIM_TOKEN, actor, scimTokenId, orgId, authMethod: null }; req.auth = { authMode: AuthMode.SCIM_TOKEN, actor, scimTokenId, orgId, authMethod: null };
break; break;
} }
+6 -4
View File
@@ -669,7 +669,6 @@ export const registerRoutes = async (
const userService = userServiceFactory({ const userService = userServiceFactory({
userDAL, userDAL,
userAliasDAL,
orgMembershipDAL, orgMembershipDAL,
tokenService, tokenService,
permissionService, permissionService,
@@ -1060,7 +1059,8 @@ export const registerRoutes = async (
secretVersionV2BridgeDAL, secretVersionV2BridgeDAL,
secretVersionTagV2BridgeDAL, secretVersionTagV2BridgeDAL,
resourceMetadataDAL, resourceMetadataDAL,
appConnectionDAL appConnectionDAL,
licenseService
}); });
const secretQueueService = secretQueueFactory({ const secretQueueService = secretQueueFactory({
@@ -1695,7 +1695,8 @@ export const registerRoutes = async (
const appConnectionService = appConnectionServiceFactory({ const appConnectionService = appConnectionServiceFactory({
appConnectionDAL, appConnectionDAL,
permissionService, permissionService,
kmsService kmsService,
licenseService
}); });
const secretSyncService = secretSyncServiceFactory({ const secretSyncService = secretSyncServiceFactory({
@@ -1706,7 +1707,8 @@ export const registerRoutes = async (
folderDAL, folderDAL,
secretSyncQueue, secretSyncQueue,
projectBotService, projectBotService,
keyStore keyStore,
licenseService
}); });
const kmipService = kmipServiceFactory({ const kmipService = kmipServiceFactory({
@@ -0,0 +1,60 @@
import z from "zod";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import {
CreateOnePassConnectionSchema,
SanitizedOnePassConnectionSchema,
UpdateOnePassConnectionSchema
} from "@app/services/app-connection/1password";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { AuthMode } from "@app/services/auth/auth-type";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerOnePassConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({
app: AppConnection.OnePass,
server,
sanitizedResponseSchema: SanitizedOnePassConnectionSchema,
createSchema: CreateOnePassConnectionSchema,
updateSchema: UpdateOnePassConnectionSchema
});
// The following endpoints are for internal Infisical App use only and not part of the public API
server.route({
method: "GET",
url: `/:connectionId/vaults`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid()
}),
response: {
200: z
.object({
id: z.string(),
name: z.string(),
type: z.string(),
items: z.number(),
attributeVersion: z.number(),
contentVersion: z.number(),
// Corresponds to ISO8601 date string
createdAt: z.string(),
updatedAt: z.string()
})
.array()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { connectionId } = req.params;
const vaults = await server.services.appConnection.onepass.listVaults(connectionId, req.permission);
return vaults;
}
});
};
@@ -1,9 +1,14 @@
import { z } from "zod"; import { z } from "zod";
import { OCIConnectionListItemSchema, SanitizedOCIConnectionSchema } from "@app/ee/services/app-connections/oci";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags } from "@app/lib/api-docs"; import { ApiDocsTags } from "@app/lib/api-docs";
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 {
OnePassConnectionListItemSchema,
SanitizedOnePassConnectionSchema
} from "@app/services/app-connection/1password";
import { Auth0ConnectionListItemSchema, SanitizedAuth0ConnectionSchema } from "@app/services/app-connection/auth0"; import { Auth0ConnectionListItemSchema, SanitizedAuth0ConnectionSchema } from "@app/services/app-connection/auth0";
import { AwsConnectionListItemSchema, SanitizedAwsConnectionSchema } from "@app/services/app-connection/aws"; import { AwsConnectionListItemSchema, SanitizedAwsConnectionSchema } from "@app/services/app-connection/aws";
import { import {
@@ -38,7 +43,6 @@ import {
} from "@app/services/app-connection/humanitec"; } from "@app/services/app-connection/humanitec";
import { LdapConnectionListItemSchema, SanitizedLdapConnectionSchema } from "@app/services/app-connection/ldap"; import { LdapConnectionListItemSchema, SanitizedLdapConnectionSchema } from "@app/services/app-connection/ldap";
import { MsSqlConnectionListItemSchema, SanitizedMsSqlConnectionSchema } from "@app/services/app-connection/mssql"; import { MsSqlConnectionListItemSchema, SanitizedMsSqlConnectionSchema } from "@app/services/app-connection/mssql";
import { OCIConnectionListItemSchema, SanitizedOCIConnectionSchema } from "@app/services/app-connection/oci";
import { import {
PostgresConnectionListItemSchema, PostgresConnectionListItemSchema,
SanitizedPostgresConnectionSchema SanitizedPostgresConnectionSchema
@@ -78,7 +82,8 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedWindmillConnectionSchema.options, ...SanitizedWindmillConnectionSchema.options,
...SanitizedLdapConnectionSchema.options, ...SanitizedLdapConnectionSchema.options,
...SanitizedTeamCityConnectionSchema.options, ...SanitizedTeamCityConnectionSchema.options,
...SanitizedOCIConnectionSchema.options ...SanitizedOCIConnectionSchema.options,
...SanitizedOnePassConnectionSchema.options
]); ]);
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
@@ -100,7 +105,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
WindmillConnectionListItemSchema, WindmillConnectionListItemSchema,
LdapConnectionListItemSchema, LdapConnectionListItemSchema,
TeamCityConnectionListItemSchema, TeamCityConnectionListItemSchema,
OCIConnectionListItemSchema OCIConnectionListItemSchema,
OnePassConnectionListItemSchema
]); ]);
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => { export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
@@ -1,5 +1,7 @@
import { registerOCIConnectionRouter } from "@app/ee/routes/v1/app-connection-routers/oci-connection-router";
import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { registerOnePassConnectionRouter } from "./1password-connection-router";
import { registerAuth0ConnectionRouter } from "./auth0-connection-router"; import { registerAuth0ConnectionRouter } from "./auth0-connection-router";
import { registerAwsConnectionRouter } from "./aws-connection-router"; import { registerAwsConnectionRouter } from "./aws-connection-router";
import { registerAzureAppConfigurationConnectionRouter } from "./azure-app-configuration-connection-router"; import { registerAzureAppConfigurationConnectionRouter } from "./azure-app-configuration-connection-router";
@@ -13,7 +15,6 @@ import { registerHCVaultConnectionRouter } from "./hc-vault-connection-router";
import { registerHumanitecConnectionRouter } from "./humanitec-connection-router"; import { registerHumanitecConnectionRouter } from "./humanitec-connection-router";
import { registerLdapConnectionRouter } from "./ldap-connection-router"; import { registerLdapConnectionRouter } from "./ldap-connection-router";
import { registerMsSqlConnectionRouter } from "./mssql-connection-router"; import { registerMsSqlConnectionRouter } from "./mssql-connection-router";
import { registerOCIConnectionRouter } from "./oci-connection-router";
import { registerPostgresConnectionRouter } from "./postgres-connection-router"; import { registerPostgresConnectionRouter } from "./postgres-connection-router";
import { registerTeamCityConnectionRouter } from "./teamcity-connection-router"; import { registerTeamCityConnectionRouter } from "./teamcity-connection-router";
import { registerTerraformCloudConnectionRouter } from "./terraform-cloud-router"; import { registerTerraformCloudConnectionRouter } from "./terraform-cloud-router";
@@ -42,5 +43,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
[AppConnection.HCVault]: registerHCVaultConnectionRouter, [AppConnection.HCVault]: registerHCVaultConnectionRouter,
[AppConnection.LDAP]: registerLdapConnectionRouter, [AppConnection.LDAP]: registerLdapConnectionRouter,
[AppConnection.TeamCity]: registerTeamCityConnectionRouter, [AppConnection.TeamCity]: registerTeamCityConnectionRouter,
[AppConnection.OCI]: registerOCIConnectionRouter [AppConnection.OCI]: registerOCIConnectionRouter,
[AppConnection.OnePass]: registerOnePassConnectionRouter
}; };
@@ -16,7 +16,12 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
method: "POST", method: "POST",
schema: { schema: {
body: z.object({ body: z.object({
inviteeEmails: z.array(z.string().trim().email()), inviteeEmails: z
.string()
.trim()
.email()
.array()
.refine((val) => val.every((el) => el === el.toLowerCase()), "Email must be lowercase"),
organizationId: z.string().trim(), organizationId: z.string().trim(),
projects: z projects: z
.object({ .object({
@@ -115,7 +120,11 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
}, },
schema: { schema: {
body: z.object({ body: z.object({
email: z.string().trim().email(), email: z
.string()
.trim()
.email()
.refine((val) => val === val.toLowerCase(), "Email must be lowercase"),
organizationId: z.string().trim(), organizationId: z.string().trim(),
code: z.string().trim() code: z.string().trim()
}), }),
@@ -312,8 +312,17 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
data: req.body data: req.body
}); });
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.UPDATE_ORG,
metadata: req.body
}
});
return { return {
message: "Successfully changed organization name", message: "Successfully updated organization",
organization organization
}; };
} }
@@ -263,6 +263,17 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
actor: req.permission.type, actor: req.permission.type,
actorOrgId: req.permission.orgId actorOrgId: req.permission.orgId
}); });
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: req.params.workspaceId,
event: {
type: EventType.DELETE_PROJECT,
metadata: workspace
}
});
return { workspace }; return { workspace };
} }
}); });
@@ -297,6 +308,17 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
projectId: req.params.workspaceId, projectId: req.params.workspaceId,
name: req.body.name name: req.body.name
}); });
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: req.params.workspaceId,
event: {
type: EventType.UPDATE_PROJECT,
metadata: req.body
}
});
return { return {
message: "Successfully changed workspace name", message: "Successfully changed workspace name",
workspace workspace
@@ -375,6 +397,17 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
actor: req.permission.type, actor: req.permission.type,
actorOrgId: req.permission.orgId actorOrgId: req.permission.orgId
}); });
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: req.params.workspaceId,
event: {
type: EventType.UPDATE_PROJECT,
metadata: req.body
}
});
return { return {
workspace workspace
}; };
@@ -411,6 +444,17 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
projectId: req.params.workspaceId, projectId: req.params.workspaceId,
autoCapitalization: req.body.autoCapitalization autoCapitalization: req.body.autoCapitalization
}); });
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: req.params.workspaceId,
event: {
type: EventType.UPDATE_PROJECT,
metadata: req.body
}
});
return { return {
message: "Successfully changed workspace settings", message: "Successfully changed workspace settings",
workspace workspace
@@ -448,6 +492,17 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
projectId: req.params.workspaceId, projectId: req.params.workspaceId,
hasDeleteProtection: req.body.hasDeleteProtection hasDeleteProtection: req.body.hasDeleteProtection
}); });
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: req.params.workspaceId,
event: {
type: EventType.UPDATE_PROJECT,
metadata: req.body
}
});
return { return {
message: "Successfully changed workspace settings", message: "Successfully changed workspace settings",
workspace workspace
@@ -486,6 +541,16 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
workspaceSlug: req.params.workspaceSlug workspaceSlug: req.params.workspaceSlug
}); });
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: workspace.id,
event: {
type: EventType.UPDATE_PROJECT,
metadata: req.body
}
});
return { return {
message: "Successfully changed workspace version limit", message: "Successfully changed workspace version limit",
workspace workspace
@@ -524,6 +589,16 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
auditLogsRetentionDays: req.body.auditLogsRetentionDays auditLogsRetentionDays: req.body.auditLogsRetentionDays
}); });
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: workspace.id,
event: {
type: EventType.UPDATE_PROJECT,
metadata: req.body
}
});
return { return {
message: "Successfully updated project's audit logs retention period", message: "Successfully updated project's audit logs retention period",
workspace workspace
@@ -0,0 +1,17 @@
import {
CreateOnePassSyncSchema,
OnePassSyncSchema,
UpdateOnePassSyncSchema
} from "@app/services/secret-sync/1password";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
export const registerOnePassSyncRouter = async (server: FastifyZodProvider) =>
registerSyncSecretsEndpoints({
destination: SecretSync.OnePass,
server,
responseSchema: OnePassSyncSchema,
createSchema: CreateOnePassSyncSchema,
updateSchema: UpdateOnePassSyncSchema
});
@@ -1,5 +1,7 @@
import { registerOCIVaultSyncRouter } from "@app/ee/routes/v1/secret-sync-routers/oci-vault-sync-router";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { registerOnePassSyncRouter } from "./1password-sync-router";
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 { registerAwsSecretsManagerSyncRouter } from "./aws-secrets-manager-sync-router";
import { registerAzureAppConfigurationSyncRouter } from "./azure-app-configuration-sync-router"; import { registerAzureAppConfigurationSyncRouter } from "./azure-app-configuration-sync-router";
@@ -10,7 +12,6 @@ import { registerGcpSyncRouter } from "./gcp-sync-router";
import { registerGitHubSyncRouter } from "./github-sync-router"; import { registerGitHubSyncRouter } from "./github-sync-router";
import { registerHCVaultSyncRouter } from "./hc-vault-sync-router"; import { registerHCVaultSyncRouter } from "./hc-vault-sync-router";
import { registerHumanitecSyncRouter } from "./humanitec-sync-router"; import { registerHumanitecSyncRouter } from "./humanitec-sync-router";
import { registerOCIVaultSyncRouter } from "./oci-vault-sync-router";
import { registerTeamCitySyncRouter } from "./teamcity-sync-router"; import { registerTeamCitySyncRouter } from "./teamcity-sync-router";
import { registerTerraformCloudSyncRouter } from "./terraform-cloud-sync-router"; import { registerTerraformCloudSyncRouter } from "./terraform-cloud-sync-router";
import { registerVercelSyncRouter } from "./vercel-sync-router"; import { registerVercelSyncRouter } from "./vercel-sync-router";
@@ -33,5 +34,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: Fastif
[SecretSync.Windmill]: registerWindmillSyncRouter, [SecretSync.Windmill]: registerWindmillSyncRouter,
[SecretSync.HCVault]: registerHCVaultSyncRouter, [SecretSync.HCVault]: registerHCVaultSyncRouter,
[SecretSync.TeamCity]: registerTeamCitySyncRouter, [SecretSync.TeamCity]: registerTeamCitySyncRouter,
[SecretSync.OCIVault]: registerOCIVaultSyncRouter [SecretSync.OCIVault]: registerOCIVaultSyncRouter,
[SecretSync.OnePass]: registerOnePassSyncRouter
}; };
@@ -1,10 +1,12 @@
import { z } from "zod"; import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { OCIVaultSyncListItemSchema, OCIVaultSyncSchema } from "@app/ee/services/secret-sync/oci-vault";
import { ApiDocsTags, SecretSyncs } from "@app/lib/api-docs"; import { ApiDocsTags, SecretSyncs } from "@app/lib/api-docs";
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 { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { OnePassSyncListItemSchema, OnePassSyncSchema } from "@app/services/secret-sync/1password";
import { import {
AwsParameterStoreSyncListItemSchema, AwsParameterStoreSyncListItemSchema,
AwsParameterStoreSyncSchema AwsParameterStoreSyncSchema
@@ -24,7 +26,6 @@ import { GcpSyncListItemSchema, GcpSyncSchema } from "@app/services/secret-sync/
import { GitHubSyncListItemSchema, GitHubSyncSchema } from "@app/services/secret-sync/github"; import { GitHubSyncListItemSchema, GitHubSyncSchema } from "@app/services/secret-sync/github";
import { HCVaultSyncListItemSchema, HCVaultSyncSchema } from "@app/services/secret-sync/hc-vault"; import { HCVaultSyncListItemSchema, HCVaultSyncSchema } from "@app/services/secret-sync/hc-vault";
import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/secret-sync/humanitec"; import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/secret-sync/humanitec";
import { OCIVaultSyncListItemSchema, OCIVaultSyncSchema } from "@app/services/secret-sync/oci-vault";
import { TeamCitySyncListItemSchema, TeamCitySyncSchema } from "@app/services/secret-sync/teamcity"; import { TeamCitySyncListItemSchema, TeamCitySyncSchema } from "@app/services/secret-sync/teamcity";
import { TerraformCloudSyncListItemSchema, TerraformCloudSyncSchema } from "@app/services/secret-sync/terraform-cloud"; import { TerraformCloudSyncListItemSchema, TerraformCloudSyncSchema } from "@app/services/secret-sync/terraform-cloud";
import { VercelSyncListItemSchema, VercelSyncSchema } from "@app/services/secret-sync/vercel"; import { VercelSyncListItemSchema, VercelSyncSchema } from "@app/services/secret-sync/vercel";
@@ -45,7 +46,8 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
WindmillSyncSchema, WindmillSyncSchema,
HCVaultSyncSchema, HCVaultSyncSchema,
TeamCitySyncSchema, TeamCitySyncSchema,
OCIVaultSyncSchema OCIVaultSyncSchema,
OnePassSyncSchema
]); ]);
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
@@ -63,7 +65,8 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
WindmillSyncListItemSchema, WindmillSyncListItemSchema,
HCVaultSyncListItemSchema, HCVaultSyncListItemSchema,
TeamCitySyncListItemSchema, TeamCitySyncListItemSchema,
OCIVaultSyncListItemSchema OCIVaultSyncListItemSchema,
OnePassSyncListItemSchema
]); ]);
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => { export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {
@@ -46,6 +46,54 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
} }
}); });
server.route({
method: "GET",
url: "/duplicate-accounts",
config: {
rateLimit: readLimit
},
schema: {
response: {
200: z.object({
users: UsersSchema.extend({
isMyAccount: z.boolean(),
organizations: z.object({ name: z.string(), slug: z.string() }).array()
}).array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }),
handler: async (req) => {
if (req.auth.authMode === AuthMode.JWT && req.auth.user.email) {
const users = await server.services.user.getAllMyAccounts(req.auth.user.email, req.permission.id);
return { users };
}
return { users: [] };
}
});
server.route({
method: "POST",
url: "/remove-duplicate-accounts",
config: {
rateLimit: writeLimit
},
schema: {
response: {
200: z.object({
message: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }),
handler: async (req) => {
if (req.auth.authMode === AuthMode.JWT && req.auth.user.email) {
await server.services.user.removeMyDuplicateAccounts(req.auth.user.email, req.permission.id);
}
return { message: "Removed all duplicate accounts" };
}
});
server.route({ server.route({
method: "GET", method: "GET",
url: "/private-key", url: "/private-key",
@@ -27,8 +27,19 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
projectId: z.string().describe(PROJECT_USERS.INVITE_MEMBER.projectId) projectId: z.string().describe(PROJECT_USERS.INVITE_MEMBER.projectId)
}), }),
body: z.object({ body: z.object({
emails: z.string().email().array().default([]).describe(PROJECT_USERS.INVITE_MEMBER.emails), emails: z
usernames: z.string().array().default([]).describe(PROJECT_USERS.INVITE_MEMBER.usernames), .string()
.email()
.array()
.default([])
.describe(PROJECT_USERS.INVITE_MEMBER.emails)
.refine((val) => val.every((el) => el === el.toLowerCase()), "Email must be lowercase"),
usernames: z
.string()
.array()
.default([])
.describe(PROJECT_USERS.INVITE_MEMBER.usernames)
.refine((val) => val.every((el) => el === el.toLowerCase()), "Username must be lowercase"),
roleSlugs: z.string().array().min(1).optional().describe(PROJECT_USERS.INVITE_MEMBER.roleSlugs) roleSlugs: z.string().array().min(1).optional().describe(PROJECT_USERS.INVITE_MEMBER.roleSlugs)
}), }),
response: { response: {
@@ -92,8 +103,19 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
projectId: z.string().describe(PROJECT_USERS.REMOVE_MEMBER.projectId) projectId: z.string().describe(PROJECT_USERS.REMOVE_MEMBER.projectId)
}), }),
body: z.object({ body: z.object({
emails: z.string().email().array().default([]).describe(PROJECT_USERS.REMOVE_MEMBER.emails), emails: z
usernames: z.string().array().default([]).describe(PROJECT_USERS.REMOVE_MEMBER.usernames) .string()
.email()
.array()
.default([])
.describe(PROJECT_USERS.REMOVE_MEMBER.emails)
.refine((val) => val.every((el) => el === el.toLowerCase()), "Email must be lowercase"),
usernames: z
.string()
.array()
.default([])
.describe(PROJECT_USERS.REMOVE_MEMBER.usernames)
.refine((val) => val.every((el) => el === el.toLowerCase()), "Username must be lowercase")
}), }),
response: { response: {
200: z.object({ 200: z.object({
+31 -12
View File
@@ -206,19 +206,18 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
} }
}); });
if (req.body.template) { await server.services.auditLog.createAuditLog({
await server.services.auditLog.createAuditLog({ ...req.auditLogInfo,
...req.auditLogInfo, orgId: req.permission.orgId,
orgId: req.permission.orgId, projectId: project.id,
event: { event: {
type: EventType.APPLY_PROJECT_TEMPLATE, type: EventType.CREATE_PROJECT,
metadata: { metadata: {
template: req.body.template, ...req.body,
projectId: project.id name: req.body.projectName
}
} }
}); }
} });
return { project }; return { project };
} }
@@ -262,6 +261,16 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
actor: req.permission.type actor: req.permission.type
}); });
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: project.id,
event: {
type: EventType.DELETE_PROJECT,
metadata: project
}
});
return project; return project;
} }
}); });
@@ -341,6 +350,16 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
actorOrgId: req.permission.orgId actorOrgId: req.permission.orgId
}); });
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: project.id,
event: {
type: EventType.UPDATE_PROJECT,
metadata: req.body
}
});
return project; return project;
} }
}); });
@@ -0,0 +1,3 @@
export enum OnePassConnectionMethod {
ApiToken = "api-token"
}
@@ -0,0 +1,66 @@
import { AxiosError } from "axios";
import { request } from "@app/lib/config/request";
import { BadRequestError } from "@app/lib/errors";
import { removeTrailingSlash } from "@app/lib/fn";
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { OnePassConnectionMethod } from "./1password-connection-enums";
import { TOnePassConnection, TOnePassConnectionConfig, TOnePassVault } from "./1password-connection-types";
export const getOnePassInstanceUrl = async (config: TOnePassConnectionConfig) => {
const instanceUrl = removeTrailingSlash(config.credentials.instanceUrl);
await blockLocalAndPrivateIpAddresses(instanceUrl);
return instanceUrl;
};
export const getOnePassConnectionListItem = () => {
return {
name: "1Password" as const,
app: AppConnection.OnePass as const,
methods: Object.values(OnePassConnectionMethod) as [OnePassConnectionMethod.ApiToken]
};
};
export const validateOnePassConnectionCredentials = async (config: TOnePassConnectionConfig) => {
const instanceUrl = await getOnePassInstanceUrl(config);
const { apiToken } = config.credentials;
try {
await request.get(`${instanceUrl}/v1/vaults`, {
headers: {
Authorization: `Bearer ${apiToken}`,
Accept: "application/json"
}
});
} catch (error: unknown) {
if (error instanceof AxiosError) {
throw new BadRequestError({
message: `Failed to validate credentials: ${error.message || "Unknown error"}`
});
}
throw new BadRequestError({
message: "Unable to validate connection: verify credentials"
});
}
return config.credentials;
};
export const listOnePassVaults = async (appConnection: TOnePassConnection) => {
const instanceUrl = await getOnePassInstanceUrl(appConnection);
const { apiToken } = appConnection.credentials;
const resp = await request.get<TOnePassVault[]>(`${instanceUrl}/v1/vaults`, {
headers: {
Authorization: `Bearer ${apiToken}`,
Accept: "application/json"
}
});
return resp.data;
};
@@ -0,0 +1,64 @@
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 { OnePassConnectionMethod } from "./1password-connection-enums";
export const OnePassConnectionAccessTokenCredentialsSchema = z.object({
apiToken: z.string().trim().min(1, "API Token required").describe(AppConnections.CREDENTIALS.ONEPASS.apiToken),
instanceUrl: z
.string()
.trim()
.url("Invalid Connect Server instance URL")
.min(1, "Instance URL required")
.describe(AppConnections.CREDENTIALS.ONEPASS.instanceUrl)
});
const BaseOnePassConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.OnePass) });
export const OnePassConnectionSchema = BaseOnePassConnectionSchema.extend({
method: z.literal(OnePassConnectionMethod.ApiToken),
credentials: OnePassConnectionAccessTokenCredentialsSchema
});
export const SanitizedOnePassConnectionSchema = z.discriminatedUnion("method", [
BaseOnePassConnectionSchema.extend({
method: z.literal(OnePassConnectionMethod.ApiToken),
credentials: OnePassConnectionAccessTokenCredentialsSchema.pick({
instanceUrl: true
})
})
]);
export const ValidateOnePassConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: z.literal(OnePassConnectionMethod.ApiToken).describe(AppConnections.CREATE(AppConnection.OnePass).method),
credentials: OnePassConnectionAccessTokenCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.OnePass).credentials
)
})
]);
export const CreateOnePassConnectionSchema = ValidateOnePassConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.OnePass)
);
export const UpdateOnePassConnectionSchema = z
.object({
credentials: OnePassConnectionAccessTokenCredentialsSchema.optional().describe(
AppConnections.UPDATE(AppConnection.OnePass).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.OnePass));
export const OnePassConnectionListItemSchema = z.object({
name: z.literal("1Password"),
app: z.literal(AppConnection.OnePass),
methods: z.nativeEnum(OnePassConnectionMethod).array()
});
@@ -0,0 +1,30 @@
import { logger } from "@app/lib/logger";
import { OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import { listOnePassVaults } from "./1password-connection-fns";
import { TOnePassConnection } from "./1password-connection-types";
type TGetAppConnectionFunc = (
app: AppConnection,
connectionId: string,
actor: OrgServiceActor
) => Promise<TOnePassConnection>;
export const onePassConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
const listVaults = async (connectionId: string, actor: OrgServiceActor) => {
const appConnection = await getAppConnection(AppConnection.OnePass, connectionId, actor);
try {
const vaults = await listOnePassVaults(appConnection);
return vaults;
} catch (error) {
logger.error(error, "Failed to establish connection with 1Password");
return [];
}
};
return {
listVaults
};
};
@@ -0,0 +1,35 @@
import z from "zod";
import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import {
CreateOnePassConnectionSchema,
OnePassConnectionSchema,
ValidateOnePassConnectionCredentialsSchema
} from "./1password-connection-schemas";
export type TOnePassConnection = z.infer<typeof OnePassConnectionSchema>;
export type TOnePassConnectionInput = z.infer<typeof CreateOnePassConnectionSchema> & {
app: AppConnection.OnePass;
};
export type TValidateOnePassConnectionCredentialsSchema = typeof ValidateOnePassConnectionCredentialsSchema;
export type TOnePassConnectionConfig = DiscriminativePick<TOnePassConnectionInput, "method" | "app" | "credentials"> & {
orgId: string;
};
export type TOnePassVault = {
id: string;
name: string;
type: string;
items: number;
attributeVersion: number;
contentVersion: number;
createdAt: string;
updatedAt: string;
};
@@ -0,0 +1,4 @@
export * from "./1password-connection-enums";
export * from "./1password-connection-fns";
export * from "./1password-connection-schemas";
export * from "./1password-connection-types";
@@ -17,7 +17,8 @@ export enum AppConnection {
HCVault = "hashicorp-vault", HCVault = "hashicorp-vault",
LDAP = "ldap", LDAP = "ldap",
TeamCity = "teamcity", TeamCity = "teamcity",
OCI = "oci" OCI = "oci",
OnePass = "1password"
} }
export enum AWSRegion { export enum AWSRegion {
@@ -66,3 +67,8 @@ export enum AWSRegion {
// South America // South America
SA_EAST_1 = "sa-east-1" // Sao Paulo SA_EAST_1 = "sa-east-1" // Sao Paulo
} }
export enum AppConnectionPlanType {
Enterprise = "enterprise",
Regular = "regular"
}
@@ -1,14 +1,25 @@
import { TAppConnections } from "@app/db/schemas/app-connections"; import { TAppConnections } from "@app/db/schemas/app-connections";
import {
getOCIConnectionListItem,
OCIConnectionMethod,
validateOCIConnectionCredentials
} from "@app/ee/services/app-connections/oci";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { generateHash } from "@app/lib/crypto/encryption"; import { generateHash } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps"; import { APP_CONNECTION_NAME_MAP, APP_CONNECTION_PLAN_MAP } from "@app/services/app-connection/app-connection-maps";
import { import {
transferSqlConnectionCredentialsToPlatform, transferSqlConnectionCredentialsToPlatform,
validateSqlConnectionCredentials validateSqlConnectionCredentials
} from "@app/services/app-connection/shared/sql"; } from "@app/services/app-connection/shared/sql";
import { KmsDataKey } from "@app/services/kms/kms-types"; import { KmsDataKey } from "@app/services/kms/kms-types";
import { AppConnection } from "./app-connection-enums"; import {
getOnePassConnectionListItem,
OnePassConnectionMethod,
validateOnePassConnectionCredentials
} from "./1password";
import { AppConnection, AppConnectionPlanType } from "./app-connection-enums";
import { TAppConnectionServiceFactoryDep } from "./app-connection-service"; import { TAppConnectionServiceFactoryDep } from "./app-connection-service";
import { import {
TAppConnection, TAppConnection,
@@ -53,7 +64,6 @@ import {
} from "./humanitec"; } from "./humanitec";
import { getLdapConnectionListItem, LdapConnectionMethod, validateLdapConnectionCredentials } from "./ldap"; import { getLdapConnectionListItem, LdapConnectionMethod, validateLdapConnectionCredentials } from "./ldap";
import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql"; import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql";
import { getOCIConnectionListItem, OCIConnectionMethod, validateOCIConnectionCredentials } from "./oci";
import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres"; import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres";
import { import {
getTeamCityConnectionListItem, getTeamCityConnectionListItem,
@@ -93,7 +103,8 @@ export const listAppConnectionOptions = () => {
getHCVaultConnectionListItem(), getHCVaultConnectionListItem(),
getLdapConnectionListItem(), getLdapConnectionListItem(),
getTeamCityConnectionListItem(), getTeamCityConnectionListItem(),
getOCIConnectionListItem() getOCIConnectionListItem(),
getOnePassConnectionListItem()
].sort((a, b) => a.name.localeCompare(b.name)); ].sort((a, b) => a.name.localeCompare(b.name));
}; };
@@ -163,7 +174,8 @@ export const validateAppConnectionCredentials = async (
[AppConnection.HCVault]: validateHCVaultConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.HCVault]: validateHCVaultConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.LDAP]: validateLdapConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.LDAP]: validateLdapConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.TeamCity]: validateTeamCityConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.TeamCity]: validateTeamCityConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.OCI]: validateOCIConnectionCredentials as TAppConnectionCredentialsValidator [AppConnection.OCI]: validateOCIConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.OnePass]: validateOnePassConnectionCredentials as TAppConnectionCredentialsValidator
}; };
return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection); return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection);
@@ -192,6 +204,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
case HumanitecConnectionMethod.ApiToken: case HumanitecConnectionMethod.ApiToken:
case TerraformCloudConnectionMethod.ApiToken: case TerraformCloudConnectionMethod.ApiToken:
case VercelConnectionMethod.ApiToken: case VercelConnectionMethod.ApiToken:
case OnePassConnectionMethod.ApiToken:
return "API Token"; return "API Token";
case PostgresConnectionMethod.UsernameAndPassword: case PostgresConnectionMethod.UsernameAndPassword:
case MsSqlConnectionMethod.UsernameAndPassword: case MsSqlConnectionMethod.UsernameAndPassword:
@@ -255,5 +268,21 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
[AppConnection.HCVault]: platformManagedCredentialsNotSupported, [AppConnection.HCVault]: platformManagedCredentialsNotSupported,
[AppConnection.LDAP]: platformManagedCredentialsNotSupported, // we could support this in the future [AppConnection.LDAP]: platformManagedCredentialsNotSupported, // we could support this in the future
[AppConnection.TeamCity]: platformManagedCredentialsNotSupported, [AppConnection.TeamCity]: platformManagedCredentialsNotSupported,
[AppConnection.OCI]: platformManagedCredentialsNotSupported [AppConnection.OCI]: platformManagedCredentialsNotSupported,
[AppConnection.OnePass]: platformManagedCredentialsNotSupported
};
export const enterpriseAppCheck = async (
licenseService: Pick<TLicenseServiceFactory, "getPlan">,
appConnection: AppConnection,
orgId: string,
errorMessage: string
) => {
if (APP_CONNECTION_PLAN_MAP[appConnection] === AppConnectionPlanType.Enterprise) {
const plan = await licenseService.getPlan(orgId);
if (!plan.enterpriseAppConnections)
throw new BadRequestError({
message: errorMessage
});
}
}; };
@@ -1,4 +1,4 @@
import { AppConnection } from "./app-connection-enums"; import { AppConnection, AppConnectionPlanType } 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",
@@ -19,5 +19,29 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.HCVault]: "Hashicorp Vault", [AppConnection.HCVault]: "Hashicorp Vault",
[AppConnection.LDAP]: "LDAP", [AppConnection.LDAP]: "LDAP",
[AppConnection.TeamCity]: "TeamCity", [AppConnection.TeamCity]: "TeamCity",
[AppConnection.OCI]: "OCI" [AppConnection.OCI]: "OCI",
[AppConnection.OnePass]: "1Password"
};
export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanType> = {
[AppConnection.AWS]: AppConnectionPlanType.Regular,
[AppConnection.GitHub]: AppConnectionPlanType.Regular,
[AppConnection.GCP]: AppConnectionPlanType.Regular,
[AppConnection.AzureKeyVault]: AppConnectionPlanType.Regular,
[AppConnection.AzureAppConfiguration]: AppConnectionPlanType.Regular,
[AppConnection.AzureClientSecrets]: AppConnectionPlanType.Regular,
[AppConnection.Databricks]: AppConnectionPlanType.Regular,
[AppConnection.Humanitec]: AppConnectionPlanType.Regular,
[AppConnection.TerraformCloud]: AppConnectionPlanType.Regular,
[AppConnection.Vercel]: AppConnectionPlanType.Regular,
[AppConnection.Postgres]: AppConnectionPlanType.Regular,
[AppConnection.MsSql]: AppConnectionPlanType.Regular,
[AppConnection.Camunda]: AppConnectionPlanType.Regular,
[AppConnection.Windmill]: AppConnectionPlanType.Regular,
[AppConnection.Auth0]: AppConnectionPlanType.Regular,
[AppConnection.HCVault]: AppConnectionPlanType.Regular,
[AppConnection.LDAP]: AppConnectionPlanType.Regular,
[AppConnection.TeamCity]: AppConnectionPlanType.Regular,
[AppConnection.OCI]: AppConnectionPlanType.Enterprise,
[AppConnection.OnePass]: AppConnectionPlanType.Regular
}; };
@@ -1,5 +1,8 @@
import { ForbiddenError, subject } from "@casl/ability"; import { ForbiddenError, subject } from "@casl/ability";
import { ValidateOCIConnectionCredentialsSchema } from "@app/ee/services/app-connections/oci";
import { ociConnectionService } from "@app/ee/services/app-connections/oci/oci-connection-service";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
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 { generateHash } from "@app/lib/crypto/encryption";
@@ -9,6 +12,7 @@ import { DiscriminativePick, OrgServiceActor } from "@app/lib/types";
import { import {
decryptAppConnection, decryptAppConnection,
encryptAppConnectionCredentials, encryptAppConnectionCredentials,
enterpriseAppCheck,
getAppConnectionMethodName, getAppConnectionMethodName,
listAppConnectionOptions, listAppConnectionOptions,
TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM, TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM,
@@ -17,6 +21,8 @@ import {
import { auth0ConnectionService } from "@app/services/app-connection/auth0/auth0-connection-service"; import { auth0ConnectionService } from "@app/services/app-connection/auth0/auth0-connection-service";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { ValidateOnePassConnectionCredentialsSchema } from "./1password";
import { onePassConnectionService } from "./1password/1password-connection-service";
import { TAppConnectionDALFactory } from "./app-connection-dal"; import { TAppConnectionDALFactory } from "./app-connection-dal";
import { AppConnection } from "./app-connection-enums"; import { AppConnection } from "./app-connection-enums";
import { APP_CONNECTION_NAME_MAP } from "./app-connection-maps"; import { APP_CONNECTION_NAME_MAP } from "./app-connection-maps";
@@ -49,8 +55,6 @@ import { ValidateHumanitecConnectionCredentialsSchema } from "./humanitec";
import { humanitecConnectionService } from "./humanitec/humanitec-connection-service"; import { humanitecConnectionService } from "./humanitec/humanitec-connection-service";
import { ValidateLdapConnectionCredentialsSchema } from "./ldap"; import { ValidateLdapConnectionCredentialsSchema } from "./ldap";
import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql"; import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql";
import { ValidateOCIConnectionCredentialsSchema } from "./oci";
import { ociConnectionService } from "./oci/oci-connection-service";
import { ValidatePostgresConnectionCredentialsSchema } from "./postgres"; import { ValidatePostgresConnectionCredentialsSchema } from "./postgres";
import { ValidateTeamCityConnectionCredentialsSchema } from "./teamcity"; import { ValidateTeamCityConnectionCredentialsSchema } from "./teamcity";
import { teamcityConnectionService } from "./teamcity/teamcity-connection-service"; import { teamcityConnectionService } from "./teamcity/teamcity-connection-service";
@@ -65,6 +69,7 @@ export type TAppConnectionServiceFactoryDep = {
appConnectionDAL: TAppConnectionDALFactory; appConnectionDAL: TAppConnectionDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
}; };
export type TAppConnectionServiceFactory = ReturnType<typeof appConnectionServiceFactory>; export type TAppConnectionServiceFactory = ReturnType<typeof appConnectionServiceFactory>;
@@ -88,13 +93,15 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
[AppConnection.HCVault]: ValidateHCVaultConnectionCredentialsSchema, [AppConnection.HCVault]: ValidateHCVaultConnectionCredentialsSchema,
[AppConnection.LDAP]: ValidateLdapConnectionCredentialsSchema, [AppConnection.LDAP]: ValidateLdapConnectionCredentialsSchema,
[AppConnection.TeamCity]: ValidateTeamCityConnectionCredentialsSchema, [AppConnection.TeamCity]: ValidateTeamCityConnectionCredentialsSchema,
[AppConnection.OCI]: ValidateOCIConnectionCredentialsSchema [AppConnection.OCI]: ValidateOCIConnectionCredentialsSchema,
[AppConnection.OnePass]: ValidateOnePassConnectionCredentialsSchema
}; };
export const appConnectionServiceFactory = ({ export const appConnectionServiceFactory = ({
appConnectionDAL, appConnectionDAL,
permissionService, permissionService,
kmsService kmsService,
licenseService
}: TAppConnectionServiceFactoryDep) => { }: TAppConnectionServiceFactoryDep) => {
const listAppConnectionsByOrg = async (actor: OrgServiceActor, app?: AppConnection) => { const listAppConnectionsByOrg = async (actor: OrgServiceActor, app?: AppConnection) => {
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
@@ -191,6 +198,13 @@ export const appConnectionServiceFactory = ({
OrgPermissionSubjects.AppConnections OrgPermissionSubjects.AppConnections
); );
await enterpriseAppCheck(
licenseService,
app,
actor.orgId,
"Failed to create app connection due to plan restriction. Upgrade plan to access enterprise app connections."
);
const validatedCredentials = await validateAppConnectionCredentials({ const validatedCredentials = await validateAppConnectionCredentials({
app, app,
credentials, credentials,
@@ -253,6 +267,13 @@ export const appConnectionServiceFactory = ({
if (!appConnection) throw new NotFoundError({ message: `Could not find App Connection with ID ${connectionId}` }); if (!appConnection) throw new NotFoundError({ message: `Could not find App Connection with ID ${connectionId}` });
await enterpriseAppCheck(
licenseService,
appConnection.app as AppConnection,
actor.orgId,
"Failed to update app connection due to plan restriction. Upgrade plan to access enterprise app connections."
);
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor.type, actor.type,
actor.id, actor.id,
@@ -399,6 +420,13 @@ export const appConnectionServiceFactory = ({
if (!appConnection) throw new NotFoundError({ message: `Could not find App Connection with ID ${connectionId}` }); if (!appConnection) throw new NotFoundError({ message: `Could not find App Connection with ID ${connectionId}` });
await enterpriseAppCheck(
licenseService,
app,
actor.orgId,
"Failed to connect app due to plan restriction. Upgrade plan to access enterprise app connections."
);
const { permission: orgPermission } = await permissionService.getOrgPermission( const { permission: orgPermission } = await permissionService.getOrgPermission(
actor.type, actor.type,
actor.id, actor.id,
@@ -468,6 +496,7 @@ export const appConnectionServiceFactory = ({
hcvault: hcVaultConnectionService(connectAppConnectionById), hcvault: hcVaultConnectionService(connectAppConnectionById),
windmill: windmillConnectionService(connectAppConnectionById), windmill: windmillConnectionService(connectAppConnectionById),
teamcity: teamcityConnectionService(connectAppConnectionById), teamcity: teamcityConnectionService(connectAppConnectionById),
oci: ociConnectionService(connectAppConnectionById) oci: ociConnectionService(connectAppConnectionById, licenseService),
onepass: onePassConnectionService(connectAppConnectionById)
}; };
}; };
@@ -1,7 +1,19 @@
import {
TOCIConnection,
TOCIConnectionConfig,
TOCIConnectionInput,
TValidateOCIConnectionCredentialsSchema
} from "@app/ee/services/app-connections/oci";
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
import { TSqlConnectionConfig } from "@app/services/app-connection/shared/sql/sql-connection-types"; import { TSqlConnectionConfig } from "@app/services/app-connection/shared/sql/sql-connection-types";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import {
TOnePassConnection,
TOnePassConnectionConfig,
TOnePassConnectionInput,
TValidateOnePassConnectionCredentialsSchema
} from "./1password";
import { AWSRegion } from "./app-connection-enums"; import { AWSRegion } from "./app-connection-enums";
import { import {
TAuth0Connection, TAuth0Connection,
@@ -76,12 +88,6 @@ import {
TValidateLdapConnectionCredentialsSchema TValidateLdapConnectionCredentialsSchema
} from "./ldap"; } from "./ldap";
import { TMsSqlConnection, TMsSqlConnectionInput, TValidateMsSqlConnectionCredentialsSchema } from "./mssql"; import { TMsSqlConnection, TMsSqlConnectionInput, TValidateMsSqlConnectionCredentialsSchema } from "./mssql";
import {
TOCIConnection,
TOCIConnectionConfig,
TOCIConnectionInput,
TValidateOCIConnectionCredentialsSchema
} from "./oci";
import { import {
TPostgresConnection, TPostgresConnection,
TPostgresConnectionInput, TPostgresConnectionInput,
@@ -132,6 +138,7 @@ export type TAppConnection = { id: string } & (
| TLdapConnection | TLdapConnection
| TTeamCityConnection | TTeamCityConnection
| TOCIConnection | TOCIConnection
| TOnePassConnection
); );
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>; export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
@@ -158,6 +165,7 @@ export type TAppConnectionInput = { id: string } & (
| TLdapConnectionInput | TLdapConnectionInput
| TTeamCityConnectionInput | TTeamCityConnectionInput
| TOCIConnectionInput | TOCIConnectionInput
| TOnePassConnectionInput
); );
export type TSqlConnectionInput = TPostgresConnectionInput | TMsSqlConnectionInput; export type TSqlConnectionInput = TPostgresConnectionInput | TMsSqlConnectionInput;
@@ -189,7 +197,8 @@ export type TAppConnectionConfig =
| THCVaultConnectionConfig | THCVaultConnectionConfig
| TLdapConnectionConfig | TLdapConnectionConfig
| TTeamCityConnectionConfig | TTeamCityConnectionConfig
| TOCIConnectionConfig; | TOCIConnectionConfig
| TOnePassConnectionConfig;
export type TValidateAppConnectionCredentialsSchema = export type TValidateAppConnectionCredentialsSchema =
| TValidateAwsConnectionCredentialsSchema | TValidateAwsConnectionCredentialsSchema
@@ -210,7 +219,8 @@ export type TValidateAppConnectionCredentialsSchema =
| TValidateHCVaultConnectionCredentialsSchema | TValidateHCVaultConnectionCredentialsSchema
| TValidateLdapConnectionCredentialsSchema | TValidateLdapConnectionCredentialsSchema
| TValidateTeamCityConnectionCredentialsSchema | TValidateTeamCityConnectionCredentialsSchema
| TValidateOCIConnectionCredentialsSchema; | TValidateOCIConnectionCredentialsSchema
| TValidateOnePassConnectionCredentialsSchema;
export type TListAwsConnectionKmsKeys = { export type TListAwsConnectionKmsKeys = {
connectionId: string; connectionId: string;
@@ -1,8 +1,7 @@
import RE2 from "re2";
import { z } from "zod"; import { z } from "zod";
import { AppConnections } from "@app/lib/api-docs"; import { AppConnections } from "@app/lib/api-docs";
import { DistinguishedNameRegex } from "@app/lib/regex"; import { DistinguishedNameRegex, LdapUrlRegex, UserPrincipalNameRegex } from "@app/lib/regex";
import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { import {
BaseAppConnectionSchema, BaseAppConnectionSchema,
@@ -14,17 +13,14 @@ import { LdapConnectionMethod, LdapProvider } from "./ldap-connection-enums";
export const LdapConnectionSimpleBindCredentialsSchema = z.object({ export const LdapConnectionSimpleBindCredentialsSchema = z.object({
provider: z.nativeEnum(LdapProvider).describe(AppConnections.CREDENTIALS.LDAP.provider), provider: z.nativeEnum(LdapProvider).describe(AppConnections.CREDENTIALS.LDAP.provider),
url: z url: z.string().trim().min(1, "URL required").regex(LdapUrlRegex).describe(AppConnections.CREDENTIALS.LDAP.url),
.string()
.trim()
.min(1, "URL required")
.regex(new RE2(/^ldaps?:\/\//))
.describe(AppConnections.CREDENTIALS.LDAP.url),
dn: z dn: z
.string() .string()
.trim() .trim()
.regex(new RE2(DistinguishedNameRegex), "Invalid DN format, ie; CN=user,OU=users,DC=example,DC=com") .min(1, "DN/UPN required")
.min(1, "Distinguished Name (DN) required") .refine((value) => DistinguishedNameRegex.test(value) || UserPrincipalNameRegex.test(value), {
message: "Invalid DN/UPN format"
})
.describe(AppConnections.CREDENTIALS.LDAP.dn), .describe(AppConnections.CREDENTIALS.LDAP.dn),
password: z.string().trim().min(1, "Password required").describe(AppConnections.CREDENTIALS.LDAP.password), password: z.string().trim().min(1, "Password required").describe(AppConnections.CREDENTIALS.LDAP.password),
sslRejectUnauthorized: z.boolean().optional().describe(AppConnections.CREDENTIALS.LDAP.sslRejectUnauthorized), sslRejectUnauthorized: z.boolean().optional().describe(AppConnections.CREDENTIALS.LDAP.sslRejectUnauthorized),
@@ -199,9 +199,12 @@ export const authLoginServiceFactory = ({
providerAuthToken, providerAuthToken,
clientPublicKey clientPublicKey
}: TLoginGenServerPublicKeyDTO) => { }: TLoginGenServerPublicKeyDTO) => {
const userEnc = await userDAL.findUserEncKeyByUsername({ // akhilmhdh: case sensitive email resolution
const usersByUsername = await userDAL.findUserEncKeyByUsername({
username: email username: email
}); });
const userEnc =
usersByUsername?.length > 1 ? usersByUsername.find((el) => el.username === email) : usersByUsername?.[0];
const serverCfg = await getServerCfg(); const serverCfg = await getServerCfg();
@@ -250,9 +253,12 @@ export const authLoginServiceFactory = ({
}: TLoginClientProofDTO) => { }: TLoginClientProofDTO) => {
const appCfg = getConfig(); const appCfg = getConfig();
const userEnc = await userDAL.findUserEncKeyByUsername({ // akhilmhdh: case sensitive email resolution
const usersByUsername = await userDAL.findUserEncKeyByUsername({
username: email username: email
}); });
const userEnc =
usersByUsername?.length > 1 ? usersByUsername.find((el) => el.username === email) : usersByUsername?.[0];
if (!userEnc) throw new Error("Failed to find user"); if (!userEnc) throw new Error("Failed to find user");
const user = await userDAL.findById(userEnc.userId); const user = await userDAL.findById(userEnc.userId);
const cfg = getConfig(); const cfg = getConfig();
@@ -649,10 +655,12 @@ export const authLoginServiceFactory = ({
* OAuth2 login for google,github, and other oauth2 provider * OAuth2 login for google,github, and other oauth2 provider
* */ * */
const oauth2Login = async ({ email, firstName, lastName, authMethod, callbackPort }: TOauthLoginDTO) => { const oauth2Login = async ({ email, firstName, lastName, authMethod, callbackPort }: TOauthLoginDTO) => {
let user = await userDAL.findUserByUsername(email); // akhilmhdh: case sensitive email resolution
const usersByUsername = await userDAL.findUserByUsername(email);
let user = usersByUsername?.length > 1 ? usersByUsername.find((el) => el.username === email) : usersByUsername?.[0];
const serverCfg = await getServerCfg(); const serverCfg = await getServerCfg();
if (serverCfg.enabledLoginMethods) { if (serverCfg.enabledLoginMethods && user) {
switch (authMethod) { switch (authMethod) {
case AuthMethod.GITHUB: { case AuthMethod.GITHUB: {
if (!serverCfg.enabledLoginMethods.includes(LoginMethod.GITHUB)) { if (!serverCfg.enabledLoginMethods.includes(LoginMethod.GITHUB)) {
@@ -715,8 +723,8 @@ export const authLoginServiceFactory = ({
} }
user = await userDAL.create({ user = await userDAL.create({
username: email, username: email.trim().toLowerCase(),
email, email: email.trim().toLowerCase(),
isEmailVerified: true, isEmailVerified: true,
firstName, firstName,
lastName, lastName,
@@ -814,11 +822,14 @@ export const authLoginServiceFactory = ({
? decodedProviderToken.orgId ? decodedProviderToken.orgId
: undefined; : undefined;
const userEnc = await userDAL.findUserEncKeyByUsername({ // akhilmhdh: case sensitive email resolution
const usersByUsername = await userDAL.findUserEncKeyByUsername({
username: email username: email
}); });
if (!userEnc) throw new BadRequestError({ message: "Invalid token" }); const userEnc =
if (!userEnc.serverEncryptedPrivateKey) usersByUsername?.length > 1 ? usersByUsername.find((el) => el.username === email) : usersByUsername?.[0];
if (!userEnc?.serverEncryptedPrivateKey)
throw new BadRequestError({ message: "Key handoff incomplete. Please try logging in again." }); throw new BadRequestError({ message: "Key handoff incomplete. Please try logging in again." });
const token = await generateUserTokens({ const token = await generateUserTokens({
@@ -121,7 +121,10 @@ export const authPaswordServiceFactory = ({
*/ */
const sendPasswordResetEmail = async (email: string) => { const sendPasswordResetEmail = async (email: string) => {
const sendEmail = async () => { const sendEmail = async () => {
const user = await userDAL.findUserByUsername(email); const users = await userDAL.findUserByUsername(email);
// akhilmhdh: case sensitive email resolution
const user = users?.length > 1 ? users.find((el) => el.username === email) : users?.[0];
if (!user) throw new BadRequestError({ message: "Failed to find user data" });
if (user && user.isAccepted) { if (user && user.isAccepted) {
const cfg = getConfig(); const cfg = getConfig();
@@ -152,7 +155,10 @@ export const authPaswordServiceFactory = ({
* */ * */
const verifyPasswordResetEmail = async (email: string, code: string) => { const verifyPasswordResetEmail = async (email: string, code: string) => {
const cfg = getConfig(); const cfg = getConfig();
const user = await userDAL.findUserByUsername(email); const users = await userDAL.findUserByUsername(email);
// akhilmhdh: case sensitive email resolution
const user = users?.length > 1 ? users.find((el) => el.username === email) : users?.[0];
if (!user) throw new BadRequestError({ message: "Failed to find user data" });
const userEnc = await userDAL.findUserEncKeyByUserId(user.id); const userEnc = await userDAL.findUserEncKeyByUserId(user.id);
@@ -189,16 +195,15 @@ export const authPaswordServiceFactory = ({
throw new BadRequestError({ message: `User encryption key not found for user with ID '${userId}'` }); throw new BadRequestError({ message: `User encryption key not found for user with ID '${userId}'` });
} }
if (!user.hashedPassword) {
throw new BadRequestError({ message: "Unable to reset password, no password is set" });
}
if (!user.authMethods?.includes(AuthMethod.EMAIL)) { if (!user.authMethods?.includes(AuthMethod.EMAIL)) {
throw new BadRequestError({ message: "Unable to reset password, no email authentication method is configured" }); throw new BadRequestError({ message: "Unable to reset password, no email authentication method is configured" });
} }
// we check the old password if the user is resetting their password while logged in // we check the old password if the user is resetting their password while logged in
if (type === ResetPasswordV2Type.LoggedInReset) { if (type === ResetPasswordV2Type.LoggedInReset) {
if (!user.hashedPassword) {
throw new BadRequestError({ message: "Unable to change password, no password is set" });
}
if (!oldPassword) { if (!oldPassword) {
throw new BadRequestError({ message: "Current password is required." }); throw new BadRequestError({ message: "Current password is required." });
} }
@@ -73,18 +73,27 @@ export const authSignupServiceFactory = ({
}: TAuthSignupDep) => { }: TAuthSignupDep) => {
// first step of signup. create user and send email // first step of signup. create user and send email
const beginEmailSignupProcess = async (email: string) => { const beginEmailSignupProcess = async (email: string) => {
const isEmailInvalid = await isDisposableEmail(email); const sanitizedEmail = email.trim().toLowerCase();
const isEmailInvalid = await isDisposableEmail(sanitizedEmail);
if (isEmailInvalid) { if (isEmailInvalid) {
throw new Error("Provided a disposable email"); throw new Error("Provided a disposable email");
} }
let user = await userDAL.findUserByUsername(email); // akhilmhdh: case sensitive email resolution
const usersByUsername = await userDAL.findUserByUsername(sanitizedEmail);
let user =
usersByUsername?.length > 1 ? usersByUsername.find((el) => el.username === sanitizedEmail) : usersByUsername?.[0];
if (user && user.isAccepted) { if (user && user.isAccepted) {
// TODO(akhilmhdh-pg): copy as old one. this needs to be changed due to security issues // TODO(akhilmhdh-pg): copy as old one. this needs to be changed due to security issues
throw new Error("Failed to send verification code for complete account"); throw new BadRequestError({ message: "Failed to send verification code for complete account" });
} }
if (!user) { if (!user) {
user = await userDAL.create({ authMethods: [AuthMethod.EMAIL], username: email, email, isGhost: false }); user = await userDAL.create({
authMethods: [AuthMethod.EMAIL],
username: sanitizedEmail,
email: sanitizedEmail,
isGhost: false
});
} }
if (!user) throw new Error("Failed to create user"); if (!user) throw new Error("Failed to create user");
@@ -96,7 +105,7 @@ export const authSignupServiceFactory = ({
await smtpService.sendMail({ await smtpService.sendMail({
template: SmtpTemplates.SignupEmailVerification, template: SmtpTemplates.SignupEmailVerification,
subjectLine: "Infisical confirmation code", subjectLine: "Infisical confirmation code",
recipients: [user.email as string], recipients: [sanitizedEmail],
substitutions: { substitutions: {
code: token code: token
} }
@@ -104,11 +113,15 @@ export const authSignupServiceFactory = ({
}; };
const verifyEmailSignup = async (email: string, code: string) => { const verifyEmailSignup = async (email: string, code: string) => {
const user = await userDAL.findUserByUsername(email); const sanitizedEmail = email.trim().toLowerCase();
const usersByUsername = await userDAL.findUserByUsername(sanitizedEmail);
const user =
usersByUsername?.length > 1 ? usersByUsername.find((el) => el.username === sanitizedEmail) : usersByUsername?.[0];
if (!user || (user && user.isAccepted)) { if (!user || (user && user.isAccepted)) {
// TODO(akhilmhdh): copy as old one. this needs to be changed due to security issues // TODO(akhilmhdh): copy as old one. this needs to be changed due to security issues
throw new Error("Failed to send verification code for complete account"); throw new Error("Failed to send verification code for complete account");
} }
const appCfg = getConfig(); const appCfg = getConfig();
await tokenService.validateTokenForUser({ await tokenService.validateTokenForUser({
type: TokenType.TOKEN_EMAIL_CONFIRMATION, type: TokenType.TOKEN_EMAIL_CONFIRMATION,
@@ -153,12 +166,15 @@ export const authSignupServiceFactory = ({
authorization, authorization,
useDefaultOrg useDefaultOrg
}: TCompleteAccountSignupDTO) => { }: TCompleteAccountSignupDTO) => {
const sanitizedEmail = email.trim().toLowerCase();
const appCfg = getConfig(); const appCfg = getConfig();
const serverCfg = await getServerCfg(); const serverCfg = await getServerCfg();
const user = await userDAL.findOne({ username: email }); const usersByUsername = await userDAL.findUserByUsername(sanitizedEmail);
const user =
usersByUsername?.length > 1 ? usersByUsername.find((el) => el.username === sanitizedEmail) : usersByUsername?.[0];
if (!user || (user && user.isAccepted)) { if (!user || (user && user.isAccepted)) {
throw new Error("Failed to complete account for complete user"); throw new BadRequestError({ message: "Failed to complete account for complete user" });
} }
let organizationId: string | null = null; let organizationId: string | null = null;
@@ -315,7 +331,7 @@ export const authSignupServiceFactory = ({
} }
const updatedMembersips = await orgDAL.updateMembership( const updatedMembersips = await orgDAL.updateMembership(
{ inviteEmail: email, status: OrgMembershipStatus.Invited }, { inviteEmail: sanitizedEmail, status: OrgMembershipStatus.Invited },
{ userId: user.id, status: OrgMembershipStatus.Accepted } { userId: user.id, status: OrgMembershipStatus.Accepted }
); );
const uniqueOrgId = [...new Set(updatedMembersips.map(({ orgId }) => orgId))]; const uniqueOrgId = [...new Set(updatedMembersips.map(({ orgId }) => orgId))];
@@ -382,9 +398,9 @@ export const authSignupServiceFactory = ({
* User signup flow when they are invited to join the org * User signup flow when they are invited to join the org
* */ * */
const completeAccountInvite = async ({ const completeAccountInvite = async ({
email,
ip, ip,
salt, salt,
email,
password, password,
verifier, verifier,
firstName, firstName,
@@ -399,7 +415,10 @@ export const authSignupServiceFactory = ({
encryptedPrivateKeyTag, encryptedPrivateKeyTag,
authorization authorization
}: TCompleteAccountInviteDTO) => { }: TCompleteAccountInviteDTO) => {
const user = await userDAL.findUserByUsername(email); const sanitizedEmail = email.trim().toLowerCase();
const usersByUsername = await userDAL.findUserByUsername(sanitizedEmail);
const user =
usersByUsername?.length > 1 ? usersByUsername.find((el) => el.username === sanitizedEmail) : usersByUsername?.[0];
if (!user || (user && user.isAccepted)) { if (!user || (user && user.isAccepted)) {
throw new Error("Failed to complete account for complete user"); throw new Error("Failed to complete account for complete user");
} }
@@ -407,7 +426,7 @@ export const authSignupServiceFactory = ({
validateSignUpAuthorization(authorization, user.id); validateSignUpAuthorization(authorization, user.id);
const [orgMembership] = await orgDAL.findMembership({ const [orgMembership] = await orgDAL.findMembership({
inviteEmail: email, inviteEmail: sanitizedEmail,
status: OrgMembershipStatus.Invited status: OrgMembershipStatus.Invited
}); });
if (!orgMembership) if (!orgMembership)
@@ -454,7 +473,7 @@ export const authSignupServiceFactory = ({
const serverGeneratedPrivateKey = await getUserPrivateKey(serverGeneratedPassword, { const serverGeneratedPrivateKey = await getUserPrivateKey(serverGeneratedPassword, {
...systemGeneratedUserEncryptionKey ...systemGeneratedUserEncryptionKey
}); });
const encKeys = await generateUserSrpKeys(email, password, { const encKeys = await generateUserSrpKeys(sanitizedEmail, password, {
publicKey: systemGeneratedUserEncryptionKey.publicKey, publicKey: systemGeneratedUserEncryptionKey.publicKey,
privateKey: serverGeneratedPrivateKey privateKey: serverGeneratedPrivateKey
}); });
@@ -505,7 +524,7 @@ export const authSignupServiceFactory = ({
} }
const updatedMembersips = await orgDAL.updateMembership( const updatedMembersips = await orgDAL.updateMembership(
{ inviteEmail: email, status: OrgMembershipStatus.Invited }, { inviteEmail: sanitizedEmail, status: OrgMembershipStatus.Invited },
{ userId: us.id, status: OrgMembershipStatus.Accepted }, { userId: us.id, status: OrgMembershipStatus.Accepted },
tx tx
); );
@@ -8,6 +8,7 @@ import {
ProjectPermissionCertificateActions, ProjectPermissionCertificateActions,
ProjectPermissionSub ProjectPermissionSub
} from "@app/ee/services/permission/project-permission"; } from "@app/ee/services/permission/project-permission";
import { NotFoundError } from "@app/lib/errors";
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal"; import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal"; import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
@@ -29,7 +30,6 @@ import {
TGetCertPrivateKeyDTO, TGetCertPrivateKeyDTO,
TRevokeCertDTO TRevokeCertDTO
} from "./certificate-types"; } from "./certificate-types";
import { NotFoundError } from "@app/lib/errors";
type TCertificateServiceFactoryDep = { type TCertificateServiceFactoryDep = {
certificateDAL: Pick<TCertificateDALFactory, "findOne" | "deleteById" | "update" | "find">; certificateDAL: Pick<TCertificateDALFactory, "findOne" | "deleteById" | "update" | "find">;
+1 -1
View File
@@ -206,7 +206,7 @@ export const orgDALFactory = (db: TDbClient) => {
.where(`${TableName.OrgMembership}.orgId`, orgId) .where(`${TableName.OrgMembership}.orgId`, orgId)
.count("*") .count("*")
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
.where({ isGhost: false }) .where({ isGhost: false, [`${TableName.OrgMembership}.isActive` as "isActive"]: true })
.first(); .first();
return parseInt((count as unknown as CountResult).count || "0", 10); return parseInt((count as unknown as CountResult).count || "0", 10);
+9 -2
View File
@@ -827,7 +827,11 @@ export const orgServiceFactory = ({
const users: Pick<TUsers, "id" | "firstName" | "lastName" | "email" | "username">[] = []; const users: Pick<TUsers, "id" | "firstName" | "lastName" | "email" | "username">[] = [];
for await (const inviteeEmail of inviteeEmails) { for await (const inviteeEmail of inviteeEmails) {
let inviteeUser = await userDAL.findUserByUsername(inviteeEmail, tx); const usersByUsername = await userDAL.findUserByUsername(inviteeEmail, tx);
let inviteeUser =
usersByUsername?.length > 1
? usersByUsername.find((el) => el.username === inviteeEmail)
: usersByUsername?.[0];
// if the user doesn't exist we create the user with the email // if the user doesn't exist we create the user with the email
if (!inviteeUser) { if (!inviteeUser) {
@@ -1239,10 +1243,13 @@ export const orgServiceFactory = ({
* magic link and issue a temporary signup token for user to complete setting up their account * magic link and issue a temporary signup token for user to complete setting up their account
*/ */
const verifyUserToOrg = async ({ orgId, email, code }: TVerifyUserToOrgDTO) => { const verifyUserToOrg = async ({ orgId, email, code }: TVerifyUserToOrgDTO) => {
const user = await userDAL.findUserByUsername(email); const usersByUsername = await userDAL.findUserByUsername(email);
const user =
usersByUsername?.length > 1 ? usersByUsername.find((el) => el.username === email) : usersByUsername?.[0];
if (!user) { if (!user) {
throw new NotFoundError({ message: "User not found" }); throw new NotFoundError({ message: "User not found" });
} }
const [orgMembership] = await orgDAL.findMembership({ const [orgMembership] = await orgDAL.findMembership({
[`${TableName.OrgMembership}.userId` as "userId"]: user.id, [`${TableName.OrgMembership}.userId` as "userId"]: user.id,
status: OrgMembershipStatus.Invited, status: OrgMembershipStatus.Invited,
+17 -1
View File
@@ -433,6 +433,21 @@ export const projectDALFactory = (db: TDbClient) => {
.select(selectAllTableCols(TableName.Project)) .select(selectAllTableCols(TableName.Project))
.first(); .first();
return project; return project;
}
const countOfOrgProjects = async (orgId: string | null, tx?: Knex) => {
try {
const doc = await (tx || db.replicaNode())(TableName.Project)
.andWhere((bd) => {
if (orgId) {
void bd.where({ orgId });
}
})
.count();
return Number(doc?.[0]?.count ?? 0);
} catch (error) {
throw new DatabaseError({ error, name: "Count of Org Projects" });
}
}; };
return { return {
@@ -448,6 +463,7 @@ export const projectDALFactory = (db: TDbClient) => {
checkProjectUpgradeStatus, checkProjectUpgradeStatus,
getProjectFromSplitId, getProjectFromSplitId,
searchProjects, searchProjects,
findProjectByEnvId findProjectByEnvId,
countOfOrgProjects
}; };
}; };
@@ -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 ONEPASS_SYNC_LIST_OPTION: TSecretSyncListItem = {
name: "1Password",
destination: SecretSync.OnePass,
connection: AppConnection.OnePass,
canImportSecrets: true
};
@@ -0,0 +1,226 @@
import { request } from "@app/lib/config/request";
import { getOnePassInstanceUrl } from "@app/services/app-connection/1password";
import {
TDeleteOnePassVariable,
TOnePassListVariables,
TOnePassListVariablesResponse,
TOnePassSyncWithCredentials,
TOnePassVariable,
TOnePassVariableDetails,
TPostOnePassVariable,
TPutOnePassVariable
} from "@app/services/secret-sync/1password/1password-sync-types";
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
const listOnePassItems = async ({ instanceUrl, apiToken, vaultId }: TOnePassListVariables) => {
const { data } = await request.get<TOnePassListVariablesResponse>(`${instanceUrl}/v1/vaults/${vaultId}/items`, {
headers: {
Authorization: `Bearer ${apiToken}`,
Accept: "application/json"
}
});
const result: Record<string, TOnePassVariable & { value: string; fieldId: string }> = {};
for await (const s of data) {
const { data: secret } = await request.get<TOnePassVariableDetails>(
`${instanceUrl}/v1/vaults/${vaultId}/items/${s.id}`,
{
headers: {
Authorization: `Bearer ${apiToken}`,
Accept: "application/json"
}
}
);
const value = secret.fields.find((f) => f.label === "value")?.value;
const fieldId = secret.fields.find((f) => f.label === "value")?.id;
// eslint-disable-next-line no-continue
if (!value || !fieldId) continue;
result[s.title] = {
...secret,
value,
fieldId
};
}
return result;
};
const createOnePassItem = async ({ instanceUrl, apiToken, vaultId, itemTitle, itemValue }: TPostOnePassVariable) => {
return request.post(
`${instanceUrl}/v1/vaults/${vaultId}/items`,
{
title: itemTitle,
category: "API_CREDENTIAL",
vault: {
id: vaultId
},
tags: ["synced-from-infisical"],
fields: [
{
label: "value",
value: itemValue,
type: "CONCEALED"
}
]
},
{
headers: {
Authorization: `Bearer ${apiToken}`,
"Content-Type": "application/json"
}
}
);
};
const updateOnePassItem = async ({
instanceUrl,
apiToken,
vaultId,
itemId,
fieldId,
itemTitle,
itemValue
}: TPutOnePassVariable) => {
return request.put(
`${instanceUrl}/v1/vaults/${vaultId}/items/${itemId}`,
{
id: itemId,
title: itemTitle,
category: "API_CREDENTIAL",
vault: {
id: vaultId
},
tags: ["synced-from-infisical"],
fields: [
{
id: fieldId,
label: "value",
value: itemValue,
type: "CONCEALED"
}
]
},
{
headers: {
Authorization: `Bearer ${apiToken}`,
"Content-Type": "application/json"
}
}
);
};
const deleteOnePassItem = async ({ instanceUrl, apiToken, vaultId, itemId }: TDeleteOnePassVariable) => {
return request.delete(`${instanceUrl}/v1/vaults/${vaultId}/items/${itemId}`, {
headers: {
Authorization: `Bearer ${apiToken}`
}
});
};
export const OnePassSyncFns = {
syncSecrets: async (secretSync: TOnePassSyncWithCredentials, secretMap: TSecretMap) => {
const {
connection,
destinationConfig: { vaultId }
} = secretSync;
const instanceUrl = await getOnePassInstanceUrl(connection);
const { apiToken } = connection.credentials;
const items = await listOnePassItems({ instanceUrl, apiToken, vaultId });
for await (const entry of Object.entries(secretMap)) {
const [key, { value }] = entry;
try {
if (key in items) {
await updateOnePassItem({
instanceUrl,
apiToken,
vaultId,
itemTitle: key,
itemValue: value,
itemId: items[key].id,
fieldId: items[key].fieldId
});
} else {
await createOnePassItem({ instanceUrl, apiToken, vaultId, itemTitle: key, itemValue: value });
}
} catch (error) {
throw new SecretSyncError({
error,
secretKey: key
});
}
}
if (secretSync.syncOptions.disableSecretDeletion) return;
for await (const [key, variable] of Object.entries(items)) {
// eslint-disable-next-line no-continue
if (!matchesSchema(key, secretSync.syncOptions.keySchema)) continue;
if (!(key in secretMap)) {
try {
await deleteOnePassItem({
instanceUrl,
apiToken,
vaultId,
itemId: variable.id
});
} catch (error) {
throw new SecretSyncError({
error,
secretKey: key
});
}
}
}
},
removeSecrets: async (secretSync: TOnePassSyncWithCredentials, secretMap: TSecretMap) => {
const {
connection,
destinationConfig: { vaultId }
} = secretSync;
const instanceUrl = await getOnePassInstanceUrl(connection);
const { apiToken } = connection.credentials;
const items = await listOnePassItems({ instanceUrl, apiToken, vaultId });
for await (const [key, item] of Object.entries(items)) {
if (key in secretMap) {
try {
await deleteOnePassItem({
apiToken,
vaultId,
instanceUrl,
itemId: item.id
});
} catch (error) {
throw new SecretSyncError({
error,
secretKey: key
});
}
}
}
},
getSecrets: async (secretSync: TOnePassSyncWithCredentials) => {
const {
connection,
destinationConfig: { vaultId }
} = secretSync;
const instanceUrl = await getOnePassInstanceUrl(connection);
const { apiToken } = connection.credentials;
return listOnePassItems({ instanceUrl, apiToken, vaultId });
}
};
@@ -0,0 +1,43 @@
import { z } from "zod";
import { SecretSyncs } from "@app/lib/api-docs";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import {
BaseSecretSyncSchema,
GenericCreateSecretSyncFieldsSchema,
GenericUpdateSecretSyncFieldsSchema
} from "@app/services/secret-sync/secret-sync-schemas";
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
const OnePassSyncDestinationConfigSchema = z.object({
vaultId: z.string().trim().min(1, "Vault required").describe(SecretSyncs.DESTINATION_CONFIG.ONEPASS.vaultId)
});
const OnePassSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
export const OnePassSyncSchema = BaseSecretSyncSchema(SecretSync.OnePass, OnePassSyncOptionsConfig).extend({
destination: z.literal(SecretSync.OnePass),
destinationConfig: OnePassSyncDestinationConfigSchema
});
export const CreateOnePassSyncSchema = GenericCreateSecretSyncFieldsSchema(
SecretSync.OnePass,
OnePassSyncOptionsConfig
).extend({
destinationConfig: OnePassSyncDestinationConfigSchema
});
export const UpdateOnePassSyncSchema = GenericUpdateSecretSyncFieldsSchema(
SecretSync.OnePass,
OnePassSyncOptionsConfig
).extend({
destinationConfig: OnePassSyncDestinationConfigSchema.optional()
});
export const OnePassSyncListItemSchema = z.object({
name: z.literal("1Password"),
connection: z.literal(AppConnection.OnePass),
destination: z.literal(SecretSync.OnePass),
canImportSecrets: z.literal(true)
});
@@ -0,0 +1,54 @@
import { z } from "zod";
import { TOnePassConnection } from "@app/services/app-connection/1password";
import { CreateOnePassSyncSchema, OnePassSyncListItemSchema, OnePassSyncSchema } from "./1password-sync-schemas";
export type TOnePassSync = z.infer<typeof OnePassSyncSchema>;
export type TOnePassSyncInput = z.infer<typeof CreateOnePassSyncSchema>;
export type TOnePassSyncListItem = z.infer<typeof OnePassSyncListItemSchema>;
export type TOnePassSyncWithCredentials = TOnePassSync & {
connection: TOnePassConnection;
};
export type TOnePassVariable = {
id: string;
title: string;
category: string; // API_CREDENTIAL, SECURE_NOTE, LOGIN, etc
};
export type TOnePassVariableDetails = TOnePassVariable & {
fields: {
id: string;
type: string; // CONCEALED, STRING
label: string;
value: string;
}[];
};
export type TOnePassListVariablesResponse = TOnePassVariable[];
export type TOnePassListVariables = {
apiToken: string;
instanceUrl: string;
vaultId: string;
};
export type TPostOnePassVariable = TOnePassListVariables & {
itemTitle: string;
itemValue: string;
};
export type TPutOnePassVariable = TOnePassListVariables & {
itemId: string;
fieldId: string;
itemTitle: string;
itemValue: string;
};
export type TDeleteOnePassVariable = TOnePassListVariables & {
itemId: string;
};
@@ -0,0 +1,4 @@
export * from "./1password-sync-constants";
export * from "./1password-sync-fns";
export * from "./1password-sync-schemas";
export * from "./1password-sync-types";
@@ -13,7 +13,8 @@ export enum SecretSync {
Windmill = "windmill", Windmill = "windmill",
HCVault = "hashicorp-vault", HCVault = "hashicorp-vault",
TeamCity = "teamcity", TeamCity = "teamcity",
OCIVault = "oci-vault" OCIVault = "oci-vault",
OnePass = "1password"
} }
export enum SecretSyncInitialSyncBehavior { export enum SecretSyncInitialSyncBehavior {
@@ -26,3 +27,8 @@ export enum SecretSyncImportBehavior {
PrioritizeSource = "prioritize-source", PrioritizeSource = "prioritize-source",
PrioritizeDestination = "prioritize-destination" PrioritizeDestination = "prioritize-destination"
} }
export enum SecretSyncPlanType {
Enterprise = "enterprise",
Regular = "regular"
}
@@ -1,6 +1,9 @@
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import RE2 from "re2"; import RE2 from "re2";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OCI_VAULT_SYNC_LIST_OPTION, OCIVaultSyncFns } from "@app/ee/services/secret-sync/oci-vault";
import { BadRequestError } from "@app/lib/errors";
import { import {
AWS_PARAMETER_STORE_SYNC_LIST_OPTION, AWS_PARAMETER_STORE_SYNC_LIST_OPTION,
AwsParameterStoreSyncFns AwsParameterStoreSyncFns
@@ -11,7 +14,7 @@ import {
} from "@app/services/secret-sync/aws-secrets-manager"; } from "@app/services/secret-sync/aws-secrets-manager";
import { DATABRICKS_SYNC_LIST_OPTION, databricksSyncFactory } from "@app/services/secret-sync/databricks"; import { DATABRICKS_SYNC_LIST_OPTION, databricksSyncFactory } from "@app/services/secret-sync/databricks";
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, SecretSyncPlanType } 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";
import { import {
TSecretMap, TSecretMap,
@@ -21,6 +24,7 @@ import {
import { TAppConnectionDALFactory } from "../app-connection/app-connection-dal"; import { TAppConnectionDALFactory } from "../app-connection/app-connection-dal";
import { TKmsServiceFactory } from "../kms/kms-service"; import { TKmsServiceFactory } from "../kms/kms-service";
import { ONEPASS_SYNC_LIST_OPTION, OnePassSyncFns } from "./1password";
import { AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION, azureAppConfigurationSyncFactory } from "./azure-app-configuration"; import { AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION, azureAppConfigurationSyncFactory } from "./azure-app-configuration";
import { AZURE_KEY_VAULT_SYNC_LIST_OPTION, azureKeyVaultSyncFactory } from "./azure-key-vault"; import { AZURE_KEY_VAULT_SYNC_LIST_OPTION, azureKeyVaultSyncFactory } from "./azure-key-vault";
import { CAMUNDA_SYNC_LIST_OPTION, camundaSyncFactory } from "./camunda"; import { CAMUNDA_SYNC_LIST_OPTION, camundaSyncFactory } from "./camunda";
@@ -29,7 +33,7 @@ import { GcpSyncFns } from "./gcp/gcp-sync-fns";
import { HC_VAULT_SYNC_LIST_OPTION, HCVaultSyncFns } from "./hc-vault"; import { HC_VAULT_SYNC_LIST_OPTION, HCVaultSyncFns } from "./hc-vault";
import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec"; import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec";
import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns"; import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns";
import { OCI_VAULT_SYNC_LIST_OPTION, OCIVaultSyncFns } from "./oci-vault"; import { SECRET_SYNC_PLAN_MAP } from "./secret-sync-maps";
import { TEAMCITY_SYNC_LIST_OPTION, TeamCitySyncFns } from "./teamcity"; import { TEAMCITY_SYNC_LIST_OPTION, TeamCitySyncFns } from "./teamcity";
import { TERRAFORM_CLOUD_SYNC_LIST_OPTION, TerraformCloudSyncFns } from "./terraform-cloud"; import { TERRAFORM_CLOUD_SYNC_LIST_OPTION, TerraformCloudSyncFns } from "./terraform-cloud";
import { VERCEL_SYNC_LIST_OPTION, VercelSyncFns } from "./vercel"; import { VERCEL_SYNC_LIST_OPTION, VercelSyncFns } from "./vercel";
@@ -50,7 +54,8 @@ const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
[SecretSync.Windmill]: WINDMILL_SYNC_LIST_OPTION, [SecretSync.Windmill]: WINDMILL_SYNC_LIST_OPTION,
[SecretSync.HCVault]: HC_VAULT_SYNC_LIST_OPTION, [SecretSync.HCVault]: HC_VAULT_SYNC_LIST_OPTION,
[SecretSync.TeamCity]: TEAMCITY_SYNC_LIST_OPTION, [SecretSync.TeamCity]: TEAMCITY_SYNC_LIST_OPTION,
[SecretSync.OCIVault]: OCI_VAULT_SYNC_LIST_OPTION [SecretSync.OCIVault]: OCI_VAULT_SYNC_LIST_OPTION,
[SecretSync.OnePass]: ONEPASS_SYNC_LIST_OPTION
}; };
export const listSecretSyncOptions = () => { export const listSecretSyncOptions = () => {
@@ -171,6 +176,8 @@ export const SecretSyncFns = {
return TeamCitySyncFns.syncSecrets(secretSync, schemaSecretMap); return TeamCitySyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.OCIVault: case SecretSync.OCIVault:
return OCIVaultSyncFns.syncSecrets(secretSync, schemaSecretMap); return OCIVaultSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.OnePass:
return OnePassSyncFns.syncSecrets(secretSync, schemaSecretMap);
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}`
@@ -239,6 +246,9 @@ export const SecretSyncFns = {
case SecretSync.OCIVault: case SecretSync.OCIVault:
secretMap = await OCIVaultSyncFns.getSecrets(secretSync); secretMap = await OCIVaultSyncFns.getSecrets(secretSync);
break; break;
case SecretSync.OnePass:
secretMap = await OnePassSyncFns.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}`
@@ -297,6 +307,8 @@ export const SecretSyncFns = {
return TeamCitySyncFns.removeSecrets(secretSync, schemaSecretMap); return TeamCitySyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.OCIVault: case SecretSync.OCIVault:
return OCIVaultSyncFns.removeSecrets(secretSync, schemaSecretMap); return OCIVaultSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.OnePass:
return OnePassSyncFns.removeSecrets(secretSync, schemaSecretMap);
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}`
@@ -327,3 +339,18 @@ export const parseSyncErrorMessage = (err: unknown): string => {
? errorMessage ? errorMessage
: `${errorMessage.substring(0, MAX_MESSAGE_LENGTH - 3)}...`; : `${errorMessage.substring(0, MAX_MESSAGE_LENGTH - 3)}...`;
}; };
export const enterpriseSyncCheck = async (
licenseService: Pick<TLicenseServiceFactory, "getPlan">,
secretSync: SecretSync,
orgId: string,
errorMessage: string
) => {
if (SECRET_SYNC_PLAN_MAP[secretSync] === SecretSyncPlanType.Enterprise) {
const plan = await licenseService.getPlan(orgId);
if (!plan.enterpriseSecretSyncs)
throw new BadRequestError({
message: errorMessage
});
}
};
@@ -1,5 +1,5 @@
import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; import { SecretSync, SecretSyncPlanType } 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",
@@ -16,7 +16,8 @@ export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
[SecretSync.Windmill]: "Windmill", [SecretSync.Windmill]: "Windmill",
[SecretSync.HCVault]: "Hashicorp Vault", [SecretSync.HCVault]: "Hashicorp Vault",
[SecretSync.TeamCity]: "TeamCity", [SecretSync.TeamCity]: "TeamCity",
[SecretSync.OCIVault]: "OCI Vault" [SecretSync.OCIVault]: "OCI Vault",
[SecretSync.OnePass]: "1Password"
}; };
export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = { export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
@@ -34,5 +35,25 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
[SecretSync.Windmill]: AppConnection.Windmill, [SecretSync.Windmill]: AppConnection.Windmill,
[SecretSync.HCVault]: AppConnection.HCVault, [SecretSync.HCVault]: AppConnection.HCVault,
[SecretSync.TeamCity]: AppConnection.TeamCity, [SecretSync.TeamCity]: AppConnection.TeamCity,
[SecretSync.OCIVault]: AppConnection.OCI [SecretSync.OCIVault]: AppConnection.OCI,
[SecretSync.OnePass]: AppConnection.OnePass
};
export const SECRET_SYNC_PLAN_MAP: Record<SecretSync, SecretSyncPlanType> = {
[SecretSync.AWSParameterStore]: SecretSyncPlanType.Regular,
[SecretSync.AWSSecretsManager]: SecretSyncPlanType.Regular,
[SecretSync.GitHub]: SecretSyncPlanType.Regular,
[SecretSync.GCPSecretManager]: SecretSyncPlanType.Regular,
[SecretSync.AzureKeyVault]: SecretSyncPlanType.Regular,
[SecretSync.AzureAppConfiguration]: SecretSyncPlanType.Regular,
[SecretSync.Databricks]: SecretSyncPlanType.Regular,
[SecretSync.Humanitec]: SecretSyncPlanType.Regular,
[SecretSync.TerraformCloud]: SecretSyncPlanType.Regular,
[SecretSync.Camunda]: SecretSyncPlanType.Regular,
[SecretSync.Vercel]: SecretSyncPlanType.Regular,
[SecretSync.Windmill]: SecretSyncPlanType.Regular,
[SecretSync.HCVault]: SecretSyncPlanType.Regular,
[SecretSync.TeamCity]: SecretSyncPlanType.Regular,
[SecretSync.OCIVault]: SecretSyncPlanType.Enterprise,
[SecretSync.OnePass]: SecretSyncPlanType.Regular
}; };
@@ -5,6 +5,7 @@ import { Job } from "bullmq";
import { ProjectMembershipRole, SecretType } from "@app/db/schemas"; import { ProjectMembershipRole, SecretType } from "@app/db/schemas";
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service"; import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore"; import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
@@ -32,7 +33,7 @@ import {
SecretSyncInitialSyncBehavior SecretSyncInitialSyncBehavior
} from "@app/services/secret-sync/secret-sync-enums"; } 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";
import { parseSyncErrorMessage, SecretSyncFns } from "@app/services/secret-sync/secret-sync-fns"; import { enterpriseSyncCheck, parseSyncErrorMessage, SecretSyncFns } from "@app/services/secret-sync/secret-sync-fns";
import { SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps"; import { SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps";
import { import {
SecretSyncAction, SecretSyncAction,
@@ -95,6 +96,7 @@ type TSecretSyncQueueFactoryDep = {
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">; secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">; resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">; folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
}; };
type SecretSyncActionJob = Job< type SecretSyncActionJob = Job<
@@ -136,7 +138,8 @@ export const secretSyncQueueFactory = ({
secretVersionV2BridgeDAL, secretVersionV2BridgeDAL,
secretVersionTagV2BridgeDAL, secretVersionTagV2BridgeDAL,
resourceMetadataDAL, resourceMetadataDAL,
folderCommitService folderCommitService,
licenseService
}: TSecretSyncQueueFactoryDep) => { }: TSecretSyncQueueFactoryDep) => {
const appCfg = getConfig(); const appCfg = getConfig();
@@ -328,7 +331,20 @@ export const secretSyncQueueFactory = ({
secretSync: TSecretSyncWithCredentials, secretSync: TSecretSyncWithCredentials,
importBehavior: SecretSyncImportBehavior importBehavior: SecretSyncImportBehavior
): Promise<TSecretMap> => { ): Promise<TSecretMap> => {
const { projectId, environment, folder } = secretSync; const {
projectId,
environment,
folder,
destination,
connection: { orgId }
} = secretSync;
await enterpriseSyncCheck(
licenseService,
destination,
orgId,
"Failed to import secrets due to plan restriction. Upgrade plan to access enterprise secret syncs."
);
if (!environment || !folder) if (!environment || !folder)
throw new Error( throw new Error(
@@ -405,6 +421,13 @@ export const secretSyncQueueFactory = ({
if (!secretSync) throw new Error(`Cannot find secret sync with ID ${syncId}`); if (!secretSync) throw new Error(`Cannot find secret sync with ID ${syncId}`);
await enterpriseSyncCheck(
licenseService,
secretSync.destination as SecretSync,
secretSync.connection.orgId,
"Failed to sync secrets due to plan restriction. Upgrade plan to access enterprise secret syncs."
);
await secretSyncDAL.updateById(syncId, { await secretSyncDAL.updateById(syncId, {
syncStatus: SecretSyncStatus.Running syncStatus: SecretSyncStatus.Running
}); });
@@ -664,6 +687,13 @@ export const secretSyncQueueFactory = ({
if (!secretSync) throw new Error(`Cannot find secret sync with ID ${syncId}`); if (!secretSync) throw new Error(`Cannot find secret sync with ID ${syncId}`);
await enterpriseSyncCheck(
licenseService,
secretSync.destination as SecretSync,
secretSync.connection.orgId,
"Failed to remove secrets due to plan restriction. Upgrade plan to access enterprise secret syncs."
);
await secretSyncDAL.updateById(syncId, { await secretSyncDAL.updateById(syncId, {
removeStatus: SecretSyncStatus.Running removeStatus: SecretSyncStatus.Running
}); });
@@ -1,6 +1,7 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { ActionProjectType } from "@app/db/schemas"; import { ActionProjectType } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { throwIfMissingSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns"; import { throwIfMissingSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { import {
@@ -16,7 +17,7 @@ import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-c
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { listSecretSyncOptions } from "@app/services/secret-sync/secret-sync-fns"; import { enterpriseSyncCheck, listSecretSyncOptions } from "@app/services/secret-sync/secret-sync-fns";
import { import {
SecretSyncStatus, SecretSyncStatus,
TCreateSecretSyncDTO, TCreateSecretSyncDTO,
@@ -49,6 +50,7 @@ type TSecretSyncServiceFactoryDep = {
TSecretSyncQueueFactory, TSecretSyncQueueFactory,
"queueSecretSyncSyncSecretsById" | "queueSecretSyncImportSecretsById" | "queueSecretSyncRemoveSecretsById" "queueSecretSyncSyncSecretsById" | "queueSecretSyncImportSecretsById" | "queueSecretSyncRemoveSecretsById"
>; >;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
}; };
export type TSecretSyncServiceFactory = ReturnType<typeof secretSyncServiceFactory>; export type TSecretSyncServiceFactory = ReturnType<typeof secretSyncServiceFactory>;
@@ -61,7 +63,8 @@ export const secretSyncServiceFactory = ({
appConnectionService, appConnectionService,
projectBotService, projectBotService,
secretSyncQueue, secretSyncQueue,
keyStore keyStore,
licenseService
}: TSecretSyncServiceFactoryDep) => { }: TSecretSyncServiceFactoryDep) => {
const listSecretSyncsByProjectId = async ( const listSecretSyncsByProjectId = async (
{ projectId, destination }: TListSecretSyncsByProjectId, { projectId, destination }: TListSecretSyncsByProjectId,
@@ -191,6 +194,13 @@ export const secretSyncServiceFactory = ({
{ projectId, secretPath, environment, ...params }: TCreateSecretSyncDTO, { projectId, secretPath, environment, ...params }: TCreateSecretSyncDTO,
actor: OrgServiceActor actor: OrgServiceActor
) => { ) => {
await enterpriseSyncCheck(
licenseService,
params.destination,
actor.orgId,
"Failed to create secret sync due to plan restriction. Upgrade plan to access enterprise secret syncs."
);
const { permission: projectPermission } = await permissionService.getProjectPermission({ const { permission: projectPermission } = await permissionService.getProjectPermission({
actor: actor.type, actor: actor.type,
actorId: actor.id, actorId: actor.id,
@@ -260,6 +270,13 @@ export const secretSyncServiceFactory = ({
message: `Could not find ${SECRET_SYNC_NAME_MAP[destination]} Sync with ID ${syncId}` message: `Could not find ${SECRET_SYNC_NAME_MAP[destination]} Sync with ID ${syncId}`
}); });
await enterpriseSyncCheck(
licenseService,
secretSync.destination as SecretSync,
actor.orgId,
"Failed to update secret sync due to plan restriction. Upgrade plan to access enterprise secret syncs."
);
const { permission } = await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
actor: actor.type, actor: actor.type,
actorId: actor.id, actorId: actor.id,
@@ -408,6 +425,13 @@ export const secretSyncServiceFactory = ({
message: `Could not find ${SECRET_SYNC_NAME_MAP[destination]} Sync with ID "${syncId}"` message: `Could not find ${SECRET_SYNC_NAME_MAP[destination]} Sync with ID "${syncId}"`
}); });
await enterpriseSyncCheck(
licenseService,
secretSync.destination as SecretSync,
actor.orgId,
"Failed to trigger secret sync due to plan restriction. Upgrade plan to access enterprise secret syncs."
);
const { permission } = await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
actor: actor.type, actor: actor.type,
actorId: actor.id, actorId: actor.id,
@@ -463,6 +487,13 @@ export const secretSyncServiceFactory = ({
message: `Could not find ${SECRET_SYNC_NAME_MAP[destination]} Sync with ID "${syncId}"` message: `Could not find ${SECRET_SYNC_NAME_MAP[destination]} Sync with ID "${syncId}"`
}); });
await enterpriseSyncCheck(
licenseService,
secretSync.destination as SecretSync,
actor.orgId,
"Failed to trigger secret sync due to plan restriction. Upgrade plan to access enterprise secret syncs."
);
const { permission } = await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
actor: actor.type, actor: actor.type,
actorId: actor.id, actorId: actor.id,
@@ -512,6 +543,13 @@ export const secretSyncServiceFactory = ({
message: `Could not find ${SECRET_SYNC_NAME_MAP[destination]} Sync with ID "${syncId}"` message: `Could not find ${SECRET_SYNC_NAME_MAP[destination]} Sync with ID "${syncId}"`
}); });
await enterpriseSyncCheck(
licenseService,
secretSync.destination as SecretSync,
actor.orgId,
"Failed to trigger secret sync due to plan restriction. Upgrade plan to access enterprise secret syncs."
);
const { permission } = await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
actor: actor.type, actor: actor.type,
actorId: actor.id, actorId: actor.id,
@@ -1,6 +1,12 @@
import { Job } from "bullmq"; import { Job } from "bullmq";
import { AuditLogInfo } from "@app/ee/services/audit-log/audit-log-types"; import { AuditLogInfo } from "@app/ee/services/audit-log/audit-log-types";
import {
TOCIVaultSync,
TOCIVaultSyncInput,
TOCIVaultSyncListItem,
TOCIVaultSyncWithCredentials
} from "@app/ee/services/secret-sync/oci-vault";
import { QueueJobs } from "@app/queue"; import { QueueJobs } from "@app/queue";
import { ResourceMetadataDTO } from "@app/services/resource-metadata/resource-metadata-schema"; import { ResourceMetadataDTO } from "@app/services/resource-metadata/resource-metadata-schema";
import { import {
@@ -36,6 +42,12 @@ import {
TWindmillSyncWithCredentials TWindmillSyncWithCredentials
} from "@app/services/secret-sync/windmill"; } from "@app/services/secret-sync/windmill";
import {
TOnePassSync,
TOnePassSyncInput,
TOnePassSyncListItem,
TOnePassSyncWithCredentials
} from "./1password/1password-sync-types";
import { import {
TAwsParameterStoreSync, TAwsParameterStoreSync,
TAwsParameterStoreSyncInput, TAwsParameterStoreSyncInput,
@@ -67,7 +79,6 @@ import {
THumanitecSyncListItem, THumanitecSyncListItem,
THumanitecSyncWithCredentials THumanitecSyncWithCredentials
} from "./humanitec"; } from "./humanitec";
import { TOCIVaultSync, TOCIVaultSyncInput, TOCIVaultSyncListItem, TOCIVaultSyncWithCredentials } from "./oci-vault";
import { import {
TTeamCitySync, TTeamCitySync,
TTeamCitySyncInput, TTeamCitySyncInput,
@@ -97,7 +108,8 @@ export type TSecretSync =
| TWindmillSync | TWindmillSync
| THCVaultSync | THCVaultSync
| TTeamCitySync | TTeamCitySync
| TOCIVaultSync; | TOCIVaultSync
| TOnePassSync;
export type TSecretSyncWithCredentials = export type TSecretSyncWithCredentials =
| TAwsParameterStoreSyncWithCredentials | TAwsParameterStoreSyncWithCredentials
@@ -114,7 +126,8 @@ export type TSecretSyncWithCredentials =
| TWindmillSyncWithCredentials | TWindmillSyncWithCredentials
| THCVaultSyncWithCredentials | THCVaultSyncWithCredentials
| TTeamCitySyncWithCredentials | TTeamCitySyncWithCredentials
| TOCIVaultSyncWithCredentials; | TOCIVaultSyncWithCredentials
| TOnePassSyncWithCredentials;
export type TSecretSyncInput = export type TSecretSyncInput =
| TAwsParameterStoreSyncInput | TAwsParameterStoreSyncInput
@@ -131,7 +144,8 @@ export type TSecretSyncInput =
| TWindmillSyncInput | TWindmillSyncInput
| THCVaultSyncInput | THCVaultSyncInput
| TTeamCitySyncInput | TTeamCitySyncInput
| TOCIVaultSyncInput; | TOCIVaultSyncInput
| TOnePassSyncInput;
export type TSecretSyncListItem = export type TSecretSyncListItem =
| TAwsParameterStoreSyncListItem | TAwsParameterStoreSyncListItem
@@ -148,7 +162,8 @@ export type TSecretSyncListItem =
| TWindmillSyncListItem | TWindmillSyncListItem
| THCVaultSyncListItem | THCVaultSyncListItem
| TTeamCitySyncListItem | TTeamCitySyncListItem
| TOCIVaultSyncListItem; | TOCIVaultSyncListItem
| TOnePassSyncListItem;
export type TSyncOptionsConfig = { export type TSyncOptionsConfig = {
canImportSecrets: boolean; canImportSecrets: boolean;
@@ -12,6 +12,7 @@ interface SecretApprovalRequestBypassedTemplateProps
environment: string; environment: string;
bypassReason: string; bypassReason: string;
approvalUrl: string; approvalUrl: string;
requestType: "change" | "access";
} }
export const SecretApprovalRequestBypassedTemplate = ({ export const SecretApprovalRequestBypassedTemplate = ({
@@ -22,7 +23,8 @@ export const SecretApprovalRequestBypassedTemplate = ({
secretPath, secretPath,
environment, environment,
bypassReason, bypassReason,
approvalUrl approvalUrl,
requestType = "change"
}: SecretApprovalRequestBypassedTemplateProps) => { }: SecretApprovalRequestBypassedTemplateProps) => {
return ( return (
<BaseEmailWrapper <BaseEmailWrapper
@@ -39,8 +41,9 @@ export const SecretApprovalRequestBypassedTemplate = ({
<Link href={`mailto:${requesterEmail}`} className="text-slate-700 no-underline"> <Link href={`mailto:${requesterEmail}`} className="text-slate-700 no-underline">
{requesterEmail} {requesterEmail}
</Link> </Link>
) has merged a secret to <strong>{secretPath}</strong> in the <strong>{environment}</strong> environment ) has {requestType === "change" ? "merged" : "accessed"} a secret {requestType === "change" ? "to" : "in"}{" "}
without obtaining the required approval. <strong>{secretPath}</strong> in the <strong>{environment}</strong> environment without obtaining the required
approval.
</Text> </Text>
<Text className="text-[14px] text-slate-700 leading-[24px]"> <Text className="text-[14px] text-slate-700 leading-[24px]">
<strong className="text-black">The following reason was provided for bypassing the policy:</strong> " <strong className="text-black">The following reason was provided for bypassing the policy:</strong> "
@@ -257,8 +257,8 @@ export const superAdminServiceFactory = ({
const adminSignUp = async ({ const adminSignUp = async ({
lastName, lastName,
firstName, firstName,
salt,
email, email,
salt,
password, password,
verifier, verifier,
publicKey, publicKey,
@@ -272,7 +272,8 @@ export const superAdminServiceFactory = ({
userAgent userAgent
}: TAdminSignUpDTO) => { }: TAdminSignUpDTO) => {
const appCfg = getConfig(); const appCfg = getConfig();
const existingUser = await userDAL.findOne({ email }); const sanitizedEmail = email.trim().toLowerCase();
const existingUser = await userDAL.findOne({ username: sanitizedEmail });
if (existingUser) throw new BadRequestError({ name: "Admin sign up", message: "User already exists" }); if (existingUser) throw new BadRequestError({ name: "Admin sign up", message: "User already exists" });
const privateKey = await getUserPrivateKey(password, { const privateKey = await getUserPrivateKey(password, {
@@ -292,8 +293,8 @@ export const superAdminServiceFactory = ({
{ {
firstName, firstName,
lastName, lastName,
username: email, username: sanitizedEmail,
email, email: sanitizedEmail,
superAdmin: true, superAdmin: true,
isGhost: false, isGhost: false,
isAccepted: true, isAccepted: true,
@@ -348,12 +349,13 @@ export const superAdminServiceFactory = ({
const bootstrapInstance = async ({ email, password, organizationName }: TAdminBootstrapInstanceDTO) => { const bootstrapInstance = async ({ email, password, organizationName }: TAdminBootstrapInstanceDTO) => {
const appCfg = getConfig(); const appCfg = getConfig();
const sanitizedEmail = email.trim().toLowerCase();
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID); const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
if (serverCfg?.initialized) { if (serverCfg?.initialized) {
throw new BadRequestError({ message: "Instance has already been set up" }); throw new BadRequestError({ message: "Instance has already been set up" });
} }
const existingUser = await userDAL.findOne({ email }); const existingUser = await userDAL.findOne({ email: sanitizedEmail });
if (existingUser) throw new BadRequestError({ name: "Instance initialization", message: "User already exists" }); if (existingUser) throw new BadRequestError({ name: "Instance initialization", message: "User already exists" });
const userInfo = await userDAL.transaction(async (tx) => { const userInfo = await userDAL.transaction(async (tx) => {
@@ -361,8 +363,8 @@ export const superAdminServiceFactory = ({
{ {
firstName: "Admin", firstName: "Admin",
lastName: "User", lastName: "User",
username: email, username: sanitizedEmail,
email, email: sanitizedEmail,
superAdmin: true, superAdmin: true,
isGhost: false, isGhost: false,
isAccepted: true, isAccepted: true,
@@ -372,7 +374,7 @@ export const superAdminServiceFactory = ({
tx tx
); );
const { tag, encoding, ciphertext, iv } = infisicalSymmetricEncypt(password); const { tag, encoding, ciphertext, iv } = infisicalSymmetricEncypt(password);
const encKeys = await generateUserSrpKeys(email, password); const encKeys = await generateUserSrpKeys(sanitizedEmail, password);
const userEnc = await userDAL.createUserEncryption( const userEnc = await userDAL.createUserEncryption(
{ {
+42 -8
View File
@@ -8,16 +8,18 @@ import {
TUserEncryptionKeys, TUserEncryptionKeys,
TUserEncryptionKeysInsert, TUserEncryptionKeysInsert,
TUserEncryptionKeysUpdate, TUserEncryptionKeysUpdate,
TUsers TUsers,
UsersSchema
} from "@app/db/schemas"; } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex"; import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
export type TUserDALFactory = ReturnType<typeof userDALFactory>; export type TUserDALFactory = ReturnType<typeof userDALFactory>;
export const userDALFactory = (db: TDbClient) => { export const userDALFactory = (db: TDbClient) => {
const userOrm = ormify(db, TableName.Users); const userOrm = ormify(db, TableName.Users);
const findUserByUsername = async (username: string, tx?: Knex) => userOrm.findOne({ username }, tx); const findUserByUsername = async (username: string, tx?: Knex) =>
(tx || db)(TableName.Users).whereRaw('lower("username") = :username', { username: username.toLowerCase() });
const getUsersByFilter = async ({ const getUsersByFilter = async ({
limit, limit,
@@ -41,7 +43,7 @@ export const userDALFactory = (db: TDbClient) => {
.whereILike("email", `%${searchTerm}%`) .whereILike("email", `%${searchTerm}%`)
.orWhereILike("firstName", `%${searchTerm}%`) .orWhereILike("firstName", `%${searchTerm}%`)
.orWhereILike("lastName", `%${searchTerm}%`) .orWhereILike("lastName", `%${searchTerm}%`)
.orWhereLike("username", `%${searchTerm}%`); .orWhereRaw('lower("username") like ?', `%${searchTerm}%`);
}); });
} }
@@ -65,12 +67,11 @@ export const userDALFactory = (db: TDbClient) => {
try { try {
return await db return await db
.replicaNode()(TableName.Users) .replicaNode()(TableName.Users)
.whereRaw('lower("username") = :username', { username: username.toLowerCase() })
.where({ .where({
username,
isGhost: false isGhost: false
}) })
.join(TableName.UserEncryptionKey, `${TableName.Users}.id`, `${TableName.UserEncryptionKey}.userId`) .join(TableName.UserEncryptionKey, `${TableName.Users}.id`, `${TableName.UserEncryptionKey}.userId`);
.first();
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "Find user enc by email" }); throw new DatabaseError({ error, name: "Find user enc by email" });
} }
@@ -168,6 +169,38 @@ export const userDALFactory = (db: TDbClient) => {
} }
}; };
const findAllMyAccounts = async (email: string) => {
try {
const doc = await db(TableName.Users)
.where({ email })
.leftJoin(TableName.OrgMembership, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
.leftJoin(TableName.Organization, `${TableName.Organization}.id`, `${TableName.OrgMembership}.orgId`)
.select(selectAllTableCols(TableName.Users))
.select(
db.ref("name").withSchema(TableName.Organization).as("orgName"),
db.ref("slug").withSchema(TableName.Organization).as("orgSlug")
);
const formattedDoc = sqlNestRelationships({
data: doc,
key: "id",
parentMapper: (el) => UsersSchema.parse(el),
childrenMapper: [
{
key: "orgSlug",
label: "organizations" as const,
mapper: ({ orgSlug, orgName }) => ({
slug: orgSlug,
name: orgName
})
}
]
});
return formattedDoc;
} catch (error) {
throw new DatabaseError({ error, name: "Upsert user enc key" });
}
};
// USER ACTION FUNCTIONS // USER ACTION FUNCTIONS
// --------------------- // ---------------------
const findOneUserAction = (filter: TUserActionsUpdate, tx?: Knex) => { const findOneUserAction = (filter: TUserActionsUpdate, tx?: Knex) => {
@@ -200,6 +233,7 @@ export const userDALFactory = (db: TDbClient) => {
createUserEncryption, createUserEncryption,
findOneUserAction, findOneUserAction,
createUserAction, createUserAction,
getUsersByFilter getUsersByFilter,
findAllMyAccounts
}; };
}; };
+48 -87
View File
@@ -5,11 +5,11 @@ import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/pe
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
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 { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
import { AuthMethod } from "../auth/auth-type"; import { AuthMethod } from "../auth/auth-type";
import { TGroupProjectDALFactory } from "../group-project/group-project-dal"; import { TGroupProjectDALFactory } from "../group-project/group-project-dal";
@@ -21,7 +21,7 @@ type TUserServiceFactoryDep = {
userDAL: Pick< userDAL: Pick<
TUserDALFactory, TUserDALFactory,
| "find" | "find"
| "findOne" | "findUserByUsername"
| "findById" | "findById"
| "transaction" | "transaction"
| "updateById" | "updateById"
@@ -31,8 +31,8 @@ type TUserServiceFactoryDep = {
| "createUserAction" | "createUserAction"
| "findUserEncKeyByUserId" | "findUserEncKeyByUserId"
| "delete" | "delete"
| "findAllMyAccounts"
>; >;
userAliasDAL: Pick<TUserAliasDALFactory, "find" | "insertMany">;
groupProjectDAL: Pick<TGroupProjectDALFactory, "findByUserId">; groupProjectDAL: Pick<TGroupProjectDALFactory, "findByUserId">;
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "find" | "insertMany" | "findOne" | "updateById">; orgMembershipDAL: Pick<TOrgMembershipDALFactory, "find" | "insertMany" | "findOne" | "updateById">;
tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser" | "validateTokenForUser">; tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser" | "validateTokenForUser">;
@@ -45,7 +45,6 @@ export type TUserServiceFactory = ReturnType<typeof userServiceFactory>;
export const userServiceFactory = ({ export const userServiceFactory = ({
userDAL, userDAL,
userAliasDAL,
orgMembershipDAL, orgMembershipDAL,
projectMembershipDAL, projectMembershipDAL,
groupProjectDAL, groupProjectDAL,
@@ -54,8 +53,11 @@ export const userServiceFactory = ({
permissionService permissionService
}: TUserServiceFactoryDep) => { }: TUserServiceFactoryDep) => {
const sendEmailVerificationCode = async (username: string) => { const sendEmailVerificationCode = async (username: string) => {
const user = await userDAL.findOne({ username }); // akhilmhdh: case sensitive email resolution
const users = await userDAL.findUserByUsername(username);
const user = users?.length > 1 ? users.find((el) => el.username === username) : users?.[0];
if (!user) throw new NotFoundError({ name: `User with username '${username}' not found` }); if (!user) throw new NotFoundError({ name: `User with username '${username}' not found` });
if (!user.email) if (!user.email)
throw new BadRequestError({ name: "Failed to send email verification code due to no email on user" }); throw new BadRequestError({ name: "Failed to send email verification code due to no email on user" });
if (user.isEmailVerified) if (user.isEmailVerified)
@@ -77,7 +79,21 @@ export const userServiceFactory = ({
}; };
const verifyEmailVerificationCode = async (username: string, code: string) => { const verifyEmailVerificationCode = async (username: string, code: string) => {
const user = await userDAL.findOne({ username }); // akhilmhdh: case sensitive email resolution
const usersByusername = await userDAL.findUserByUsername(username);
logger.info(
usersByusername.map((user) => ({
id: user.id,
email: user.email,
username: user.username,
isEmailVerified: user.isEmailVerified
})),
`Verify email users: [username=${username}]`
);
const user =
usersByusername?.length > 1 ? usersByusername.find((el) => el.username === username) : usersByusername?.[0];
if (!user) throw new NotFoundError({ name: `User with username '${username}' not found` }); if (!user) throw new NotFoundError({ name: `User with username '${username}' not found` });
if (!user.email) if (!user.email)
throw new BadRequestError({ name: "Failed to verify email verification code due to no email on user" }); throw new BadRequestError({ name: "Failed to verify email verification code due to no email on user" });
@@ -90,84 +106,8 @@ export const userServiceFactory = ({
code code
}); });
const { email } = user; await userDAL.updateById(user.id, {
isEmailVerified: true
await userDAL.transaction(async (tx) => {
await userDAL.updateById(
user.id,
{
isEmailVerified: true
},
tx
);
// check if there are verified users with the same email.
const users = await userDAL.find(
{
email,
isEmailVerified: true
},
{ tx }
);
if (users.length > 1) {
// merge users
const mergeUser = users.find((u) => u.id !== user.id);
if (!mergeUser) throw new NotFoundError({ name: "Failed to find merge user" });
const mergeUserOrgMembershipSet = new Set(
(await orgMembershipDAL.find({ userId: mergeUser.id }, { tx })).map((m) => m.orgId)
);
const myOrgMemberships = (await orgMembershipDAL.find({ userId: user.id }, { tx })).filter(
(m) => !mergeUserOrgMembershipSet.has(m.orgId)
);
const userAliases = await userAliasDAL.find(
{
userId: user.id
},
{ tx }
);
await userDAL.deleteById(user.id, tx);
if (myOrgMemberships.length) {
await orgMembershipDAL.insertMany(
myOrgMemberships.map((orgMembership) => ({
...orgMembership,
userId: mergeUser.id
})),
tx
);
}
if (userAliases.length) {
await userAliasDAL.insertMany(
userAliases.map((userAlias) => ({
...userAlias,
userId: mergeUser.id
})),
tx
);
}
} else {
await userDAL.delete(
{
email,
isAccepted: false,
isEmailVerified: false
},
tx
);
// update current user's username to [email]
await userDAL.updateById(
user.id,
{
username: email
},
tx
);
}
}); });
}; };
@@ -212,6 +152,23 @@ export const userServiceFactory = ({
return updatedUser; return updatedUser;
}; };
const getAllMyAccounts = async (email: string, userId: string) => {
const users = await userDAL.findAllMyAccounts(email);
return users?.map((el) => ({ ...el, isMyAccount: el.id === userId }));
};
const removeMyDuplicateAccounts = async (email: string, userId: string) => {
const users = await userDAL.find({ email });
const duplicatedAccounts = users?.filter((el) => el.id !== userId);
const myAccount = users?.find((el) => el.id === userId);
if (duplicatedAccounts.length && myAccount) {
await userDAL.transaction(async (tx) => {
await userDAL.delete({ $in: { id: duplicatedAccounts?.map((el) => el.id) } }, tx);
await userDAL.updateById(userId, { username: (myAccount.email || myAccount.username).toLowerCase() }, tx);
});
}
};
const getMe = async (userId: string) => { const getMe = async (userId: string) => {
const user = await userDAL.findUserEncKeyByUserId(userId); const user = await userDAL.findUserEncKeyByUserId(userId);
if (!user) throw new NotFoundError({ message: `User with ID '${userId}' not found`, name: "GetMe" }); if (!user) throw new NotFoundError({ message: `User with ID '${userId}' not found`, name: "GetMe" });
@@ -313,9 +270,11 @@ export const userServiceFactory = ({
}; };
const listUserGroups = async ({ username, actorOrgId, actor, actorId, actorAuthMethod }: TListUserGroupsDTO) => { const listUserGroups = async ({ username, actorOrgId, actor, actorId, actorAuthMethod }: TListUserGroupsDTO) => {
const user = await userDAL.findOne({ // akhilmhdh: case sensitive email resolution
username const usersByusername = await userDAL.findUserByUsername(username);
}); const user =
usersByusername?.length > 1 ? usersByusername.find((el) => el.username === username) : usersByusername?.[0];
if (!user) throw new NotFoundError({ name: `User with username '${username}' not found` });
// This makes it so the user can always read information about themselves, but no one else if they don't have the Members Read permission. // This makes it so the user can always read information about themselves, but no one else if they don't have the Members Read permission.
if (user.id !== actorId) { if (user.id !== actorId) {
@@ -346,7 +305,9 @@ export const userServiceFactory = ({
getUserAction, getUserAction,
unlockUser, unlockUser,
getUserPrivateKey, getUserPrivateKey,
getAllMyAccounts,
getUserProjectFavorites, getUserProjectFavorites,
removeMyDuplicateAccounts,
updateUserProjectFavorites updateUserProjectFavorites
}; };
}; };
@@ -0,0 +1,4 @@
---
title: "Available"
openapi: "GET /api/v1/app-connections/1password/available"
---
@@ -0,0 +1,8 @@
---
title: "Create"
openapi: "POST /api/v1/app-connections/1password"
---
<Note>
Check out the configuration docs for [1Password Connections](/integrations/app-connections/1password) to learn how to obtain the required credentials.
</Note>
@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/app-connections/1password/{connectionId}"
---
@@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v1/app-connections/1password/{connectionId}"
---

Some files were not shown because too many files have changed in this diff Show More