This commit is contained in:
x032205
2025-05-26 12:56:15 -04:00
110 changed files with 2204 additions and 489 deletions

View File

@@ -154,7 +154,8 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
requestId: z.string().trim()
}),
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: {
200: z.object({
@@ -170,7 +171,8 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
requestId: req.params.requestId,
status: req.body.status
status: req.body.status,
bypassReason: req.body.bypassReason
});
return { review };

View File

@@ -6,6 +6,7 @@ import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { ms } from "@app/lib/ms";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { EnforcementLevel } from "@app/lib/types";
import { triggerWorkflowIntegrationNotification } from "@app/lib/workflow-integrations/trigger-notification";
import { TriggerFeature } from "@app/lib/workflow-integrations/types";
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 { TGroupDALFactory } from "../group/group-dal";
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 { ProjectUserAdditionalPrivilegeTemporaryMode } from "../project-user-additional-privilege/project-user-additional-privilege-types";
import { TAccessApprovalRequestDALFactory } from "./access-approval-request-dal";
@@ -323,26 +325,22 @@ export const accessApprovalRequestServiceFactory = ({
status,
actorId,
actorAuthMethod,
actorOrgId
actorOrgId,
bypassReason
}: TReviewAccessRequestDTO) => {
const accessApprovalRequest = await accessApprovalRequestDAL.findById(requestId);
if (!accessApprovalRequest) {
throw new NotFoundError({ message: `Secret approval request with ID '${requestId}' not found` });
}
const { policy } = accessApprovalRequest;
const { policy, environment } = accessApprovalRequest;
if (policy.deletedAt) {
throw new BadRequestError({
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,
actorId,
projectId: accessApprovalRequest.projectId,
@@ -355,6 +353,20 @@ export const accessApprovalRequestServiceFactory = ({
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 (
!hasRole(ProjectMembershipRole.Admin) &&
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" });
}
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 });
if (existingReviews.some((review) => review.status === ApprovalStatus.REJECTED)) {
throw new BadRequestError({ message: "The request has already been rejected by another reviewer" });
}
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,
reviewerUserId: actorId
},
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,
requestId: accessApprovalRequest.id,
@@ -385,19 +425,26 @@ export const accessApprovalRequestServiceFactory = ({
},
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 (approvedReviews.length === policy.approvals) {
if (
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) {
throw new BadRequestError({ message: "Temporary range is required for temporary access" });
}
let privilegeId: string | null = null;
if (!accessApprovalRequest.isTemporary && !accessApprovalRequest.temporaryRange) {
// Permanent access
const privilege = await additionalPrivilegeDAL.create(
@@ -409,7 +456,7 @@ export const accessApprovalRequestServiceFactory = ({
},
tx
);
privilegeId = privilege.id;
privilegeIdToSet = privilege.id;
} else {
// Temporary access
const relativeTempAllocatedTimeInMs = ms(accessApprovalRequest.temporaryRange!);
@@ -421,23 +468,57 @@ export const accessApprovalRequestServiceFactory = ({
projectId: accessApprovalRequest.projectId,
slug: `requested-privilege-${slugify(alphaNumericNanoId(12))}`,
permissions: JSON.stringify(accessApprovalRequest.permissions),
isTemporary: true,
isTemporary: true, // Explicitly set to true for the privilege
temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative,
temporaryRange: accessApprovalRequest.temporaryRange!,
temporaryAccessStartTime: startTime,
temporaryAccessEndTime: new Date(new Date(startTime).getTime() + relativeTempAllocatedTimeInMs)
temporaryAccessEndTime: new Date(startTime.getTime() + relativeTempAllocatedTimeInMs)
},
tx
);
privilegeId = privilege.id;
privilegeIdToSet = privilege.id;
}
await accessApprovalRequestDAL.updateById(accessApprovalRequest.id, { privilegeId }, tx);
await accessApprovalRequestDAL.updateById(accessApprovalRequest.id, { privilegeId: privilegeIdToSet }, 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;

View File

@@ -17,6 +17,8 @@ export type TGetAccessRequestCountDTO = {
export type TReviewAccessRequestDTO = {
requestId: string;
status: ApprovalStatus;
envName?: string;
bypassReason?: string;
} & Omit<TProjectPermission, "projectId">;
export type TCreateAccessApprovalRequestDTO = {

View File

@@ -192,6 +192,11 @@ export const licenseServiceFactory = ({
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(
FEATURE_CACHE_KEY(org.id),
LICENSE_SERVER_CLOUD_PLAN_TTL,

View File

@@ -61,7 +61,8 @@ const buildAdminPermissionRules = () => {
ProjectPermissionApprovalActions.Edit,
ProjectPermissionApprovalActions.Create,
ProjectPermissionApprovalActions.Delete,
ProjectPermissionApprovalActions.AllowChangeBypass
ProjectPermissionApprovalActions.AllowChangeBypass,
ProjectPermissionApprovalActions.AllowAccessBypass
],
ProjectPermissionSub.SecretApproval
);

View File

@@ -39,7 +39,8 @@ export enum ProjectPermissionApprovalActions {
Create = "create",
Edit = "edit",
Delete = "delete",
AllowChangeBypass = "allow-change-bypass"
AllowChangeBypass = "allow-change-bypass",
AllowAccessBypass = "allow-access-bypass"
}
export enum ProjectPermissionCmekActions {

View File

@@ -2084,6 +2084,10 @@ export const AppConnections = {
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.",
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.",
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."
},
ONEPASS: {
vaultId: "The ID of the 1Password vault to sync secrets to."
}
}
};

View File

@@ -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;
}
});
};

View File

@@ -5,6 +5,10 @@ import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags } from "@app/lib/api-docs";
import { readLimit } from "@app/server/config/rateLimiter";
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 { AwsConnectionListItemSchema, SanitizedAwsConnectionSchema } from "@app/services/app-connection/aws";
import {
@@ -80,7 +84,8 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedWindmillConnectionSchema.options,
...SanitizedLdapConnectionSchema.options,
...SanitizedTeamCityConnectionSchema.options,
...SanitizedOCIConnectionSchema.options
...SanitizedOCIConnectionSchema.options,
...SanitizedOnePassConnectionSchema.options
]);
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
@@ -103,7 +108,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
WindmillConnectionListItemSchema,
LdapConnectionListItemSchema,
TeamCityConnectionListItemSchema,
OCIConnectionListItemSchema
OCIConnectionListItemSchema,
OnePassConnectionListItemSchema
]);
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {

View File

@@ -1,6 +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 { registerOnePassConnectionRouter } from "./1password-connection-router";
import { registerAuth0ConnectionRouter } from "./auth0-connection-router";
import { registerAwsConnectionRouter } from "./aws-connection-router";
import { registerAzureAppConfigurationConnectionRouter } from "./azure-app-configuration-connection-router";
@@ -44,5 +45,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
[AppConnection.HCVault]: registerHCVaultConnectionRouter,
[AppConnection.LDAP]: registerLdapConnectionRouter,
[AppConnection.TeamCity]: registerTeamCityConnectionRouter,
[AppConnection.OCI]: registerOCIConnectionRouter
[AppConnection.OCI]: registerOCIConnectionRouter,
[AppConnection.OnePass]: registerOnePassConnectionRouter
};

View File

@@ -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
});

View File

@@ -1,6 +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 { registerOCIVaultSyncRouter } from "../../../../ee/routes/v1/secret-sync-routers/oci-vault-sync-router";
import { registerOnePassSyncRouter } from "./1password-sync-router";
import { registerAwsParameterStoreSyncRouter } from "./aws-parameter-store-sync-router";
import { registerAwsSecretsManagerSyncRouter } from "./aws-secrets-manager-sync-router";
import { registerAzureAppConfigurationSyncRouter } from "./azure-app-configuration-sync-router";
@@ -33,5 +34,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: Fastif
[SecretSync.Windmill]: registerWindmillSyncRouter,
[SecretSync.HCVault]: registerHCVaultSyncRouter,
[SecretSync.TeamCity]: registerTeamCitySyncRouter,
[SecretSync.OCIVault]: registerOCIVaultSyncRouter
[SecretSync.OCIVault]: registerOCIVaultSyncRouter,
[SecretSync.OnePass]: registerOnePassSyncRouter
};

View File

@@ -6,6 +6,7 @@ import { ApiDocsTags, SecretSyncs } from "@app/lib/api-docs";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { OnePassSyncListItemSchema, OnePassSyncSchema } from "@app/services/secret-sync/1password";
import {
AwsParameterStoreSyncListItemSchema,
AwsParameterStoreSyncSchema
@@ -45,7 +46,8 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
WindmillSyncSchema,
HCVaultSyncSchema,
TeamCitySyncSchema,
OCIVaultSyncSchema
OCIVaultSyncSchema,
OnePassSyncSchema
]);
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
@@ -63,7 +65,8 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
WindmillSyncListItemSchema,
HCVaultSyncListItemSchema,
TeamCitySyncListItemSchema,
OCIVaultSyncListItemSchema
OCIVaultSyncListItemSchema,
OnePassSyncListItemSchema
]);
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {

View File

@@ -0,0 +1,3 @@
export enum OnePassConnectionMethod {
ApiToken = "api-token"
}

View File

@@ -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;
};

View File

@@ -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()
});

View File

@@ -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
};
};

View File

@@ -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;
};

View File

@@ -0,0 +1,4 @@
export * from "./1password-connection-enums";
export * from "./1password-connection-fns";
export * from "./1password-connection-schemas";
export * from "./1password-connection-types";

View File

@@ -18,7 +18,8 @@ export enum AppConnection {
HCVault = "hashicorp-vault",
LDAP = "ldap",
TeamCity = "teamcity",
OCI = "oci"
OCI = "oci",
OnePass = "1password"
}
export enum AWSRegion {

View File

@@ -14,6 +14,11 @@ import {
} from "@app/services/app-connection/shared/sql";
import { KmsDataKey } from "@app/services/kms/kms-types";
import {
getOnePassConnectionListItem,
OnePassConnectionMethod,
validateOnePassConnectionCredentials
} from "./1password";
import { AppConnection, AppConnectionPlanType } from "./app-connection-enums";
import { TAppConnectionServiceFactoryDep } from "./app-connection-service";
import {
@@ -101,7 +106,8 @@ export const listAppConnectionOptions = () => {
getHCVaultConnectionListItem(),
getLdapConnectionListItem(),
getTeamCityConnectionListItem(),
getOCIConnectionListItem()
getOCIConnectionListItem(),
getOnePassConnectionListItem()
].sort((a, b) => a.name.localeCompare(b.name));
};
@@ -172,7 +178,8 @@ export const validateAppConnectionCredentials = async (
[AppConnection.HCVault]: validateHCVaultConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.LDAP]: validateLdapConnectionCredentials 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);
@@ -201,6 +208,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
case HumanitecConnectionMethod.ApiToken:
case TerraformCloudConnectionMethod.ApiToken:
case VercelConnectionMethod.ApiToken:
case OnePassConnectionMethod.ApiToken:
return "API Token";
case PostgresConnectionMethod.UsernameAndPassword:
case MsSqlConnectionMethod.UsernameAndPassword:
@@ -266,7 +274,8 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
[AppConnection.HCVault]: platformManagedCredentialsNotSupported,
[AppConnection.LDAP]: platformManagedCredentialsNotSupported, // we could support this in the future
[AppConnection.TeamCity]: platformManagedCredentialsNotSupported,
[AppConnection.OCI]: platformManagedCredentialsNotSupported
[AppConnection.OCI]: platformManagedCredentialsNotSupported,
[AppConnection.OnePass]: platformManagedCredentialsNotSupported
};
export const enterpriseAppCheck = async (

View File

@@ -20,7 +20,8 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.HCVault]: "Hashicorp Vault",
[AppConnection.LDAP]: "LDAP",
[AppConnection.TeamCity]: "TeamCity",
[AppConnection.OCI]: "OCI"
[AppConnection.OCI]: "OCI",
[AppConnection.OnePass]: "1Password"
};
export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanType> = {
@@ -43,5 +44,6 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
[AppConnection.LDAP]: AppConnectionPlanType.Regular,
[AppConnection.TeamCity]: AppConnectionPlanType.Regular,
[AppConnection.OCI]: AppConnectionPlanType.Enterprise,
[AppConnection.OnePass]: AppConnectionPlanType.Regular,
[AppConnection.MySql]: AppConnectionPlanType.Regular
};

View File

@@ -21,6 +21,8 @@ import {
import { auth0ConnectionService } from "@app/services/app-connection/auth0/auth0-connection-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 { AppConnection } from "./app-connection-enums";
import { APP_CONNECTION_NAME_MAP } from "./app-connection-maps";
@@ -93,7 +95,8 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
[AppConnection.HCVault]: ValidateHCVaultConnectionCredentialsSchema,
[AppConnection.LDAP]: ValidateLdapConnectionCredentialsSchema,
[AppConnection.TeamCity]: ValidateTeamCityConnectionCredentialsSchema,
[AppConnection.OCI]: ValidateOCIConnectionCredentialsSchema
[AppConnection.OCI]: ValidateOCIConnectionCredentialsSchema,
[AppConnection.OnePass]: ValidateOnePassConnectionCredentialsSchema
};
export const appConnectionServiceFactory = ({
@@ -495,6 +498,7 @@ export const appConnectionServiceFactory = ({
hcvault: hcVaultConnectionService(connectAppConnectionById),
windmill: windmillConnectionService(connectAppConnectionById),
teamcity: teamcityConnectionService(connectAppConnectionById),
oci: ociConnectionService(connectAppConnectionById, licenseService)
oci: ociConnectionService(connectAppConnectionById, licenseService),
onepass: onePassConnectionService(connectAppConnectionById)
};
};

View File

@@ -1,13 +1,19 @@
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
import { TSqlConnectionConfig } from "@app/services/app-connection/shared/sql/sql-connection-types";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import {
TOCIConnection,
TOCIConnectionConfig,
TOCIConnectionInput,
TValidateOCIConnectionCredentialsSchema
} from "../../ee/services/app-connections/oci";
} from "@app/ee/services/app-connections/oci";
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
import { TSqlConnectionConfig } from "@app/services/app-connection/shared/sql/sql-connection-types";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import {
TOnePassConnection,
TOnePassConnectionConfig,
TOnePassConnectionInput,
TValidateOnePassConnectionCredentialsSchema
} from "./1password";
import { AWSRegion } from "./app-connection-enums";
import {
TAuth0Connection,
@@ -134,6 +140,7 @@ export type TAppConnection = { id: string } & (
| TLdapConnection
| TTeamCityConnection
| TOCIConnection
| TOnePassConnection
);
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
@@ -161,6 +168,7 @@ export type TAppConnectionInput = { id: string } & (
| TLdapConnectionInput
| TTeamCityConnectionInput
| TOCIConnectionInput
| TOnePassConnectionInput
);
export type TSqlConnectionInput = TPostgresConnectionInput | TMsSqlConnectionInput | TMySqlConnectionInput;
@@ -192,7 +200,8 @@ export type TAppConnectionConfig =
| THCVaultConnectionConfig
| TLdapConnectionConfig
| TTeamCityConnectionConfig
| TOCIConnectionConfig;
| TOCIConnectionConfig
| TOnePassConnectionConfig;
export type TValidateAppConnectionCredentialsSchema =
| TValidateAwsConnectionCredentialsSchema
@@ -214,7 +223,8 @@ export type TValidateAppConnectionCredentialsSchema =
| TValidateHCVaultConnectionCredentialsSchema
| TValidateLdapConnectionCredentialsSchema
| TValidateTeamCityConnectionCredentialsSchema
| TValidateOCIConnectionCredentialsSchema;
| TValidateOCIConnectionCredentialsSchema
| TValidateOnePassConnectionCredentialsSchema;
export type TListAwsConnectionKmsKeys = {
connectionId: string;

View File

@@ -0,0 +1,10 @@
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types";
export const ONEPASS_SYNC_LIST_OPTION: TSecretSyncListItem = {
name: "1Password",
destination: SecretSync.OnePass,
connection: AppConnection.OnePass,
canImportSecrets: true
};

View File

@@ -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 });
}
};

View File

@@ -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)
});

View File

@@ -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;
};

View File

@@ -0,0 +1,4 @@
export * from "./1password-sync-constants";
export * from "./1password-sync-fns";
export * from "./1password-sync-schemas";
export * from "./1password-sync-types";

View File

@@ -13,7 +13,8 @@ export enum SecretSync {
Windmill = "windmill",
HCVault = "hashicorp-vault",
TeamCity = "teamcity",
OCIVault = "oci-vault"
OCIVault = "oci-vault",
OnePass = "1password"
}
export enum SecretSyncInitialSyncBehavior {

View File

@@ -24,6 +24,7 @@ import {
import { TAppConnectionDALFactory } from "../app-connection/app-connection-dal";
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_KEY_VAULT_SYNC_LIST_OPTION, azureKeyVaultSyncFactory } from "./azure-key-vault";
import { CAMUNDA_SYNC_LIST_OPTION, camundaSyncFactory } from "./camunda";
@@ -53,7 +54,8 @@ const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
[SecretSync.Windmill]: WINDMILL_SYNC_LIST_OPTION,
[SecretSync.HCVault]: HC_VAULT_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 = () => {
@@ -174,6 +176,8 @@ export const SecretSyncFns = {
return TeamCitySyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.OCIVault:
return OCIVaultSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.OnePass:
return OnePassSyncFns.syncSecrets(secretSync, schemaSecretMap);
default:
throw new Error(
`Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
@@ -242,6 +246,9 @@ export const SecretSyncFns = {
case SecretSync.OCIVault:
secretMap = await OCIVaultSyncFns.getSecrets(secretSync);
break;
case SecretSync.OnePass:
secretMap = await OnePassSyncFns.getSecrets(secretSync);
break;
default:
throw new Error(
`Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
@@ -300,6 +307,8 @@ export const SecretSyncFns = {
return TeamCitySyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.OCIVault:
return OCIVaultSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.OnePass:
return OnePassSyncFns.removeSecrets(secretSync, schemaSecretMap);
default:
throw new Error(
`Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`

View File

@@ -16,7 +16,8 @@ export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
[SecretSync.Windmill]: "Windmill",
[SecretSync.HCVault]: "Hashicorp Vault",
[SecretSync.TeamCity]: "TeamCity",
[SecretSync.OCIVault]: "OCI Vault"
[SecretSync.OCIVault]: "OCI Vault",
[SecretSync.OnePass]: "1Password"
};
export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
@@ -34,7 +35,8 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
[SecretSync.Windmill]: AppConnection.Windmill,
[SecretSync.HCVault]: AppConnection.HCVault,
[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> = {
@@ -52,5 +54,6 @@ export const SECRET_SYNC_PLAN_MAP: Record<SecretSync, SecretSyncPlanType> = {
[SecretSync.Windmill]: SecretSyncPlanType.Regular,
[SecretSync.HCVault]: SecretSyncPlanType.Regular,
[SecretSync.TeamCity]: SecretSyncPlanType.Regular,
[SecretSync.OCIVault]: SecretSyncPlanType.Enterprise
[SecretSync.OCIVault]: SecretSyncPlanType.Enterprise,
[SecretSync.OnePass]: SecretSyncPlanType.Regular
};

View File

@@ -42,6 +42,12 @@ import {
TWindmillSyncWithCredentials
} from "@app/services/secret-sync/windmill";
import {
TOnePassSync,
TOnePassSyncInput,
TOnePassSyncListItem,
TOnePassSyncWithCredentials
} from "./1password/1password-sync-types";
import {
TAwsParameterStoreSync,
TAwsParameterStoreSyncInput,
@@ -102,7 +108,8 @@ export type TSecretSync =
| TWindmillSync
| THCVaultSync
| TTeamCitySync
| TOCIVaultSync;
| TOCIVaultSync
| TOnePassSync;
export type TSecretSyncWithCredentials =
| TAwsParameterStoreSyncWithCredentials
@@ -119,7 +126,8 @@ export type TSecretSyncWithCredentials =
| TWindmillSyncWithCredentials
| THCVaultSyncWithCredentials
| TTeamCitySyncWithCredentials
| TOCIVaultSyncWithCredentials;
| TOCIVaultSyncWithCredentials
| TOnePassSyncWithCredentials;
export type TSecretSyncInput =
| TAwsParameterStoreSyncInput
@@ -136,7 +144,8 @@ export type TSecretSyncInput =
| TWindmillSyncInput
| THCVaultSyncInput
| TTeamCitySyncInput
| TOCIVaultSyncInput;
| TOCIVaultSyncInput
| TOnePassSyncInput;
export type TSecretSyncListItem =
| TAwsParameterStoreSyncListItem
@@ -153,7 +162,8 @@ export type TSecretSyncListItem =
| TWindmillSyncListItem
| THCVaultSyncListItem
| TTeamCitySyncListItem
| TOCIVaultSyncListItem;
| TOCIVaultSyncListItem
| TOnePassSyncListItem;
export type TSyncOptionsConfig = {
canImportSecrets: boolean;

View File

@@ -12,6 +12,7 @@ interface SecretApprovalRequestBypassedTemplateProps
environment: string;
bypassReason: string;
approvalUrl: string;
requestType: "change" | "access";
}
export const SecretApprovalRequestBypassedTemplate = ({
@@ -22,7 +23,8 @@ export const SecretApprovalRequestBypassedTemplate = ({
secretPath,
environment,
bypassReason,
approvalUrl
approvalUrl,
requestType = "change"
}: SecretApprovalRequestBypassedTemplateProps) => {
return (
<BaseEmailWrapper
@@ -39,8 +41,9 @@ export const SecretApprovalRequestBypassedTemplate = ({
<Link href={`mailto:${requesterEmail}`} className="text-slate-700 no-underline">
{requesterEmail}
</Link>
) has merged a secret to <strong>{secretPath}</strong> in the <strong>{environment}</strong> environment
without obtaining the required approval.
) has {requestType === "change" ? "merged" : "accessed"} a secret {requestType === "change" ? "to" : "in"}{" "}
<strong>{secretPath}</strong> in the <strong>{environment}</strong> environment without obtaining the required
approval.
</Text>
<Text className="text-[14px] text-slate-700 leading-[24px]">
<strong className="text-black">The following reason was provided for bypassing the policy:</strong> "

View File

@@ -0,0 +1,4 @@
---
title: "Available"
openapi: "GET /api/v1/app-connections/1password/available"
---

View File

@@ -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>

View File

@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/app-connections/1password/{connectionId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v1/app-connections/1password/{connectionId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v1/app-connections/1password/connection-name/{connectionName}"
---

View File

@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v1/app-connections/1password"
---

View File

@@ -0,0 +1,8 @@
---
title: "Update"
openapi: "PATCH /api/v1/app-connections/1password/{connectionId}"
---
<Note>
Check out the configuration docs for [1Password Connections](/integrations/app-connections/1password) to learn how to obtain the required credentials.
</Note>

View File

@@ -0,0 +1,4 @@
---
title: "Create"
openapi: "POST /api/v1/secret-syncs/1password"
---

View File

@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/secret-syncs/1password/{syncId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v1/secret-syncs/1password/{syncId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v1/secret-syncs/1password/sync-name/{syncName}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Import Secrets"
openapi: "POST /api/v1/secret-syncs/1password/{syncId}/import-secrets"
---

View File

@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v1/secret-syncs/1password"
---

View File

@@ -0,0 +1,4 @@
---
title: "Remove Secrets"
openapi: "POST /api/v1/secret-syncs/1password/{syncId}/remove-secrets"
---

View File

@@ -0,0 +1,4 @@
---
title: "Sync Secrets"
openapi: "POST /api/v1/secret-syncs/1password/{syncId}/sync-secrets"
---

View File

@@ -0,0 +1,4 @@
---
title: "Update"
openapi: "PATCH /api/v1/secret-syncs/1password/{syncId}"
---

View File

@@ -3,10 +3,10 @@ title: "Access Requests"
description: "Learn how to request access to sensitive resources in Infisical."
---
In certain situations, developers need to expand their access to a certain new project or a sensitive environment. For those use cases, it is helpful to utilize Infisical's **Access Requests** functionality.
In certain situations, developers need to expand their access to a certain new project or a sensitive environment. For those use cases, it is helpful to utilize Infisical's **Access Requests** functionality.
This functionality works in the following way:
1. A project administrator sets up an access policy that assigns access managers (also known as eligible approvers) to a certain sensitive folder or environment.
This functionality works in the following way:
1. A project administrator sets up an access policy that assigns access managers (also known as eligible approvers) to a certain sensitive folder or environment.
![Create Access Request Policy Modal](/images/platform/access-controls/create-access-request-policy.png)
![Access Request Policies](/images/platform/access-controls/access-request-policies.png)
@@ -19,9 +19,8 @@ This functionality works in the following way:
![Access Request Bypass](/images/platform/access-controls/access-request-bypass.png)
<Info>
If the access request matches with a policy that has a **Soft** enforcement level, the requester may bypass the policy and get access to the resource without full approval.
If the access request matches with a policy that allows break-glass approval bypasses, the requester may bypass the policy and get access to the resource without full approval.
</Info>
5. As soon as the request is approved, developer is able to access the sought resources.
5. As soon as the request is approved, developer is able to access the sought resources.
![Access Request Dashboard](/images/platform/access-controls/access-requests-completed.png)

View File

@@ -33,6 +33,10 @@ First, you would need to create a set of policies for a certain environment. In
The enforcement level determines how strict the policy is. A **Hard** enforcement level means that any change that matches the policy will need full approval prior merging. A **Soft** enforcement level allows for break glass functionality on the request. If a change request is bypassed, the approvers will be notified via email.
<Note>
Enabling the "Bypass Approvals" toggle during policy creation will create a **Soft** enforcement level. Disabling the toggle makes the enforcement level **Hard**.
</Note>
### Self approvals
If the **Self Approvals** option is enabled, users who are designated as approvers on the policy can approve requests that they themselves have submitted.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 820 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 780 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 436 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 420 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 427 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 715 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 692 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 678 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 727 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 726 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 747 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -0,0 +1,123 @@
---
title: "1Password Connection"
description: "Learn how to configure a 1Password Connection for Infisical."
---
Infisical supports the use of [Service Accounts](https://developer.1password.com/docs/service-accounts) to connect with 1Password.
## Setup 1Password Connect Server
<Note>
If you already have a Connect Server for your vault you may skip this step.
</Note>
<Steps>
<Step title="Navigate to 'Developer' and click 'Other'">
![Developer Page](/images/app-connections/1password/developer-page.png)
</Step>
<Step title="Select 'Connect Server'">
![Click Connect Server](/images/app-connections/1password/click-connect-server.png)
</Step>
<Step title="Configure Connect Server">
1. Input a name for your Connect Server
2. Click "Choose Vaults" and select the vaults you want to connect
3. For each selected vault, click **Edit Access** and **Enable All**
4. Click "Add Environment"
![Configure Connect Server](/images/app-connections/1password/configure-connect-server.png)
</Step>
<Step title="Configure Access Token">
1. Input a name and expiration for the token
2. Click "Choose Vaults" and select the vaults you want to connect
3. For each selected vault, click **Edit Access** and **Enable All**
4. Click "Issue Token"
![Set Up Access Token](/images/app-connections/1password/set-up-access-token.png)
</Step>
<Step title="Deploy Connect Server">
Download the Credentials File and set up your Connect Server.
<Note>
Follow [this guide](https://developer.1password.com/docs/connect/get-started#step-2-deploy-1password-connect-server) to deploy a Connect Server.
</Note>
Make sure to save the **Access Token** for later use.
![Deploy Server](/images/app-connections/1password/deploy-server.png)
</Step>
</Steps>
## Create 1Password Connection in Infisical
<Tabs>
<Tab title="Infisical UI">
<Steps>
<Step title="Navigate to App Connections">
In your Infisical dashboard, go to **Organization Settings** and select the [**App Connections**](https://app.infisical.com/organization/app-connections) tab.
![App Connections Tab](/images/app-connections/general/add-connection.png)
</Step>
<Step title="Select 1Password Connection">
Click the **+ Add Connection** button and select the **1Password Connection** option from the available integrations.
![Select 1Password Connection](/images/app-connections/1password/app-connection-option.png)
</Step>
<Step title="Fill out the 1Password Connection Modal">
Complete the 1Password Connection form by entering:
- A descriptive name for the connection
- An optional description for future reference
- The URL at which your 1Password Connect Server instance is hosted
- The Access Token from earlier steps
![1Password Connection Modal](/images/app-connections/1password/app-connection-modal.png)
</Step>
<Step title="Connection Created">
After clicking Create, your **1Password Connection** is established and ready to use with your Infisical projects.
![1Password Connection Created](/images/app-connections/1password/app-connection-created.png)
</Step>
</Steps>
</Tab>
<Tab title="API">
To create an 1Password Connection, make an API request to the [Create 1Password Connection](/api-reference/endpoints/app-connections/1password/create) API endpoint.
### Sample request
```bash Request
curl --request POST \
--url https://app.infisical.com/api/v1/app-connections/1password \
--header 'Content-Type: application/json' \
--data '{
"name": "my-1password-connection",
"method": "api-token",
"credentials": {
"instanceUrl": "https://1pass.example.com",
"apiToken": "[PRIVATE TOKEN]"
}
}'
```
### Sample response
```bash Response
{
"appConnection": {
"id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6",
"name": "my-1password-connection",
"description": null,
"version": 1,
"orgId": "6f03caa1-a5de-43ce-b127-95a145d3464c",
"createdAt": "2025-04-23T19:46:34.831Z",
"updatedAt": "2025-04-23T19:46:34.831Z",
"isPlatformManagedCredentials": false,
"credentialsHash": "7c2d371dec195f82a6a0d5b41c970a229cfcaf88e894a5b6395e2dbd0280661f",
"app": "1password",
"method": "api-token",
"credentials": {
"instanceUrl": "https://1pass.example.com"
}
}
}
```
</Tab>
</Tabs>

View File

@@ -0,0 +1,163 @@
---
title: "1Password Sync"
description: "Learn how to configure a 1Password Sync for Infisical."
---
**Prerequisites:**
- Create an [1Password Connection](/integrations/app-connections/1password)
<Tabs>
<Tab title="Infisical UI">
<Steps>
<Step title="Add Sync">
Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button.
![Secret Syncs Tab](/images/secret-syncs/general/secret-sync-tab.png)
</Step>
<Step title="Select '1Password'">
![Select 1Password](/images/secret-syncs/1password/select-option.png)
</Step>
<Step title="Configure source">
Configure the **Source** from where secrets should be retrieved, then click **Next**.
![Configure Source](/images/secret-syncs/1password/configure-source.png)
- **Environment**: The project environment to retrieve secrets from.
- **Secret Path**: The folder path to retrieve secrets from.
<Tip>
If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports).
</Tip>
</Step>
<Step title="Configure destination">
Configure the **Destination** to where secrets should be deployed, then click **Next**.
![Configure Destination](/images/secret-syncs/1password/configure-destination.png)
- **1Password Connection**: The 1Password Connection to authenticate with.
- **Vault**: The 1Password vault to sync secrets to.
</Step>
<Step title="Configure sync options">
Configure the **Sync Options** to specify how secrets should be synced, then click **Next**.
![Configure Sync Options](/images/secret-syncs/1password/configure-sync-options.png)
- **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync.
- **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical.
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over 1Password when keys conflict.
- **Import Secrets (Prioritize 1Password)**: Imports secrets from the destination endpoint before syncing, prioritizing values from 1Password over Infisical when keys conflict.
- **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name.
<Note>
We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched.
</Note>
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
- **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical.
</Step>
<Step title="Configure details">
Configure the **Details** of your 1Password Sync, then click **Next**.
![Configure Details](/images/secret-syncs/1password/configure-details.png)
- **Name**: The name of your sync. Must be slug-friendly.
- **Description**: An optional description for your sync.
</Step>
<Step title="Review configuration">
Review your 1Password Sync configuration, then click **Create Sync**.
![Review Configuration](/images/secret-syncs/1password/review-configuration.png)
</Step>
<Step title="Sync created">
If enabled, your 1Password Sync will begin syncing your secrets to the destination endpoint.
![Sync Created](/images/secret-syncs/1password/sync-created.png)
</Step>
</Steps>
</Tab>
<Tab title="API">
To create an **1Password Sync**, make an API request to the [Create 1Password Sync](/api-reference/endpoints/secret-syncs/1password/create) API endpoint.
### Sample request
```bash Request
curl --request POST \
--url https://app.infisical.com/api/v1/secret-syncs/1password \
--header 'Content-Type: application/json' \
--data '{
"name": "my-1password-sync",
"projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"description": "an example sync",
"connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"environment": "dev",
"secretPath": "/my-secrets",
"isEnabled": true,
"syncOptions": {
"initialSyncBehavior": "overwrite-destination"
},
"destinationConfig": {
"vaultId": "..."
}
}'
```
### Sample response
```bash Response
{
"secretSync": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "my-1password-sync",
"description": "an example sync",
"isEnabled": true,
"version": 1,
"folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"createdAt": "2023-11-07T05:31:56Z",
"updatedAt": "2023-11-07T05:31:56Z",
"syncStatus": "succeeded",
"lastSyncJobId": "123",
"lastSyncMessage": null,
"lastSyncedAt": "2023-11-07T05:31:56Z",
"importStatus": null,
"lastImportJobId": null,
"lastImportMessage": null,
"lastImportedAt": null,
"removeStatus": null,
"lastRemoveJobId": null,
"lastRemoveMessage": null,
"lastRemovedAt": null,
"syncOptions": {
"initialSyncBehavior": "overwrite-destination"
},
"projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"connection": {
"app": "1password",
"name": "my-1password-connection",
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
},
"environment": {
"slug": "dev",
"name": "Development",
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
},
"folder": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"path": "/my-secrets"
},
"destination": "1password",
"destinationConfig": {
"vaultId": "..."
}
}
}
```
</Tab>
</Tabs>
## FAQ
<AccordionGroup>
<Accordion title="How are different 1Password item types treated?">
Infisical can only perform CRUD operations on the following item types:
- API Credentials
</Accordion>
</AccordionGroup>

View File

@@ -178,13 +178,14 @@ Supports conditions and permission inversion
#### Subject: `secret-approval`
| Action | Description |
| --------------------- | ---------------------------------------------------------------------------- |
| `read` | View approval policies and requests |
| `create` | Create new approval policies |
| `edit` | Modify approval policies |
| `delete` | Remove approval policies |
| `allow-change-bypass` | Allow request creators to bypass policy in break-glass situations |
| Action | Description |
| --------------------- | ----------------------------------------------------------------------------------- |
| `read` | View approval policies and requests |
| `create` | Create new approval policies |
| `edit` | Modify approval policies |
| `delete` | Remove approval policies |
| `allow-change-bypass` | Allow request creators to merge changes without approval in break-glass situations |
| `allow-access-bypass` | Allow request creators to access secrets without approval in break-glass situations |
#### Subject: `secret-rotation`

View File

@@ -470,6 +470,7 @@
{
"group": "Connections",
"pages": [
"integrations/app-connections/1password",
"integrations/app-connections/auth0",
"integrations/app-connections/aws",
"integrations/app-connections/azure-app-configuration",
@@ -500,6 +501,7 @@
{
"group": "Syncs",
"pages": [
"integrations/secret-syncs/1password",
"integrations/secret-syncs/aws-parameter-store",
"integrations/secret-syncs/aws-secrets-manager",
"integrations/secret-syncs/azure-app-configuration",
@@ -1041,6 +1043,18 @@
"pages": [
"api-reference/endpoints/app-connections/list",
"api-reference/endpoints/app-connections/options",
{
"group": "1Password",
"pages": [
"api-reference/endpoints/app-connections/1password/list",
"api-reference/endpoints/app-connections/1password/available",
"api-reference/endpoints/app-connections/1password/get-by-id",
"api-reference/endpoints/app-connections/1password/get-by-name",
"api-reference/endpoints/app-connections/1password/create",
"api-reference/endpoints/app-connections/1password/update",
"api-reference/endpoints/app-connections/1password/delete"
]
},
{
"group": "Auth0",
"pages": [
@@ -1276,6 +1290,20 @@
"pages": [
"api-reference/endpoints/secret-syncs/list",
"api-reference/endpoints/secret-syncs/options",
{
"group": "1Password",
"pages": [
"api-reference/endpoints/secret-syncs/1password/list",
"api-reference/endpoints/secret-syncs/1password/get-by-id",
"api-reference/endpoints/secret-syncs/1password/get-by-name",
"api-reference/endpoints/secret-syncs/1password/create",
"api-reference/endpoints/secret-syncs/1password/update",
"api-reference/endpoints/secret-syncs/1password/delete",
"api-reference/endpoints/secret-syncs/1password/sync-secrets",
"api-reference/endpoints/secret-syncs/1password/import-secrets",
"api-reference/endpoints/secret-syncs/1password/remove-secrets"
]
},
{
"group": "AWS Parameter Store",
"pages": [

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

View File

@@ -0,0 +1,74 @@
import { Controller, useFormContext, useWatch } from "react-hook-form";
import { SingleValue } from "react-select";
import { faCircleInfo } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
import { FilterableSelect, FormControl, Tooltip } from "@app/components/v2";
import {
TOnePassVault,
useOnePassConnectionListVaults
} from "@app/hooks/api/appConnections/1password";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TSecretSyncForm } from "../schemas";
export const OnePassSyncFields = () => {
const { control, setValue } = useFormContext<
TSecretSyncForm & { destination: SecretSync.OnePass }
>();
const connectionId = useWatch({ name: "connection.id", control });
const { data: vaults, isLoading: isVaultsLoading } = useOnePassConnectionListVaults(
connectionId,
{
enabled: Boolean(connectionId)
}
);
return (
<>
<SecretSyncConnectionField
onChange={() => {
setValue("destinationConfig.vaultId", "");
}}
/>
<Controller
name="destinationConfig.vaultId"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Vault"
helperText={
<Tooltip
className="max-w-md"
content="Ensure the vault exists in the connection's OnePass instance URL."
>
<div>
<span>Don&#39;t see the vault you&#39;re looking for?</span>{" "}
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
</div>
</Tooltip>
}
>
<FilterableSelect
menuPlacement="top"
isLoading={isVaultsLoading && Boolean(connectionId)}
isDisabled={!connectionId}
value={vaults?.find((v) => v.id === value) ?? null}
onChange={(option) => onChange((option as SingleValue<TOnePassVault>)?.id ?? null)}
options={vaults}
placeholder="Select a vault..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
/>
</FormControl>
)}
/>
</>
);
};

View File

@@ -3,6 +3,7 @@ import { useFormContext } from "react-hook-form";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TSecretSyncForm } from "../schemas";
import { OnePassSyncFields } from "./1PasswordSyncFields";
import { AwsParameterStoreSyncFields } from "./AwsParameterStoreSyncFields";
import { AwsSecretsManagerSyncFields } from "./AwsSecretsManagerSyncFields";
import { AzureAppConfigurationSyncFields } from "./AzureAppConfigurationSyncFields";
@@ -55,6 +56,8 @@ export const SecretSyncDestinationFields = () => {
return <TeamCitySyncFields />;
case SecretSync.OCIVault:
return <OCIVaultSyncFields />;
case SecretSync.OnePass:
return <OnePassSyncFields />;
default:
throw new Error(`Unhandled Destination Config Field: ${destination}`);
}

View File

@@ -49,6 +49,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
case SecretSync.Windmill:
case SecretSync.HCVault:
case SecretSync.TeamCity:
case SecretSync.OnePass:
case SecretSync.OCIVault:
AdditionalSyncOptionsFieldsComponent = null;
break;

View File

@@ -0,0 +1,12 @@
import { useFormContext } from "react-hook-form";
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
import { GenericFieldLabel } from "@app/components/v2";
import { SecretSync } from "@app/hooks/api/secretSyncs";
export const OnePassSyncReviewFields = () => {
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.OnePass }>();
const vaultId = watch("destinationConfig.vaultId");
return <GenericFieldLabel label="Vault ID">{vaultId}</GenericFieldLabel>;
};

View File

@@ -24,6 +24,7 @@ import { GitHubSyncReviewFields } from "./GitHubSyncReviewFields";
import { HCVaultSyncReviewFields } from "./HCVaultSyncReviewFields";
import { HumanitecSyncReviewFields } from "./HumanitecSyncReviewFields";
import { OCIVaultSyncReviewFields } from "./OCIVaultSyncReviewFields";
import { OnePassSyncReviewFields } from "./OnePassSyncReviewFields";
import { TeamCitySyncReviewFields } from "./TeamCitySyncReviewFields";
import { TerraformCloudSyncReviewFields } from "./TerraformCloudSyncReviewFields";
import { VercelSyncReviewFields } from "./VercelSyncReviewFields";
@@ -96,6 +97,9 @@ export const SecretSyncReviewFields = () => {
case SecretSync.OCIVault:
DestinationFieldsComponent = <OCIVaultSyncReviewFields />;
break;
case SecretSync.OnePass:
DestinationFieldsComponent = <OnePassSyncReviewFields />;
break;
default:
throw new Error(`Unhandled Destination Review Fields: ${destination}`);
}

View File

@@ -0,0 +1,13 @@
import { z } from "zod";
import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema";
import { SecretSync } from "@app/hooks/api/secretSyncs";
export const OnePassSyncDestinationSchema = BaseSecretSyncSchema().merge(
z.object({
destination: z.literal(SecretSync.OnePass),
destinationConfig: z.object({
vaultId: z.string().trim().min(1, "Vault ID required")
})
})
);

View File

@@ -1,5 +1,6 @@
import { z } from "zod";
import { OnePassSyncDestinationSchema } from "./1password-sync-destination-schema";
import { AwsParameterStoreSyncDestinationSchema } from "./aws-parameter-store-sync-destination-schema";
import { AwsSecretsManagerSyncDestinationSchema } from "./aws-secrets-manager-sync-destination-schema";
import { AzureAppConfigurationSyncDestinationSchema } from "./azure-app-configuration-sync-destination-schema";
@@ -31,7 +32,8 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
WindmillSyncDestinationSchema,
HCVaultSyncDestinationSchema,
TeamCitySyncDestinationSchema,
OCIVaultSyncDestinationSchema
OCIVaultSyncDestinationSchema,
OnePassSyncDestinationSchema
]);
export const SecretSyncFormSchema = SecretSyncUnionSchema;

View File

@@ -29,7 +29,8 @@ export enum ProjectPermissionApprovalActions {
Create = "create",
Edit = "edit",
Delete = "delete",
AllowChangeBypass = "allow-change-bypass"
AllowChangeBypass = "allow-change-bypass",
AllowAccessBypass = "allow-access-bypass"
}
export enum ProjectPermissionDynamicSecretActions {

View File

@@ -24,6 +24,7 @@ import {
LdapConnectionMethod,
MsSqlConnectionMethod,
MySqlConnectionMethod,
OnePassConnectionMethod,
PostgresConnectionMethod,
TAppConnection,
TeamCityConnectionMethod,
@@ -65,7 +66,8 @@ export const APP_CONNECTION_MAP: Record<
[AppConnection.HCVault]: { name: "Hashicorp Vault", image: "Vault.png", size: 65 },
[AppConnection.LDAP]: { name: "LDAP", image: "LDAP.png", size: 65 },
[AppConnection.TeamCity]: { name: "TeamCity", image: "TeamCity.png" },
[AppConnection.OCI]: { name: "OCI", image: "Oracle.png", enterprise: true }
[AppConnection.OCI]: { name: "OCI", image: "Oracle.png", enterprise: true },
[AppConnection.OnePass]: { name: "1Password", image: "1Password.png" }
};
export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => {
@@ -91,6 +93,7 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
case HumanitecConnectionMethod.ApiToken:
case TerraformCloudConnectionMethod.ApiToken:
case VercelConnectionMethod.ApiToken:
case OnePassConnectionMethod.ApiToken:
return { name: "API Token", icon: faKey };
case PostgresConnectionMethod.UsernameAndPassword:
case MsSqlConnectionMethod.UsernameAndPassword:

View File

@@ -51,6 +51,10 @@ export const SECRET_SYNC_MAP: Record<SecretSync, { name: string; image: string }
[SecretSync.OCIVault]: {
name: "OCI Vault",
image: "Oracle.png"
},
[SecretSync.OnePass]: {
name: "1Password",
image: "1Password.png"
}
};
@@ -69,7 +73,8 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
[SecretSync.Windmill]: AppConnection.Windmill,
[SecretSync.HCVault]: AppConnection.HCVault,
[SecretSync.TeamCity]: AppConnection.TeamCity,
[SecretSync.OCIVault]: AppConnection.OCI
[SecretSync.OCIVault]: AppConnection.OCI,
[SecretSync.OnePass]: AppConnection.OnePass
};
export const SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP: Record<

View File

@@ -131,20 +131,27 @@ export const useReviewAccessRequest = () => {
projectSlug: string;
envSlug?: string;
requestedBy?: string;
bypassReason?: string;
}
>({
mutationFn: async ({ requestId, status }) => {
mutationFn: async ({ requestId, status, bypassReason }) => {
const { data } = await apiRequest.post(
`/api/v1/access-approvals/requests/${requestId}/review`,
{
status
status,
bypassReason
}
);
return data;
},
onSuccess: (_, { projectSlug, envSlug, requestedBy }) => {
onSuccess: (_, { projectSlug, envSlug, requestedBy, bypassReason }) => {
queryClient.invalidateQueries({
queryKey: accessApprovalKeys.getAccessApprovalRequests(projectSlug, envSlug, requestedBy)
queryKey: accessApprovalKeys.getAccessApprovalRequests(
projectSlug,
envSlug,
requestedBy,
bypassReason
)
});
queryClient.invalidateQueries({
queryKey: accessApprovalKeys.getAccessApprovalRequestCount(projectSlug)

View File

@@ -19,8 +19,12 @@ export const accessApprovalKeys = {
getAccessApprovalPolicyOfABoard: (workspaceId: string, environment: string) =>
[{ workspaceId, environment }, "access-approval-policy"] as const,
getAccessApprovalRequests: (projectSlug: string, envSlug?: string, requestedBy?: string) =>
[{ projectSlug, envSlug, requestedBy }, "access-approvals-requests"] as const,
getAccessApprovalRequests: (
projectSlug: string,
envSlug?: string,
requestedBy?: string,
bypassReason?: string
) => [{ projectSlug, envSlug, requestedBy, bypassReason }, "access-approvals-requests"] as const,
getAccessApprovalRequestCount: (projectSlug: string) =>
[{ projectSlug }, "access-approval-request-count"] as const
};

View File

@@ -0,0 +1,2 @@
export * from "./queries";
export * from "./types";

View File

@@ -0,0 +1,37 @@
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { appConnectionKeys } from "../queries";
import { TOnePassVault } from "./types";
const onePassConnectionKeys = {
all: [...appConnectionKeys.all, "1password"] as const,
listVaults: (connectionId: string) =>
[...onePassConnectionKeys.all, "vaults", connectionId] as const
};
export const useOnePassConnectionListVaults = (
connectionId: string,
options?: Omit<
UseQueryOptions<
TOnePassVault[],
unknown,
TOnePassVault[],
ReturnType<typeof onePassConnectionKeys.listVaults>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: onePassConnectionKeys.listVaults(connectionId),
queryFn: async () => {
const { data } = await apiRequest.get<TOnePassVault[]>(
`/api/v1/app-connections/1password/${connectionId}/vaults`
);
return data;
},
...options
});
};

View File

@@ -0,0 +1,12 @@
export type TOnePassVault = {
id: string;
name: string;
type: string;
items: number;
attributeVersion: number;
contentVersion: number;
createdAt: string;
updatedAt: string;
};

View File

@@ -18,5 +18,6 @@ export enum AppConnection {
HCVault = "hashicorp-vault",
LDAP = "ldap",
TeamCity = "teamcity",
OCI = "oci"
OCI = "oci",
OnePass = "1password"
}

View File

@@ -0,0 +1,14 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
export enum OnePassConnectionMethod {
ApiToken = "api-token"
}
export type TOnePassConnection = TRootAppConnection & { app: AppConnection.OnePass } & {
method: OnePassConnectionMethod.ApiToken;
credentials: {
apiToken: string;
instanceUrl: string;
};
};

View File

@@ -92,6 +92,10 @@ export type TOCIConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.OCI;
};
export type TOnePassConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.OnePass;
};
export type TAppConnectionOption =
| TAwsConnectionOption
| TGitHubConnectionOption
@@ -111,7 +115,8 @@ export type TAppConnectionOption =
| TAuth0ConnectionOption
| THCVaultConnectionOption
| TTeamCityConnectionOption
| TOCIConnectionOption;
| TOCIConnectionOption
| TOnePassConnectionOption;
export type TAppConnectionOptionMap = {
[AppConnection.AWS]: TAwsConnectionOption;
@@ -134,4 +139,5 @@ export type TAppConnectionOptionMap = {
[AppConnection.LDAP]: TLdapConnectionOption;
[AppConnection.TeamCity]: TTeamCityConnectionOption;
[AppConnection.OCI]: TOCIConnectionOption;
[AppConnection.OnePass]: TOnePassConnectionOption;
};

View File

@@ -1,4 +1,5 @@
import { AppConnection } from "../enums";
import { TOnePassConnection } from "./1password-connection";
import { TAppConnectionOption } from "./app-options";
import { TAuth0Connection } from "./auth0-connection";
import { TAwsConnection } from "./aws-connection";
@@ -21,6 +22,7 @@ import { TTerraformCloudConnection } from "./terraform-cloud-connection";
import { TVercelConnection } from "./vercel-connection";
import { TWindmillConnection } from "./windmill-connection";
export * from "./1password-connection";
export * from "./auth0-connection";
export * from "./aws-connection";
export * from "./azure-app-configuration-connection";
@@ -62,7 +64,8 @@ export type TAppConnection =
| THCVaultConnection
| TLdapConnection
| TTeamCityConnection
| TOCIConnection;
| TOCIConnection
| TOnePassConnection;
export type TAvailableAppConnection = Pick<TAppConnection, "name" | "id">;
@@ -110,4 +113,5 @@ export type TAppConnectionMap = {
[AppConnection.LDAP]: TLdapConnection;
[AppConnection.TeamCity]: TTeamCityConnection;
[AppConnection.OCI]: TOCIConnection;
[AppConnection.OnePass]: TOnePassConnection;
};

View File

@@ -13,7 +13,8 @@ export enum SecretSync {
Windmill = "windmill",
HCVault = "hashicorp-vault",
TeamCity = "teamcity",
OCIVault = "oci-vault"
OCIVault = "oci-vault",
OnePass = "1password"
}
export enum SecretSyncStatus {

View File

@@ -0,0 +1,15 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync";
export type TOnePassSync = TRootSecretSync & {
destination: SecretSync.OnePass;
destinationConfig: {
vaultId: string;
};
connection: {
app: AppConnection.OnePass;
name: string;
id: string;
};
};

View File

@@ -1,6 +1,7 @@
import { SecretSync, SecretSyncImportBehavior } from "@app/hooks/api/secretSyncs";
import { DiscriminativePick } from "@app/types";
import { TOnePassSync } from "./1password-sync";
import { TAwsParameterStoreSync } from "./aws-parameter-store-sync";
import { TAwsSecretsManagerSync } from "./aws-secrets-manager-sync";
import { TAzureAppConfigurationSync } from "./azure-app-configuration-sync";
@@ -39,7 +40,8 @@ export type TSecretSync =
| TWindmillSync
| THCVaultSync
| TTeamCitySync
| TOCIVaultSync;
| TOCIVaultSync
| TOnePassSync;
export type TListSecretSyncs = { secretSyncs: TSecretSync[] };

View File

@@ -0,0 +1,150 @@
import { Controller, FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import {
Button,
FormControl,
Input,
ModalClose,
SecretInput,
Select,
SelectItem
} from "@app/components/v2";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import { OnePassConnectionMethod, TOnePassConnection } from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
} from "./GenericAppConnectionFields";
type Props = {
appConnection?: TOnePassConnection;
onSubmit: (formData: FormData) => void;
};
const rootSchema = genericAppConnectionFieldsSchema.extend({
app: z.literal(AppConnection.OnePass)
});
const formSchema = z.discriminatedUnion("method", [
rootSchema.extend({
method: z.literal(OnePassConnectionMethod.ApiToken),
credentials: z.object({
apiToken: z.string().trim().min(1, "API Token required"),
instanceUrl: z.string().trim().url("Invalid Connect Server instance URL")
})
})
]);
type FormData = z.infer<typeof formSchema>;
export const OnePassConnectionForm = ({ appConnection, onSubmit }: Props) => {
const isUpdate = Boolean(appConnection);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: appConnection ?? {
app: AppConnection.OnePass,
method: OnePassConnectionMethod.ApiToken
}
});
const {
handleSubmit,
control,
formState: { isSubmitting, isDirty }
} = form;
return (
<FormProvider {...form}>
<form onSubmit={handleSubmit(onSubmit)}>
{!isUpdate && <GenericAppConnectionsFields />}
<Controller
name="method"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipText={`The method you would like to use to connect with ${
APP_CONNECTION_MAP[AppConnection.OnePass].name
}. This field cannot be changed after creation.`}
errorText={error?.message}
isError={Boolean(error?.message)}
label="Method"
>
<Select
isDisabled={isUpdate}
value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500"
position="popper"
dropdownContainerClassName="max-w-none"
>
{Object.values(OnePassConnectionMethod).map((method) => {
return (
<SelectItem value={method} key={method}>
{getAppConnectionMethodDetails(method).name}{" "}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
<Controller
name="credentials.instanceUrl"
control={control}
shouldUnregister
render={({ field, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Instance URL"
tooltipClassName="max-w-sm"
tooltipText="The URL of the 1Password Connect Server instance to authenticate with."
>
<Input {...field} placeholder="https://1pass.example.com" />
</FormControl>
)}
/>
<Controller
name="credentials.apiToken"
control={control}
shouldUnregister
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="API Token"
>
<SecretInput
containerClassName="text-gray-400 group-focus-within:!border-primary-400/50 border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
colorSchema="secondary"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
{isUpdate ? "Update Credentials" : "Connect to 1Password"}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
</FormProvider>
);
};

View File

@@ -9,6 +9,7 @@ import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { DiscriminativePick } from "@app/types";
import { AppConnectionHeader } from "../AppConnectionHeader";
import { OnePassConnectionForm } from "./1PasswordConnectionForm";
import { Auth0ConnectionForm } from "./Auth0ConnectionForm";
import { AwsConnectionForm } from "./AwsConnectionForm";
import { AzureAppConfigurationConnectionForm } from "./AzureAppConfigurationConnectionForm";
@@ -107,6 +108,8 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
return <TeamCityConnectionForm onSubmit={onSubmit} />;
case AppConnection.OCI:
return <OCIConnectionForm onSubmit={onSubmit} />;
case AppConnection.OnePass:
return <OnePassConnectionForm onSubmit={onSubmit} />;
default:
throw new Error(`Unhandled App ${app}`);
}
@@ -183,6 +186,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
return <TeamCityConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.OCI:
return <OCIConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.OnePass:
return <OnePassConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
default:
throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`);

View File

@@ -58,7 +58,8 @@ const ApprovalPolicyActionSchema = z.object({
[ProjectPermissionApprovalActions.Edit]: z.boolean().optional(),
[ProjectPermissionApprovalActions.Delete]: z.boolean().optional(),
[ProjectPermissionApprovalActions.Create]: z.boolean().optional(),
[ProjectPermissionApprovalActions.AllowChangeBypass]: z.boolean().optional()
[ProjectPermissionApprovalActions.AllowChangeBypass]: z.boolean().optional(),
[ProjectPermissionApprovalActions.AllowAccessBypass]: z.boolean().optional()
});
const CmekPolicyActionSchema = z.object({
@@ -578,6 +579,7 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
const canEdit = action.includes(ProjectPermissionApprovalActions.Edit);
const canRead = action.includes(ProjectPermissionApprovalActions.Read);
const canChangeBypass = action.includes(ProjectPermissionApprovalActions.AllowChangeBypass);
const canAccessBypass = action.includes(ProjectPermissionApprovalActions.AllowAccessBypass);
if (!formVal[subject]) formVal[subject] = [{}];
@@ -588,6 +590,8 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
if (canRead) formVal[subject]![0][ProjectPermissionApprovalActions.Read] = true;
if (canChangeBypass)
formVal[subject]![0][ProjectPermissionApprovalActions.AllowChangeBypass] = true;
if (canAccessBypass)
formVal[subject]![0][ProjectPermissionApprovalActions.AllowAccessBypass] = true;
return;
}
@@ -1212,7 +1216,8 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
{ label: "Create", value: ProjectPermissionApprovalActions.Create },
{ label: "Modify", value: ProjectPermissionApprovalActions.Edit },
{ label: "Remove", value: ProjectPermissionApprovalActions.Delete },
{ label: "Allow Change Bypass", value: ProjectPermissionApprovalActions.AllowChangeBypass }
{ label: "Allow Change Bypass", value: ProjectPermissionApprovalActions.AllowChangeBypass },
{ label: "Allow Access Bypass", value: ProjectPermissionApprovalActions.AllowAccessBypass }
]
},
[ProjectPermissionSub.SecretRotation]: {

View File

@@ -0,0 +1,14 @@
import { TOnePassSync } from "@app/hooks/api/secretSyncs/types/1password-sync";
import { getSecretSyncDestinationColValues } from "../helpers";
import { SecretSyncTableCell } from "../SecretSyncTableCell";
type Props = {
secretSync: TOnePassSync;
};
export const OnePassSyncDestinationCol = ({ secretSync }: Props) => {
const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync);
return <SecretSyncTableCell primaryText={primaryText} secondaryText={secondaryText} />;
};

View File

@@ -1,5 +1,6 @@
import { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs";
import { OnePassSyncDestinationCol } from "./1PasswordSyncDestinationCol";
import { AwsParameterStoreSyncDestinationCol } from "./AwsParameterStoreSyncDestinationCol";
import { AwsSecretsManagerSyncDestinationCol } from "./AwsSecretsManagerSyncDestinationCol";
import { AzureAppConfigurationDestinationSyncCol } from "./AzureAppConfigurationDestinationSyncCol";
@@ -52,6 +53,8 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => {
return <TeamCitySyncDestinationCol secretSync={secretSync} />;
case SecretSync.OCIVault:
return <OCIVaultSyncDestinationCol secretSync={secretSync} />;
case SecretSync.OnePass:
return <OnePassSyncDestinationCol secretSync={secretSync} />;
default:
throw new Error(
`Unhandled Secret Sync Destination Col: ${(secretSync as TSecretSync).destination}`

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