Compare commits

...

109 Commits

Author SHA1 Message Date
Scott Wilson
ef70de1e0b fix: add noopenner to doc link 2025-05-15 20:05:56 -07:00
Scott Wilson
7e9ee7b5e3 fix: add empty display for sso general tab if no sso is enabled 2025-05-15 20:01:08 -07:00
x032205
3306a9ca69 Merge pull request #3608 from Infisical/key-schema-tweak
allow underscores in key schema
2025-05-15 18:55:45 -04:00
Maidul Islam
e9af34a6ba Merge pull request #3607 from Infisical/key-schema-doc-tweaks
feat(docs): Key Schema Tweaks
2025-05-15 15:51:23 -07:00
x032205
3de8ed169f allow underscores in key schema 2025-05-15 18:49:30 -04:00
Scott Wilson
d1eb350bdd Merge pull request #3606 from Infisical/oidc-groups-claim-handle-string
improvement(oidc-group-membership-mapping): Update OIDC group claims to handle single group string
2025-05-15 14:47:46 -07:00
Scott Wilson
0c1ccf7c2e fix: update oidc group claims to handle single group string 2025-05-15 14:39:07 -07:00
Maidul Islam
b55a39dd24 Merge pull request #3604 from Infisical/misc/add-identity-support-for-audit-log-retention
misc: add identity support for audit log retention
2025-05-15 09:25:49 -07:00
Sheen
7b880f85cc misc: add identity support for audit log retention 2025-05-15 16:19:47 +00:00
x032205
c7dc595e1a doc overview update 2025-05-15 12:05:06 -04:00
x032205
6e494f198b Merge pull request #3603 from Infisical/fix-oci-machine-identity
fix oci machine identity
2025-05-15 11:42:58 -04:00
x032205
e1f3eaf1a0 Comment for regex 2025-05-15 11:41:00 -04:00
x032205
1e11702c58 remove unused import 2025-05-15 01:17:38 -04:00
x032205
3b81cdb16e fix oci machine identity 2025-05-15 01:12:33 -04:00
x032205
6584166815 Merge pull request #3598 from Infisical/ENG-2755
feat(secret-sync): Secret Key Schema
2025-05-14 23:57:18 -04:00
x032205
827cb35194 review fixes 2025-05-14 23:52:05 -04:00
Maidul Islam
89a6a0ba13 Merge pull request #3602 from Infisical/general-oidc-group-mapping-docs
docs(oidc-group-membership-mapping): Add general OIDC group membership mapping documentation
2025-05-14 16:25:26 -07:00
Scott Wilson
3b9a50d65d improvements: address feedback 2025-05-14 16:20:50 -07:00
Scott Wilson
beb7200233 fix: correct overview image links 2025-05-14 14:29:46 -07:00
Scott Wilson
18e3d132a2 documentation: add general oidc group membership mapping documentation 2025-05-14 14:22:35 -07:00
x032205
3f74d3a80d update import 2025-05-14 13:49:25 -04:00
x032205
4a44dc6119 format a frontend file 2025-05-14 13:45:45 -04:00
x032205
dd4bc4bc73 more doc tweaks 2025-05-14 13:43:23 -04:00
x032205
6188de43e4 Merge pull request #3574 from Infisical/ENG-2706
feat(machine-identities): oracle cloud machine identity auth
2025-05-14 12:56:16 -04:00
Daniel Hougaard
36310387e0 Update oci-auth.mdx 2025-05-14 20:44:41 +04:00
x032205
43f3960225 Merge branch 'main' into ENG-2706 2025-05-14 12:35:17 -04:00
Scott Wilson
2f0a442866 Merge pull request #3573 from Infisical/duplicate-project-roles
feature(project/org-roles): Add ability to duplicate org and project roles
2025-05-14 09:23:02 -07:00
Scott Wilson
7e05bc86a9 improvement: address feedback 2025-05-14 08:58:29 -07:00
x032205
b0c4fddf86 review fixes 2025-05-14 11:23:12 -04:00
Maidul Islam
f5578d39a6 Merge pull request #3597 from Infisical/linux-upgrade-docs
add linux upgrade docs
2025-05-14 07:45:01 -07:00
Sheen
5a3fbc0401 Merge pull request #3599 from Infisical/misc/updated-custom-cert-to-be-crt-formawt
misc: update custom cert to be crt format for docs
2025-05-14 14:24:29 +08:00
Sheen Capadngan
7c52e000cd misc: update custom cert to be crt format for docs 2025-05-14 14:12:08 +08:00
x032205
cccd4ba9e5 doc changes and other tweaks 2025-05-14 01:32:09 -04:00
x032205
63f0f8e299 final release 2025-05-14 01:16:42 -04:00
Maidul Islam
c8a3837432 refine docs 2025-05-13 22:02:49 -07:00
Vlad Matsiiako
2dd407b136 Merge pull request #3596 from Infisical/pulumi-documentation-update
Adding Pulumi documentation
2025-05-13 22:21:33 -06:00
Maidul Islam
4e1a5565d8 add linux upgrade docs 2025-05-13 20:40:29 -07:00
x032205
bae62421ae with stripSchema and filterForSchema 2025-05-13 23:08:54 -04:00
ArshBallagan
d397002704 Update pulumi.mdx 2025-05-13 20:29:06 -06:00
ArshBallagan
f5b1f671e3 Update pulumi.mdx 2025-05-13 20:17:23 -06:00
ArshBallagan
0597c5f0c0 Adding Pulumi documentation 2025-05-13 20:14:08 -06:00
Scott Wilson
eb3afc8034 Merge pull request #3595 from Infisical/remove-legacy-native-integrations-notice
improvement(native-integrations): Remove legacy badge/banner from native integrations UI
2025-05-13 18:51:03 -07:00
Scott Wilson
b67457fe93 chore: remove unused imports 2025-05-13 18:46:53 -07:00
Scott Wilson
75abdbe938 remove legacy badge/banner from native integrations UI 2025-05-13 18:41:14 -07:00
x032205
9b6a315825 Merge pull request #3593 from Infisical/ENG-2742
Fixed project roles not being editable in some cases
2025-05-13 17:10:23 -04:00
x032205
13b2f65b7e lint fix 2025-05-13 16:51:05 -04:00
x032205
6cf1e046b0 Fixed project roles not being editable in some cases 2025-05-13 16:38:26 -04:00
Scott Wilson
f6e1441dc0 Merge pull request #3570 from Infisical/policy-templates
feature(project-roles): Project Role Templates
2025-05-13 12:47:40 -07:00
Scott Wilson
7ed96164e5 improvement: address feedback 2025-05-13 12:25:24 -07:00
Scott Wilson
9eeb72ac80 fix: correct import 2025-05-13 12:18:35 -07:00
Scott Wilson
f6e566a028 merge main 2025-05-13 12:10:49 -07:00
x032205
a34c74e958 Merge pull request #3580 from Infisical/feat/return-metadata-with-identity-create
Return metadata with identity post endpoints
2025-05-13 14:22:34 -04:00
x032205
eef7a875a1 Merge pull request #3585 from Infisical/ENG-2748
feat(docs): Self approval
2025-05-13 14:05:59 -04:00
x032205
09938a911b nit fix 2025-05-13 13:58:52 -04:00
x032205
af08c41008 Merge pull request #3567 from Infisical/ENG-2636
feat(secret-sync): OCI Vault
2025-05-13 13:25:11 -04:00
x032205
443c8854ea Merge branch 'main' into ENG-2636 2025-05-13 13:16:59 -04:00
x032205
f7a25e7601 Merge pull request #3592 from Infisical/lint-fix
lint fix
2025-05-13 13:16:06 -04:00
Maidul Islam
98a4e6c96d Merge pull request #3591 from akhilmhdh/fix/ui-skew
feat: added new cache control for index html
2025-05-13 12:30:50 -04:00
Maidul Islam
c93ce06409 Merge pull request #3589 from Infisical/misc/updated-org-delete-flow
misc: updated org delete flow to clear session
2025-05-13 11:41:09 -04:00
=
672e4baec4 feat: added new cache control for index html 2025-05-13 21:03:15 +05:30
Sheen Capadngan
9c611daada misc: updated org delete flow to clear session 2025-05-13 16:09:26 +08:00
x032205
52858dad79 Update docs/documentation/platform/pr-workflows.mdx
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-05-12 21:07:57 -04:00
x032205
1d7a6ea50e Update docs/documentation/platform/pr-workflows.mdx
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-05-12 21:07:34 -04:00
x032205
c031233247 feat(docs): Self approval 2025-05-12 21:04:05 -04:00
x032205
70fff1f2da review fixes 2025-05-12 19:38:00 -04:00
x032205
3f8eaa0679 remove schema change 2025-05-12 18:13:14 -04:00
Scott Wilson
50d0035d7b fix: correct remove oci secret if secret value is empty logic 2025-05-12 14:56:13 -07:00
x032205
50f5248e3e Merge branch 'main' into feat/return-metadata-with-identity-create 2025-05-12 17:50:13 -04:00
x032205
8d7b573988 final reviews 2025-05-12 17:39:29 -04:00
x032205
4acdbd24e9 remove useless schema 2025-05-12 16:50:47 -04:00
x032205
c3c907788a review fixes 2025-05-12 16:42:48 -04:00
x032205
0b4675e7b5 Merge branch 'main' into ENG-2636 2025-05-12 14:56:01 -04:00
x032205
091e521180 review fixes 2025-05-12 14:49:45 -04:00
x032205
d5dbc7d7e0 erge branch 'daniel/unblock-dev' into ENG-2706 2025-05-12 10:52:40 -04:00
x032205
0af9415aa6 Merge branch 'main' into ENG-2706 2025-05-12 10:18:33 -04:00
x032205
2793ac22aa remove duplicate field 2025-05-11 22:27:09 -04:00
x032205
31fad03af8 Return metadata with identity post endpoints 2025-05-09 23:41:11 -04:00
x032205
ce612877b8 docs 2025-05-09 22:47:20 -04:00
x032205
4ad8b468d5 Merge branch 'main' into ENG-2706 2025-05-09 22:37:22 -04:00
x032205
5742fc648b add tenancy OCID requirement 2025-05-09 22:33:02 -04:00
Scott Wilson
aa68a3ef58 feature: add org role duplication 2025-05-09 14:29:18 -07:00
x032205
578a0d7d93 review fixes 2025-05-09 02:54:49 -04:00
x032205
cd71db416d cancel deletion + update on creation for scheduled for deletion secrets 2025-05-09 02:34:50 -04:00
x032205
9d682ca874 added RE2 to regex 2025-05-09 02:10:53 -04:00
x032205
9054db80ad truncation and UI tweaks 2025-05-09 02:05:30 -04:00
x032205
5bb8756c67 only list compartments which the user is authorized to 'use vaults' in 2025-05-09 01:49:34 -04:00
x032205
8b7cb4c4eb Merge branch 'main' into ENG-2636 2025-05-09 01:34:19 -04:00
x032205
a6ee6fc4ea docs, grammar fixes, frontend tweak 2025-05-09 01:29:11 -04:00
x032205
b21c17572d block local and private IPs on host header 2025-05-09 00:08:02 -04:00
Scott Wilson
44c7be54cf improvement: address feedback 2025-05-08 20:22:42 -07:00
Scott Wilson
45c08b3f09 improvement: improve role not found error display 2025-05-08 20:15:47 -07:00
Scott Wilson
57a29577fe feature: duplicate project role 2025-05-08 20:10:25 -07:00
x032205
2700a96df4 Remove unused package 2025-05-08 21:30:40 -04:00
x032205
7457ef3b66 bug fix 2025-05-08 21:24:03 -04:00
x032205
806df70dd7 tweaks 2025-05-08 21:03:58 -04:00
x032205
8eda358c17 schema gen 2025-05-08 20:59:05 -04:00
x032205
b34aabe72b merges 2025-05-08 20:56:04 -04:00
x032205
dfaed3c513 oci machine identity auth option 2025-05-08 20:42:58 -04:00
Scott Wilson
5b7627585f improvements: address feedback 2025-05-08 16:17:25 -07:00
Scott Wilson
800ea5ce78 feature: project role templates 2025-05-08 16:02:41 -07:00
x032205
72abc08f04 Merge branch 'main' into ENG-2636 2025-05-08 10:29:52 -04:00
x032205
d6b31cde44 greptile review fixes 2025-05-08 01:16:42 -04:00
x032205
2c94f9ec3c revert eslint memory increase 2025-05-08 00:50:31 -04:00
x032205
42ad63b58d increase max old space size for lint:fix 2025-05-08 00:44:03 -04:00
x032205
f2d5112585 Merge branch 'main' into ENG-2636 2025-05-08 00:27:28 -04:00
x032205
9c7b25de49 docs + tweaks 2025-05-08 00:25:19 -04:00
x032205
36954a9df9 secret sync + tweaks 2025-05-07 17:57:00 -04:00
x032205
581840a701 fixed app connection endpoints 2025-05-07 13:53:05 -04:00
x032205
326742c2d5 feat(app-connections): OCI 2025-05-07 10:59:27 -04:00
231 changed files with 8169 additions and 572 deletions

1941
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -209,6 +209,7 @@
"mysql2": "^3.9.8",
"nanoid": "^3.3.8",
"nodemailer": "^6.9.9",
"oci-sdk": "^2.108.0",
"odbc": "^2.4.9",
"openid-client": "^5.6.5",
"ora": "^7.0.1",

View File

@@ -68,6 +68,7 @@ import { TIdentityJwtAuthServiceFactory } from "@app/services/identity-jwt-auth/
import { TIdentityKubernetesAuthServiceFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-service";
import { TIdentityLdapAuthServiceFactory } from "@app/services/identity-ldap-auth/identity-ldap-auth-service";
import { TAllowedFields } from "@app/services/identity-ldap-auth/identity-ldap-auth-types";
import { TIdentityOciAuthServiceFactory } from "@app/services/identity-oci-auth/identity-oci-auth-service";
import { TIdentityOidcAuthServiceFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-service";
import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
import { TIdentityTokenAuthServiceFactory } from "@app/services/identity-token-auth/identity-token-auth-service";
@@ -209,6 +210,7 @@ declare module "fastify" {
identityGcpAuth: TIdentityGcpAuthServiceFactory;
identityAwsAuth: TIdentityAwsAuthServiceFactory;
identityAzureAuth: TIdentityAzureAuthServiceFactory;
identityOciAuth: TIdentityOciAuthServiceFactory;
identityOidcAuth: TIdentityOidcAuthServiceFactory;
identityJwtAuth: TIdentityJwtAuthServiceFactory;
identityLdapAuth: TIdentityLdapAuthServiceFactory;

View File

@@ -119,6 +119,9 @@ import {
TIdentityMetadata,
TIdentityMetadataInsert,
TIdentityMetadataUpdate,
TIdentityOciAuths,
TIdentityOciAuthsInsert,
TIdentityOciAuthsUpdate,
TIdentityOidcAuths,
TIdentityOidcAuthsInsert,
TIdentityOidcAuthsUpdate,
@@ -738,6 +741,11 @@ declare module "knex/types/tables" {
TIdentityAzureAuthsInsert,
TIdentityAzureAuthsUpdate
>;
[TableName.IdentityOciAuth]: KnexOriginal.CompositeTableType<
TIdentityOciAuths,
TIdentityOciAuthsInsert,
TIdentityOciAuthsUpdate
>;
[TableName.IdentityOidcAuth]: KnexOriginal.CompositeTableType<
TIdentityOidcAuths,
TIdentityOidcAuthsInsert,

View File

@@ -0,0 +1,30 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable(TableName.IdentityOciAuth))) {
await knex.schema.createTable(TableName.IdentityOciAuth, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.bigInteger("accessTokenTTL").defaultTo(7200).notNullable();
t.bigInteger("accessTokenMaxTTL").defaultTo(7200).notNullable();
t.bigInteger("accessTokenNumUsesLimit").defaultTo(0).notNullable();
t.jsonb("accessTokenTrustedIps").notNullable();
t.timestamps(true, true, true);
t.uuid("identityId").notNullable().unique();
t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE");
t.string("type").notNullable();
t.string("tenancyOcid").notNullable();
t.string("allowedUsernames").nullable();
});
}
await createOnUpdateTrigger(knex, TableName.IdentityOciAuth);
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.IdentityOciAuth);
await dropOnUpdateTrigger(knex, TableName.IdentityOciAuth);
}

View File

@@ -0,0 +1,26 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const IdentityOciAuthsSchema = z.object({
id: z.string().uuid(),
accessTokenTTL: z.coerce.number().default(7200),
accessTokenMaxTTL: z.coerce.number().default(7200),
accessTokenNumUsesLimit: z.coerce.number().default(0),
accessTokenTrustedIps: z.unknown(),
createdAt: z.date(),
updatedAt: z.date(),
identityId: z.string().uuid(),
type: z.string(),
tenancyOcid: z.string(),
allowedUsernames: z.string().nullable().optional()
});
export type TIdentityOciAuths = z.infer<typeof IdentityOciAuthsSchema>;
export type TIdentityOciAuthsInsert = Omit<z.input<typeof IdentityOciAuthsSchema>, TImmutableDBKeys>;
export type TIdentityOciAuthsUpdate = Partial<Omit<z.input<typeof IdentityOciAuthsSchema>, TImmutableDBKeys>>;

View File

@@ -37,6 +37,7 @@ export * from "./identity-gcp-auths";
export * from "./identity-jwt-auths";
export * from "./identity-kubernetes-auths";
export * from "./identity-metadata";
export * from "./identity-oci-auths";
export * from "./identity-oidc-auths";
export * from "./identity-org-memberships";
export * from "./identity-project-additional-privilege";

View File

@@ -79,6 +79,7 @@ export enum TableName {
IdentityAzureAuth = "identity_azure_auths",
IdentityUaClientSecret = "identity_ua_client_secrets",
IdentityAwsAuth = "identity_aws_auths",
IdentityOciAuth = "identity_oci_auths",
IdentityOidcAuth = "identity_oidc_auths",
IdentityJwtAuth = "identity_jwt_auths",
IdentityLdapAuth = "identity_ldap_auths",
@@ -233,6 +234,7 @@ export enum IdentityAuthMethod {
GCP_AUTH = "gcp-auth",
AWS_AUTH = "aws-auth",
AZURE_AUTH = "azure-auth",
OCI_AUTH = "oci-auth",
OIDC_AUTH = "oidc-auth",
JWT_AUTH = "jwt-auth",
LDAP_AUTH = "ldap-auth"

View File

@@ -97,7 +97,7 @@ export const registerSshCertificateTemplateRouter = async (server: FastifyZodPro
allowCustomKeyIds: z.boolean().describe(SSH_CERTIFICATE_TEMPLATES.CREATE.allowCustomKeyIds)
})
.refine((data) => ms(data.maxTTL) >= ms(data.ttl), {
message: "Max TLL must be greater than or equal to TTL",
message: "Max TTL must be greater than or equal to TTL",
path: ["maxTTL"]
}),
response: {

View File

@@ -162,6 +162,12 @@ export enum EventType {
REVOKE_IDENTITY_AWS_AUTH = "revoke-identity-aws-auth",
GET_IDENTITY_AWS_AUTH = "get-identity-aws-auth",
LOGIN_IDENTITY_OCI_AUTH = "login-identity-oci-auth",
ADD_IDENTITY_OCI_AUTH = "add-identity-oci-auth",
UPDATE_IDENTITY_OCI_AUTH = "update-identity-oci-auth",
REVOKE_IDENTITY_OCI_AUTH = "revoke-identity-oci-auth",
GET_IDENTITY_OCI_AUTH = "get-identity-oci-auth",
LOGIN_IDENTITY_AZURE_AUTH = "login-identity-azure-auth",
ADD_IDENTITY_AZURE_AUTH = "add-identity-azure-auth",
UPDATE_IDENTITY_AZURE_AUTH = "update-identity-azure-auth",
@@ -1009,6 +1015,55 @@ interface GetIdentityAwsAuthEvent {
};
}
interface LoginIdentityOciAuthEvent {
type: EventType.LOGIN_IDENTITY_OCI_AUTH;
metadata: {
identityId: string;
identityOciAuthId: string;
identityAccessTokenId: string;
};
}
interface AddIdentityOciAuthEvent {
type: EventType.ADD_IDENTITY_OCI_AUTH;
metadata: {
identityId: string;
tenancyOcid: string;
allowedUsernames: string | null;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: Array<TIdentityTrustedIp>;
};
}
interface DeleteIdentityOciAuthEvent {
type: EventType.REVOKE_IDENTITY_OCI_AUTH;
metadata: {
identityId: string;
};
}
interface UpdateIdentityOciAuthEvent {
type: EventType.UPDATE_IDENTITY_OCI_AUTH;
metadata: {
identityId: string;
tenancyOcid?: string;
allowedUsernames: string | null;
accessTokenTTL?: number;
accessTokenMaxTTL?: number;
accessTokenNumUsesLimit?: number;
accessTokenTrustedIps?: Array<TIdentityTrustedIp>;
};
}
interface GetIdentityOciAuthEvent {
type: EventType.GET_IDENTITY_OCI_AUTH;
metadata: {
identityId: string;
};
}
interface LoginIdentityAzureAuthEvent {
type: EventType.LOGIN_IDENTITY_AZURE_AUTH;
metadata: {
@@ -2914,6 +2969,11 @@ export type Event =
| UpdateIdentityAwsAuthEvent
| GetIdentityAwsAuthEvent
| DeleteIdentityAwsAuthEvent
| LoginIdentityOciAuthEvent
| AddIdentityOciAuthEvent
| UpdateIdentityOciAuthEvent
| GetIdentityOciAuthEvent
| DeleteIdentityOciAuthEvent
| LoginIdentityAzureAuthEvent
| AddIdentityAzureAuthEvent
| DeleteIdentityAzureAuthEvent

View File

@@ -714,13 +714,15 @@ export const oidcConfigServiceFactory = ({
}
}
const groups = typeof claims.groups === "string" ? [claims.groups] : (claims.groups as string[] | undefined);
oidcLogin({
email: claims.email,
externalId: claims.sub,
firstName: claims.given_name ?? "",
lastName: claims.family_name ?? "",
orgId: org.id,
groups: claims.groups as string[] | undefined,
groups,
callbackPort,
manageGroupMemberships: oidcCfg.manageGroupMemberships
})

View File

@@ -126,7 +126,6 @@ const buildAdminPermissionRules = () => {
can(
[
ProjectPermissionSecretActions.DescribeAndReadValue,
ProjectPermissionSecretActions.DescribeSecret,
ProjectPermissionSecretActions.ReadValue,
ProjectPermissionSecretActions.Create,
@@ -207,7 +206,6 @@ const buildMemberPermissionRules = () => {
can(
[
ProjectPermissionSecretActions.DescribeAndReadValue,
ProjectPermissionSecretActions.DescribeSecret,
ProjectPermissionSecretActions.ReadValue,
ProjectPermissionSecretActions.Edit,
@@ -386,9 +384,10 @@ const buildMemberPermissionRules = () => {
const buildViewerPermissionRules = () => {
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
can(ProjectPermissionSecretActions.DescribeAndReadValue, ProjectPermissionSub.Secrets);
can(ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSub.Secrets);
can(ProjectPermissionSecretActions.ReadValue, ProjectPermissionSub.Secrets);
can(
[ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSecretActions.ReadValue],
ProjectPermissionSub.Secrets
);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders);
can(ProjectPermissionDynamicSecretActions.ReadRootCredential, ProjectPermissionSub.DynamicSecrets);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports);

View File

@@ -14,6 +14,7 @@ export enum ApiDocsTags {
UniversalAuth = "Universal Auth",
GcpAuth = "GCP Auth",
AwsAuth = "AWS Auth",
OciAuth = "OCI Auth",
AzureAuth = "Azure Auth",
KubernetesAuth = "Kubernetes Auth",
JwtAuth = "JWT Auth",
@@ -271,6 +272,40 @@ export const AWS_AUTH = {
}
} as const;
export const OCI_AUTH = {
LOGIN: {
identityId: "The ID of the identity to login.",
userOcid: "The OCID of the user attempting login.",
headers: "The headers of the signed request."
},
ATTACH: {
identityId: "The ID of the identity to attach the configuration onto.",
tenancyOcid: "The OCID of your tenancy.",
allowedUsernames:
"The comma-separated list of trusted OCI account usernames that are allowed to authenticate with Infisical.",
accessTokenTTL: "The lifetime for an access token in seconds.",
accessTokenMaxTTL: "The maximum lifetime for an access token in seconds.",
accessTokenNumUsesLimit: "The maximum number of times that an access token can be used.",
accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from."
},
UPDATE: {
identityId: "The ID of the identity to update the auth method for.",
tenancyOcid: "The OCID of your tenancy.",
allowedUsernames:
"The comma-separated list of trusted OCI account usernames that are allowed to authenticate with Infisical.",
accessTokenTTL: "The new lifetime for an access token in seconds.",
accessTokenMaxTTL: "The new maximum lifetime for an access token in seconds.",
accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used.",
accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from."
},
RETRIEVE: {
identityId: "The ID of the identity to retrieve the auth method for."
},
REVOKE: {
identityId: "The ID of the identity to revoke the auth method for."
}
} as const;
export const AZURE_AUTH = {
LOGIN: {
identityId: "The ID of the identity to login."
@@ -2039,6 +2074,13 @@ export const AppConnections = {
AZURE_CLIENT_SECRETS: {
code: "The OAuth code to use to connect with Azure Client Secrets.",
tenantId: "The Tenant ID to use to connect with Azure Client Secrets."
},
OCI: {
userOcid: "The OCID (Oracle Cloud Identifier) of the user making the request.",
tenancyOcid: "The OCID (Oracle Cloud Identifier) of the tenancy in Oracle Cloud Infrastructure.",
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."
}
}
};
@@ -2102,6 +2144,7 @@ export const SecretSyncs = {
const destinationName = SECRET_SYNC_NAME_MAP[destination];
return {
initialSyncBehavior: `Specify how Infisical should resolve the initial sync to the ${destinationName} destination.`,
keySchema: `Specify the format to use for structuring secret keys in the ${destinationName} destination.`,
disableSecretDeletion: `Enable this flag to prevent removal of secrets from the ${destinationName} destination when syncing.`
};
},
@@ -2186,6 +2229,11 @@ export const SecretSyncs = {
TEAMCITY: {
project: "The TeamCity project to sync secrets to.",
buildConfig: "The TeamCity build configuration to sync secrets to."
},
OCI_VAULT: {
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."
}
}
};

View File

@@ -57,7 +57,9 @@ export const registerServeUI = async (
reply.callNotFound();
return;
}
return reply.sendFile("index.html");
// reference: https://github.com/fastify/fastify-static?tab=readme-ov-file#managing-cache-control-headers
// to avoid ui bundle skew on new deployment
return reply.sendFile("index.html", { maxAge: 0, immutable: false });
}
});
}

View File

@@ -162,6 +162,8 @@ import { identityKubernetesAuthDALFactory } from "@app/services/identity-kuberne
import { identityKubernetesAuthServiceFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-service";
import { identityLdapAuthDALFactory } from "@app/services/identity-ldap-auth/identity-ldap-auth-dal";
import { identityLdapAuthServiceFactory } from "@app/services/identity-ldap-auth/identity-ldap-auth-service";
import { identityOciAuthDALFactory } from "@app/services/identity-oci-auth/identity-oci-auth-dal";
import { identityOciAuthServiceFactory } from "@app/services/identity-oci-auth/identity-oci-auth-service";
import { identityOidcAuthDALFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-dal";
import { identityOidcAuthServiceFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-service";
import { identityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
@@ -355,6 +357,7 @@ export const registerRoutes = async (
const identityUaClientSecretDAL = identityUaClientSecretDALFactory(db);
const identityAwsAuthDAL = identityAwsAuthDALFactory(db);
const identityGcpAuthDAL = identityGcpAuthDALFactory(db);
const identityOciAuthDAL = identityOciAuthDALFactory(db);
const identityOidcAuthDAL = identityOidcAuthDALFactory(db);
const identityJwtAuthDAL = identityJwtAuthDALFactory(db);
const identityAzureAuthDAL = identityAzureAuthDALFactory(db);
@@ -1451,6 +1454,14 @@ export const registerRoutes = async (
licenseService
});
const identityOciAuthService = identityOciAuthServiceFactory({
identityAccessTokenDAL,
identityOciAuthDAL,
identityOrgMembershipDAL,
licenseService,
permissionService
});
const identityOidcAuthService = identityOidcAuthServiceFactory({
identityOidcAuthDAL,
identityOrgMembershipDAL,
@@ -1737,6 +1748,7 @@ export const registerRoutes = async (
identityGcpAuth: identityGcpAuthService,
identityAwsAuth: identityAwsAuthService,
identityAzureAuth: identityAzureAuthService,
identityOciAuth: identityOciAuthService,
identityOidcAuth: identityOidcAuthService,
identityJwtAuth: identityJwtAuthService,
identityLdapAuth: identityLdapAuthService,

View File

@@ -38,6 +38,7 @@ import {
} from "@app/services/app-connection/humanitec";
import { LdapConnectionListItemSchema, SanitizedLdapConnectionSchema } from "@app/services/app-connection/ldap";
import { MsSqlConnectionListItemSchema, SanitizedMsSqlConnectionSchema } from "@app/services/app-connection/mssql";
import { OCIConnectionListItemSchema, SanitizedOCIConnectionSchema } from "@app/services/app-connection/oci";
import {
PostgresConnectionListItemSchema,
SanitizedPostgresConnectionSchema
@@ -76,7 +77,8 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedAzureClientSecretsConnectionSchema.options,
...SanitizedWindmillConnectionSchema.options,
...SanitizedLdapConnectionSchema.options,
...SanitizedTeamCityConnectionSchema.options
...SanitizedTeamCityConnectionSchema.options,
...SanitizedOCIConnectionSchema.options
]);
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
@@ -97,7 +99,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
AzureClientSecretsConnectionListItemSchema,
WindmillConnectionListItemSchema,
LdapConnectionListItemSchema,
TeamCityConnectionListItemSchema
TeamCityConnectionListItemSchema,
OCIConnectionListItemSchema
]);
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {

View File

@@ -13,6 +13,7 @@ import { registerHCVaultConnectionRouter } from "./hc-vault-connection-router";
import { registerHumanitecConnectionRouter } from "./humanitec-connection-router";
import { registerLdapConnectionRouter } from "./ldap-connection-router";
import { registerMsSqlConnectionRouter } from "./mssql-connection-router";
import { registerOCIConnectionRouter } from "./oci-connection-router";
import { registerPostgresConnectionRouter } from "./postgres-connection-router";
import { registerTeamCityConnectionRouter } from "./teamcity-connection-router";
import { registerTerraformCloudConnectionRouter } from "./terraform-cloud-router";
@@ -40,5 +41,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
[AppConnection.Auth0]: registerAuth0ConnectionRouter,
[AppConnection.HCVault]: registerHCVaultConnectionRouter,
[AppConnection.LDAP]: registerLdapConnectionRouter,
[AppConnection.TeamCity]: registerTeamCityConnectionRouter
[AppConnection.TeamCity]: registerTeamCityConnectionRouter,
[AppConnection.OCI]: registerOCIConnectionRouter
};

View File

@@ -0,0 +1,123 @@
import z from "zod";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
CreateOCIConnectionSchema,
SanitizedOCIConnectionSchema,
UpdateOCIConnectionSchema
} from "@app/services/app-connection/oci";
import { AuthMode } from "@app/services/auth/auth-type";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerOCIConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({
app: AppConnection.OCI,
server,
sanitizedResponseSchema: SanitizedOCIConnectionSchema,
createSchema: CreateOCIConnectionSchema,
updateSchema: UpdateOCIConnectionSchema
});
// The following endpoints are for internal Infisical App use only and not part of the public API
server.route({
method: "GET",
url: `/:connectionId/compartments`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid()
}),
response: {
200: z
.object({
id: z.string(),
name: z.string()
})
.array()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { connectionId } = req.params;
const compartments = await server.services.appConnection.oci.listCompartments(connectionId, req.permission);
return compartments;
}
});
server.route({
method: "GET",
url: `/:connectionId/vaults`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid()
}),
querystring: z.object({
compartmentOcid: z.string().min(1, "Compartment OCID required")
}),
response: {
200: z
.object({
id: z.string(),
displayName: z.string()
})
.array()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { connectionId } = req.params;
const { compartmentOcid } = req.query;
const vaults = await server.services.appConnection.oci.listVaults(
{ connectionId, compartmentOcid },
req.permission
);
return vaults;
}
});
server.route({
method: "GET",
url: `/:connectionId/vault-keys`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid()
}),
querystring: z.object({
compartmentOcid: z.string().min(1, "Compartment OCID required"),
vaultOcid: z.string().min(1, "Vault OCID required")
}),
response: {
200: z
.object({
id: z.string(),
displayName: z.string()
})
.array()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { connectionId } = req.params;
const { compartmentOcid, vaultOcid } = req.query;
const keys = await server.services.appConnection.oci.listVaultKeys(
{ connectionId, compartmentOcid, vaultOcid },
req.permission
);
return keys;
}
});
};

View File

@@ -0,0 +1,338 @@
import { z } from "zod";
import { IdentityOciAuthsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, OCI_AUTH } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
import { validateTenancy, validateUsernames } from "@app/services/identity-oci-auth/identity-oci-auth-validators";
import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns";
export const registerIdentityOciAuthRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/oci-auth/login",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.OciAuth],
description: "Login with OCI Auth",
body: z.object({
identityId: z.string().trim().describe(OCI_AUTH.LOGIN.identityId),
userOcid: z.string().trim().describe(OCI_AUTH.LOGIN.userOcid),
headers: z
.object({
authorization: z.string(),
host: z.string(),
"x-date": z.string()
})
.describe(OCI_AUTH.LOGIN.headers)
}),
response: {
200: z.object({
accessToken: z.string(),
expiresIn: z.coerce.number(),
accessTokenMaxTTL: z.coerce.number(),
tokenType: z.literal("Bearer")
})
}
},
handler: async (req) => {
const { identityOciAuth, accessToken, identityAccessToken, identityMembershipOrg } =
await server.services.identityOciAuth.login(req.body);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityMembershipOrg?.orgId,
event: {
type: EventType.LOGIN_IDENTITY_OCI_AUTH,
metadata: {
identityId: identityOciAuth.identityId,
identityAccessTokenId: identityAccessToken.id,
identityOciAuthId: identityOciAuth.id
}
}
});
return {
accessToken,
tokenType: "Bearer" as const,
expiresIn: identityOciAuth.accessTokenTTL,
accessTokenMaxTTL: identityOciAuth.accessTokenMaxTTL
};
}
});
server.route({
method: "POST",
url: "/oci-auth/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.OciAuth],
description: "Attach OCI Auth configuration onto identity",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().trim().describe(OCI_AUTH.ATTACH.identityId)
}),
body: z
.object({
tenancyOcid: validateTenancy.describe(OCI_AUTH.ATTACH.tenancyOcid),
allowedUsernames: validateUsernames.describe(OCI_AUTH.ATTACH.allowedUsernames),
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
.describe(OCI_AUTH.ATTACH.accessTokenTrustedIps),
accessTokenTTL: z
.number()
.int()
.min(0)
.max(315360000)
.default(2592000)
.describe(OCI_AUTH.ATTACH.accessTokenTTL),
accessTokenMaxTTL: z
.number()
.int()
.min(1)
.max(315360000)
.default(2592000)
.describe(OCI_AUTH.ATTACH.accessTokenMaxTTL),
accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(OCI_AUTH.ATTACH.accessTokenNumUsesLimit)
})
.refine(
(val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
"Access Token TTL cannot be greater than Access Token Max TTL."
),
response: {
200: z.object({
identityOciAuth: IdentityOciAuthsSchema
})
}
},
handler: async (req) => {
const identityOciAuth = await server.services.identityOciAuth.attachOciAuth({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body,
identityId: req.params.identityId,
isActorSuperAdmin: isSuperAdmin(req.auth)
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityOciAuth.orgId,
event: {
type: EventType.ADD_IDENTITY_OCI_AUTH,
metadata: {
identityId: identityOciAuth.identityId,
tenancyOcid: identityOciAuth.tenancyOcid,
allowedUsernames: identityOciAuth.allowedUsernames || null,
accessTokenTTL: identityOciAuth.accessTokenTTL,
accessTokenMaxTTL: identityOciAuth.accessTokenMaxTTL,
accessTokenTrustedIps: identityOciAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
accessTokenNumUsesLimit: identityOciAuth.accessTokenNumUsesLimit
}
}
});
return { identityOciAuth };
}
});
server.route({
method: "PATCH",
url: "/oci-auth/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.OciAuth],
description: "Update OCI Auth configuration on identity",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().describe(OCI_AUTH.UPDATE.identityId)
}),
body: z
.object({
tenancyOcid: validateTenancy.describe(OCI_AUTH.UPDATE.tenancyOcid),
allowedUsernames: validateUsernames.describe(OCI_AUTH.UPDATE.allowedUsernames),
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.optional()
.describe(OCI_AUTH.UPDATE.accessTokenTrustedIps),
accessTokenTTL: z.number().int().min(0).max(315360000).optional().describe(OCI_AUTH.UPDATE.accessTokenTTL),
accessTokenNumUsesLimit: z.number().int().min(0).optional().describe(OCI_AUTH.UPDATE.accessTokenNumUsesLimit),
accessTokenMaxTTL: z
.number()
.int()
.max(315360000)
.min(0)
.optional()
.describe(OCI_AUTH.UPDATE.accessTokenMaxTTL)
})
.refine(
(val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true),
"Access Token TTL cannot be greater than Access Token Max TTL."
),
response: {
200: z.object({
identityOciAuth: IdentityOciAuthsSchema
})
}
},
handler: async (req) => {
const identityOciAuth = await server.services.identityOciAuth.updateOciAuth({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body,
identityId: req.params.identityId,
allowedUsernames: req.body.allowedUsernames || null
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityOciAuth.orgId,
event: {
type: EventType.UPDATE_IDENTITY_OCI_AUTH,
metadata: {
identityId: identityOciAuth.identityId,
tenancyOcid: identityOciAuth.tenancyOcid,
allowedUsernames: identityOciAuth.allowedUsernames || null,
accessTokenTTL: identityOciAuth.accessTokenTTL,
accessTokenMaxTTL: identityOciAuth.accessTokenMaxTTL,
accessTokenTrustedIps: identityOciAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
accessTokenNumUsesLimit: identityOciAuth.accessTokenNumUsesLimit
}
}
});
return { identityOciAuth };
}
});
server.route({
method: "GET",
url: "/oci-auth/identities/:identityId",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.OciAuth],
description: "Retrieve OCI Auth configuration on identity",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().describe(OCI_AUTH.RETRIEVE.identityId)
}),
response: {
200: z.object({
identityOciAuth: IdentityOciAuthsSchema
})
}
},
handler: async (req) => {
const identityOciAuth = await server.services.identityOciAuth.getOciAuth({
identityId: req.params.identityId,
actor: req.permission.type,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityOciAuth.orgId,
event: {
type: EventType.GET_IDENTITY_OCI_AUTH,
metadata: {
identityId: identityOciAuth.identityId
}
}
});
return { identityOciAuth };
}
});
server.route({
method: "DELETE",
url: "/oci-auth/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
tags: [ApiDocsTags.OciAuth],
description: "Delete OCI Auth configuration on identity",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().describe(OCI_AUTH.REVOKE.identityId)
}),
response: {
200: z.object({
identityOciAuth: IdentityOciAuthsSchema
})
}
},
handler: async (req) => {
const identityOciAuth = await server.services.identityOciAuth.revokeIdentityOciAuth({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
identityId: req.params.identityId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityOciAuth.orgId,
event: {
type: EventType.REVOKE_IDENTITY_OCI_AUTH,
metadata: {
identityId: identityOciAuth.identityId
}
}
});
return { identityOciAuth };
}
});
};

View File

@@ -52,7 +52,8 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
response: {
200: z.object({
identity: IdentitiesSchema.extend({
authMethods: z.array(z.string())
authMethods: z.array(z.string()),
metadata: z.object({ id: z.string(), key: z.string(), value: z.string() }).array()
})
})
}
@@ -123,7 +124,9 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
}),
response: {
200: z.object({
identity: IdentitiesSchema
identity: IdentitiesSchema.extend({
metadata: z.object({ id: z.string(), key: z.string(), value: z.string() }).array()
})
})
}
},
@@ -227,8 +230,8 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
identity: IdentityOrgMembershipsSchema.extend({
metadata: z
.object({
key: z.string().trim().min(1),
id: z.string().trim().min(1),
key: z.string().trim().min(1),
value: z.string().trim().min(1)
})
.array()

View File

@@ -20,6 +20,7 @@ import { registerIdentityGcpAuthRouter } from "./identity-gcp-auth-router";
import { registerIdentityJwtAuthRouter } from "./identity-jwt-auth-router";
import { registerIdentityKubernetesRouter } from "./identity-kubernetes-auth-router";
import { registerIdentityLdapAuthRouter } from "./identity-ldap-auth-router";
import { registerIdentityOciAuthRouter } from "./identity-oci-auth-router";
import { registerIdentityOidcAuthRouter } from "./identity-oidc-auth-router";
import { registerIdentityRouter } from "./identity-router";
import { registerIdentityTokenAuthRouter } from "./identity-token-auth-router";
@@ -63,6 +64,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
await authRouter.register(registerIdentityAccessTokenRouter);
await authRouter.register(registerIdentityAwsAuthRouter);
await authRouter.register(registerIdentityAzureAuthRouter);
await authRouter.register(registerIdentityOciAuthRouter);
await authRouter.register(registerIdentityOidcAuthRouter);
await authRouter.register(registerIdentityJwtAuthRouter);
await authRouter.register(registerIdentityLdapAuthRouter);

View File

@@ -511,7 +511,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const workspace = await server.services.project.updateAuditLogsRetention({
actorId: req.permission.id,

View File

@@ -10,6 +10,7 @@ import { registerGcpSyncRouter } from "./gcp-sync-router";
import { registerGitHubSyncRouter } from "./github-sync-router";
import { registerHCVaultSyncRouter } from "./hc-vault-sync-router";
import { registerHumanitecSyncRouter } from "./humanitec-sync-router";
import { registerOCIVaultSyncRouter } from "./oci-vault-sync-router";
import { registerTeamCitySyncRouter } from "./teamcity-sync-router";
import { registerTerraformCloudSyncRouter } from "./terraform-cloud-sync-router";
import { registerVercelSyncRouter } from "./vercel-sync-router";
@@ -31,5 +32,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: Fastif
[SecretSync.Vercel]: registerVercelSyncRouter,
[SecretSync.Windmill]: registerWindmillSyncRouter,
[SecretSync.HCVault]: registerHCVaultSyncRouter,
[SecretSync.TeamCity]: registerTeamCitySyncRouter
[SecretSync.TeamCity]: registerTeamCitySyncRouter,
[SecretSync.OCIVault]: registerOCIVaultSyncRouter
};

View File

@@ -0,0 +1,17 @@
import {
CreateOCIVaultSyncSchema,
OCIVaultSyncSchema,
UpdateOCIVaultSyncSchema
} from "@app/services/secret-sync/oci-vault";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
export const registerOCIVaultSyncRouter = async (server: FastifyZodProvider) =>
registerSyncSecretsEndpoints({
destination: SecretSync.OCIVault,
server,
responseSchema: OCIVaultSyncSchema,
createSchema: CreateOCIVaultSyncSchema,
updateSchema: UpdateOCIVaultSyncSchema
});

View File

@@ -24,6 +24,7 @@ import { GcpSyncListItemSchema, GcpSyncSchema } from "@app/services/secret-sync/
import { GitHubSyncListItemSchema, GitHubSyncSchema } from "@app/services/secret-sync/github";
import { HCVaultSyncListItemSchema, HCVaultSyncSchema } from "@app/services/secret-sync/hc-vault";
import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/secret-sync/humanitec";
import { OCIVaultSyncListItemSchema, OCIVaultSyncSchema } from "@app/services/secret-sync/oci-vault";
import { TeamCitySyncListItemSchema, TeamCitySyncSchema } from "@app/services/secret-sync/teamcity";
import { TerraformCloudSyncListItemSchema, TerraformCloudSyncSchema } from "@app/services/secret-sync/terraform-cloud";
import { VercelSyncListItemSchema, VercelSyncSchema } from "@app/services/secret-sync/vercel";
@@ -43,7 +44,8 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
VercelSyncSchema,
WindmillSyncSchema,
HCVaultSyncSchema,
TeamCitySyncSchema
TeamCitySyncSchema,
OCIVaultSyncSchema
]);
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
@@ -60,7 +62,8 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
VercelSyncListItemSchema,
WindmillSyncListItemSchema,
HCVaultSyncListItemSchema,
TeamCitySyncListItemSchema
TeamCitySyncListItemSchema,
OCIVaultSyncListItemSchema
]);
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {

View File

@@ -16,7 +16,8 @@ export enum AppConnection {
Auth0 = "auth0",
HCVault = "hashicorp-vault",
LDAP = "ldap",
TeamCity = "teamcity"
TeamCity = "teamcity",
OCI = "oci"
}
export enum AWSRegion {

View File

@@ -53,6 +53,7 @@ import {
} from "./humanitec";
import { getLdapConnectionListItem, LdapConnectionMethod, validateLdapConnectionCredentials } from "./ldap";
import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql";
import { getOCIConnectionListItem, OCIConnectionMethod, validateOCIConnectionCredentials } from "./oci";
import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres";
import {
getTeamCityConnectionListItem,
@@ -91,7 +92,8 @@ export const listAppConnectionOptions = () => {
getAuth0ConnectionListItem(),
getHCVaultConnectionListItem(),
getLdapConnectionListItem(),
getTeamCityConnectionListItem()
getTeamCityConnectionListItem(),
getOCIConnectionListItem()
].sort((a, b) => a.name.localeCompare(b.name));
};
@@ -160,7 +162,8 @@ export const validateAppConnectionCredentials = async (
[AppConnection.Windmill]: validateWindmillConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.HCVault]: validateHCVaultConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.LDAP]: validateLdapConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.TeamCity]: validateTeamCityConnectionCredentials as TAppConnectionCredentialsValidator
[AppConnection.TeamCity]: validateTeamCityConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.OCI]: validateOCIConnectionCredentials as TAppConnectionCredentialsValidator
};
return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection);
@@ -176,6 +179,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
case GitHubConnectionMethod.OAuth:
return "OAuth";
case AwsConnectionMethod.AccessKey:
case OCIConnectionMethod.AccessKey:
return "Access Key";
case AwsConnectionMethod.AssumeRole:
return "Assume Role";
@@ -250,5 +254,6 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
[AppConnection.Auth0]: platformManagedCredentialsNotSupported,
[AppConnection.HCVault]: platformManagedCredentialsNotSupported,
[AppConnection.LDAP]: platformManagedCredentialsNotSupported, // we could support this in the future
[AppConnection.TeamCity]: platformManagedCredentialsNotSupported
[AppConnection.TeamCity]: platformManagedCredentialsNotSupported,
[AppConnection.OCI]: platformManagedCredentialsNotSupported
};

View File

@@ -18,5 +18,6 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.Auth0]: "Auth0",
[AppConnection.HCVault]: "Hashicorp Vault",
[AppConnection.LDAP]: "LDAP",
[AppConnection.TeamCity]: "TeamCity"
[AppConnection.TeamCity]: "TeamCity",
[AppConnection.OCI]: "OCI"
};

View File

@@ -49,6 +49,8 @@ import { ValidateHumanitecConnectionCredentialsSchema } from "./humanitec";
import { humanitecConnectionService } from "./humanitec/humanitec-connection-service";
import { ValidateLdapConnectionCredentialsSchema } from "./ldap";
import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql";
import { ValidateOCIConnectionCredentialsSchema } from "./oci";
import { ociConnectionService } from "./oci/oci-connection-service";
import { ValidatePostgresConnectionCredentialsSchema } from "./postgres";
import { ValidateTeamCityConnectionCredentialsSchema } from "./teamcity";
import { teamcityConnectionService } from "./teamcity/teamcity-connection-service";
@@ -85,7 +87,8 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
[AppConnection.Auth0]: ValidateAuth0ConnectionCredentialsSchema,
[AppConnection.HCVault]: ValidateHCVaultConnectionCredentialsSchema,
[AppConnection.LDAP]: ValidateLdapConnectionCredentialsSchema,
[AppConnection.TeamCity]: ValidateTeamCityConnectionCredentialsSchema
[AppConnection.TeamCity]: ValidateTeamCityConnectionCredentialsSchema,
[AppConnection.OCI]: ValidateOCIConnectionCredentialsSchema
};
export const appConnectionServiceFactory = ({
@@ -464,6 +467,7 @@ export const appConnectionServiceFactory = ({
auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
hcvault: hcVaultConnectionService(connectAppConnectionById),
windmill: windmillConnectionService(connectAppConnectionById),
teamcity: teamcityConnectionService(connectAppConnectionById)
teamcity: teamcityConnectionService(connectAppConnectionById),
oci: ociConnectionService(connectAppConnectionById)
};
};

View File

@@ -76,6 +76,12 @@ import {
TValidateLdapConnectionCredentialsSchema
} from "./ldap";
import { TMsSqlConnection, TMsSqlConnectionInput, TValidateMsSqlConnectionCredentialsSchema } from "./mssql";
import {
TOCIConnection,
TOCIConnectionConfig,
TOCIConnectionInput,
TValidateOCIConnectionCredentialsSchema
} from "./oci";
import {
TPostgresConnection,
TPostgresConnectionInput,
@@ -125,6 +131,7 @@ export type TAppConnection = { id: string } & (
| THCVaultConnection
| TLdapConnection
| TTeamCityConnection
| TOCIConnection
);
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
@@ -150,6 +157,7 @@ export type TAppConnectionInput = { id: string } & (
| THCVaultConnectionInput
| TLdapConnectionInput
| TTeamCityConnectionInput
| TOCIConnectionInput
);
export type TSqlConnectionInput = TPostgresConnectionInput | TMsSqlConnectionInput;
@@ -180,7 +188,8 @@ export type TAppConnectionConfig =
| TAuth0ConnectionConfig
| THCVaultConnectionConfig
| TLdapConnectionConfig
| TTeamCityConnectionConfig;
| TTeamCityConnectionConfig
| TOCIConnectionConfig;
export type TValidateAppConnectionCredentialsSchema =
| TValidateAwsConnectionCredentialsSchema
@@ -200,7 +209,8 @@ export type TValidateAppConnectionCredentialsSchema =
| TValidateAuth0ConnectionCredentialsSchema
| TValidateHCVaultConnectionCredentialsSchema
| TValidateLdapConnectionCredentialsSchema
| TValidateTeamCityConnectionCredentialsSchema;
| TValidateTeamCityConnectionCredentialsSchema
| TValidateOCIConnectionCredentialsSchema;
export type TListAwsConnectionKmsKeys = {
connectionId: string;

View File

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

View File

@@ -0,0 +1,3 @@
export enum OCIConnectionMethod {
AccessKey = "access-key"
}

View File

@@ -0,0 +1,139 @@
import { common, identity, keymanagement } from "oci-sdk";
import { BadRequestError } from "@app/lib/errors";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { OCIConnectionMethod } from "./oci-connection-enums";
import { TOCIConnection, TOCIConnectionConfig } from "./oci-connection-types";
export const getOCIProvider = async (config: TOCIConnectionConfig) => {
const {
credentials: { fingerprint, privateKey, region, tenancyOcid, userOcid }
} = config;
const provider = new common.SimpleAuthenticationDetailsProvider(
tenancyOcid,
userOcid,
fingerprint,
privateKey,
null,
common.Region.fromRegionId(region)
);
return provider;
};
export const getOCIConnectionListItem = () => {
return {
name: "OCI" as const,
app: AppConnection.OCI as const,
methods: Object.values(OCIConnectionMethod) as [OCIConnectionMethod.AccessKey]
};
};
export const validateOCIConnectionCredentials = async (config: TOCIConnectionConfig) => {
const provider = await getOCIProvider(config);
try {
const identityClient = new identity.IdentityClient({
authenticationDetailsProvider: provider
});
// Get user details - a lightweight call that validates all credentials
await identityClient.getUser({ userId: config.credentials.userOcid });
} catch (error: unknown) {
if (error instanceof Error) {
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 listOCICompartments = async (appConnection: TOCIConnection) => {
const provider = await getOCIProvider(appConnection);
const identityClient = new identity.IdentityClient({ authenticationDetailsProvider: provider });
const keyManagementClient = new keymanagement.KmsVaultClient({
authenticationDetailsProvider: provider
});
const rootCompartment = await identityClient
.getTenancy({
tenancyId: appConnection.credentials.tenancyOcid
})
.then((response) => ({
...response.tenancy,
id: appConnection.credentials.tenancyOcid,
name: response.tenancy.name ? `${response.tenancy.name} (root)` : "root"
}));
const compartments = await identityClient.listCompartments({
compartmentId: appConnection.credentials.tenancyOcid,
compartmentIdInSubtree: true,
accessLevel: identity.requests.ListCompartmentsRequest.AccessLevel.Any,
lifecycleState: identity.models.Compartment.LifecycleState.Active
});
const allCompartments = [rootCompartment, ...compartments.items];
const filteredCompartments = [];
for await (const compartment of allCompartments) {
try {
// Check if user can list vaults in this compartment
await keyManagementClient.listVaults({
compartmentId: compartment.id,
limit: 1
});
filteredCompartments.push(compartment);
} catch (error) {
// Do nothing
}
}
return filteredCompartments;
};
export const listOCIVaults = async (appConnection: TOCIConnection, compartmentOcid: string) => {
const provider = await getOCIProvider(appConnection);
const keyManagementClient = new keymanagement.KmsVaultClient({
authenticationDetailsProvider: provider
});
const vaults = await keyManagementClient.listVaults({
compartmentId: compartmentOcid
});
return vaults.items.filter((v) => v.lifecycleState === keymanagement.models.Vault.LifecycleState.Active);
};
export const listOCIVaultKeys = async (appConnection: TOCIConnection, compartmentOcid: string, vaultOcid: string) => {
const provider = await getOCIProvider(appConnection);
const kmsVaultClient = new keymanagement.KmsVaultClient({
authenticationDetailsProvider: provider
});
const vault = await kmsVaultClient.getVault({
vaultId: vaultOcid
});
const keyManagementClient = new keymanagement.KmsManagementClient({
authenticationDetailsProvider: provider
});
keyManagementClient.endpoint = vault.vault.managementEndpoint;
const keys = await keyManagementClient.listKeys({
compartmentId: compartmentOcid
});
return keys.items.filter((v) => v.lifecycleState === keymanagement.models.KeySummary.LifecycleState.Enabled);
};

View File

@@ -0,0 +1,65 @@
import z from "zod";
import { AppConnections } from "@app/lib/api-docs";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
BaseAppConnectionSchema,
GenericCreateAppConnectionFieldsSchema,
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { OCIConnectionMethod } from "./oci-connection-enums";
export const OCIConnectionAccessTokenCredentialsSchema = z.object({
userOcid: z.string().trim().min(1, "User OCID required").describe(AppConnections.CREDENTIALS.OCI.userOcid),
tenancyOcid: z.string().trim().min(1, "Tenancy OCID required").describe(AppConnections.CREDENTIALS.OCI.tenancyOcid),
region: z.string().trim().min(1, "Region required").describe(AppConnections.CREDENTIALS.OCI.region),
fingerprint: z.string().trim().min(1, "Fingerprint required").describe(AppConnections.CREDENTIALS.OCI.fingerprint),
privateKey: z.string().trim().min(1, "Private Key required").describe(AppConnections.CREDENTIALS.OCI.privateKey)
});
const BaseOCIConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.OCI) });
export const OCIConnectionSchema = BaseOCIConnectionSchema.extend({
method: z.literal(OCIConnectionMethod.AccessKey),
credentials: OCIConnectionAccessTokenCredentialsSchema
});
export const SanitizedOCIConnectionSchema = z.discriminatedUnion("method", [
BaseOCIConnectionSchema.extend({
method: z.literal(OCIConnectionMethod.AccessKey),
credentials: OCIConnectionAccessTokenCredentialsSchema.pick({
userOcid: true,
tenancyOcid: true,
region: true,
fingerprint: true
})
})
]);
export const ValidateOCIConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: z.literal(OCIConnectionMethod.AccessKey).describe(AppConnections.CREATE(AppConnection.OCI).method),
credentials: OCIConnectionAccessTokenCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.OCI).credentials
)
})
]);
export const CreateOCIConnectionSchema = ValidateOCIConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.OCI)
);
export const UpdateOCIConnectionSchema = z
.object({
credentials: OCIConnectionAccessTokenCredentialsSchema.optional().describe(
AppConnections.UPDATE(AppConnection.OCI).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.OCI));
export const OCIConnectionListItemSchema = z.object({
name: z.literal("OCI"),
app: z.literal(AppConnection.OCI),
methods: z.nativeEnum(OCIConnectionMethod).array()
});

View File

@@ -0,0 +1,70 @@
import { logger } from "@app/lib/logger";
import { OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import { listOCICompartments, listOCIVaultKeys, listOCIVaults } from "./oci-connection-fns";
import { TOCIConnection } from "./oci-connection-types";
type TGetAppConnectionFunc = (
app: AppConnection,
connectionId: string,
actor: OrgServiceActor
) => Promise<TOCIConnection>;
type TListOCIVaultsDTO = {
connectionId: string;
compartmentOcid: string;
};
type TListOCIVaultKeysDTO = {
connectionId: string;
compartmentOcid: string;
vaultOcid: string;
};
export const ociConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
const listCompartments = async (connectionId: string, actor: OrgServiceActor) => {
const appConnection = await getAppConnection(AppConnection.OCI, connectionId, actor);
try {
const compartments = await listOCICompartments(appConnection);
return compartments;
} catch (error) {
logger.error(error, "Failed to establish connection with OCI");
return [];
}
};
const listVaults = async ({ connectionId, compartmentOcid }: TListOCIVaultsDTO, actor: OrgServiceActor) => {
const appConnection = await getAppConnection(AppConnection.OCI, connectionId, actor);
try {
const vaults = await listOCIVaults(appConnection, compartmentOcid);
return vaults;
} catch (error) {
logger.error(error, "Failed to establish connection with OCI");
return [];
}
};
const listVaultKeys = async (
{ connectionId, compartmentOcid, vaultOcid }: TListOCIVaultKeysDTO,
actor: OrgServiceActor
) => {
const appConnection = await getAppConnection(AppConnection.OCI, connectionId, actor);
try {
const keys = await listOCIVaultKeys(appConnection, compartmentOcid, vaultOcid);
return keys;
} catch (error) {
logger.error(error, "Failed to establish connection with OCI");
return [];
}
};
return {
listCompartments,
listVaults,
listVaultKeys
};
};

View File

@@ -0,0 +1,22 @@
import z from "zod";
import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import {
CreateOCIConnectionSchema,
OCIConnectionSchema,
ValidateOCIConnectionCredentialsSchema
} from "./oci-connection-schemas";
export type TOCIConnection = z.infer<typeof OCIConnectionSchema>;
export type TOCIConnectionInput = z.infer<typeof CreateOCIConnectionSchema> & {
app: AppConnection.OCI;
};
export type TValidateOCIConnectionCredentialsSchema = typeof ValidateOCIConnectionCredentialsSchema;
export type TOCIConnectionConfig = DiscriminativePick<TOCIConnectionInput, "method" | "app" | "credentials"> & {
orgId: string;
};

View File

@@ -36,6 +36,7 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
`${TableName.Identity}.id`,
`${TableName.IdentityKubernetesAuth}.identityId`
)
.leftJoin(TableName.IdentityOciAuth, `${TableName.Identity}.id`, `${TableName.IdentityOciAuth}.identityId`)
.leftJoin(TableName.IdentityOidcAuth, `${TableName.Identity}.id`, `${TableName.IdentityOidcAuth}.identityId`)
.leftJoin(TableName.IdentityTokenAuth, `${TableName.Identity}.id`, `${TableName.IdentityTokenAuth}.identityId`)
.leftJoin(TableName.IdentityJwtAuth, `${TableName.Identity}.id`, `${TableName.IdentityJwtAuth}.identityId`)
@@ -46,6 +47,7 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityAwsAuth).as("accessTokenTrustedIpsAws"),
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityAzureAuth).as("accessTokenTrustedIpsAzure"),
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityKubernetesAuth).as("accessTokenTrustedIpsK8s"),
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityOciAuth).as("accessTokenTrustedIpsOci"),
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityOidcAuth).as("accessTokenTrustedIpsOidc"),
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityTokenAuth).as("accessTokenTrustedIpsToken"),
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityJwtAuth).as("accessTokenTrustedIpsJwt"),
@@ -63,6 +65,7 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
trustedIpsAwsAuth: doc.accessTokenTrustedIpsAws,
trustedIpsAzureAuth: doc.accessTokenTrustedIpsAzure,
trustedIpsKubernetesAuth: doc.accessTokenTrustedIpsK8s,
trustedIpsOciAuth: doc.accessTokenTrustedIpsOci,
trustedIpsOidcAuth: doc.accessTokenTrustedIpsOidc,
trustedIpsAccessTokenAuth: doc.accessTokenTrustedIpsToken,
trustedIpsAccessJwtAuth: doc.accessTokenTrustedIpsJwt,

View File

@@ -182,6 +182,7 @@ export const identityAccessTokenServiceFactory = ({
[IdentityAuthMethod.UNIVERSAL_AUTH]: identityAccessToken.trustedIpsUniversalAuth,
[IdentityAuthMethod.GCP_AUTH]: identityAccessToken.trustedIpsGcpAuth,
[IdentityAuthMethod.AWS_AUTH]: identityAccessToken.trustedIpsAwsAuth,
[IdentityAuthMethod.OCI_AUTH]: identityAccessToken.trustedIpsOciAuth,
[IdentityAuthMethod.AZURE_AUTH]: identityAccessToken.trustedIpsAzureAuth,
[IdentityAuthMethod.KUBERNETES_AUTH]: identityAccessToken.trustedIpsKubernetesAuth,
[IdentityAuthMethod.OIDC_AUTH]: identityAccessToken.trustedIpsOidcAuth,

View File

@@ -0,0 +1,9 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TIdentityOciAuthDALFactory = ReturnType<typeof identityOciAuthDALFactory>;
export const identityOciAuthDALFactory = (db: TDbClient) => {
return ormify(db, TableName.IdentityOciAuth);
};

View File

@@ -0,0 +1,368 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { ForbiddenError } from "@casl/ability";
import { AxiosError } from "axios";
import jwt from "jsonwebtoken";
import RE2 from "re2";
import { IdentityAuthMethod } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import {
constructPermissionErrorMessage,
validatePrivilegeChangeOperation
} from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request";
import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { logger } from "@app/lib/logger";
import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns";
import { TIdentityOciAuthDALFactory } from "./identity-oci-auth-dal";
import {
TAttachOciAuthDTO,
TGetOciAuthDTO,
TLoginOciAuthDTO,
TOciGetUserResponse,
TRevokeOciAuthDTO,
TUpdateOciAuthDTO
} from "./identity-oci-auth-types";
type TIdentityOciAuthServiceFactoryDep = {
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create" | "delete">;
identityOciAuthDAL: Pick<TIdentityOciAuthDALFactory, "findOne" | "transaction" | "create" | "updateById" | "delete">;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
};
export type TIdentityOciAuthServiceFactory = ReturnType<typeof identityOciAuthServiceFactory>;
export const identityOciAuthServiceFactory = ({
identityAccessTokenDAL,
identityOciAuthDAL,
identityOrgMembershipDAL,
licenseService,
permissionService
}: TIdentityOciAuthServiceFactoryDep) => {
const login = async ({ identityId, headers, userOcid }: TLoginOciAuthDTO) => {
const identityOciAuth = await identityOciAuthDAL.findOne({ identityId });
if (!identityOciAuth) {
throw new NotFoundError({ message: "OCI auth method not found for identity, did you configure OCI auth?" });
}
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: identityOciAuth.identityId });
// Validate OCI host format. Ensures that the host is in "identity.<region>.oraclecloud.com" format.
if (!headers.host || !new RE2("^identity\\.([a-z]{2}-[a-z]+-[1-9])\\.oraclecloud\\.com$").test(headers.host)) {
throw new BadRequestError({
message: "Invalid OCI host format. Expected format: identity.<region>.oraclecloud.com"
});
}
const { data } = await request
.get<TOciGetUserResponse>(`https://${headers.host}/20160918/users/${userOcid}`, {
headers
})
.catch((err: AxiosError) => {
logger.error(err.response, "OciIdentityLogin: Failed to authenticate with Oracle Cloud");
throw err;
});
if (data.compartmentId !== identityOciAuth.tenancyOcid) {
throw new UnauthorizedError({
message: "Access denied: OCI account isn't part of tenancy."
});
}
if (identityOciAuth.allowedUsernames) {
const isAccountAllowed = identityOciAuth.allowedUsernames.split(",").some((name) => name.trim() === data.name);
if (!isAccountAllowed)
throw new UnauthorizedError({
message: "Access denied: OCI account username not allowed."
});
}
// Generate the token
const identityAccessToken = await identityOciAuthDAL.transaction(async (tx) => {
const newToken = await identityAccessTokenDAL.create(
{
identityId: identityOciAuth.identityId,
isAccessTokenRevoked: false,
accessTokenTTL: identityOciAuth.accessTokenTTL,
accessTokenMaxTTL: identityOciAuth.accessTokenMaxTTL,
accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityOciAuth.accessTokenNumUsesLimit,
authMethod: IdentityAuthMethod.OCI_AUTH
},
tx
);
return newToken;
});
const appCfg = getConfig();
const accessToken = jwt.sign(
{
identityId: identityOciAuth.identityId,
identityAccessTokenId: identityAccessToken.id,
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
} as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET,
Number(identityAccessToken.accessTokenTTL) === 0
? undefined
: {
expiresIn: Number(identityAccessToken.accessTokenTTL)
}
);
return {
identityOciAuth,
accessToken,
identityAccessToken,
identityMembershipOrg
};
};
const attachOciAuth = async ({
identityId,
tenancyOcid,
allowedUsernames,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps,
actorId,
actorAuthMethod,
actor,
actorOrgId,
isActorSuperAdmin
}: TAttachOciAuthDTO) => {
await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin);
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OCI_AUTH)) {
throw new BadRequestError({
message: "Failed to add OCI Auth to already configured identity"
});
}
if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) {
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
}
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity);
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => {
if (
!plan.ipAllowlisting &&
accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" &&
accessTokenTrustedIp.ipAddress !== "::/0"
)
throw new BadRequestError({
message:
"Failed to add IP access range to access token due to plan restriction. Upgrade plan to add IP access range."
});
if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress))
throw new BadRequestError({
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
});
return extractIPDetails(accessTokenTrustedIp.ipAddress);
});
const identityOciAuth = await identityOciAuthDAL.transaction(async (tx) => {
const doc = await identityOciAuthDAL.create(
{
identityId: identityMembershipOrg.identityId,
type: "iam",
tenancyOcid,
allowedUsernames,
accessTokenMaxTTL,
accessTokenTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps)
},
tx
);
return doc;
});
return { ...identityOciAuth, orgId: identityMembershipOrg.orgId };
};
const updateOciAuth = async ({
identityId,
tenancyOcid,
allowedUsernames,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TUpdateOciAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OCI_AUTH)) {
throw new NotFoundError({
message: "The identity does not have OCI Auth attached"
});
}
const identityOciAuth = await identityOciAuthDAL.findOne({ identityId });
if (
(accessTokenMaxTTL || identityOciAuth.accessTokenMaxTTL) > 0 &&
(accessTokenTTL || identityOciAuth.accessTokenTTL) > (accessTokenMaxTTL || identityOciAuth.accessTokenMaxTTL)
) {
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
}
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => {
if (
!plan.ipAllowlisting &&
accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" &&
accessTokenTrustedIp.ipAddress !== "::/0"
)
throw new BadRequestError({
message:
"Failed to add IP access range to access token due to plan restriction. Upgrade plan to add IP access range."
});
if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress))
throw new BadRequestError({
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
});
return extractIPDetails(accessTokenTrustedIp.ipAddress);
});
const updatedOciAuth = await identityOciAuthDAL.updateById(identityOciAuth.id, {
tenancyOcid,
allowedUsernames,
accessTokenMaxTTL,
accessTokenTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps: reformattedAccessTokenTrustedIps
? JSON.stringify(reformattedAccessTokenTrustedIps)
: undefined
});
return { ...updatedOciAuth, orgId: identityMembershipOrg.orgId };
};
const getOciAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetOciAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OCI_AUTH)) {
throw new BadRequestError({
message: "The identity does not have OCI Auth attached"
});
}
const ociIdentityAuth = await identityOciAuthDAL.findOne({ identityId });
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
return { ...ociIdentityAuth, orgId: identityMembershipOrg.orgId };
};
const revokeIdentityOciAuth = async ({
identityId,
actorId,
actor,
actorAuthMethod,
actorOrgId
}: TRevokeOciAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OCI_AUTH)) {
throw new BadRequestError({
message: "The identity does not have OCI auth"
});
}
const { permission, membership } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
const { permission: rolePermission } = await permissionService.getOrgPermission(
ActorType.IDENTITY,
identityMembershipOrg.identityId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
const permissionBoundary = validatePrivilegeChangeOperation(
membership.shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.RevokeAuth,
OrgPermissionSubjects.Identity,
permission,
rolePermission
);
if (!permissionBoundary.isValid)
throw new PermissionBoundaryError({
message: constructPermissionErrorMessage(
"Failed to revoke OCI auth of identity with more privileged role",
membership.shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.RevokeAuth,
OrgPermissionSubjects.Identity
),
details: { missingPermissions: permissionBoundary.missingPermissions }
});
const revokedIdentityOciAuth = await identityOciAuthDAL.transaction(async (tx) => {
const deletedOciAuth = await identityOciAuthDAL.delete({ identityId }, tx);
await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.OCI_AUTH }, tx);
return { ...deletedOciAuth?.[0], orgId: identityMembershipOrg.orgId };
});
return revokedIdentityOciAuth;
};
return {
login,
attachOciAuth,
updateOciAuth,
getOciAuth,
revokeIdentityOciAuth
};
};

View File

@@ -0,0 +1,53 @@
import { TProjectPermission } from "@app/lib/types";
export type TLoginOciAuthDTO = {
identityId: string;
userOcid: string;
headers: {
authorization: string;
host: string;
"x-date": string;
};
};
export type TAttachOciAuthDTO = {
identityId: string;
tenancyOcid: string;
allowedUsernames: string | null;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: { ipAddress: string }[];
isActorSuperAdmin?: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TUpdateOciAuthDTO = {
identityId: string;
tenancyOcid: string;
allowedUsernames: string | null;
accessTokenTTL?: number;
accessTokenMaxTTL?: number;
accessTokenNumUsesLimit?: number;
accessTokenTrustedIps?: { ipAddress: string }[];
} & Omit<TProjectPermission, "projectId">;
export type TGetOciAuthDTO = {
identityId: string;
} & Omit<TProjectPermission, "projectId">;
export type TRevokeOciAuthDTO = {
identityId: string;
} & Omit<TProjectPermission, "projectId">;
export type TOciGetUserResponse = {
email: string;
emailVerified: boolean;
timeModified: string;
isMfaActivated: boolean;
id: string;
compartmentId: string;
name: string;
timeCreated: string;
freeformTags: { [key: string]: string };
lifecycleState: string;
};

View File

@@ -0,0 +1,32 @@
import RE2 from "re2";
import { z } from "zod";
const usernameSchema = z
.string()
.min(1, "Username cannot be empty")
.refine((val) => new RE2("^[a-zA-Z0-9._@-]+$").test(val), "Invalid OCI username format");
export const validateUsernames = z
.string()
.trim()
.max(500, "Input exceeds the maximum limit of 500 characters")
.nullish()
.transform((val) => {
if (!val) return [];
return val
.split(",")
.map((s) => s.trim())
.filter(Boolean);
})
.refine((arr) => arr.every((name) => usernameSchema.safeParse(name).success), {
message: "One or more usernames are invalid"
})
.transform((arr) => (arr.length > 0 ? arr.join(", ") : null));
export const validateTenancy = z
.string()
.trim()
.min(1, "Tenancy OCID cannot be empty.")
.refine(
(val) => new RE2("^ocid1\\.tenancy\\.oc1\\..+$").test(val),
"Invalid Tenancy OCID format. Must start with ocid1.tenancy.oc1."
);

View File

@@ -8,6 +8,7 @@ import {
TIdentityAzureAuths,
TIdentityGcpAuths,
TIdentityKubernetesAuths,
TIdentityOciAuths,
TIdentityOidcAuths,
TIdentityTokenAuths,
TIdentityUniversalAuths
@@ -66,6 +67,11 @@ export const identityProjectDALFactory = (db: TDbClient) => {
`${TableName.IdentityProjectMembership}.identityId`,
`${TableName.IdentityKubernetesAuth}.identityId`
)
.leftJoin(
TableName.IdentityOciAuth,
`${TableName.IdentityProjectMembership}.identityId`,
`${TableName.IdentityOciAuth}.identityId`
)
.leftJoin(
TableName.IdentityOidcAuth,
`${TableName.IdentityProjectMembership}.identityId`,
@@ -107,6 +113,7 @@ export const identityProjectDALFactory = (db: TDbClient) => {
db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth),
db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth),
db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth),
db.ref("id").as("ociId").withSchema(TableName.IdentityOciAuth),
db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth),
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth)
@@ -270,6 +277,11 @@ export const identityProjectDALFactory = (db: TDbClient) => {
`${TableName.Identity}.id`,
`${TableName.IdentityKubernetesAuth}.identityId`
)
.leftJoin<TIdentityOciAuths>(
TableName.IdentityOciAuth,
`${TableName.Identity}.id`,
`${TableName.IdentityOciAuth}.identityId`
)
.leftJoin<TIdentityOidcAuths>(
TableName.IdentityOidcAuth,
`${TableName.Identity}.id`,
@@ -309,6 +321,7 @@ export const identityProjectDALFactory = (db: TDbClient) => {
db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth),
db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth),
db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth),
db.ref("id").as("ociId").withSchema(TableName.IdentityOciAuth),
db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth),
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth)
@@ -336,6 +349,7 @@ export const identityProjectDALFactory = (db: TDbClient) => {
awsId,
gcpId,
kubernetesId,
ociId,
oidcId,
azureId,
tokenId,
@@ -356,6 +370,7 @@ export const identityProjectDALFactory = (db: TDbClient) => {
awsId,
gcpId,
kubernetesId,
ociId,
oidcId,
azureId,
tokenId

View File

@@ -5,6 +5,7 @@ export const buildAuthMethods = ({
gcpId,
awsId,
kubernetesId,
ociId,
oidcId,
azureId,
tokenId,
@@ -15,6 +16,7 @@ export const buildAuthMethods = ({
gcpId?: string;
awsId?: string;
kubernetesId?: string;
ociId?: string;
oidcId?: string;
azureId?: string;
tokenId?: string;
@@ -26,6 +28,7 @@ export const buildAuthMethods = ({
...[gcpId ? IdentityAuthMethod.GCP_AUTH : null],
...[awsId ? IdentityAuthMethod.AWS_AUTH : null],
...[kubernetesId ? IdentityAuthMethod.KUBERNETES_AUTH : null],
...[ociId ? IdentityAuthMethod.OCI_AUTH : null],
...[oidcId ? IdentityAuthMethod.OIDC_AUTH : null],
...[azureId ? IdentityAuthMethod.AZURE_AUTH : null],
...[tokenId ? IdentityAuthMethod.TOKEN_AUTH : null],

View File

@@ -8,6 +8,7 @@ import {
TIdentityGcpAuths,
TIdentityJwtAuths,
TIdentityKubernetesAuths,
TIdentityOciAuths,
TIdentityOidcAuths,
TIdentityOrgMemberships,
TIdentityTokenAuths,
@@ -62,6 +63,11 @@ export const identityOrgDALFactory = (db: TDbClient) => {
`${TableName.IdentityOrgMembership}.identityId`,
`${TableName.IdentityKubernetesAuth}.identityId`
)
.leftJoin<TIdentityOciAuths>(
TableName.IdentityOciAuth,
`${TableName.IdentityOrgMembership}.identityId`,
`${TableName.IdentityOciAuth}.identityId`
)
.leftJoin<TIdentityOidcAuths>(
TableName.IdentityOidcAuth,
`${TableName.IdentityOrgMembership}.identityId`,
@@ -95,6 +101,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth),
db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth),
db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth),
db.ref("id").as("ociId").withSchema(TableName.IdentityOciAuth),
db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth),
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth),
@@ -186,6 +193,11 @@ export const identityOrgDALFactory = (db: TDbClient) => {
"paginatedIdentity.identityId",
`${TableName.IdentityKubernetesAuth}.identityId`
)
.leftJoin<TIdentityOciAuths>(
TableName.IdentityOciAuth,
"paginatedIdentity.identityId",
`${TableName.IdentityOciAuth}.identityId`
)
.leftJoin<TIdentityOidcAuths>(
TableName.IdentityOidcAuth,
"paginatedIdentity.identityId",
@@ -226,6 +238,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth),
db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth),
db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth),
db.ref("id").as("ociId").withSchema(TableName.IdentityOciAuth),
db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth),
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth),
@@ -269,6 +282,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
gcpId,
jwtId,
kubernetesId,
ociId,
oidcId,
azureId,
tokenId,
@@ -301,6 +315,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
awsId,
gcpId,
kubernetesId,
ociId,
oidcId,
azureId,
tokenId,
@@ -401,6 +416,11 @@ export const identityOrgDALFactory = (db: TDbClient) => {
`${TableName.IdentityOrgMembership}.identityId`,
`${TableName.IdentityKubernetesAuth}.identityId`
)
.leftJoin(
TableName.IdentityOciAuth,
`${TableName.IdentityOrgMembership}.identityId`,
`${TableName.IdentityOciAuth}.identityId`
)
.leftJoin(
TableName.IdentityOidcAuth,
`${TableName.IdentityOrgMembership}.identityId`,
@@ -441,6 +461,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth),
db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth),
db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth),
db.ref("id").as("ociId").withSchema(TableName.IdentityOciAuth),
db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth),
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth),
@@ -485,6 +506,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
gcpId,
jwtId,
kubernetesId,
ociId,
oidcId,
azureId,
tokenId,
@@ -517,6 +539,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
awsId,
gcpId,
kubernetesId,
ociId,
oidcId,
azureId,
tokenId,

View File

@@ -106,18 +106,29 @@ export const identityServiceFactory = ({
},
tx
);
let insertedMetadata: Array<{
id: string;
key: string;
value: string;
}> = [];
if (metadata && metadata.length) {
await identityMetadataDAL.insertMany(
metadata.map(({ key, value }) => ({
identityId: newIdentity.id,
orgId,
key,
value
})),
tx
);
const rowsToInsert = metadata.map(({ key, value }) => ({
identityId: newIdentity.id,
orgId,
key,
value
}));
insertedMetadata = await identityMetadataDAL.insertMany(rowsToInsert, tx);
}
return { ...newIdentity, authMethods: [] };
return {
...newIdentity,
authMethods: [],
metadata: insertedMetadata
};
});
await licenseService.updateSubscriptionOrgMemberCount(orgId);
@@ -189,21 +200,31 @@ export const identityServiceFactory = ({
tx
);
}
let insertedMetadata: Array<{
id: string;
key: string;
value: string;
}> = [];
if (metadata) {
await identityMetadataDAL.delete({ orgId: identityOrgMembership.orgId, identityId: id }, tx);
if (metadata.length) {
await identityMetadataDAL.insertMany(
metadata.map(({ key, value }) => ({
identityId: newIdentity.id,
orgId: identityOrgMembership.orgId,
key,
value
})),
tx
);
const rowsToInsert = metadata.map(({ key, value }) => ({
identityId: newIdentity.id,
orgId: identityOrgMembership.orgId,
key,
value
}));
insertedMetadata = await identityMetadataDAL.insertMany(rowsToInsert, tx);
}
}
return newIdentity;
return {
...newIdentity,
metadata: insertedMetadata
};
});
return { ...identity, orgId: identityOrgMembership.orgId };
@@ -224,6 +245,7 @@ export const identityServiceFactory = ({
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
return identity;
};

View File

@@ -2,6 +2,7 @@ import AWS, { AWSError } from "aws-sdk";
import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns";
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";
import { TAwsParameterStoreSyncWithCredentials } from "./aws-parameter-store-sync-types";
@@ -389,6 +390,9 @@ export const AwsParameterStoreSyncFns = {
for (const entry of Object.entries(awsParameterStoreSecretsRecord)) {
const [key, parameter] = entry;
// eslint-disable-next-line no-continue
if (!matchesSchema(key, syncOptions.keySchema)) continue;
if (!(key in secretMap) || !secretMap[key].value) {
parametersToDelete.push(parameter);
}

View File

@@ -27,6 +27,7 @@ import {
import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns";
import { AwsSecretsManagerSyncMappingBehavior } from "@app/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-enums";
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
import { TAwsSecretsManagerSyncWithCredentials } from "./aws-secrets-manager-sync-types";
@@ -399,6 +400,9 @@ export const AwsSecretsManagerSyncFns = {
if (syncOptions.disableSecretDeletion) return;
for await (const secretKey of Object.keys(awsSecretsRecord)) {
// eslint-disable-next-line no-continue
if (!matchesSchema(secretKey, syncOptions.keySchema)) continue;
if (!(secretKey in secretMap) || !secretMap[secretKey].value) {
try {
await deleteSecret(client, secretKey);

View File

@@ -7,6 +7,7 @@ import { TAppConnectionDALFactory } from "@app/services/app-connection/app-conne
import { getAzureConnectionAccessToken } from "@app/services/app-connection/azure-key-vault";
import { isAzureKeyVaultReference } from "@app/services/integration-auth/integration-sync-secret-fns";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
import { TAzureAppConfigurationSyncWithCredentials } from "./azure-app-configuration-sync-types";
@@ -139,6 +140,9 @@ export const azureAppConfigurationSyncFactory = ({
if (secretSync.syncOptions.disableSecretDeletion) return;
for await (const key of Object.keys(azureAppConfigSecrets)) {
// eslint-disable-next-line no-continue
if (!matchesSchema(key, secretSync.syncOptions.keySchema)) continue;
const azureSecret = azureAppConfigSecrets[key];
if (
!(key in secretMap) ||

View File

@@ -5,6 +5,7 @@ import { request } from "@app/lib/config/request";
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
import { getAzureConnectionAccessToken } from "@app/services/app-connection/azure-key-vault";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
import { SecretSyncError } from "../secret-sync-errors";
@@ -192,7 +193,9 @@ export const azureKeyVaultSyncFactory = ({ kmsService, appConnectionDAL }: TAzur
if (secretSync.syncOptions.disableSecretDeletion) return;
for await (const deleteSecretKey of deleteSecrets.filter(
(secret) => !setSecrets.find((setSecret) => setSecret.key === secret)
(secret) =>
matchesSchema(secret, secretSync.syncOptions.keySchema) &&
!setSecrets.find((setSecret) => setSecret.key === secret)
)) {
await request.delete(`${secretSync.destinationConfig.vaultBaseUrl}/secrets/${deleteSecretKey}?api-version=7.3`, {
headers: {

View File

@@ -12,6 +12,7 @@ import {
TCamundaSyncWithCredentials
} from "@app/services/secret-sync/camunda/camunda-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 "../secret-sync-types";
@@ -116,6 +117,9 @@ export const camundaSyncFactory = ({ kmsService, appConnectionDAL }: TCamundaSec
if (secretSync.syncOptions.disableSecretDeletion) return;
for await (const secret of Object.keys(camundaSecrets)) {
// eslint-disable-next-line no-continue
if (!matchesSchema(secret, secretSync.syncOptions.keySchema)) continue;
if (!(secret in secretMap) || !secretMap[secret].value) {
try {
await deleteCamundaSecret({

View File

@@ -11,6 +11,7 @@ import {
TDatabricksSyncWithCredentials
} from "@app/services/secret-sync/databricks/databricks-sync-types";
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
import { SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps";
import { TSecretMap } from "../secret-sync-types";
@@ -115,6 +116,9 @@ export const databricksSyncFactory = ({ kmsService, appConnectionDAL }: TDatabri
if (secretSync.syncOptions.disableSecretDeletion) return;
for await (const secret of databricksSecretKeys) {
// eslint-disable-next-line no-continue
if (!matchesSchema(secret.key, secretSync.syncOptions.keySchema)) continue;
if (!(secret.key in secretMap)) {
await deleteDatabricksSecrets({
key: secret.key,

View File

@@ -4,6 +4,7 @@ import { request } from "@app/lib/config/request";
import { logger } from "@app/lib/logger";
import { getGcpConnectionAuthToken } from "@app/services/app-connection/gcp";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
import { SecretSyncError } from "../secret-sync-errors";
import { TSecretMap } from "../secret-sync-types";
@@ -153,6 +154,9 @@ export const GcpSyncFns = {
}
for await (const key of Object.keys(gcpSecrets)) {
// eslint-disable-next-line no-continue
if (!matchesSchema(key, secretSync.syncOptions.keySchema)) continue;
try {
if (!(key in secretMap) || !secretMap[key].value) {
// eslint-disable-next-line no-continue

View File

@@ -4,6 +4,7 @@ import sodium from "libsodium-wrappers";
import { getGitHubClient } from "@app/services/app-connection/github";
import { GitHubSyncScope, GitHubSyncVisibility } from "@app/services/secret-sync/github/github-sync-enums";
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
import { SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps";
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
@@ -222,6 +223,9 @@ export const GithubSyncFns = {
if (secretSync.syncOptions.disableSecretDeletion) return;
for await (const encryptedSecret of encryptedSecrets) {
// eslint-disable-next-line no-continue
if (!matchesSchema(encryptedSecret.name, secretSync.syncOptions.keySchema)) continue;
if (!(encryptedSecret.name in secretMap)) {
await deleteSecret(client, secretSync, encryptedSecret);
}

View File

@@ -11,6 +11,7 @@ import {
TPostHCVaultVariable
} from "@app/services/secret-sync/hc-vault/hc-vault-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 listHCVaultVariables = async ({ instanceUrl, namespace, mount, accessToken, path }: THCVaultListVariables) => {
@@ -68,7 +69,7 @@ export const HCVaultSyncFns = {
const {
connection,
destinationConfig: { mount, path },
syncOptions: { disableSecretDeletion }
syncOptions: { disableSecretDeletion, keySchema }
} = secretSync;
const { namespace } = connection.credentials;
@@ -95,6 +96,9 @@ export const HCVaultSyncFns = {
if (disableSecretDeletion) return;
for await (const [key] of Object.entries(variables)) {
// eslint-disable-next-line no-continue
if (!matchesSchema(key, keySchema)) continue;
if (!(key in secretMap)) {
delete variables[key];
tainted = true;

View File

@@ -2,6 +2,7 @@ import { request } from "@app/lib/config/request";
import { logger } from "@app/lib/logger";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
import { SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps";
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
@@ -199,6 +200,9 @@ export const HumanitecSyncFns = {
if (secretSync.syncOptions.disableSecretDeletion) return;
for await (const humanitecSecret of humanitecSecrets) {
// eslint-disable-next-line no-continue
if (!matchesSchema(humanitecSecret.key, secretSync.syncOptions.keySchema)) continue;
if (!secretMap[humanitecSecret.key]) {
await deleteSecret(secretSync, humanitecSecret);
}

View File

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

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 OCI_VAULT_SYNC_LIST_OPTION: TSecretSyncListItem = {
name: "OCI Vault",
destination: SecretSync.OCIVault,
connection: AppConnection.OCI,
canImportSecrets: true
};

View File

@@ -0,0 +1,296 @@
import { secrets, vault } from "oci-sdk";
import { delay } from "@app/lib/delay";
import { getOCIProvider } from "@app/services/app-connection/oci";
import {
TCreateOCIVaultVariable,
TDeleteOCIVaultVariable,
TOCIVaultListVariables,
TOCIVaultSyncWithCredentials,
TUnmarkOCIVaultVariableFromDeletion,
TUpdateOCIVaultVariable
} from "@app/services/secret-sync/oci-vault/oci-vault-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 listOCIVaultVariables = async ({ provider, compartmentId, vaultId, onlyActive }: TOCIVaultListVariables) => {
const vaultsClient = new vault.VaultsClient({ authenticationDetailsProvider: provider });
const secretsClient = new secrets.SecretsClient({ authenticationDetailsProvider: provider });
const secretsRes = await vaultsClient.listSecrets({
compartmentId,
vaultId,
lifecycleState: onlyActive ? vault.models.SecretSummary.LifecycleState.Active : undefined
});
const result: Record<string, vault.models.SecretSummary & { name: string; value: string }> = {};
for await (const s of secretsRes.items) {
let secretValue = "";
if (s.lifecycleState === vault.models.SecretSummary.LifecycleState.Active) {
const secretBundle = await secretsClient.getSecretBundle({
secretId: s.id
});
secretValue = Buffer.from(secretBundle.secretBundle.secretBundleContent?.content || "", "base64").toString(
"utf-8"
);
}
result[s.secretName] = {
...s,
name: s.secretName,
value: secretValue
};
}
return result;
};
const createOCIVaultVariable = async ({
provider,
compartmentId,
vaultId,
keyId,
name,
value
}: TCreateOCIVaultVariable) => {
if (!value) return;
const vaultsClient = new vault.VaultsClient({ authenticationDetailsProvider: provider });
return vaultsClient.createSecret({
createSecretDetails: {
compartmentId,
vaultId,
keyId,
secretName: name,
enableAutoGeneration: false,
secretContent: {
content: Buffer.from(value).toString("base64"),
contentType: "BASE64"
}
}
});
};
const updateOCIVaultVariable = async ({ provider, secretId, value }: TUpdateOCIVaultVariable) => {
if (!value) return;
const vaultsClient = new vault.VaultsClient({ authenticationDetailsProvider: provider });
return vaultsClient.updateSecret({
secretId,
updateSecretDetails: {
enableAutoGeneration: false,
secretContent: {
content: Buffer.from(value).toString("base64"),
contentType: "BASE64"
}
}
});
};
const deleteOCIVaultVariable = async ({ provider, secretId }: TDeleteOCIVaultVariable) => {
const vaultsClient = new vault.VaultsClient({ authenticationDetailsProvider: provider });
// Schedule a secret deletion 7 days from now. OCI Vault requires a MINIMUM buffer period of 7 days
return vaultsClient.scheduleSecretDeletion({
secretId,
scheduleSecretDeletionDetails: {
timeOfDeletion: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
}
});
};
const unmarkOCIVaultVariableFromDeletion = async ({ provider, secretId }: TUnmarkOCIVaultVariableFromDeletion) => {
const vaultsClient = new vault.VaultsClient({ authenticationDetailsProvider: provider });
return vaultsClient.cancelSecretDeletion({
secretId
});
};
export const OCIVaultSyncFns = {
syncSecrets: async (secretSync: TOCIVaultSyncWithCredentials, secretMap: TSecretMap) => {
const {
connection,
destinationConfig: { compartmentOcid, vaultOcid, keyOcid }
} = secretSync;
const provider = await getOCIProvider(connection);
const variables = await listOCIVaultVariables({ provider, compartmentId: compartmentOcid, vaultId: vaultOcid });
// Throw an error if any keys are updating in OCI vault to prevent skipped updates
if (
Object.entries(variables).some(
([, secret]) =>
secret.lifecycleState === vault.models.SecretSummary.LifecycleState.Updating ||
secret.lifecycleState === vault.models.SecretSummary.LifecycleState.CancellingDeletion ||
secret.lifecycleState === vault.models.SecretSummary.LifecycleState.Creating ||
secret.lifecycleState === vault.models.SecretSummary.LifecycleState.Deleting ||
secret.lifecycleState === vault.models.SecretSummary.LifecycleState.SchedulingDeletion
)
) {
throw new SecretSyncError({
error: "Cannot sync while keys are updating in OCI Vault."
});
}
// Create secrets
for await (const entry of Object.entries(secretMap)) {
const [key, { value }] = entry;
// skip secrets that don't have a value set
if (!value) {
// eslint-disable-next-line no-continue
continue;
}
const existingVariable = Object.values(variables).find((v) => v.secretName === key);
if (!existingVariable) {
try {
await createOCIVaultVariable({
compartmentId: compartmentOcid,
vaultId: vaultOcid,
provider,
keyId: keyOcid,
name: key,
value
});
} catch (error) {
throw new SecretSyncError({
error,
secretKey: key
});
}
} else if (existingVariable.lifecycleState === vault.models.SecretSummary.LifecycleState.PendingDeletion) {
// If a secret exists but is pending deletion, cancel the deletion and update the secret
await unmarkOCIVaultVariableFromDeletion({
provider,
compartmentId: compartmentOcid,
vaultId: vaultOcid,
secretId: existingVariable.id
});
const vaultsClient = new vault.VaultsClient({ authenticationDetailsProvider: provider });
const MAX_RETRIES = 10;
for (let i = 0; i < MAX_RETRIES; i += 1) {
// eslint-disable-next-line no-await-in-loop
await delay(5000);
// eslint-disable-next-line no-await-in-loop
const secret = await vaultsClient.getSecret({
secretId: existingVariable.id
});
if (secret.secret.lifecycleState === vault.models.SecretSummary.LifecycleState.Active) {
// eslint-disable-next-line no-await-in-loop
await updateOCIVaultVariable({
provider,
compartmentId: compartmentOcid,
vaultId: vaultOcid,
secretId: existingVariable.id,
value
});
break;
}
if (i === MAX_RETRIES - 1) {
throw new SecretSyncError({
error: "Failed to update secret after cancelling deletion.",
secretKey: key
});
}
}
}
}
// Update and delete secrets
for await (const [key, variable] of Object.entries(variables)) {
// eslint-disable-next-line no-continue
if (!matchesSchema(key, secretSync.syncOptions.keySchema)) continue;
// Only update / delete active secrets
if (variable.lifecycleState === vault.models.SecretSummary.LifecycleState.Active) {
if (key in secretMap && secretMap[key].value.length > 0) {
if (variable.value !== secretMap[key].value) {
try {
await updateOCIVaultVariable({
compartmentId: compartmentOcid,
vaultId: vaultOcid,
provider,
secretId: variable.id,
value: secretMap[key].value
});
} catch (error) {
throw new SecretSyncError({
error,
secretKey: key
});
}
}
} else if (!secretSync.syncOptions.disableSecretDeletion) {
try {
await deleteOCIVaultVariable({
compartmentId: compartmentOcid,
vaultId: vaultOcid,
provider,
secretId: variable.id
});
} catch (error) {
throw new SecretSyncError({
error,
secretKey: key
});
}
}
}
}
},
removeSecrets: async (secretSync: TOCIVaultSyncWithCredentials, secretMap: TSecretMap) => {
const {
connection,
destinationConfig: { compartmentOcid, vaultOcid }
} = secretSync;
const provider = await getOCIProvider(connection);
const variables = await listOCIVaultVariables({
provider,
compartmentId: compartmentOcid,
vaultId: vaultOcid,
onlyActive: true
});
for await (const [key, variable] of Object.entries(variables)) {
if (key in secretMap) {
try {
await deleteOCIVaultVariable({
compartmentId: compartmentOcid,
vaultId: vaultOcid,
provider,
secretId: variable.id
});
} catch (error) {
throw new SecretSyncError({
error,
secretKey: key
});
}
}
}
},
getSecrets: async (secretSync: TOCIVaultSyncWithCredentials) => {
const {
connection,
destinationConfig: { compartmentOcid, vaultOcid }
} = secretSync;
const provider = await getOCIProvider(connection);
return listOCIVaultVariables({ provider, compartmentId: compartmentOcid, vaultId: vaultOcid, onlyActive: true });
}
};

View File

@@ -0,0 +1,70 @@
import RE2 from "re2";
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 OCIVaultSyncDestinationConfigSchema = z.object({
compartmentOcid: z
.string()
.trim()
.min(1, "Compartment OCID required")
.refine(
(val) => new RE2("^ocid1\\.(tenancy|compartment)\\.oc1\\..+$").test(val),
"Invalid Compartment OCID format. Must start with ocid1.tenancy.oc1. or ocid1.compartment.oc1."
)
.describe(SecretSyncs.DESTINATION_CONFIG.OCI_VAULT.compartmentOcid),
vaultOcid: z
.string()
.trim()
.min(1, "Vault OCID required")
.refine(
(val) => new RE2("^ocid1\\.vault\\.oc1\\..+$").test(val),
"Invalid Vault OCID format. Must start with ocid1.vault.oc1."
)
.describe(SecretSyncs.DESTINATION_CONFIG.OCI_VAULT.vaultOcid),
keyOcid: z
.string()
.trim()
.min(1, "Key OCID required")
.refine(
(val) => new RE2("^ocid1\\.key\\.oc1\\..+$").test(val),
"Invalid Key OCID format. Must start with ocid1.key.oc1."
)
.describe(SecretSyncs.DESTINATION_CONFIG.OCI_VAULT.keyOcid)
});
const OCIVaultSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
export const OCIVaultSyncSchema = BaseSecretSyncSchema(SecretSync.OCIVault, OCIVaultSyncOptionsConfig).extend({
destination: z.literal(SecretSync.OCIVault),
destinationConfig: OCIVaultSyncDestinationConfigSchema
});
export const CreateOCIVaultSyncSchema = GenericCreateSecretSyncFieldsSchema(
SecretSync.OCIVault,
OCIVaultSyncOptionsConfig
).extend({
destinationConfig: OCIVaultSyncDestinationConfigSchema
});
export const UpdateOCIVaultSyncSchema = GenericUpdateSecretSyncFieldsSchema(
SecretSync.OCIVault,
OCIVaultSyncOptionsConfig
).extend({
destinationConfig: OCIVaultSyncDestinationConfigSchema.optional()
});
export const OCIVaultSyncListItemSchema = z.object({
name: z.literal("OCI Vault"),
connection: z.literal(AppConnection.OCI),
destination: z.literal(SecretSync.OCIVault),
canImportSecrets: z.literal(true)
});

View File

@@ -0,0 +1,48 @@
import { SimpleAuthenticationDetailsProvider } from "oci-sdk";
import { z } from "zod";
import { TOCIConnection } from "@app/services/app-connection/oci";
import { CreateOCIVaultSyncSchema, OCIVaultSyncListItemSchema, OCIVaultSyncSchema } from "./oci-vault-sync-schemas";
export type TOCIVaultSync = z.infer<typeof OCIVaultSyncSchema>;
export type TOCIVaultSyncInput = z.infer<typeof CreateOCIVaultSyncSchema>;
export type TOCIVaultSyncListItem = z.infer<typeof OCIVaultSyncListItemSchema>;
export type TOCIVaultSyncWithCredentials = TOCIVaultSync & {
connection: TOCIConnection;
};
export type TOCIVaultVariable = {
id: string;
name: string;
value: string;
};
export type TOCIVaultListVariables = {
provider: SimpleAuthenticationDetailsProvider;
compartmentId: string;
vaultId: string;
onlyActive?: boolean; // Whether to filter for only active secrets. Removes deleted / scheduled for deletion secrets
};
export type TCreateOCIVaultVariable = TOCIVaultListVariables & {
keyId: string;
name: string;
value: string;
};
export type TUpdateOCIVaultVariable = TOCIVaultListVariables & {
secretId: string;
value: string;
};
export type TDeleteOCIVaultVariable = TOCIVaultListVariables & {
secretId: string;
};
export type TUnmarkOCIVaultVariableFromDeletion = TOCIVaultListVariables & {
secretId: string;
};

View File

@@ -12,7 +12,8 @@ export enum SecretSync {
Vercel = "vercel",
Windmill = "windmill",
HCVault = "hashicorp-vault",
TeamCity = "teamcity"
TeamCity = "teamcity",
OCIVault = "oci-vault"
}
export enum SecretSyncInitialSyncBehavior {

View File

@@ -1,4 +1,5 @@
import { AxiosError } from "axios";
import RE2 from "re2";
import {
AWS_PARAMETER_STORE_SYNC_LIST_OPTION,
@@ -28,6 +29,7 @@ import { GcpSyncFns } from "./gcp/gcp-sync-fns";
import { HC_VAULT_SYNC_LIST_OPTION, HCVaultSyncFns } from "./hc-vault";
import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec";
import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns";
import { OCI_VAULT_SYNC_LIST_OPTION, OCIVaultSyncFns } from "./oci-vault";
import { TEAMCITY_SYNC_LIST_OPTION, TeamCitySyncFns } from "./teamcity";
import { TERRAFORM_CLOUD_SYNC_LIST_OPTION, TerraformCloudSyncFns } from "./terraform-cloud";
import { VERCEL_SYNC_LIST_OPTION, VercelSyncFns } from "./vercel";
@@ -47,7 +49,8 @@ const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
[SecretSync.Vercel]: VERCEL_SYNC_LIST_OPTION,
[SecretSync.Windmill]: WINDMILL_SYNC_LIST_OPTION,
[SecretSync.HCVault]: HC_VAULT_SYNC_LIST_OPTION,
[SecretSync.TeamCity]: TEAMCITY_SYNC_LIST_OPTION
[SecretSync.TeamCity]: TEAMCITY_SYNC_LIST_OPTION,
[SecretSync.OCIVault]: OCI_VAULT_SYNC_LIST_OPTION
};
export const listSecretSyncOptions = () => {
@@ -59,45 +62,63 @@ type TSyncSecretDeps = {
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
};
// const addAffixes = (secretSync: TSecretSyncWithCredentials, unprocessedSecretMap: TSecretMap) => {
// let secretMap = { ...unprocessedSecretMap };
//
// const { appendSuffix, prependPrefix } = secretSync.syncOptions;
//
// if (appendSuffix || prependPrefix) {
// secretMap = {};
// Object.entries(unprocessedSecretMap).forEach(([key, value]) => {
// secretMap[`${prependPrefix || ""}${key}${appendSuffix || ""}`] = value;
// });
// }
//
// return secretMap;
// };
//
// const stripAffixes = (secretSync: TSecretSyncWithCredentials, unprocessedSecretMap: TSecretMap) => {
// let secretMap = { ...unprocessedSecretMap };
//
// const { appendSuffix, prependPrefix } = secretSync.syncOptions;
//
// if (appendSuffix || prependPrefix) {
// secretMap = {};
// Object.entries(unprocessedSecretMap).forEach(([key, value]) => {
// let processedKey = key;
//
// if (prependPrefix && processedKey.startsWith(prependPrefix)) {
// processedKey = processedKey.slice(prependPrefix.length);
// }
//
// if (appendSuffix && processedKey.endsWith(appendSuffix)) {
// processedKey = processedKey.slice(0, -appendSuffix.length);
// }
//
// secretMap[processedKey] = value;
// });
// }
//
// return secretMap;
// };
// Add schema to secret keys
const addSchema = (unprocessedSecretMap: TSecretMap, schema?: string): TSecretMap => {
if (!schema) return unprocessedSecretMap;
const processedSecretMap: TSecretMap = {};
for (const [key, value] of Object.entries(unprocessedSecretMap)) {
const newKey = new RE2("{{secretKey}}").replace(schema, key);
processedSecretMap[newKey] = value;
}
return processedSecretMap;
};
// Strip schema from secret keys
const stripSchema = (unprocessedSecretMap: TSecretMap, schema?: string): TSecretMap => {
if (!schema) return unprocessedSecretMap;
const [prefix, suffix] = schema.split("{{secretKey}}");
const strippedMap: TSecretMap = {};
for (const [key, value] of Object.entries(unprocessedSecretMap)) {
if (!key.startsWith(prefix) || !key.endsWith(suffix)) {
// eslint-disable-next-line no-continue
continue;
}
const strippedKey = key.slice(prefix.length, key.length - suffix.length);
strippedMap[strippedKey] = value;
}
return strippedMap;
};
// Checks if a key matches a schema
export const matchesSchema = (key: string, schema?: string): boolean => {
if (!schema) return true;
const [prefix, suffix] = schema.split("{{secretKey}}");
if (prefix === undefined || suffix === undefined) return true;
return key.startsWith(prefix) && key.endsWith(suffix);
};
// Filter only for secrets with keys that match the schema
const filterForSchema = (secretMap: TSecretMap, schema?: string): TSecretMap => {
const filteredMap: TSecretMap = {};
for (const [key, value] of Object.entries(secretMap)) {
if (matchesSchema(key, schema)) {
filteredMap[key] = value;
}
}
return filteredMap;
};
export const SecretSyncFns = {
syncSecrets: (
@@ -105,49 +126,51 @@ export const SecretSyncFns = {
secretMap: TSecretMap,
{ kmsService, appConnectionDAL }: TSyncSecretDeps
): Promise<void> => {
// const affixedSecretMap = addAffixes(secretSync, secretMap);
const schemaSecretMap = addSchema(secretMap, secretSync.syncOptions.keySchema);
switch (secretSync.destination) {
case SecretSync.AWSParameterStore:
return AwsParameterStoreSyncFns.syncSecrets(secretSync, secretMap);
return AwsParameterStoreSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.AWSSecretsManager:
return AwsSecretsManagerSyncFns.syncSecrets(secretSync, secretMap);
return AwsSecretsManagerSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.GitHub:
return GithubSyncFns.syncSecrets(secretSync, secretMap);
return GithubSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.GCPSecretManager:
return GcpSyncFns.syncSecrets(secretSync, secretMap);
return GcpSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.AzureKeyVault:
return azureKeyVaultSyncFactory({
appConnectionDAL,
kmsService
}).syncSecrets(secretSync, secretMap);
}).syncSecrets(secretSync, schemaSecretMap);
case SecretSync.AzureAppConfiguration:
return azureAppConfigurationSyncFactory({
appConnectionDAL,
kmsService
}).syncSecrets(secretSync, secretMap);
}).syncSecrets(secretSync, schemaSecretMap);
case SecretSync.Databricks:
return databricksSyncFactory({
appConnectionDAL,
kmsService
}).syncSecrets(secretSync, secretMap);
}).syncSecrets(secretSync, schemaSecretMap);
case SecretSync.Humanitec:
return HumanitecSyncFns.syncSecrets(secretSync, secretMap);
return HumanitecSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.TerraformCloud:
return TerraformCloudSyncFns.syncSecrets(secretSync, secretMap);
return TerraformCloudSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.Camunda:
return camundaSyncFactory({
appConnectionDAL,
kmsService
}).syncSecrets(secretSync, secretMap);
}).syncSecrets(secretSync, schemaSecretMap);
case SecretSync.Vercel:
return VercelSyncFns.syncSecrets(secretSync, secretMap);
return VercelSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.Windmill:
return WindmillSyncFns.syncSecrets(secretSync, secretMap);
return WindmillSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.HCVault:
return HCVaultSyncFns.syncSecrets(secretSync, secretMap);
return HCVaultSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.TeamCity:
return TeamCitySyncFns.syncSecrets(secretSync, secretMap);
return TeamCitySyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.OCIVault:
return OCIVaultSyncFns.syncSecrets(secretSync, schemaSecretMap);
default:
throw new Error(
`Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
@@ -213,63 +236,67 @@ export const SecretSyncFns = {
case SecretSync.TeamCity:
secretMap = await TeamCitySyncFns.getSecrets(secretSync);
break;
case SecretSync.OCIVault:
secretMap = await OCIVaultSyncFns.getSecrets(secretSync);
break;
default:
throw new Error(
`Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
);
}
return secretMap;
// return stripAffixes(secretSync, secretMap);
return stripSchema(filterForSchema(secretMap), secretSync.syncOptions.keySchema);
},
removeSecrets: (
secretSync: TSecretSyncWithCredentials,
secretMap: TSecretMap,
{ kmsService, appConnectionDAL }: TSyncSecretDeps
): Promise<void> => {
// const affixedSecretMap = addAffixes(secretSync, secretMap);
const schemaSecretMap = addSchema(secretMap, secretSync.syncOptions.keySchema);
switch (secretSync.destination) {
case SecretSync.AWSParameterStore:
return AwsParameterStoreSyncFns.removeSecrets(secretSync, secretMap);
return AwsParameterStoreSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.AWSSecretsManager:
return AwsSecretsManagerSyncFns.removeSecrets(secretSync, secretMap);
return AwsSecretsManagerSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.GitHub:
return GithubSyncFns.removeSecrets(secretSync, secretMap);
return GithubSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.GCPSecretManager:
return GcpSyncFns.removeSecrets(secretSync, secretMap);
return GcpSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.AzureKeyVault:
return azureKeyVaultSyncFactory({
appConnectionDAL,
kmsService
}).removeSecrets(secretSync, secretMap);
}).removeSecrets(secretSync, schemaSecretMap);
case SecretSync.AzureAppConfiguration:
return azureAppConfigurationSyncFactory({
appConnectionDAL,
kmsService
}).removeSecrets(secretSync, secretMap);
}).removeSecrets(secretSync, schemaSecretMap);
case SecretSync.Databricks:
return databricksSyncFactory({
appConnectionDAL,
kmsService
}).removeSecrets(secretSync, secretMap);
}).removeSecrets(secretSync, schemaSecretMap);
case SecretSync.Humanitec:
return HumanitecSyncFns.removeSecrets(secretSync, secretMap);
return HumanitecSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.TerraformCloud:
return TerraformCloudSyncFns.removeSecrets(secretSync, secretMap);
return TerraformCloudSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.Camunda:
return camundaSyncFactory({
appConnectionDAL,
kmsService
}).removeSecrets(secretSync, secretMap);
}).removeSecrets(secretSync, schemaSecretMap);
case SecretSync.Vercel:
return VercelSyncFns.removeSecrets(secretSync, secretMap);
return VercelSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.Windmill:
return WindmillSyncFns.removeSecrets(secretSync, secretMap);
return WindmillSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.HCVault:
return HCVaultSyncFns.removeSecrets(secretSync, secretMap);
return HCVaultSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.TeamCity:
return TeamCitySyncFns.removeSecrets(secretSync, secretMap);
return TeamCitySyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.OCIVault:
return OCIVaultSyncFns.removeSecrets(secretSync, schemaSecretMap);
default:
throw new Error(
`Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`

View File

@@ -15,7 +15,8 @@ export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
[SecretSync.Vercel]: "Vercel",
[SecretSync.Windmill]: "Windmill",
[SecretSync.HCVault]: "Hashicorp Vault",
[SecretSync.TeamCity]: "TeamCity"
[SecretSync.TeamCity]: "TeamCity",
[SecretSync.OCIVault]: "OCI Vault"
};
export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
@@ -32,5 +33,6 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
[SecretSync.Vercel]: AppConnection.Vercel,
[SecretSync.Windmill]: AppConnection.Windmill,
[SecretSync.HCVault]: AppConnection.HCVault,
[SecretSync.TeamCity]: AppConnection.TeamCity
[SecretSync.TeamCity]: AppConnection.TeamCity,
[SecretSync.OCIVault]: AppConnection.OCI
};

View File

@@ -1,3 +1,4 @@
import RE2 from "re2";
import { AnyZodObject, z } from "zod";
import { SecretSyncsSchema } from "@app/db/schemas/secret-syncs";
@@ -24,6 +25,14 @@ const BaseSyncOptionsSchema = <T extends AnyZodObject | undefined = undefined>({
? z.nativeEnum(SecretSyncInitialSyncBehavior)
: z.literal(SecretSyncInitialSyncBehavior.OverwriteDestination)
).describe(SecretSyncs.SYNC_OPTIONS(destination).initialSyncBehavior),
keySchema: z
.string()
.optional()
.refine((val) => !val || new RE2(/^(?:[a-zA-Z0-9_\-/]*)(?:\{\{secretKey\}\})(?:[a-zA-Z0-9_\-/]*)$/).test(val), {
message:
"Key schema must include one {{secretKey}} and only contain letters, numbers, dashes, underscores, slashes, and the {{secretKey}} placeholder."
})
.describe(SecretSyncs.SYNC_OPTIONS(destination).keySchema),
disableSecretDeletion: z.boolean().optional().describe(SecretSyncs.SYNC_OPTIONS(destination).disableSecretDeletion)
});

View File

@@ -67,6 +67,7 @@ import {
THumanitecSyncListItem,
THumanitecSyncWithCredentials
} from "./humanitec";
import { TOCIVaultSync, TOCIVaultSyncInput, TOCIVaultSyncListItem, TOCIVaultSyncWithCredentials } from "./oci-vault";
import {
TTeamCitySync,
TTeamCitySyncInput,
@@ -95,7 +96,8 @@ export type TSecretSync =
| TVercelSync
| TWindmillSync
| THCVaultSync
| TTeamCitySync;
| TTeamCitySync
| TOCIVaultSync;
export type TSecretSyncWithCredentials =
| TAwsParameterStoreSyncWithCredentials
@@ -111,7 +113,8 @@ export type TSecretSyncWithCredentials =
| TVercelSyncWithCredentials
| TWindmillSyncWithCredentials
| THCVaultSyncWithCredentials
| TTeamCitySyncWithCredentials;
| TTeamCitySyncWithCredentials
| TOCIVaultSyncWithCredentials;
export type TSecretSyncInput =
| TAwsParameterStoreSyncInput
@@ -127,7 +130,8 @@ export type TSecretSyncInput =
| TVercelSyncInput
| TWindmillSyncInput
| THCVaultSyncInput
| TTeamCitySyncInput;
| TTeamCitySyncInput
| TOCIVaultSyncInput;
export type TSecretSyncListItem =
| TAwsParameterStoreSyncListItem
@@ -143,7 +147,8 @@ export type TSecretSyncListItem =
| TVercelSyncListItem
| TWindmillSyncListItem
| THCVaultSyncListItem
| TTeamCitySyncListItem;
| TTeamCitySyncListItem
| TOCIVaultSyncListItem;
export type TSyncOptionsConfig = {
canImportSecrets: boolean;

View File

@@ -1,6 +1,7 @@
import { request } from "@app/lib/config/request";
import { getTeamCityInstanceUrl } from "@app/services/app-connection/teamcity";
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";
import {
TDeleteTeamCityVariable,
@@ -125,6 +126,9 @@ export const TeamCitySyncFns = {
const variables = await listTeamCityVariables({ instanceUrl, accessToken, project, buildConfig });
for await (const [key, variable] of Object.entries(variables)) {
// eslint-disable-next-line no-continue
if (!matchesSchema(key, secretSync.syncOptions.keySchema)) continue;
if (!(key in secretMap)) {
try {
await deleteTeamCityVariable({

View File

@@ -4,6 +4,7 @@ import { AxiosResponse } from "axios";
import { request } from "@app/lib/config/request";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
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";
import { SECRET_SYNC_NAME_MAP } from "../secret-sync-maps";
@@ -231,6 +232,9 @@ export const TerraformCloudSyncFns = {
if (secretSync.syncOptions.disableSecretDeletion) return;
for (const terraformCloudVariable of terraformCloudVariables) {
// eslint-disable-next-line no-continue
if (!matchesSchema(terraformCloudVariable.key, secretSync.syncOptions.keySchema)) continue;
if (!Object.prototype.hasOwnProperty.call(secretMap, terraformCloudVariable.key)) {
await deleteVariable(secretSync, terraformCloudVariable);
}

View File

@@ -2,6 +2,7 @@
import { request } from "@app/lib/config/request";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
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";
import { VercelEnvironmentType } from "./vercel-sync-enums";
@@ -290,6 +291,9 @@ export const VercelSyncFns = {
if (secretSync.syncOptions.disableSecretDeletion) return;
for await (const vercelSecret of vercelSecrets) {
// eslint-disable-next-line no-continue
if (!matchesSchema(vercelSecret.key, secretSync.syncOptions.keySchema)) continue;
if (!secretMap[vercelSecret.key]) {
await deleteSecret(secretSync, vercelSecret);
}

View File

@@ -1,6 +1,7 @@
import { request } from "@app/lib/config/request";
import { getWindmillInstanceUrl } from "@app/services/app-connection/windmill";
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
import {
TDeleteWindmillVariable,
TPostWindmillVariable,
@@ -128,7 +129,7 @@ export const WindmillSyncFns = {
const {
connection,
destinationConfig: { path },
syncOptions: { disableSecretDeletion }
syncOptions: { disableSecretDeletion, keySchema }
} = secretSync;
// url needs to be lowercase
@@ -169,6 +170,9 @@ export const WindmillSyncFns = {
if (disableSecretDeletion) return;
for await (const [key, variable] of Object.entries(variables)) {
// eslint-disable-next-line no-continue
if (!matchesSchema(key, keySchema)) continue;
if (!(key in secretMap)) {
try {
await deleteWindmillVariable({

View File

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

View File

@@ -0,0 +1,8 @@
---
title: "Create"
openapi: "POST /api/v1/app-connections/oci"
---
<Note>
Check out the configuration docs for [OCI Connections](/integrations/app-connections/oci) to learn how to obtain the required credentials.
</Note>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,4 @@
---
title: "Attach"
openapi: "POST /api/v1/auth/oci-auth/identities/{identityId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Login"
openapi: "POST /api/v1/auth/oci-auth/login"
---

View File

@@ -0,0 +1,4 @@
---
title: "Retrieve"
openapi: "GET /api/v1/auth/oci-auth/identities/{identityId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Revoke"
openapi: "DELETE /api/v1/auth/oci-auth/identities/{identityId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Update"
openapi: "PATCH /api/v1/auth/oci-auth/identities/{identityId}"
---

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,17 +13,17 @@ To interact with the Infisical API, you will need to obtain an access token. Fol
<AccordionGroup>
<Accordion title="Why can I not create, read, update, or delete an identity?">
There are a few reasons for why this might happen:
- You have insufficient organization permissions to create, read, update, delete identities.
- The identity you are trying to read, update, or delete is more privileged than yourself.
- The role you are trying to create an identity for or update an identity to is more privileged than yours.
</Accordion>
<Accordion title="Why is the Infisical API rejecting my identity credentials?">
There are a few reasons for why this might happen:
- The client secret or access token has expired.
- The identity is insufficently permissioned to interact with the resources you wish to access.
- The identity is insufficiently permissioned to interact with the resources you wish to access.
- You are attempting to access a `/raw` secrets endpoint that requires your project to disable E2EE.
- The client secret/access token is being used from an untrusted IP.
</Accordion>
</AccordionGroup>
</AccordionGroup>

View File

@@ -10,15 +10,15 @@ In this brief, we'll explore how to fetch a secret back from a project on [Infis
<Step title="Create a project with a secret">
To create a project, head to your Organization Overview and press **Add New Project**; we'll call the project **Demo App**.
![create project](../../images/getting-started/api/org-create-project-1.png)
![create project](../../images/getting-started/api/org-create-project-2.png)
Next, let's head to the **Development** environment of the project and add a secret `FOO=BAR` to it.
![explore project env](../../images/getting-started/api/project-explore-env.png)
![create secret](../../images/getting-started/api/project-create-secret.png)
![project dashboard](../../images/getting-started/api/project-dashboard.png)
<Note>
@@ -29,13 +29,13 @@ In this brief, we'll explore how to fetch a secret back from a project on [Infis
Next, we need to create an identity to represent your application. To create one, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**.
![identities organization](../../images/platform/identities/identities-org.png)
When creating an identity, you specify an organization level [role](/documentation/platform/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles.
![identities organization create](../../images/platform/identities/identities-org-create.png)
Once you've created an identity, you'll be prompted to configure the **Universal Auth** authentication method for it.
![identities organization create auth method](../../images/platform/identities/identities-org-create-auth-method.png)
</Step>
@@ -44,7 +44,7 @@ In this brief, we'll explore how to fetch a secret back from a project on [Infis
of the identity and a **Client Secret** for it; you can think of these credentials akin to a username
and password used to authenticate with the Infisical API. With that, press on the key icon on the identity to generate a **Client Secret**
for it.
![identities client secret create](../../images/platform/identities/identities-org-client-secret.png)
![identities client secret create](../../images/platform/identities/identities-org-client-secret-create-1.png)
![identities client secret create](../../images/platform/identities/identities-org-client-secret-create-2.png)
@@ -55,14 +55,14 @@ In this brief, we'll explore how to fetch a secret back from a project on [Infis
Next, select the identity you want to add to the project and the role you want to assign it.
![identities project](../../images/platform/identities/identities-project.png)
![identities project create](../../images/platform/identities/identities-project-create.png)
</Step>
<Step title="Get an access token for the Infisical API">
To access the Infisical API as the identity, you should first perform a login operation
that is to exchange the **Client ID** and **Client Secret** of the identity for an access token
by making a request to the `/api/v1/auth/universal-auth/login` endpoint.
#### Sample request
```
@@ -71,9 +71,9 @@ In this brief, we'll explore how to fetch a secret back from a project on [Infis
--data-urlencode 'clientSecret=<client_secret>' \
--data-urlencode 'clientId=<client_id>'
```
#### Sample response
```
{
"accessToken": "...",
@@ -83,9 +83,9 @@ In this brief, we'll explore how to fetch a secret back from a project on [Infis
```
Next, we can use the access token to authenticate with the [Infisical API](/api-reference/overview/introduction) to read/write secrets
<Note>
Each identity access token has a time-to-live (TLL) which you can infer from the response of the login operation;
Each identity access token has a time-to-live (TTL) which you can infer from the response of the login operation;
the default TTL is `7200` seconds which can be adjusted.
If an identity access token expires, it can no longer authenticate with the Infisical API. In this case,
@@ -96,12 +96,12 @@ In this brief, we'll explore how to fetch a secret back from a project on [Infis
Finally, you can fetch the secret `FOO=BAR` back from **Step 1** by including the access token in the previous step in another request to the `/api/v3/secrets/raw/{secretName}` endpoint.
### Sample request
```
curl --location --request GET 'http://localhost:8080/api/v3/secrets/raw/FOO?workspaceId=657830d579cfc8415d06ce5b&environment=dev' \
--header 'Authorization: Bearer <access_token>'
```
### Sample response
```
@@ -118,11 +118,11 @@ In this brief, we'll explore how to fetch a secret back from a project on [Infis
}
}
```
Note that you can fetch a list of secrets back by making a request to the `/api/v3/secrets/raw` endpoint.
</Step>
</Steps>
See also:
- [API Reference](/api-reference/overview/introduction)
- [API Reference](/api-reference/overview/introduction)

View File

@@ -311,7 +311,7 @@ access the Infisical API using the AWS Auth authentication method.
</Tip>
<Note>
Each identity access token has a time-to-live (TLL) which you can infer from the response of the login operation;
Each identity access token has a time-to-live (TTL) which you can infer from the response of the login operation;
the default TTL is `7200` seconds which can be adjusted.
If an identity access token expires, it can no longer authenticate with the Infisical API. In this case,

View File

@@ -173,7 +173,7 @@ access the Infisical API using the Azure Auth authentication method.
We recommend using one of Infisical's clients like SDKs or the Infisical Agent to authenticate with Infisical using Azure Auth as they handle the authentication process including retrieving the client access token.
</Tip>
<Note>
Each identity access token has a time-to-live (TLL) which you can infer from the response of the login operation;
Each identity access token has a time-to-live (TTL) which you can infer from the response of the login operation;
the default TTL is `7200` seconds which can be adjusted.
If an identity access token expires, it can no longer authenticate with the Infisical API. In this case,
a new access token should be obtained by performing another login operation.

View File

@@ -168,7 +168,7 @@ access the Infisical API using the GCP ID Token authentication method.
We recommend using one of Infisical's clients like SDKs or the Infisical Agent to authenticate with Infisical using GCP IAM Auth as they handle the authentication process including generating the signed JWT token.
</Tip>
<Note>
Each identity access token has a time-to-live (TLL) which you can infer from the response of the login operation;
Each identity access token has a time-to-live (TTL) which you can infer from the response of the login operation;
the default TTL is `7200` seconds which can be adjusted.
If an identity access token expires, it can no longer authenticate with the Infisical API. In this case,
a new access token should be obtained by performing another login operation.
@@ -179,7 +179,7 @@ access the Infisical API using the GCP ID Token authentication method.
</Tab>
<Tab title="GCP IAM Auth">
## Diagram
The following sequence diagram illustrates the GCP IAM Auth workflow for authenticating GCP IAM service accounts with Infisical.
@@ -352,7 +352,7 @@ access the Infisical API using the GCP IAM authentication method.
We recommend using one of Infisical's clients like SDKs or the Infisical Agent to authenticate with Infisical using GCP IAM Auth as they handle the authentication process including generating the signed JWT token.
</Tip>
<Note>
Each identity access token has a time-to-live (TLL) which you can infer from the response of the login operation;
Each identity access token has a time-to-live (TTL) which you can infer from the response of the login operation;
the default TTL is `7200` seconds which can be adjusted.
If an identity access token expires, it can no longer authenticate with the Infisical API. In this case,
a new access token should be obtained by performing another login operation.
@@ -361,5 +361,5 @@ access the Infisical API using the GCP IAM authentication method.
</Steps>
</Tab>
</Tabs>

View File

@@ -56,7 +56,7 @@ In the following steps, we explore how to create and use identities for your app
<Step title="Obtaining the token reviewer JWT for Infisical">
<Tabs>
<Tab title="Option 1: Reviewer JWT Token">
<Note>
**When to use this option**: Choose this approach when you want centralized authentication management. Only one service account needs special permissions, and your application service accounts remain unchanged.
</Note>
@@ -190,7 +190,7 @@ In the following steps, we explore how to create and use identities for your app
Here's some more guidance on each field:
- Kubernetes Host / Base Kubernetes API URL: The host string, host:port pair, or URL to the base of the Kubernetes API server. This can usually be obtained by running `kubectl cluster-info`.
- Token Reviewer JWT: A long-lived service account JWT token for Infisical to access the [TokenReview API](https://kubernetes.io/docs/reference/kubernetes-api/authentication-resources/token-review-v1/) to validate other service account JWT tokens submitted by applications/pods. This is the JWT token obtained from step 1.5(Reviewer Tab). If omitted, the client's own JWT will be used instead, which requires the client to have the `system:auth-delegator` ClusterRole binding.
- Token Reviewer JWT: A long-lived service account JWT token for Infisical to access the [TokenReview API](https://kubernetes.io/docs/reference/kubernetes-api/authentication-resources/token-review-v1/) to validate other service account JWT tokens submitted by applications/pods. This is the JWT token obtained from step 1.5(Reviewer Tab). If omitted, the client's own JWT will be used instead, which requires the client to have the `system:auth-delegator` ClusterRole binding.
This is shown in step 1, option 2.
- Allowed Service Account Names: A comma-separated list of trusted service account names that are allowed to authenticate with Infisical.
- Allowed Namespaces: A comma-separated list of trusted namespaces that service accounts must belong to authenticate with Infisical.
@@ -257,7 +257,7 @@ In the following steps, we explore how to create and use identities for your app
</Tip>
<Note>
Each identity access token has a time-to-live (TLL) which you can infer from the response of the login operation;
Each identity access token has a time-to-live (TTL) which you can infer from the response of the login operation;
the default TTL is `7200` seconds which can be adjusted.
If an identity access token exceeds its max ttl, it can no longer authenticate with the Infisical API. In this case,
@@ -280,7 +280,7 @@ In the following steps, we explore how to create and use identities for your app
There are a few reasons for why this might happen:
- The access token has expired.
- The identity is insufficently permissioned to interact with the resources you wish to access.
- The identity is insufficiently permissioned to interact with the resources you wish to access.
- The client access token is being used from an untrusted IP.
</Accordion>

View File

@@ -0,0 +1,212 @@
---
title: OCI Auth
description: "Learn how to authenticate with Infisical using OCI user accounts."
---
**OCI Auth** is an OCI-native authentication method that verifies Oracle Cloud Infrastructure users through signature validation, allowing secure access to Infisical resources.
## Diagram
The following sequence diagram illustrates the OCI Auth workflow for authenticating OCI users with Infisical.
```mermaid
sequenceDiagram
participant Client
participant Infisical
participant OCI
Note over Client,Client: Step 1: Sign user identity request
Note over Client,Infisical: Step 2: Login Operation
Client->>Infisical: Send signed request details to /api/v1/auth/oci-auth/login
Note over Infisical,OCI: Step 3: Request verification
Infisical->>OCI: Forward signed request
OCI-->>Infisical: Return user details
Note over Infisical: Step 4: Identity property validation
Infisical->>Client: Return short-lived access token
Note over Client,Infisical: Step 5: Access Infisical API with token
Client->>Infisical: Make authenticated requests using the short-lived access token
```
## Concept
At a high level, Infisical authenticates an OCI user by verifying its identity and checking that it meets specific requirements (e.g., its username is authorized, its part of a tenancy) at the `/api/v1/auth/oci-auth/login` endpoint. If successful,
then Infisical returns a short-lived access token that can be used to make authenticated requests to the Infisical API.
To be more specific:
1. The client [signs](https://docs.oracle.com/en-us/iaas/Content/API/Concepts/signingrequests.htm) a `/20160918/users/{userId}` request using an OCI user's [private key](https://docs.oracle.com/en-us/iaas/Content/API/Concepts/apisigningkey.htm#Required_Keys_and_OCIDs); this is done using the [OCI SDK](https://infisical.com/docs/documentation/platform/identities/oci-auth#accessing-the-infisical-api-with-the-identity) or API.
2. The client sends the signed request's headers and their user OCID to Infisical at the `/api/v1/auth/oci-auth/login` endpoint.
3. Infisical reconstructs the request and sends it to OCI via the [Get User](https://docs.oracle.com/en/engineered-systems/private-cloud-appliance/3.0-latest/ceapi/op-20160918-users-user_id-get.html) endpoint for verification and obtains the identity associated with the OCI user.
4. Infisical checks the user's properties against set criteria such as **Allowed Usernames** and **Tenancy OCID**.
5. If all checks pass, Infisical returns a short-lived access token that the client can use to make authenticated requests to the Infisical API.
## Prerequisite
In order to sign requests, you must have an OCI user with credentials such as the private key. If you're unaware of how to create a user and obtain the needed credentials, expand the menu below.
<Accordion title="Creating an OCI user">
<Steps>
<Step title="Search for 'Domains' and click as shown">
![Search Domains](/images/app-connections/oci/search-domains.png)
</Step>
<Step title="Select domain">
Select the domain in which you want to create the Infisical user account.
![Select Domain](/images/app-connections/oci/select-domain.png)
</Step>
<Step title="Navigate to 'Users'">
![Select Users](/images/app-connections/oci/select-users.png)
</Step>
<Step title="Click 'Create user'">
![Click Create User](/images/app-connections/oci/click-create-user.png)
</Step>
<Step title="Create user">
The name, email, and username can be anything.
![Create User](/images/app-connections/oci/create-user.png)
</Step>
<Step title="Navigate to 'API keys'">
After you've created a user, you'll be redirected to the user's page. Navigate to 'API keys'.
![Select API Keys](/images/app-connections/oci/select-api-keys.png)
</Step>
<Step title="Add API key">
Click on 'Add API key' and then download or import the private key. After you've obtained the private key, click 'Add'.
![Add API Key](/images/app-connections/oci/add-api-key.png)
<Note>
At the end of the downloaded private key file, you'll see `OCI_API_KEY`. This is not apart of the private key, and should not be included when you use the private key to sign requests.
</Note>
</Step>
<Step title="Store configuration">
After creating the API key, you'll be shown a modal with relevant information. Save the highlighted values (and the private key) for later steps.
![User Info](/images/app-connections/oci/user-info.png)
</Step>
</Steps>
</Accordion>
## Guide
In the following steps, we explore how to create and use identities for your workloads and applications on OCI to
access the Infisical API using the OCI request signing authentication method.
### Creating an identity
To create an identity, head to your Organization Settings > Access Control > [Identities](https://app.infisical.com/organization/access-management?selectedTab=identities) and press **Create identity**.
![identities organization](/images/platform/identities/identities-org.png)
When creating an identity, you specify an organization-level [role](/documentation/platform/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > [Organization Roles](https://app.infisical.com/organization/access-management?selectedTab=roles).
![identities organization create](/images/platform/identities/identities-org-create.png)
Input some details for your new identity:
- **Name (required):** A friendly name for the identity.
- **Role (required):** A role from the [**Organization Roles**](https://app.infisical.com/organization/access-management?selectedTab=roles) tab for the identity to assume. The organization role assigned will determine what organization-level resources this identity can have access to.
Once you've created an identity, you'll be redirected to a page where you can manage the identity.
![identities page](/images/platform/identities/identities-page.png)
Since the identity has been configured with [Universal Auth](https://infisical.com/docs/documentation/platform/identities/universal-auth) by default, you should reconfigure it to use OCI Auth instead. To do this, click the cog next to **Universal Auth** and then select **Delete** in the options dropdown.
![identities press cog](/images/platform/identities/identities-press-cog.png)
![identities page remove default auth](/images/platform/identities/identities-page-remove-default-auth.png)
Now create a new OCI Auth Method.
![identities create oci auth method](/images/platform/identities/identities-org-create-oci-auth-method.png)
Here's some information about each field:
- **Tenancy OCID:** The OCID of your tenancy. All users authenticating must be part of this Tenancy.
- **Allowed Usernames:** A comma-separated list of trusted OCI users that are allowed to authenticate with Infisical.
- **Access Token TTL (default is `2592000` equivalent to 30 days):** The lifetime for an access token in seconds. This value will be referenced at renewal time.
- **Access Token Max TTL (default is `2592000` equivalent to 30 days):** The maximum lifetime for an access token in seconds. This value will be referenced at renewal time.
- **Access Token Max Number of Uses (default is `0`):** The maximum number of times that an access token can be used; a value of `0` implies an infinite number of uses.
- **Access Token Trusted IPs:** The IPs or CIDR ranges that access tokens can be used from. By default, each token is given the `0.0.0.0/0`, allowing usage from any network address.
### Adding an identity to a project
In order to allow an identity to access project-level resources such as secrets, you must add it to the relevant projects.
To do this, head over to the project you want to add the identity to and navigate to Project Settings > Access Control > Machine Identities and press **Add Identity**.
![identities project](/images/platform/identities/identities-project.png)
Select the identity you want to add to the project and the project-level role you want it to assume. The project role given to the identity will determine what project-level resources this identity can access.
![identities project create](/images/platform/identities/identities-project-create.png)
### Accessing the Infisical API with the identity
To access the Infisical API as the identity, you need to construct a signed [Get User](https://docs.oracle.com/en/engineered-systems/private-cloud-appliance/3.0-latest/ceapi/op-20160918-users-user_id-get.html) request using [OCI Signature v1](https://docs.oracle.com/en-us/iaas/Content/API/Concepts/signingrequests.htm#Request_Signatures) and then make a request to the `/api/v1/auth/oci-auth/login` endpoint passing the signed header data and user OCID.
Below is an example of how you can authenticate with Infisical using the `oci-sdk` for NodeJS.
```typescript
import { common } from "oci-sdk";
// Change these credentials to match your OCI user
const tenancyId = "ocid1.tenancy.oc1..example";
const userId = "ocid1.user.oc1..example";
const fingerprint = "00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00";
const region = "us-ashburn-1";
const privateKey = "..."; // Must be PEM format
const provider = new common.SimpleAuthenticationDetailsProvider(
tenancyId,
userId,
fingerprint,
privateKey,
null,
common.Region.fromRegionId(region),
);
// Build request
const headers = new Headers({
host: `identity.${region}.oraclecloud.com`,
});
const request: common.HttpRequest = {
method: "GET",
uri: `/20160918/users/${userId}`,
headers,
body: null,
};
// Sign request
const signer = new common.DefaultRequestSigner(provider);
await signer.signHttpRequest(request);
// Forward signed request to Infisical
const requestAsJson = {
identityId: "2dd11664-68e3-471d-b366-907206ab1bff",
userOcid: userId,
headers: Object.fromEntries(request.headers.entries()),
};
const res = await fetch("https://app.infisical.com/api/v1/auth/oci-auth/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(requestAsJson),
});
const json = await res.json();
console.log("Infisical Response:", json);
```
<Note>
Each identity access token has a time-to-live (TTL) which you can infer from the response of the login operation; the default TTL is `7200` seconds, which can be adjusted.
If an identity access token expires, it can no longer access the Infisical API. A new access token should be obtained by performing another login operation.
</Note>

View File

@@ -163,7 +163,7 @@ In the following steps, we explore how to create and use identities to access th
}
```
<Note>
Each identity access token has a time-to-live (TLL) which you can infer from the response of the login operation;
Each identity access token has a time-to-live (TTL) which you can infer from the response of the login operation;
the default TTL is `7200` seconds which can be adjusted.
If an identity access token expires, it can no longer authenticate with the Infisical API. In this case,

View File

@@ -159,7 +159,7 @@ In the following steps, we explore how to create and use identities to access th
</Tip>
<Note>
Each identity access token has a time-to-live (TLL) which you can infer from the response of the login operation;
Each identity access token has a time-to-live (TTL) which you can infer from the response of the login operation;
the default TTL is `7200` seconds which can be adjusted.
If an identity access token expires, it can no longer authenticate with the Infisical API. In this case,

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