diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 1a30923d5..42207e621 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -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." } } }; diff --git a/backend/src/server/routes/v1/app-connection-routers/1password-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/1password-connection-router.ts new file mode 100644 index 000000000..1100776d3 --- /dev/null +++ b/backend/src/server/routes/v1/app-connection-routers/1password-connection-router.ts @@ -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; + } + }); +}; diff --git a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts index a05a2f263..0fea749c0 100644 --- a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts +++ b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts @@ -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 { @@ -78,7 +82,8 @@ const SanitizedAppConnectionSchema = z.union([ ...SanitizedWindmillConnectionSchema.options, ...SanitizedLdapConnectionSchema.options, ...SanitizedTeamCityConnectionSchema.options, - ...SanitizedOCIConnectionSchema.options + ...SanitizedOCIConnectionSchema.options, + ...SanitizedOnePassConnectionSchema.options ]); const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ @@ -100,7 +105,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ WindmillConnectionListItemSchema, LdapConnectionListItemSchema, TeamCityConnectionListItemSchema, - OCIConnectionListItemSchema + OCIConnectionListItemSchema, + OnePassConnectionListItemSchema ]); export const registerAppConnectionRouter = async (server: FastifyZodProvider) => { diff --git a/backend/src/server/routes/v1/app-connection-routers/index.ts b/backend/src/server/routes/v1/app-connection-routers/index.ts index 04c6a802f..1c46b4ea9 100644 --- a/backend/src/server/routes/v1/app-connection-routers/index.ts +++ b/backend/src/server/routes/v1/app-connection-routers/index.ts @@ -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"; @@ -42,5 +43,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record + registerSyncSecretsEndpoints({ + destination: SecretSync.OnePass, + server, + responseSchema: OnePassSyncSchema, + createSchema: CreateOnePassSyncSchema, + updateSchema: UpdateOnePassSyncSchema + }); diff --git a/backend/src/server/routes/v1/secret-sync-routers/index.ts b/backend/src/server/routes/v1/secret-sync-routers/index.ts index c22a40432..fbc636ffc 100644 --- a/backend/src/server/routes/v1/secret-sync-routers/index.ts +++ b/backend/src/server/routes/v1/secret-sync-routers/index.ts @@ -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 { diff --git a/backend/src/services/app-connection/1password/1password-connection-enums.ts b/backend/src/services/app-connection/1password/1password-connection-enums.ts new file mode 100644 index 000000000..85b28ee5a --- /dev/null +++ b/backend/src/services/app-connection/1password/1password-connection-enums.ts @@ -0,0 +1,3 @@ +export enum OnePassConnectionMethod { + ApiToken = "api-token" +} diff --git a/backend/src/services/app-connection/1password/1password-connection-fns.ts b/backend/src/services/app-connection/1password/1password-connection-fns.ts new file mode 100644 index 000000000..d8a18576f --- /dev/null +++ b/backend/src/services/app-connection/1password/1password-connection-fns.ts @@ -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(`${instanceUrl}/v1/vaults`, { + headers: { + Authorization: `Bearer ${apiToken}`, + Accept: "application/json" + } + }); + + return resp.data; +}; diff --git a/backend/src/services/app-connection/1password/1password-connection-schemas.ts b/backend/src/services/app-connection/1password/1password-connection-schemas.ts new file mode 100644 index 000000000..da63dc32a --- /dev/null +++ b/backend/src/services/app-connection/1password/1password-connection-schemas.ts @@ -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() +}); diff --git a/backend/src/services/app-connection/1password/1password-connection-service.ts b/backend/src/services/app-connection/1password/1password-connection-service.ts new file mode 100644 index 000000000..8e1df9536 --- /dev/null +++ b/backend/src/services/app-connection/1password/1password-connection-service.ts @@ -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; + +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 + }; +}; diff --git a/backend/src/services/app-connection/1password/1password-connection-types.ts b/backend/src/services/app-connection/1password/1password-connection-types.ts new file mode 100644 index 000000000..99d6bf94a --- /dev/null +++ b/backend/src/services/app-connection/1password/1password-connection-types.ts @@ -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; + +export type TOnePassConnectionInput = z.infer & { + app: AppConnection.OnePass; +}; + +export type TValidateOnePassConnectionCredentialsSchema = typeof ValidateOnePassConnectionCredentialsSchema; + +export type TOnePassConnectionConfig = DiscriminativePick & { + orgId: string; +}; + +export type TOnePassVault = { + id: string; + name: string; + type: string; + items: number; + + attributeVersion: number; + contentVersion: number; + + createdAt: string; + updatedAt: string; +}; diff --git a/backend/src/services/app-connection/1password/index.ts b/backend/src/services/app-connection/1password/index.ts new file mode 100644 index 000000000..333cc347e --- /dev/null +++ b/backend/src/services/app-connection/1password/index.ts @@ -0,0 +1,4 @@ +export * from "./1password-connection-enums"; +export * from "./1password-connection-fns"; +export * from "./1password-connection-schemas"; +export * from "./1password-connection-types"; diff --git a/backend/src/services/app-connection/app-connection-enums.ts b/backend/src/services/app-connection/app-connection-enums.ts index afae27039..25c6394fa 100644 --- a/backend/src/services/app-connection/app-connection-enums.ts +++ b/backend/src/services/app-connection/app-connection-enums.ts @@ -17,7 +17,8 @@ export enum AppConnection { HCVault = "hashicorp-vault", LDAP = "ldap", TeamCity = "teamcity", - OCI = "oci" + OCI = "oci", + OnePass = "1password" } export enum AWSRegion { diff --git a/backend/src/services/app-connection/app-connection-fns.ts b/backend/src/services/app-connection/app-connection-fns.ts index 12b58ee36..86e728008 100644 --- a/backend/src/services/app-connection/app-connection-fns.ts +++ b/backend/src/services/app-connection/app-connection-fns.ts @@ -1,4 +1,9 @@ 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 { BadRequestError } from "@app/lib/errors"; @@ -10,10 +15,10 @@ import { import { KmsDataKey } from "@app/services/kms/kms-types"; import { - getOCIConnectionListItem, - OCIConnectionMethod, - validateOCIConnectionCredentials -} from "../../ee/services/app-connections/oci"; + getOnePassConnectionListItem, + OnePassConnectionMethod, + validateOnePassConnectionCredentials +} from "./1password"; import { AppConnection, AppConnectionPlanType } from "./app-connection-enums"; import { TAppConnectionServiceFactoryDep } from "./app-connection-service"; import { @@ -98,7 +103,8 @@ export const listAppConnectionOptions = () => { getHCVaultConnectionListItem(), getLdapConnectionListItem(), getTeamCityConnectionListItem(), - getOCIConnectionListItem() + getOCIConnectionListItem(), + getOnePassConnectionListItem() ].sort((a, b) => a.name.localeCompare(b.name)); }; @@ -168,7 +174,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); @@ -197,6 +204,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: @@ -260,7 +268,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 ( diff --git a/backend/src/services/app-connection/app-connection-maps.ts b/backend/src/services/app-connection/app-connection-maps.ts index b4b968031..ddd0b1087 100644 --- a/backend/src/services/app-connection/app-connection-maps.ts +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -19,7 +19,8 @@ export const APP_CONNECTION_NAME_MAP: Record = { [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 = { @@ -41,5 +42,6 @@ export const APP_CONNECTION_PLAN_MAP: Record>>; @@ -158,6 +165,7 @@ export type TAppConnectionInput = { id: string } & ( | TLdapConnectionInput | TTeamCityConnectionInput | TOCIConnectionInput + | TOnePassConnectionInput ); export type TSqlConnectionInput = TPostgresConnectionInput | TMsSqlConnectionInput; @@ -189,7 +197,8 @@ export type TAppConnectionConfig = | THCVaultConnectionConfig | TLdapConnectionConfig | TTeamCityConnectionConfig - | TOCIConnectionConfig; + | TOCIConnectionConfig + | TOnePassConnectionConfig; export type TValidateAppConnectionCredentialsSchema = | TValidateAwsConnectionCredentialsSchema @@ -210,7 +219,8 @@ export type TValidateAppConnectionCredentialsSchema = | TValidateHCVaultConnectionCredentialsSchema | TValidateLdapConnectionCredentialsSchema | TValidateTeamCityConnectionCredentialsSchema - | TValidateOCIConnectionCredentialsSchema; + | TValidateOCIConnectionCredentialsSchema + | TValidateOnePassConnectionCredentialsSchema; export type TListAwsConnectionKmsKeys = { connectionId: string; diff --git a/backend/src/services/secret-sync/1password/1password-sync-constants.ts b/backend/src/services/secret-sync/1password/1password-sync-constants.ts new file mode 100644 index 000000000..01226a026 --- /dev/null +++ b/backend/src/services/secret-sync/1password/1password-sync-constants.ts @@ -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 +}; diff --git a/backend/src/services/secret-sync/1password/1password-sync-fns.ts b/backend/src/services/secret-sync/1password/1password-sync-fns.ts new file mode 100644 index 000000000..c832fbbdb --- /dev/null +++ b/backend/src/services/secret-sync/1password/1password-sync-fns.ts @@ -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(`${instanceUrl}/v1/vaults/${vaultId}/items`, { + headers: { + Authorization: `Bearer ${apiToken}`, + Accept: "application/json" + } + }); + + const result: Record = {}; + + for await (const s of data) { + const { data: secret } = await request.get( + `${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 }); + } +}; diff --git a/backend/src/services/secret-sync/1password/1password-sync-schemas.ts b/backend/src/services/secret-sync/1password/1password-sync-schemas.ts new file mode 100644 index 000000000..2f77a1dad --- /dev/null +++ b/backend/src/services/secret-sync/1password/1password-sync-schemas.ts @@ -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) +}); diff --git a/backend/src/services/secret-sync/1password/1password-sync-types.ts b/backend/src/services/secret-sync/1password/1password-sync-types.ts new file mode 100644 index 000000000..af4db7369 --- /dev/null +++ b/backend/src/services/secret-sync/1password/1password-sync-types.ts @@ -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; + +export type TOnePassSyncInput = z.infer; + +export type TOnePassSyncListItem = z.infer; + +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; +}; diff --git a/backend/src/services/secret-sync/1password/index.ts b/backend/src/services/secret-sync/1password/index.ts new file mode 100644 index 000000000..db098b299 --- /dev/null +++ b/backend/src/services/secret-sync/1password/index.ts @@ -0,0 +1,4 @@ +export * from "./1password-sync-constants"; +export * from "./1password-sync-fns"; +export * from "./1password-sync-schemas"; +export * from "./1password-sync-types"; diff --git a/backend/src/services/secret-sync/secret-sync-enums.ts b/backend/src/services/secret-sync/secret-sync-enums.ts index e829e2131..24f7d05f8 100644 --- a/backend/src/services/secret-sync/secret-sync-enums.ts +++ b/backend/src/services/secret-sync/secret-sync-enums.ts @@ -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 { diff --git a/backend/src/services/secret-sync/secret-sync-fns.ts b/backend/src/services/secret-sync/secret-sync-fns.ts index 8f58b6fcc..dbf3a3699 100644 --- a/backend/src/services/secret-sync/secret-sync-fns.ts +++ b/backend/src/services/secret-sync/secret-sync-fns.ts @@ -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.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}` diff --git a/backend/src/services/secret-sync/secret-sync-maps.ts b/backend/src/services/secret-sync/secret-sync-maps.ts index 5b2906f1c..832c15bf8 100644 --- a/backend/src/services/secret-sync/secret-sync-maps.ts +++ b/backend/src/services/secret-sync/secret-sync-maps.ts @@ -16,7 +16,8 @@ export const SECRET_SYNC_NAME_MAP: Record = { [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 = { @@ -34,7 +35,8 @@ export const SECRET_SYNC_CONNECTION_MAP: Record = { [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 = { @@ -52,5 +54,6 @@ export const SECRET_SYNC_PLAN_MAP: Record = { [SecretSync.Windmill]: SecretSyncPlanType.Regular, [SecretSync.HCVault]: SecretSyncPlanType.Regular, [SecretSync.TeamCity]: SecretSyncPlanType.Regular, - [SecretSync.OCIVault]: SecretSyncPlanType.Enterprise + [SecretSync.OCIVault]: SecretSyncPlanType.Enterprise, + [SecretSync.OnePass]: SecretSyncPlanType.Regular }; diff --git a/backend/src/services/secret-sync/secret-sync-types.ts b/backend/src/services/secret-sync/secret-sync-types.ts index adc4888ab..22f7848ad 100644 --- a/backend/src/services/secret-sync/secret-sync-types.ts +++ b/backend/src/services/secret-sync/secret-sync-types.ts @@ -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; diff --git a/docs/api-reference/endpoints/app-connections/1password/available.mdx b/docs/api-reference/endpoints/app-connections/1password/available.mdx new file mode 100644 index 000000000..3797a7556 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/1password/available.mdx @@ -0,0 +1,4 @@ +--- +title: "Available" +openapi: "GET /api/v1/app-connections/1password/available" +--- diff --git a/docs/api-reference/endpoints/app-connections/1password/create.mdx b/docs/api-reference/endpoints/app-connections/1password/create.mdx new file mode 100644 index 000000000..03562b50f --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/1password/create.mdx @@ -0,0 +1,8 @@ +--- +title: "Create" +openapi: "POST /api/v1/app-connections/1password" +--- + + + Check out the configuration docs for [1Password Connections](/integrations/app-connections/1password) to learn how to obtain the required credentials. + diff --git a/docs/api-reference/endpoints/app-connections/1password/delete.mdx b/docs/api-reference/endpoints/app-connections/1password/delete.mdx new file mode 100644 index 000000000..24e7a2b16 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/1password/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/app-connections/1password/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/1password/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/1password/get-by-id.mdx new file mode 100644 index 000000000..bcab50f12 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/1password/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/app-connections/1password/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/1password/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/1password/get-by-name.mdx new file mode 100644 index 000000000..8cb10c351 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/1password/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/app-connections/1password/connection-name/{connectionName}" +--- diff --git a/docs/api-reference/endpoints/app-connections/1password/list.mdx b/docs/api-reference/endpoints/app-connections/1password/list.mdx new file mode 100644 index 000000000..4fa88de81 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/1password/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/app-connections/1password" +--- diff --git a/docs/api-reference/endpoints/app-connections/1password/update.mdx b/docs/api-reference/endpoints/app-connections/1password/update.mdx new file mode 100644 index 000000000..cbd52a6c6 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/1password/update.mdx @@ -0,0 +1,8 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/app-connections/1password/{connectionId}" +--- + + + Check out the configuration docs for [1Password Connections](/integrations/app-connections/1password) to learn how to obtain the required credentials. + diff --git a/docs/api-reference/endpoints/secret-syncs/1password/create.mdx b/docs/api-reference/endpoints/secret-syncs/1password/create.mdx new file mode 100644 index 000000000..b8c8a0d9d --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/1password/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/secret-syncs/1password" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/1password/delete.mdx b/docs/api-reference/endpoints/secret-syncs/1password/delete.mdx new file mode 100644 index 000000000..4949636bd --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/1password/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/secret-syncs/1password/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/1password/get-by-id.mdx b/docs/api-reference/endpoints/secret-syncs/1password/get-by-id.mdx new file mode 100644 index 000000000..522b94499 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/1password/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/secret-syncs/1password/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/1password/get-by-name.mdx b/docs/api-reference/endpoints/secret-syncs/1password/get-by-name.mdx new file mode 100644 index 000000000..9a904a6cf --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/1password/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/secret-syncs/1password/sync-name/{syncName}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/1password/import-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/1password/import-secrets.mdx new file mode 100644 index 000000000..75553aedd --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/1password/import-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Import Secrets" +openapi: "POST /api/v1/secret-syncs/1password/{syncId}/import-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/1password/list.mdx b/docs/api-reference/endpoints/secret-syncs/1password/list.mdx new file mode 100644 index 000000000..b7c7ad00d --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/1password/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/secret-syncs/1password" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/1password/remove-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/1password/remove-secrets.mdx new file mode 100644 index 000000000..03ce4de83 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/1password/remove-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Secrets" +openapi: "POST /api/v1/secret-syncs/1password/{syncId}/remove-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/1password/sync-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/1password/sync-secrets.mdx new file mode 100644 index 000000000..183cd0722 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/1password/sync-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Sync Secrets" +openapi: "POST /api/v1/secret-syncs/1password/{syncId}/sync-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/1password/update.mdx b/docs/api-reference/endpoints/secret-syncs/1password/update.mdx new file mode 100644 index 000000000..c7dcf5f1c --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/1password/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/secret-syncs/1password/{syncId}" +--- diff --git a/docs/images/app-connections/1password/app-connection-created.png b/docs/images/app-connections/1password/app-connection-created.png new file mode 100644 index 000000000..adfd1b260 Binary files /dev/null and b/docs/images/app-connections/1password/app-connection-created.png differ diff --git a/docs/images/app-connections/1password/app-connection-modal.png b/docs/images/app-connections/1password/app-connection-modal.png new file mode 100644 index 000000000..cf828de3c Binary files /dev/null and b/docs/images/app-connections/1password/app-connection-modal.png differ diff --git a/docs/images/app-connections/1password/app-connection-option.png b/docs/images/app-connections/1password/app-connection-option.png new file mode 100644 index 000000000..bd07c0a80 Binary files /dev/null and b/docs/images/app-connections/1password/app-connection-option.png differ diff --git a/docs/images/app-connections/1password/click-connect-server.png b/docs/images/app-connections/1password/click-connect-server.png new file mode 100644 index 000000000..f3720c2d4 Binary files /dev/null and b/docs/images/app-connections/1password/click-connect-server.png differ diff --git a/docs/images/app-connections/1password/configure-connect-server.png b/docs/images/app-connections/1password/configure-connect-server.png new file mode 100644 index 000000000..89015d499 Binary files /dev/null and b/docs/images/app-connections/1password/configure-connect-server.png differ diff --git a/docs/images/app-connections/1password/deploy-server.png b/docs/images/app-connections/1password/deploy-server.png new file mode 100644 index 000000000..cf29ea2e4 Binary files /dev/null and b/docs/images/app-connections/1password/deploy-server.png differ diff --git a/docs/images/app-connections/1password/developer-page.png b/docs/images/app-connections/1password/developer-page.png new file mode 100644 index 000000000..7df91dfcf Binary files /dev/null and b/docs/images/app-connections/1password/developer-page.png differ diff --git a/docs/images/app-connections/1password/set-up-access-token.png b/docs/images/app-connections/1password/set-up-access-token.png new file mode 100644 index 000000000..c0730d5c3 Binary files /dev/null and b/docs/images/app-connections/1password/set-up-access-token.png differ diff --git a/docs/images/secret-syncs/1password/configure-destination.png b/docs/images/secret-syncs/1password/configure-destination.png new file mode 100644 index 000000000..af5191486 Binary files /dev/null and b/docs/images/secret-syncs/1password/configure-destination.png differ diff --git a/docs/images/secret-syncs/1password/configure-details.png b/docs/images/secret-syncs/1password/configure-details.png new file mode 100644 index 000000000..69ce333e3 Binary files /dev/null and b/docs/images/secret-syncs/1password/configure-details.png differ diff --git a/docs/images/secret-syncs/1password/configure-source.png b/docs/images/secret-syncs/1password/configure-source.png new file mode 100644 index 000000000..ee08db72b Binary files /dev/null and b/docs/images/secret-syncs/1password/configure-source.png differ diff --git a/docs/images/secret-syncs/1password/configure-sync-options.png b/docs/images/secret-syncs/1password/configure-sync-options.png new file mode 100644 index 000000000..f0b3488e2 Binary files /dev/null and b/docs/images/secret-syncs/1password/configure-sync-options.png differ diff --git a/docs/images/secret-syncs/1password/review-configuration.png b/docs/images/secret-syncs/1password/review-configuration.png new file mode 100644 index 000000000..5663e7da2 Binary files /dev/null and b/docs/images/secret-syncs/1password/review-configuration.png differ diff --git a/docs/images/secret-syncs/1password/select-option.png b/docs/images/secret-syncs/1password/select-option.png new file mode 100644 index 000000000..a19b8189d Binary files /dev/null and b/docs/images/secret-syncs/1password/select-option.png differ diff --git a/docs/images/secret-syncs/1password/sync-created.png b/docs/images/secret-syncs/1password/sync-created.png new file mode 100644 index 000000000..fbe8c90d6 Binary files /dev/null and b/docs/images/secret-syncs/1password/sync-created.png differ diff --git a/docs/integrations/app-connections/1password.mdx b/docs/integrations/app-connections/1password.mdx new file mode 100644 index 000000000..0c3926a1b --- /dev/null +++ b/docs/integrations/app-connections/1password.mdx @@ -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 + + + If you already have a Connect Server for your vault you may skip this step. + + + + + ![Developer Page](/images/app-connections/1password/developer-page.png) + + + ![Click Connect Server](/images/app-connections/1password/click-connect-server.png) + + + 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) + + + 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) + + + Download the Credentials File and set up your Connect Server. + + + Follow [this guide](https://developer.1password.com/docs/connect/get-started#step-2-deploy-1password-connect-server) to deploy a Connect Server. + + + Make sure to save the **Access Token** for later use. + + ![Deploy Server](/images/app-connections/1password/deploy-server.png) + + + +## Create 1Password Connection in Infisical + + + + + + 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) + + + 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) + + + 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) + + + 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) + + + + + 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" + } + } + } + ``` + + diff --git a/docs/integrations/secret-syncs/1password.mdx b/docs/integrations/secret-syncs/1password.mdx new file mode 100644 index 000000000..a33f54c8d --- /dev/null +++ b/docs/integrations/secret-syncs/1password.mdx @@ -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) + + + + + + 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) + + + ![Select 1Password](/images/secret-syncs/1password/select-option.png) + + + 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. + + + If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports). + + + + 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. + + + 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. + + We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. + + - **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. + + + 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. + + + Review your 1Password Sync configuration, then click **Create Sync**. + + ![Review Configuration](/images/secret-syncs/1password/review-configuration.png) + + + If enabled, your 1Password Sync will begin syncing your secrets to the destination endpoint. + + ![Sync Created](/images/secret-syncs/1password/sync-created.png) + + + + + 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": "..." + } + } + } + ``` + + + +## FAQ + + + + Infisical can only perform CRUD operations on the following item types: + - API Credentials + + diff --git a/docs/mint.json b/docs/mint.json index a81c9db67..bf7e8533a 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -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": [ diff --git a/frontend/public/images/integrations/1Password.png b/frontend/public/images/integrations/1Password.png new file mode 100644 index 000000000..8518b41e6 Binary files /dev/null and b/frontend/public/images/integrations/1Password.png differ diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/1PasswordSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/1PasswordSyncFields.tsx new file mode 100644 index 000000000..1202ec875 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/1PasswordSyncFields.tsx @@ -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 ( + <> + { + setValue("destinationConfig.vaultId", ""); + }} + /> + + ( + +
+ Don't see the vault you're looking for?{" "} + +
+ + } + > + v.id === value) ?? null} + onChange={(option) => onChange((option as SingleValue)?.id ?? null)} + options={vaults} + placeholder="Select a vault..." + getOptionLabel={(option) => option.name} + getOptionValue={(option) => option.id} + /> +
+ )} + /> + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx index 2cac1ae20..48541b272 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx @@ -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 ; case SecretSync.OCIVault: return ; + case SecretSync.OnePass: + return ; default: throw new Error(`Unhandled Destination Config Field: ${destination}`); } diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx index 7c2b13936..00d86e700 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx @@ -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; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/OnePassSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/OnePassSyncReviewFields.tsx new file mode 100644 index 000000000..1c31fb6c3 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/OnePassSyncReviewFields.tsx @@ -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(); + const vaultId = watch("destinationConfig.vaultId"); + + return {vaultId}; +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx index 144ccb2a8..a53eec5e7 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx @@ -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 = ; break; + case SecretSync.OnePass: + DestinationFieldsComponent = ; + break; default: throw new Error(`Unhandled Destination Review Fields: ${destination}`); } diff --git a/frontend/src/components/secret-syncs/forms/schemas/1password-sync-destination-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/1password-sync-destination-schema.ts new file mode 100644 index 000000000..36b144776 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/schemas/1password-sync-destination-schema.ts @@ -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") + }) + }) +); diff --git a/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts index 232b8cedf..792226dae 100644 --- a/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts +++ b/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts @@ -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; diff --git a/frontend/src/helpers/appConnections.ts b/frontend/src/helpers/appConnections.ts index 77bbb22ae..dd0daf968 100644 --- a/frontend/src/helpers/appConnections.ts +++ b/frontend/src/helpers/appConnections.ts @@ -23,6 +23,7 @@ import { HumanitecConnectionMethod, LdapConnectionMethod, MsSqlConnectionMethod, + OnePassConnectionMethod, PostgresConnectionMethod, TAppConnection, TeamCityConnectionMethod, @@ -63,7 +64,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"]) => { @@ -89,6 +91,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: diff --git a/frontend/src/helpers/secretSyncs.ts b/frontend/src/helpers/secretSyncs.ts index 80df92ac3..88a0f7517 100644 --- a/frontend/src/helpers/secretSyncs.ts +++ b/frontend/src/helpers/secretSyncs.ts @@ -51,6 +51,10 @@ export const SECRET_SYNC_MAP: Record = { [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< diff --git a/frontend/src/hooks/api/appConnections/1password/index.ts b/frontend/src/hooks/api/appConnections/1password/index.ts new file mode 100644 index 000000000..2c1906d36 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/1password/index.ts @@ -0,0 +1,2 @@ +export * from "./queries"; +export * from "./types"; diff --git a/frontend/src/hooks/api/appConnections/1password/queries.tsx b/frontend/src/hooks/api/appConnections/1password/queries.tsx new file mode 100644 index 000000000..f73562652 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/1password/queries.tsx @@ -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 + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: onePassConnectionKeys.listVaults(connectionId), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/app-connections/1password/${connectionId}/vaults` + ); + + return data; + }, + ...options + }); +}; diff --git a/frontend/src/hooks/api/appConnections/1password/types.ts b/frontend/src/hooks/api/appConnections/1password/types.ts new file mode 100644 index 000000000..9386e9ddd --- /dev/null +++ b/frontend/src/hooks/api/appConnections/1password/types.ts @@ -0,0 +1,12 @@ +export type TOnePassVault = { + id: string; + name: string; + type: string; + items: number; + + attributeVersion: number; + contentVersion: number; + + createdAt: string; + updatedAt: string; +}; diff --git a/frontend/src/hooks/api/appConnections/enums.ts b/frontend/src/hooks/api/appConnections/enums.ts index 06a5056af..d099936d6 100644 --- a/frontend/src/hooks/api/appConnections/enums.ts +++ b/frontend/src/hooks/api/appConnections/enums.ts @@ -17,5 +17,6 @@ export enum AppConnection { HCVault = "hashicorp-vault", LDAP = "ldap", TeamCity = "teamcity", - OCI = "oci" + OCI = "oci", + OnePass = "1password" } diff --git a/frontend/src/hooks/api/appConnections/types/1password-connection.ts b/frontend/src/hooks/api/appConnections/types/1password-connection.ts new file mode 100644 index 000000000..fcf307a15 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/types/1password-connection.ts @@ -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; + }; +}; diff --git a/frontend/src/hooks/api/appConnections/types/app-options.ts b/frontend/src/hooks/api/appConnections/types/app-options.ts index 79cbb81b9..771422ad2 100644 --- a/frontend/src/hooks/api/appConnections/types/app-options.ts +++ b/frontend/src/hooks/api/appConnections/types/app-options.ts @@ -88,6 +88,10 @@ export type TOCIConnectionOption = TAppConnectionOptionBase & { app: AppConnection.OCI; }; +export type TOnePassConnectionOption = TAppConnectionOptionBase & { + app: AppConnection.OnePass; +}; + export type TAppConnectionOption = | TAwsConnectionOption | TGitHubConnectionOption @@ -106,7 +110,8 @@ export type TAppConnectionOption = | TAuth0ConnectionOption | THCVaultConnectionOption | TTeamCityConnectionOption - | TOCIConnectionOption; + | TOCIConnectionOption + | TOnePassConnectionOption; export type TAppConnectionOptionMap = { [AppConnection.AWS]: TAwsConnectionOption; @@ -128,4 +133,5 @@ export type TAppConnectionOptionMap = { [AppConnection.LDAP]: TLdapConnectionOption; [AppConnection.TeamCity]: TTeamCityConnectionOption; [AppConnection.OCI]: TOCIConnectionOption; + [AppConnection.OnePass]: TOnePassConnectionOption; }; diff --git a/frontend/src/hooks/api/appConnections/types/index.ts b/frontend/src/hooks/api/appConnections/types/index.ts index 2b29c2cd4..b53c9f751 100644 --- a/frontend/src/hooks/api/appConnections/types/index.ts +++ b/frontend/src/hooks/api/appConnections/types/index.ts @@ -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"; @@ -20,6 +21,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"; @@ -59,7 +61,8 @@ export type TAppConnection = | THCVaultConnection | TLdapConnection | TTeamCityConnection - | TOCIConnection; + | TOCIConnection + | TOnePassConnection; export type TAvailableAppConnection = Pick; @@ -106,4 +109,5 @@ export type TAppConnectionMap = { [AppConnection.LDAP]: TLdapConnection; [AppConnection.TeamCity]: TTeamCityConnection; [AppConnection.OCI]: TOCIConnection; + [AppConnection.OnePass]: TOnePassConnection; }; diff --git a/frontend/src/hooks/api/secretSyncs/enums.ts b/frontend/src/hooks/api/secretSyncs/enums.ts index 65a31e427..7185563d5 100644 --- a/frontend/src/hooks/api/secretSyncs/enums.ts +++ b/frontend/src/hooks/api/secretSyncs/enums.ts @@ -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 { diff --git a/frontend/src/hooks/api/secretSyncs/types/1password-sync.ts b/frontend/src/hooks/api/secretSyncs/types/1password-sync.ts new file mode 100644 index 000000000..98556d2af --- /dev/null +++ b/frontend/src/hooks/api/secretSyncs/types/1password-sync.ts @@ -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; + }; +}; diff --git a/frontend/src/hooks/api/secretSyncs/types/index.ts b/frontend/src/hooks/api/secretSyncs/types/index.ts index 85daa07fd..f28a0820b 100644 --- a/frontend/src/hooks/api/secretSyncs/types/index.ts +++ b/frontend/src/hooks/api/secretSyncs/types/index.ts @@ -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[] }; diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/1PasswordConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/1PasswordConnectionForm.tsx new file mode 100644 index 000000000..1c266aeee --- /dev/null +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/1PasswordConnectionForm.tsx @@ -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; + +export const OnePassConnectionForm = ({ appConnection, onSubmit }: Props) => { + const isUpdate = Boolean(appConnection); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: appConnection ?? { + app: AppConnection.OnePass, + method: OnePassConnectionMethod.ApiToken + } + }); + + const { + handleSubmit, + control, + formState: { isSubmitting, isDirty } + } = form; + + return ( + +
+ {!isUpdate && } + ( + + + + )} + /> + ( + + + + )} + /> + ( + + onChange(e.target.value)} + /> + + )} + /> +
+ + + + +
+ +
+ ); +}; diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx index 238dc4e04..0e35ac4fd 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx @@ -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"; @@ -104,6 +105,8 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => { return ; case AppConnection.OCI: return ; + case AppConnection.OnePass: + return ; default: throw new Error(`Unhandled App ${app}`); } @@ -178,6 +181,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => { return ; case AppConnection.OCI: return ; + case AppConnection.OnePass: + return ; default: throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`); diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/1PasswordSyncDestinationCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/1PasswordSyncDestinationCol.tsx new file mode 100644 index 000000000..ad90908c1 --- /dev/null +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/1PasswordSyncDestinationCol.tsx @@ -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 ; +}; diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx index 898980881..cd5ff2cf7 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx @@ -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 ; case SecretSync.OCIVault: return ; + case SecretSync.OnePass: + return ; default: throw new Error( `Unhandled Secret Sync Destination Col: ${(secretSync as TSecretSync).destination}` diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts index 4bf0eeed2..4943df471 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts @@ -106,6 +106,10 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => { primaryText = destinationConfig.compartmentOcid; secondaryText = destinationConfig.vaultOcid; break; + case SecretSync.OnePass: + primaryText = destinationConfig.vaultId; + secondaryText = "Vault ID"; + break; default: throw new Error(`Unhandled Destination Col Values ${destination}`); } diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/1PasswordSyncDestinationSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/1PasswordSyncDestinationSection.tsx new file mode 100644 index 000000000..09d598f31 --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/1PasswordSyncDestinationSection.tsx @@ -0,0 +1,14 @@ +import { GenericFieldLabel } from "@app/components/secret-syncs"; +import { TOnePassSync } from "@app/hooks/api/secretSyncs/types/1password-sync"; + +type Props = { + secretSync: TOnePassSync; +}; + +export const OnePassSyncDestinationSection = ({ secretSync }: Props) => { + const { + destinationConfig: { vaultId } + } = secretSync; + + return {vaultId}; +}; diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx index b0c989ee2..f443c6106 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx @@ -10,6 +10,7 @@ import { ProjectPermissionSecretSyncActions } from "@app/context/ProjectPermissi import { APP_CONNECTION_MAP } from "@app/helpers/appConnections"; import { SecretSync, TSecretSync } from "@app/hooks/api/secretSyncs"; +import { OnePassSyncDestinationSection } from "./1PasswordSyncDestinationSection"; import { AwsParameterStoreSyncDestinationSection } from "./AwsParameterStoreSyncDestinationSection"; import { AwsSecretsManagerSyncDestinationSection } from "./AwsSecretsManagerSyncDestinationSection"; import { AzureAppConfigurationSyncDestinationSection } from "./AzureAppConfigurationSyncDestinationSection"; @@ -85,6 +86,9 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }: case SecretSync.OCIVault: DestinationComponents = ; break; + case SecretSync.OnePass: + DestinationComponents = ; + break; default: throw new Error(`Unhandled Destination Section components: ${destination}`); } diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx index a53016d53..fbd66b9f5 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx @@ -50,6 +50,7 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) = case SecretSync.HCVault: case SecretSync.TeamCity: case SecretSync.OCIVault: + case SecretSync.OnePass: AdditionalSyncOptionsComponent = null; break; default: