mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 15:27:46 +00:00
Finish preliminary azure auth method
This commit is contained in:
Vendored
+2
@@ -33,6 +33,7 @@ import { TGroupProjectServiceFactory } from "@app/services/group-project/group-p
|
|||||||
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
|
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
|
||||||
import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
|
import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
|
||||||
import { TIdentityAwsAuthServiceFactory } from "@app/services/identity-aws-auth/identity-aws-auth-service";
|
import { TIdentityAwsAuthServiceFactory } from "@app/services/identity-aws-auth/identity-aws-auth-service";
|
||||||
|
import { TIdentityAzureAuthServiceFactory } from "@app/services/identity-azure-auth/identity-azure-auth-service";
|
||||||
import { TIdentityGcpAuthServiceFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-service";
|
import { TIdentityGcpAuthServiceFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-service";
|
||||||
import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
|
import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
|
||||||
import { TIdentityUaServiceFactory } from "@app/services/identity-ua/identity-ua-service";
|
import { TIdentityUaServiceFactory } from "@app/services/identity-ua/identity-ua-service";
|
||||||
@@ -119,6 +120,7 @@ declare module "fastify" {
|
|||||||
identityUa: TIdentityUaServiceFactory;
|
identityUa: TIdentityUaServiceFactory;
|
||||||
identityGcpAuth: TIdentityGcpAuthServiceFactory;
|
identityGcpAuth: TIdentityGcpAuthServiceFactory;
|
||||||
identityAwsAuth: TIdentityAwsAuthServiceFactory;
|
identityAwsAuth: TIdentityAwsAuthServiceFactory;
|
||||||
|
identityAzureAuth: TIdentityAzureAuthServiceFactory;
|
||||||
accessApprovalPolicy: TAccessApprovalPolicyServiceFactory;
|
accessApprovalPolicy: TAccessApprovalPolicyServiceFactory;
|
||||||
accessApprovalRequest: TAccessApprovalRequestServiceFactory;
|
accessApprovalRequest: TAccessApprovalRequestServiceFactory;
|
||||||
secretApprovalPolicy: TSecretApprovalPolicyServiceFactory;
|
secretApprovalPolicy: TSecretApprovalPolicyServiceFactory;
|
||||||
|
|||||||
Vendored
+8
@@ -62,6 +62,9 @@ import {
|
|||||||
TIdentityAwsAuths,
|
TIdentityAwsAuths,
|
||||||
TIdentityAwsAuthsInsert,
|
TIdentityAwsAuthsInsert,
|
||||||
TIdentityAwsAuthsUpdate,
|
TIdentityAwsAuthsUpdate,
|
||||||
|
TIdentityAzureAuths,
|
||||||
|
TIdentityAzureAuthsInsert,
|
||||||
|
TIdentityAzureAuthsUpdate,
|
||||||
TIdentityGcpAuths,
|
TIdentityGcpAuths,
|
||||||
TIdentityGcpAuthsInsert,
|
TIdentityGcpAuthsInsert,
|
||||||
TIdentityGcpAuthsUpdate,
|
TIdentityGcpAuthsUpdate,
|
||||||
@@ -348,6 +351,11 @@ declare module "knex/types/tables" {
|
|||||||
TIdentityAwsAuthsInsert,
|
TIdentityAwsAuthsInsert,
|
||||||
TIdentityAwsAuthsUpdate
|
TIdentityAwsAuthsUpdate
|
||||||
>;
|
>;
|
||||||
|
[TableName.IdentityAzureAuth]: Knex.CompositeTableType<
|
||||||
|
TIdentityAzureAuths,
|
||||||
|
TIdentityAzureAuthsInsert,
|
||||||
|
TIdentityAzureAuthsUpdate
|
||||||
|
>;
|
||||||
[TableName.IdentityUaClientSecret]: Knex.CompositeTableType<
|
[TableName.IdentityUaClientSecret]: Knex.CompositeTableType<
|
||||||
TIdentityUaClientSecrets,
|
TIdentityUaClientSecrets,
|
||||||
TIdentityUaClientSecretsInsert,
|
TIdentityUaClientSecretsInsert,
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
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.IdentityAzureAuth))) {
|
||||||
|
await knex.schema.createTable(TableName.IdentityAzureAuth, (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("tenantId").notNullable();
|
||||||
|
t.string("resource").notNullable();
|
||||||
|
t.string("allowedServicePrincipalIds").notNullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await createOnUpdateTrigger(knex, TableName.IdentityAzureAuth);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.dropTableIfExists(TableName.IdentityAzureAuth);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.IdentityAzureAuth);
|
||||||
|
}
|
||||||
@@ -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 IdentityAzureAuthsSchema = 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(),
|
||||||
|
tenantId: z.string(),
|
||||||
|
resource: z.string(),
|
||||||
|
allowedServicePrincipalIds: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TIdentityAzureAuths = z.infer<typeof IdentityAzureAuthsSchema>;
|
||||||
|
export type TIdentityAzureAuthsInsert = Omit<z.input<typeof IdentityAzureAuthsSchema>, TImmutableDBKeys>;
|
||||||
|
export type TIdentityAzureAuthsUpdate = Partial<Omit<z.input<typeof IdentityAzureAuthsSchema>, TImmutableDBKeys>>;
|
||||||
@@ -18,6 +18,7 @@ export * from "./groups";
|
|||||||
export * from "./identities";
|
export * from "./identities";
|
||||||
export * from "./identity-access-tokens";
|
export * from "./identity-access-tokens";
|
||||||
export * from "./identity-aws-auths";
|
export * from "./identity-aws-auths";
|
||||||
|
export * from "./identity-azure-auths";
|
||||||
export * from "./identity-gcp-auths";
|
export * from "./identity-gcp-auths";
|
||||||
export * from "./identity-org-memberships";
|
export * from "./identity-org-memberships";
|
||||||
export * from "./identity-project-additional-privilege";
|
export * from "./identity-project-additional-privilege";
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export enum TableName {
|
|||||||
IdentityAccessToken = "identity_access_tokens",
|
IdentityAccessToken = "identity_access_tokens",
|
||||||
IdentityUniversalAuth = "identity_universal_auths",
|
IdentityUniversalAuth = "identity_universal_auths",
|
||||||
IdentityGcpAuth = "identity_gcp_auths",
|
IdentityGcpAuth = "identity_gcp_auths",
|
||||||
|
IdentityAzureAuth = "identity_azure_auths",
|
||||||
IdentityUaClientSecret = "identity_ua_client_secrets",
|
IdentityUaClientSecret = "identity_ua_client_secrets",
|
||||||
IdentityAwsAuth = "identity_aws_auths",
|
IdentityAwsAuth = "identity_aws_auths",
|
||||||
IdentityOrgMembership = "identity_org_memberships",
|
IdentityOrgMembership = "identity_org_memberships",
|
||||||
@@ -147,5 +148,6 @@ export enum ProjectUpgradeStatus {
|
|||||||
export enum IdentityAuthMethod {
|
export enum IdentityAuthMethod {
|
||||||
Univeral = "universal-auth",
|
Univeral = "universal-auth",
|
||||||
GCP_AUTH = "gcp-auth",
|
GCP_AUTH = "gcp-auth",
|
||||||
AWS_AUTH = "aws-auth"
|
AWS_AUTH = "aws-auth",
|
||||||
|
AZURE_AUTH = "azure-auth"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,6 +74,10 @@ export enum EventType {
|
|||||||
ADD_IDENTITY_AWS_AUTH = "add-identity-aws-auth",
|
ADD_IDENTITY_AWS_AUTH = "add-identity-aws-auth",
|
||||||
UPDATE_IDENTITY_AWS_AUTH = "update-identity-aws-auth",
|
UPDATE_IDENTITY_AWS_AUTH = "update-identity-aws-auth",
|
||||||
GET_IDENTITY_AWS_AUTH = "get-identity-aws-auth",
|
GET_IDENTITY_AWS_AUTH = "get-identity-aws-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",
|
||||||
|
GET_IDENTITY_AZURE_AUTH = "get-identity-azure-auth",
|
||||||
CREATE_ENVIRONMENT = "create-environment",
|
CREATE_ENVIRONMENT = "create-environment",
|
||||||
UPDATE_ENVIRONMENT = "update-environment",
|
UPDATE_ENVIRONMENT = "update-environment",
|
||||||
DELETE_ENVIRONMENT = "delete-environment",
|
DELETE_ENVIRONMENT = "delete-environment",
|
||||||
@@ -504,6 +508,48 @@ interface GetIdentityAwsAuthEvent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface LoginIdentityAzureAuthEvent {
|
||||||
|
type: EventType.LOGIN_IDENTITY_AZURE_AUTH;
|
||||||
|
metadata: {
|
||||||
|
identityId: string;
|
||||||
|
identityAzureAuthId: string;
|
||||||
|
identityAccessTokenId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AddIdentityAzureAuthEvent {
|
||||||
|
type: EventType.ADD_IDENTITY_AZURE_AUTH;
|
||||||
|
metadata: {
|
||||||
|
identityId: string;
|
||||||
|
tenantId: string;
|
||||||
|
resource: string;
|
||||||
|
accessTokenTTL: number;
|
||||||
|
accessTokenMaxTTL: number;
|
||||||
|
accessTokenNumUsesLimit: number;
|
||||||
|
accessTokenTrustedIps: Array<TIdentityTrustedIp>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpdateIdentityAzureAuthEvent {
|
||||||
|
type: EventType.UPDATE_IDENTITY_AZURE_AUTH;
|
||||||
|
metadata: {
|
||||||
|
identityId: string;
|
||||||
|
tenantId?: string;
|
||||||
|
resource?: string;
|
||||||
|
accessTokenTTL?: number;
|
||||||
|
accessTokenMaxTTL?: number;
|
||||||
|
accessTokenNumUsesLimit?: number;
|
||||||
|
accessTokenTrustedIps?: Array<TIdentityTrustedIp>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetIdentityAzureAuthEvent {
|
||||||
|
type: EventType.GET_IDENTITY_AZURE_AUTH;
|
||||||
|
metadata: {
|
||||||
|
identityId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface CreateEnvironmentEvent {
|
interface CreateEnvironmentEvent {
|
||||||
type: EventType.CREATE_ENVIRONMENT;
|
type: EventType.CREATE_ENVIRONMENT;
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -766,6 +812,10 @@ export type Event =
|
|||||||
| AddIdentityAwsAuthEvent
|
| AddIdentityAwsAuthEvent
|
||||||
| UpdateIdentityAwsAuthEvent
|
| UpdateIdentityAwsAuthEvent
|
||||||
| GetIdentityAwsAuthEvent
|
| GetIdentityAwsAuthEvent
|
||||||
|
| LoginIdentityAzureAuthEvent
|
||||||
|
| AddIdentityAzureAuthEvent
|
||||||
|
| UpdateIdentityAzureAuthEvent
|
||||||
|
| GetIdentityAzureAuthEvent
|
||||||
| CreateEnvironmentEvent
|
| CreateEnvironmentEvent
|
||||||
| UpdateEnvironmentEvent
|
| UpdateEnvironmentEvent
|
||||||
| DeleteEnvironmentEvent
|
| DeleteEnvironmentEvent
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ import { identityAccessTokenDALFactory } from "@app/services/identity-access-tok
|
|||||||
import { identityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
|
import { identityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
|
||||||
import { identityAwsAuthDALFactory } from "@app/services/identity-aws-auth/identity-aws-auth-dal";
|
import { identityAwsAuthDALFactory } from "@app/services/identity-aws-auth/identity-aws-auth-dal";
|
||||||
import { identityAwsAuthServiceFactory } from "@app/services/identity-aws-auth/identity-aws-auth-service";
|
import { identityAwsAuthServiceFactory } from "@app/services/identity-aws-auth/identity-aws-auth-service";
|
||||||
|
import { identityAzureAuthDALFactory } from "@app/services/identity-azure-auth/identity-azure-auth-dal";
|
||||||
|
import { identityAzureAuthServiceFactory } from "@app/services/identity-azure-auth/identity-azure-auth-service";
|
||||||
import { identityGcpAuthDALFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-dal";
|
import { identityGcpAuthDALFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-dal";
|
||||||
import { identityGcpAuthServiceFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-service";
|
import { identityGcpAuthServiceFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-service";
|
||||||
import { identityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
|
import { identityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
|
||||||
@@ -209,8 +211,8 @@ export const registerRoutes = async (
|
|||||||
const identityUaDAL = identityUaDALFactory(db);
|
const identityUaDAL = identityUaDALFactory(db);
|
||||||
const identityUaClientSecretDAL = identityUaClientSecretDALFactory(db);
|
const identityUaClientSecretDAL = identityUaClientSecretDALFactory(db);
|
||||||
const identityAwsAuthDAL = identityAwsAuthDALFactory(db);
|
const identityAwsAuthDAL = identityAwsAuthDALFactory(db);
|
||||||
|
|
||||||
const identityGcpAuthDAL = identityGcpAuthDALFactory(db);
|
const identityGcpAuthDAL = identityGcpAuthDALFactory(db);
|
||||||
|
const identityAzureAuthDAL = identityAzureAuthDALFactory(db);
|
||||||
|
|
||||||
const auditLogDAL = auditLogDALFactory(db);
|
const auditLogDAL = auditLogDALFactory(db);
|
||||||
const auditLogStreamDAL = auditLogStreamDALFactory(db);
|
const auditLogStreamDAL = auditLogStreamDALFactory(db);
|
||||||
@@ -730,6 +732,15 @@ export const registerRoutes = async (
|
|||||||
permissionService
|
permissionService
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const identityAzureAuthService = identityAzureAuthServiceFactory({
|
||||||
|
identityAzureAuthDAL,
|
||||||
|
identityOrgMembershipDAL,
|
||||||
|
identityAccessTokenDAL,
|
||||||
|
identityDAL,
|
||||||
|
permissionService,
|
||||||
|
licenseService
|
||||||
|
});
|
||||||
|
|
||||||
const dynamicSecretProviders = buildDynamicSecretProviders();
|
const dynamicSecretProviders = buildDynamicSecretProviders();
|
||||||
const dynamicSecretQueueService = dynamicSecretLeaseQueueServiceFactory({
|
const dynamicSecretQueueService = dynamicSecretLeaseQueueServiceFactory({
|
||||||
queueService,
|
queueService,
|
||||||
@@ -800,6 +811,7 @@ export const registerRoutes = async (
|
|||||||
identityUa: identityUaService,
|
identityUa: identityUaService,
|
||||||
identityGcpAuth: identityGcpAuthService,
|
identityGcpAuth: identityGcpAuthService,
|
||||||
identityAwsAuth: identityAwsAuthService,
|
identityAwsAuth: identityAwsAuthService,
|
||||||
|
identityAzureAuth: identityAzureAuthService,
|
||||||
secretApprovalPolicy: sapService,
|
secretApprovalPolicy: sapService,
|
||||||
accessApprovalPolicy: accessApprovalPolicyService,
|
accessApprovalPolicy: accessApprovalPolicyService,
|
||||||
accessApprovalRequest: accessApprovalRequestService,
|
accessApprovalRequest: accessApprovalRequestService,
|
||||||
|
|||||||
@@ -0,0 +1,261 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { IdentityAzureAuthsSchema } from "@app/db/schemas";
|
||||||
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
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";
|
||||||
|
|
||||||
|
export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/azure-auth/login",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "Login with Azure Auth",
|
||||||
|
body: z.object({
|
||||||
|
identityId: z.string(),
|
||||||
|
jwt: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
accessToken: z.string(),
|
||||||
|
expiresIn: z.coerce.number(),
|
||||||
|
accessTokenMaxTTL: z.coerce.number(),
|
||||||
|
tokenType: z.literal("Bearer")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const { identityAzureAuth, accessToken, identityAccessToken, identityMembershipOrg } =
|
||||||
|
await server.services.identityAzureAuth.login(req.body);
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: identityMembershipOrg.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.LOGIN_IDENTITY_AZURE_AUTH,
|
||||||
|
metadata: {
|
||||||
|
identityId: identityAzureAuth.identityId,
|
||||||
|
identityAccessTokenId: identityAccessToken.id,
|
||||||
|
identityAzureAuthId: identityAzureAuth.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken,
|
||||||
|
tokenType: "Bearer" as const,
|
||||||
|
expiresIn: identityAzureAuth.accessTokenTTL,
|
||||||
|
accessTokenMaxTTL: identityAzureAuth.accessTokenMaxTTL
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/azure-auth/identities/:identityId",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Attach Azure Auth configuration onto identity",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
params: z.object({
|
||||||
|
identityId: z.string().trim()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
tenantId: z.string().trim(),
|
||||||
|
resource: z.string().trim(),
|
||||||
|
allowedServicePrincipalIds: z.string().trim(),
|
||||||
|
accessTokenTrustedIps: z
|
||||||
|
.object({
|
||||||
|
ipAddress: z.string().trim()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.min(1)
|
||||||
|
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]),
|
||||||
|
accessTokenTTL: z
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.min(1)
|
||||||
|
.refine((value) => value !== 0, {
|
||||||
|
message: "accessTokenTTL must have a non zero number"
|
||||||
|
})
|
||||||
|
.default(2592000),
|
||||||
|
accessTokenMaxTTL: z
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.refine((value) => value !== 0, {
|
||||||
|
message: "accessTokenMaxTTL must have a non zero number"
|
||||||
|
})
|
||||||
|
.default(2592000),
|
||||||
|
accessTokenNumUsesLimit: z.number().int().min(0).default(0)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
identityAzureAuth: IdentityAzureAuthsSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const identityAzureAuth = await server.services.identityAzureAuth.attachAzureAuth({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
...req.body,
|
||||||
|
identityId: req.params.identityId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: identityAzureAuth.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.ADD_IDENTITY_AZURE_AUTH,
|
||||||
|
metadata: {
|
||||||
|
identityId: identityAzureAuth.identityId,
|
||||||
|
tenantId: identityAzureAuth.tenantId,
|
||||||
|
resource: identityAzureAuth.resource,
|
||||||
|
accessTokenTTL: identityAzureAuth.accessTokenTTL,
|
||||||
|
accessTokenMaxTTL: identityAzureAuth.accessTokenMaxTTL,
|
||||||
|
accessTokenTrustedIps: identityAzureAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
|
||||||
|
accessTokenNumUsesLimit: identityAzureAuth.accessTokenNumUsesLimit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { identityAzureAuth };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "PATCH",
|
||||||
|
url: "/azure-auth/identities/:identityId",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Update Azure Auth configuration on identity",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
params: z.object({
|
||||||
|
identityId: z.string().trim()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
tenantId: z.string().trim().optional(),
|
||||||
|
resource: z.string().trim().optional(),
|
||||||
|
allowedServicePrincipalIds: z.string().trim().optional(),
|
||||||
|
accessTokenTrustedIps: z
|
||||||
|
.object({
|
||||||
|
ipAddress: z.string().trim()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.min(1)
|
||||||
|
.optional(),
|
||||||
|
accessTokenTTL: z.number().int().min(0).optional(),
|
||||||
|
accessTokenNumUsesLimit: z.number().int().min(0).optional(),
|
||||||
|
accessTokenMaxTTL: z
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.refine((value) => value !== 0, {
|
||||||
|
message: "accessTokenMaxTTL must have a non zero number"
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
identityAzureAuth: IdentityAzureAuthsSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const identityAzureAuth = await server.services.identityAzureAuth.updateAzureAuth({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
...req.body,
|
||||||
|
identityId: req.params.identityId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: identityAzureAuth.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.UPDATE_IDENTITY_AZURE_AUTH,
|
||||||
|
metadata: {
|
||||||
|
identityId: identityAzureAuth.identityId,
|
||||||
|
tenantId: identityAzureAuth.tenantId,
|
||||||
|
resource: identityAzureAuth.resource,
|
||||||
|
accessTokenTTL: identityAzureAuth.accessTokenTTL,
|
||||||
|
accessTokenMaxTTL: identityAzureAuth.accessTokenMaxTTL,
|
||||||
|
accessTokenTrustedIps: identityAzureAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
|
||||||
|
accessTokenNumUsesLimit: identityAzureAuth.accessTokenNumUsesLimit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { identityAzureAuth };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/azure-auth/identities/:identityId",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Retrieve Azure Auth configuration on identity",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
params: z.object({
|
||||||
|
identityId: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
identityAzureAuth: IdentityAzureAuthsSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const identityAzureAuth = await server.services.identityAzureAuth.getAzureAuth({
|
||||||
|
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: identityAzureAuth.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_IDENTITY_AZURE_AUTH,
|
||||||
|
metadata: {
|
||||||
|
identityId: identityAzureAuth.identityId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { identityAzureAuth };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -3,6 +3,7 @@ import { registerAuthRoutes } from "./auth-router";
|
|||||||
import { registerProjectBotRouter } from "./bot-router";
|
import { registerProjectBotRouter } from "./bot-router";
|
||||||
import { registerIdentityAccessTokenRouter } from "./identity-access-token-router";
|
import { registerIdentityAccessTokenRouter } from "./identity-access-token-router";
|
||||||
import { registerIdentityAwsAuthRouter } from "./identity-aws-iam-auth-router";
|
import { registerIdentityAwsAuthRouter } from "./identity-aws-iam-auth-router";
|
||||||
|
import { registerIdentityAzureAuthRouter } from "./identity-azure-auth-router";
|
||||||
import { registerIdentityGcpAuthRouter } from "./identity-gcp-auth-router";
|
import { registerIdentityGcpAuthRouter } from "./identity-gcp-auth-router";
|
||||||
import { registerIdentityRouter } from "./identity-router";
|
import { registerIdentityRouter } from "./identity-router";
|
||||||
import { registerIdentityUaRouter } from "./identity-ua";
|
import { registerIdentityUaRouter } from "./identity-ua";
|
||||||
@@ -32,6 +33,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
|||||||
await authRouter.register(registerIdentityGcpAuthRouter);
|
await authRouter.register(registerIdentityGcpAuthRouter);
|
||||||
await authRouter.register(registerIdentityAccessTokenRouter);
|
await authRouter.register(registerIdentityAccessTokenRouter);
|
||||||
await authRouter.register(registerIdentityAwsAuthRouter);
|
await authRouter.register(registerIdentityAwsAuthRouter);
|
||||||
|
await authRouter.register(registerIdentityAzureAuthRouter);
|
||||||
},
|
},
|
||||||
{ prefix: "/auth" }
|
{ prefix: "/auth" }
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TIdentityAzureAuthDALFactory = ReturnType<typeof identityAzureAuthDALFactory>;
|
||||||
|
|
||||||
|
export const identityAzureAuthDALFactory = (db: TDbClient) => {
|
||||||
|
const azureAuthOrm = ormify(db, TableName.IdentityAzureAuth);
|
||||||
|
return azureAuthOrm;
|
||||||
|
};
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
|
||||||
|
import { UnauthorizedError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import { TAzureAuthJwtPayload, TAzureJwksUriResponse, TDecodedAzureAuthJwt } from "./identity-azure-auth-types";
|
||||||
|
|
||||||
|
export const validateAzureIdentity = async ({
|
||||||
|
tenantId,
|
||||||
|
resource,
|
||||||
|
jwt: azureJwt
|
||||||
|
}: {
|
||||||
|
tenantId: string;
|
||||||
|
resource: string;
|
||||||
|
jwt: string;
|
||||||
|
}) => {
|
||||||
|
const jwksUri = `https://login.microsoftonline.com/${tenantId}/discovery/keys`;
|
||||||
|
|
||||||
|
const decodedJwt = jwt.decode(azureJwt, { complete: true }) as TDecodedAzureAuthJwt;
|
||||||
|
const { kid } = decodedJwt.header;
|
||||||
|
|
||||||
|
const { data }: { data: TAzureJwksUriResponse } = await axios.get(jwksUri);
|
||||||
|
const signingKeys = data.keys;
|
||||||
|
|
||||||
|
const signingKey = signingKeys.find((key) => key.kid === kid);
|
||||||
|
if (!signingKey) throw new UnauthorizedError();
|
||||||
|
|
||||||
|
const publicKey = `-----BEGIN CERTIFICATE-----\n${signingKey.x5c[0]}\n-----END CERTIFICATE-----`;
|
||||||
|
|
||||||
|
return jwt.verify(azureJwt, publicKey, {
|
||||||
|
audience: resource,
|
||||||
|
issuer: `https://sts.windows.net/${tenantId}/`
|
||||||
|
}) as TAzureAuthJwtPayload;
|
||||||
|
};
|
||||||
@@ -0,0 +1,286 @@
|
|||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
|
||||||
|
import { IdentityAuthMethod } from "@app/db/schemas";
|
||||||
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||||
|
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||||
|
|
||||||
|
import { AuthTokenType } from "../auth/auth-type";
|
||||||
|
import { TIdentityDALFactory } from "../identity/identity-dal";
|
||||||
|
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 { TIdentityAzureAuthDALFactory } from "./identity-azure-auth-dal";
|
||||||
|
import { validateAzureIdentity } from "./identity-azure-auth-fns";
|
||||||
|
import {
|
||||||
|
TAttachAzureAuthDTO,
|
||||||
|
TGetAzureAuthDTO,
|
||||||
|
TLoginAzureAuthDTO,
|
||||||
|
TUpdateAzureAuthDTO
|
||||||
|
} from "./identity-azure-auth-types";
|
||||||
|
|
||||||
|
type TIdentityAzureAuthServiceFactoryDep = {
|
||||||
|
identityAzureAuthDAL: TIdentityAzureAuthDALFactory; // TODO: Pick
|
||||||
|
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
|
||||||
|
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create">;
|
||||||
|
identityDAL: Pick<TIdentityDALFactory, "updateById">;
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TIdentityAzureAuthServiceFactory = ReturnType<typeof identityAzureAuthServiceFactory>;
|
||||||
|
|
||||||
|
export const identityAzureAuthServiceFactory = ({
|
||||||
|
identityAzureAuthDAL,
|
||||||
|
identityOrgMembershipDAL,
|
||||||
|
identityAccessTokenDAL,
|
||||||
|
identityDAL,
|
||||||
|
permissionService,
|
||||||
|
licenseService
|
||||||
|
}: TIdentityAzureAuthServiceFactoryDep) => {
|
||||||
|
const login = async ({ identityId, jwt: azureJwt }: TLoginAzureAuthDTO) => {
|
||||||
|
const identityAzureAuth = await identityAzureAuthDAL.findOne({ identityId });
|
||||||
|
if (!identityAzureAuth) throw new UnauthorizedError();
|
||||||
|
|
||||||
|
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: identityAzureAuth.identityId });
|
||||||
|
if (!identityMembershipOrg) throw new UnauthorizedError();
|
||||||
|
|
||||||
|
const azureIdentity = await validateAzureIdentity({
|
||||||
|
tenantId: identityAzureAuth.tenantId,
|
||||||
|
resource: identityAzureAuth.resource,
|
||||||
|
jwt: azureJwt
|
||||||
|
});
|
||||||
|
|
||||||
|
if (azureIdentity.tid !== identityAzureAuth.tenantId) throw new UnauthorizedError();
|
||||||
|
|
||||||
|
if (identityAzureAuth.allowedServicePrincipalIds) {
|
||||||
|
// validate if the service principal id is in the list of allowed service principal ids
|
||||||
|
|
||||||
|
const isServicePrincipalAllowed = identityAzureAuth.allowedServicePrincipalIds
|
||||||
|
.split(",")
|
||||||
|
.map((servicePrincipalId) => servicePrincipalId.trim())
|
||||||
|
.some((servicePrincipalId) => servicePrincipalId === azureIdentity.appid);
|
||||||
|
|
||||||
|
if (!isServicePrincipalAllowed) throw new UnauthorizedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
const identityAccessToken = await identityAzureAuthDAL.transaction(async (tx) => {
|
||||||
|
const newToken = await identityAccessTokenDAL.create(
|
||||||
|
{
|
||||||
|
identityId: identityAzureAuth.identityId,
|
||||||
|
isAccessTokenRevoked: false,
|
||||||
|
accessTokenTTL: identityAzureAuth.accessTokenTTL,
|
||||||
|
accessTokenMaxTTL: identityAzureAuth.accessTokenMaxTTL,
|
||||||
|
accessTokenNumUses: 0,
|
||||||
|
accessTokenNumUsesLimit: identityAzureAuth.accessTokenNumUsesLimit
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
return newToken;
|
||||||
|
});
|
||||||
|
|
||||||
|
const appCfg = getConfig();
|
||||||
|
const accessToken = jwt.sign(
|
||||||
|
{
|
||||||
|
identityId: identityAzureAuth.identityId,
|
||||||
|
identityAccessTokenId: identityAccessToken.id,
|
||||||
|
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
||||||
|
} as TIdentityAccessTokenJwtPayload,
|
||||||
|
appCfg.AUTH_SECRET,
|
||||||
|
{
|
||||||
|
expiresIn:
|
||||||
|
Number(identityAccessToken.accessTokenMaxTTL) === 0
|
||||||
|
? undefined
|
||||||
|
: Number(identityAccessToken.accessTokenMaxTTL)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return { accessToken, identityAzureAuth, identityAccessToken, identityMembershipOrg };
|
||||||
|
};
|
||||||
|
|
||||||
|
const attachAzureAuth = async ({
|
||||||
|
identityId,
|
||||||
|
tenantId,
|
||||||
|
resource,
|
||||||
|
allowedServicePrincipalIds,
|
||||||
|
accessTokenTTL,
|
||||||
|
accessTokenMaxTTL,
|
||||||
|
accessTokenNumUsesLimit,
|
||||||
|
accessTokenTrustedIps,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actor,
|
||||||
|
actorOrgId
|
||||||
|
}: TAttachAzureAuthDTO) => {
|
||||||
|
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||||
|
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
|
||||||
|
if (identityMembershipOrg.identity.authMethod)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to add Azure 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(OrgPermissionActions.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 identityAzureAuth = await identityAzureAuthDAL.transaction(async (tx) => {
|
||||||
|
const doc = await identityAzureAuthDAL.create(
|
||||||
|
{
|
||||||
|
identityId: identityMembershipOrg.identityId,
|
||||||
|
tenantId,
|
||||||
|
resource,
|
||||||
|
allowedServicePrincipalIds,
|
||||||
|
accessTokenMaxTTL,
|
||||||
|
accessTokenTTL,
|
||||||
|
accessTokenNumUsesLimit,
|
||||||
|
accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps)
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
await identityDAL.updateById(
|
||||||
|
identityMembershipOrg.identityId,
|
||||||
|
{
|
||||||
|
authMethod: IdentityAuthMethod.AZURE_AUTH
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
return doc;
|
||||||
|
});
|
||||||
|
return { ...identityAzureAuth, orgId: identityMembershipOrg.orgId };
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateAzureAuth = async ({
|
||||||
|
identityId,
|
||||||
|
tenantId,
|
||||||
|
resource,
|
||||||
|
allowedServicePrincipalIds,
|
||||||
|
accessTokenTTL,
|
||||||
|
accessTokenMaxTTL,
|
||||||
|
accessTokenNumUsesLimit,
|
||||||
|
accessTokenTrustedIps,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actor,
|
||||||
|
actorOrgId
|
||||||
|
}: TUpdateAzureAuthDTO) => {
|
||||||
|
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||||
|
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
|
||||||
|
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.AZURE_AUTH)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to update Azure Auth"
|
||||||
|
});
|
||||||
|
|
||||||
|
const identityGcpAuth = await identityAzureAuthDAL.findOne({ identityId });
|
||||||
|
|
||||||
|
if (
|
||||||
|
(accessTokenMaxTTL || identityGcpAuth.accessTokenMaxTTL) > 0 &&
|
||||||
|
(accessTokenTTL || identityGcpAuth.accessTokenMaxTTL) > (accessTokenMaxTTL || identityGcpAuth.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(OrgPermissionActions.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 updatedAzureAuth = await identityAzureAuthDAL.updateById(identityGcpAuth.id, {
|
||||||
|
tenantId,
|
||||||
|
resource,
|
||||||
|
allowedServicePrincipalIds,
|
||||||
|
accessTokenMaxTTL,
|
||||||
|
accessTokenTTL,
|
||||||
|
accessTokenNumUsesLimit,
|
||||||
|
accessTokenTrustedIps: reformattedAccessTokenTrustedIps
|
||||||
|
? JSON.stringify(reformattedAccessTokenTrustedIps)
|
||||||
|
: undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...updatedAzureAuth,
|
||||||
|
orgId: identityMembershipOrg.orgId
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAzureAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetAzureAuthDTO) => {
|
||||||
|
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||||
|
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
|
||||||
|
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.AZURE_AUTH)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "The identity does not have Azure Auth attached"
|
||||||
|
});
|
||||||
|
|
||||||
|
const identityAzureAuth = await identityAzureAuthDAL.findOne({ identityId });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
identityMembershipOrg.orgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
|
||||||
|
|
||||||
|
return { ...identityAzureAuth, orgId: identityMembershipOrg.orgId };
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
login,
|
||||||
|
attachAzureAuth,
|
||||||
|
updateAzureAuth,
|
||||||
|
getAzureAuth
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
|
export type TLoginAzureAuthDTO = {
|
||||||
|
identityId: string;
|
||||||
|
jwt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TAttachAzureAuthDTO = {
|
||||||
|
identityId: string;
|
||||||
|
tenantId: string;
|
||||||
|
resource: string;
|
||||||
|
allowedServicePrincipalIds: string;
|
||||||
|
accessTokenTTL: number;
|
||||||
|
accessTokenMaxTTL: number;
|
||||||
|
accessTokenNumUsesLimit: number;
|
||||||
|
accessTokenTrustedIps: { ipAddress: string }[];
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TUpdateAzureAuthDTO = {
|
||||||
|
identityId: string;
|
||||||
|
tenantId?: string;
|
||||||
|
resource?: string;
|
||||||
|
allowedServicePrincipalIds?: string;
|
||||||
|
accessTokenTTL?: number;
|
||||||
|
accessTokenMaxTTL?: number;
|
||||||
|
accessTokenNumUsesLimit?: number;
|
||||||
|
accessTokenTrustedIps?: { ipAddress: string }[];
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TGetAzureAuthDTO = {
|
||||||
|
identityId: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TAzureJwksUriResponse = {
|
||||||
|
keys: {
|
||||||
|
kty: string;
|
||||||
|
use: string;
|
||||||
|
kid: string;
|
||||||
|
x5t: string;
|
||||||
|
n: string;
|
||||||
|
e: string;
|
||||||
|
x5c: string[];
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type TUserPayload = {
|
||||||
|
aud: string;
|
||||||
|
iss: string;
|
||||||
|
iat: number;
|
||||||
|
nbf: number;
|
||||||
|
exp: number;
|
||||||
|
acr: string;
|
||||||
|
aio: string;
|
||||||
|
amr: string[];
|
||||||
|
appid: string;
|
||||||
|
appidacr: string;
|
||||||
|
family_name: string;
|
||||||
|
given_name: string;
|
||||||
|
groups: string[];
|
||||||
|
idtyp: string;
|
||||||
|
ipaddr: string;
|
||||||
|
name: string;
|
||||||
|
oid: string;
|
||||||
|
puid: string;
|
||||||
|
rh: string;
|
||||||
|
scp: string;
|
||||||
|
sub: string;
|
||||||
|
tid: string;
|
||||||
|
unique_name: string;
|
||||||
|
upn: string;
|
||||||
|
uti: string;
|
||||||
|
ver: string;
|
||||||
|
wids: string[];
|
||||||
|
xms_cae: string;
|
||||||
|
xms_cc: string[];
|
||||||
|
xms_filter_index: string[];
|
||||||
|
xms_rd: string;
|
||||||
|
xms_ssm: string;
|
||||||
|
xms_tcdt: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TAppRegistrationPayload = {
|
||||||
|
aud: string;
|
||||||
|
iss: string;
|
||||||
|
iat: number;
|
||||||
|
nbf: number;
|
||||||
|
exp: number;
|
||||||
|
aio: string;
|
||||||
|
appid: string;
|
||||||
|
appidacr: string;
|
||||||
|
idp: string;
|
||||||
|
idtyp: string;
|
||||||
|
oid: string;
|
||||||
|
rh: string;
|
||||||
|
sub: string;
|
||||||
|
tid: string;
|
||||||
|
uti: string;
|
||||||
|
ver: string;
|
||||||
|
xms_cae: string;
|
||||||
|
xms_cc: string[];
|
||||||
|
xms_rd: string;
|
||||||
|
xms_ssm: string;
|
||||||
|
xms_tcdt: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TAzureAuthJwtPayload = TUserPayload | TAppRegistrationPayload;
|
||||||
|
|
||||||
|
export type TDecodedAzureAuthJwt = {
|
||||||
|
header: {
|
||||||
|
type: string;
|
||||||
|
alg: string;
|
||||||
|
x5t: string;
|
||||||
|
kid: string;
|
||||||
|
};
|
||||||
|
payload: TAzureAuthJwtPayload;
|
||||||
|
signature: string;
|
||||||
|
metadata: {
|
||||||
|
[key: string]: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -3,5 +3,6 @@ import { IdentityAuthMethod } from "./enums";
|
|||||||
export const identityAuthToNameMap: { [I in IdentityAuthMethod]: string } = {
|
export const identityAuthToNameMap: { [I in IdentityAuthMethod]: string } = {
|
||||||
[IdentityAuthMethod.UNIVERSAL_AUTH]: "Universal Auth",
|
[IdentityAuthMethod.UNIVERSAL_AUTH]: "Universal Auth",
|
||||||
[IdentityAuthMethod.GCP_AUTH]: "GCP Auth",
|
[IdentityAuthMethod.GCP_AUTH]: "GCP Auth",
|
||||||
[IdentityAuthMethod.AWS_AUTH]: "AWS Auth"
|
[IdentityAuthMethod.AWS_AUTH]: "AWS Auth",
|
||||||
|
[IdentityAuthMethod.AZURE_AUTH]: "Azure Auth"
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export enum IdentityAuthMethod {
|
export enum IdentityAuthMethod {
|
||||||
UNIVERSAL_AUTH = "universal-auth",
|
UNIVERSAL_AUTH = "universal-auth",
|
||||||
GCP_AUTH = "gcp-auth",
|
GCP_AUTH = "gcp-auth",
|
||||||
AWS_AUTH = "aws-auth"
|
AWS_AUTH = "aws-auth",
|
||||||
|
AZURE_AUTH = "azure-auth"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export { identityAuthToNameMap } from "./constants";
|
|||||||
export { IdentityAuthMethod } from "./enums";
|
export { IdentityAuthMethod } from "./enums";
|
||||||
export {
|
export {
|
||||||
useAddIdentityAwsAuth,
|
useAddIdentityAwsAuth,
|
||||||
|
useAddIdentityAzureAuth,
|
||||||
useAddIdentityGcpAuth,
|
useAddIdentityGcpAuth,
|
||||||
useAddIdentityUniversalAuth,
|
useAddIdentityUniversalAuth,
|
||||||
useCreateIdentity,
|
useCreateIdentity,
|
||||||
@@ -10,10 +11,12 @@ export {
|
|||||||
useRevokeIdentityUniversalAuthClientSecret,
|
useRevokeIdentityUniversalAuthClientSecret,
|
||||||
useUpdateIdentity,
|
useUpdateIdentity,
|
||||||
useUpdateIdentityAwsAuth,
|
useUpdateIdentityAwsAuth,
|
||||||
|
useUpdateIdentityAzureAuth,
|
||||||
useUpdateIdentityGcpAuth,
|
useUpdateIdentityGcpAuth,
|
||||||
useUpdateIdentityUniversalAuth} from "./mutations";
|
useUpdateIdentityUniversalAuth} from "./mutations";
|
||||||
export {
|
export {
|
||||||
useGetIdentityAwsAuth,
|
useGetIdentityAwsAuth,
|
||||||
|
useGetIdentityAzureAuth,
|
||||||
useGetIdentityGcpAuth,
|
useGetIdentityGcpAuth,
|
||||||
useGetIdentityUniversalAuth,
|
useGetIdentityUniversalAuth,
|
||||||
useGetIdentityUniversalAuthClientSecrets
|
useGetIdentityUniversalAuthClientSecrets
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { organizationKeys } from "../organization/queries";
|
|||||||
import { identitiesKeys } from "./queries";
|
import { identitiesKeys } from "./queries";
|
||||||
import {
|
import {
|
||||||
AddIdentityAwsAuthDTO,
|
AddIdentityAwsAuthDTO,
|
||||||
|
AddIdentityAzureAuthDTO,
|
||||||
AddIdentityGcpAuthDTO,
|
AddIdentityGcpAuthDTO,
|
||||||
AddIdentityUniversalAuthDTO,
|
AddIdentityUniversalAuthDTO,
|
||||||
ClientSecretData,
|
ClientSecretData,
|
||||||
@@ -16,13 +17,14 @@ import {
|
|||||||
DeleteIdentityUniversalAuthClientSecretDTO,
|
DeleteIdentityUniversalAuthClientSecretDTO,
|
||||||
Identity,
|
Identity,
|
||||||
IdentityAwsAuth,
|
IdentityAwsAuth,
|
||||||
|
IdentityAzureAuth,
|
||||||
IdentityGcpAuth,
|
IdentityGcpAuth,
|
||||||
IdentityUniversalAuth,
|
IdentityUniversalAuth,
|
||||||
UpdateIdentityAwsAuthDTO,
|
UpdateIdentityAwsAuthDTO,
|
||||||
|
UpdateIdentityAzureAuthDTO,
|
||||||
UpdateIdentityDTO,
|
UpdateIdentityDTO,
|
||||||
UpdateIdentityGcpAuthDTO,
|
UpdateIdentityGcpAuthDTO,
|
||||||
UpdateIdentityUniversalAuthDTO
|
UpdateIdentityUniversalAuthDTO} from "./types";
|
||||||
} from "./types";
|
|
||||||
|
|
||||||
export const useCreateIdentity = () => {
|
export const useCreateIdentity = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@@ -323,3 +325,75 @@ export const useUpdateIdentityAwsAuth = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useAddIdentityAzureAuth = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation<IdentityAzureAuth, {}, AddIdentityAzureAuthDTO>({
|
||||||
|
mutationFn: async ({
|
||||||
|
identityId,
|
||||||
|
tenantId,
|
||||||
|
resource,
|
||||||
|
allowedServicePrincipalIds,
|
||||||
|
accessTokenTTL,
|
||||||
|
accessTokenMaxTTL,
|
||||||
|
accessTokenNumUsesLimit,
|
||||||
|
accessTokenTrustedIps
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
data: { identityAzureAuth }
|
||||||
|
} = await apiRequest.post<{ identityAzureAuth: IdentityAzureAuth }>(
|
||||||
|
`/api/v1/auth/azure-auth/identities/${identityId}`,
|
||||||
|
{
|
||||||
|
tenantId,
|
||||||
|
resource,
|
||||||
|
allowedServicePrincipalIds,
|
||||||
|
accessTokenTTL,
|
||||||
|
accessTokenMaxTTL,
|
||||||
|
accessTokenNumUsesLimit,
|
||||||
|
accessTokenTrustedIps
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return identityAzureAuth;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { organizationId }) => {
|
||||||
|
queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateIdentityAzureAuth = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation<IdentityAzureAuth, {}, UpdateIdentityAzureAuthDTO>({
|
||||||
|
mutationFn: async ({
|
||||||
|
identityId,
|
||||||
|
tenantId,
|
||||||
|
resource,
|
||||||
|
allowedServicePrincipalIds,
|
||||||
|
accessTokenTTL,
|
||||||
|
accessTokenMaxTTL,
|
||||||
|
accessTokenNumUsesLimit,
|
||||||
|
accessTokenTrustedIps
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
data: { identityAzureAuth }
|
||||||
|
} = await apiRequest.patch<{ identityAzureAuth: IdentityAzureAuth }>(
|
||||||
|
`/api/v1/auth/azure-auth/identities/${identityId}`,
|
||||||
|
{
|
||||||
|
tenantId,
|
||||||
|
resource,
|
||||||
|
allowedServicePrincipalIds,
|
||||||
|
accessTokenTTL,
|
||||||
|
accessTokenMaxTTL,
|
||||||
|
accessTokenNumUsesLimit,
|
||||||
|
accessTokenTrustedIps
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return identityAzureAuth;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { organizationId }) => {
|
||||||
|
queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -2,7 +2,12 @@ import { useQuery } from "@tanstack/react-query";
|
|||||||
|
|
||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
import { ClientSecretData, IdentityAwsAuth, IdentityGcpAuth, IdentityUniversalAuth } from "./types";
|
import {
|
||||||
|
ClientSecretData,
|
||||||
|
IdentityAwsAuth,
|
||||||
|
IdentityAzureAuth,
|
||||||
|
IdentityGcpAuth,
|
||||||
|
IdentityUniversalAuth} from "./types";
|
||||||
|
|
||||||
export const identitiesKeys = {
|
export const identitiesKeys = {
|
||||||
getIdentityUniversalAuth: (identityId: string) =>
|
getIdentityUniversalAuth: (identityId: string) =>
|
||||||
@@ -10,7 +15,8 @@ export const identitiesKeys = {
|
|||||||
getIdentityUniversalAuthClientSecrets: (identityId: string) =>
|
getIdentityUniversalAuthClientSecrets: (identityId: string) =>
|
||||||
[{ identityId }, "identity-universal-auth-client-secrets"] as const,
|
[{ identityId }, "identity-universal-auth-client-secrets"] as const,
|
||||||
getIdentityGcpAuth: (identityId: string) => [{ identityId }, "identity-gcp-auth"] as const,
|
getIdentityGcpAuth: (identityId: string) => [{ identityId }, "identity-gcp-auth"] as const,
|
||||||
getIdentityAwsAuth: (identityId: string) => [{ identityId }, "identity-aws-auth"] as const
|
getIdentityAwsAuth: (identityId: string) => [{ identityId }, "identity-aws-auth"] as const,
|
||||||
|
getIdentityAzureAuth: (identityId: string) => [{ identityId }, "identity-azure-auth"] as const
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGetIdentityUniversalAuth = (identityId: string) => {
|
export const useGetIdentityUniversalAuth = (identityId: string) => {
|
||||||
@@ -72,3 +78,18 @@ export const useGetIdentityAwsAuth = (identityId: string) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useGetIdentityAzureAuth = (identityId: string) => {
|
||||||
|
return useQuery({
|
||||||
|
enabled: Boolean(identityId),
|
||||||
|
queryKey: identitiesKeys.getIdentityAzureAuth(identityId),
|
||||||
|
queryFn: async () => {
|
||||||
|
const {
|
||||||
|
data: { identityAzureAuth }
|
||||||
|
} = await apiRequest.get<{ identityAzureAuth: IdentityAzureAuth }>(
|
||||||
|
`/api/v1/auth/azure-auth/identities/${identityId}`
|
||||||
|
);
|
||||||
|
return identityAzureAuth;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -195,6 +195,45 @@ export type UpdateIdentityAwsAuthDTO = {
|
|||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type IdentityAzureAuth = {
|
||||||
|
identityId: string;
|
||||||
|
tenantId: string;
|
||||||
|
resource: string;
|
||||||
|
allowedServicePrincipalIds: string;
|
||||||
|
accessTokenTTL: number;
|
||||||
|
accessTokenMaxTTL: number;
|
||||||
|
accessTokenNumUsesLimit: number;
|
||||||
|
accessTokenTrustedIps: IdentityTrustedIp[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AddIdentityAzureAuthDTO = {
|
||||||
|
organizationId: string;
|
||||||
|
identityId: string;
|
||||||
|
tenantId: string;
|
||||||
|
resource: string;
|
||||||
|
allowedServicePrincipalIds: string;
|
||||||
|
accessTokenTTL: number;
|
||||||
|
accessTokenMaxTTL: number;
|
||||||
|
accessTokenNumUsesLimit: number;
|
||||||
|
accessTokenTrustedIps: {
|
||||||
|
ipAddress: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdateIdentityAzureAuthDTO = {
|
||||||
|
organizationId: string;
|
||||||
|
identityId: string;
|
||||||
|
tenantId?: string;
|
||||||
|
resource?: string;
|
||||||
|
allowedServicePrincipalIds?: string;
|
||||||
|
accessTokenTTL?: number;
|
||||||
|
accessTokenMaxTTL?: number;
|
||||||
|
accessTokenNumUsesLimit?: number;
|
||||||
|
accessTokenTrustedIps?: {
|
||||||
|
ipAddress: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
export type CreateIdentityUniversalAuthClientSecretDTO = {
|
export type CreateIdentityUniversalAuthClientSecretDTO = {
|
||||||
identityId: string;
|
identityId: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|||||||
+12
-1
@@ -15,6 +15,7 @@ import { IdentityAuthMethod } from "@app/hooks/api/identities";
|
|||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
import { IdentityAwsAuthForm } from "./IdentityAwsAuthForm";
|
import { IdentityAwsAuthForm } from "./IdentityAwsAuthForm";
|
||||||
|
import { IdentityAzureAuthForm } from "./IdentityAzureAuthForm";
|
||||||
import { IdentityGcpAuthForm } from "./IdentityGcpAuthForm";
|
import { IdentityGcpAuthForm } from "./IdentityGcpAuthForm";
|
||||||
import { IdentityUniversalAuthForm } from "./IdentityUniversalAuthForm";
|
import { IdentityUniversalAuthForm } from "./IdentityUniversalAuthForm";
|
||||||
|
|
||||||
@@ -30,7 +31,8 @@ type Props = {
|
|||||||
const identityAuthMethods = [
|
const identityAuthMethods = [
|
||||||
{ label: "Universal Auth", value: IdentityAuthMethod.UNIVERSAL_AUTH },
|
{ label: "Universal Auth", value: IdentityAuthMethod.UNIVERSAL_AUTH },
|
||||||
{ label: "GCP Auth", value: IdentityAuthMethod.GCP_AUTH },
|
{ label: "GCP Auth", value: IdentityAuthMethod.GCP_AUTH },
|
||||||
{ label: "AWS Auth", value: IdentityAuthMethod.AWS_AUTH }
|
{ label: "AWS Auth", value: IdentityAuthMethod.AWS_AUTH },
|
||||||
|
{ label: "Azure Auth", value: IdentityAuthMethod.AZURE_AUTH }
|
||||||
];
|
];
|
||||||
|
|
||||||
const schema = yup
|
const schema = yup
|
||||||
@@ -86,6 +88,15 @@ export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpTog
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
case IdentityAuthMethod.AZURE_AUTH: {
|
||||||
|
return (
|
||||||
|
<IdentityAzureAuthForm
|
||||||
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
|
identityAuthMethodData={identityAuthMethodData}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
case IdentityAuthMethod.UNIVERSAL_AUTH: {
|
case IdentityAuthMethod.UNIVERSAL_AUTH: {
|
||||||
return (
|
return (
|
||||||
<IdentityUniversalAuthForm
|
<IdentityUniversalAuthForm
|
||||||
|
|||||||
+350
@@ -0,0 +1,350 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||||
|
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { Button, FormControl, IconButton, Input } from "@app/components/v2";
|
||||||
|
import { useOrganization, useSubscription } from "@app/context";
|
||||||
|
import {
|
||||||
|
useAddIdentityAzureAuth,
|
||||||
|
useGetIdentityAzureAuth,
|
||||||
|
useUpdateIdentityAzureAuth
|
||||||
|
} from "@app/hooks/api";
|
||||||
|
import { IdentityAuthMethod } from "@app/hooks/api/identities";
|
||||||
|
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
||||||
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
const schema = z
|
||||||
|
.object({
|
||||||
|
tenantId: z.string(),
|
||||||
|
resource: z.string(),
|
||||||
|
allowedServicePrincipalIds: z.string(),
|
||||||
|
accessTokenTTL: z.string(),
|
||||||
|
accessTokenMaxTTL: z.string(),
|
||||||
|
accessTokenNumUsesLimit: z.string(),
|
||||||
|
accessTokenTrustedIps: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
ipAddress: z.string().max(50)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.min(1)
|
||||||
|
})
|
||||||
|
.required();
|
||||||
|
|
||||||
|
export type FormData = z.infer<typeof schema>;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
||||||
|
handlePopUpToggle: (
|
||||||
|
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
|
||||||
|
state?: boolean
|
||||||
|
) => void;
|
||||||
|
identityAuthMethodData: {
|
||||||
|
identityId: string;
|
||||||
|
name: string;
|
||||||
|
authMethod?: IdentityAuthMethod;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IdentityAzureAuthForm = ({
|
||||||
|
handlePopUpOpen,
|
||||||
|
handlePopUpToggle,
|
||||||
|
identityAuthMethodData
|
||||||
|
}: Props) => {
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
const orgId = currentOrg?.id || "";
|
||||||
|
const { subscription } = useSubscription();
|
||||||
|
|
||||||
|
const { mutateAsync: addMutateAsync } = useAddIdentityAzureAuth();
|
||||||
|
const { mutateAsync: updateMutateAsync } = useUpdateIdentityAzureAuth();
|
||||||
|
|
||||||
|
const { data } = useGetIdentityAzureAuth(identityAuthMethodData?.identityId ?? "");
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
formState: { isSubmitting }
|
||||||
|
} = useForm<FormData>({
|
||||||
|
resolver: zodResolver(schema),
|
||||||
|
defaultValues: {
|
||||||
|
tenantId: "",
|
||||||
|
resource: "https://management.azure.com/",
|
||||||
|
allowedServicePrincipalIds: "",
|
||||||
|
accessTokenTTL: "2592000",
|
||||||
|
accessTokenMaxTTL: "2592000",
|
||||||
|
accessTokenNumUsesLimit: "0",
|
||||||
|
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
fields: accessTokenTrustedIpsFields,
|
||||||
|
append: appendAccessTokenTrustedIp,
|
||||||
|
remove: removeAccessTokenTrustedIp
|
||||||
|
} = useFieldArray({ control, name: "accessTokenTrustedIps" });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
reset({
|
||||||
|
tenantId: data.tenantId,
|
||||||
|
resource: data.resource,
|
||||||
|
allowedServicePrincipalIds: data.allowedServicePrincipalIds,
|
||||||
|
accessTokenTTL: String(data.accessTokenTTL),
|
||||||
|
accessTokenMaxTTL: String(data.accessTokenMaxTTL),
|
||||||
|
accessTokenNumUsesLimit: String(data.accessTokenNumUsesLimit),
|
||||||
|
accessTokenTrustedIps: data.accessTokenTrustedIps.map(
|
||||||
|
({ ipAddress, prefix }: IdentityTrustedIp) => {
|
||||||
|
return {
|
||||||
|
ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
reset({
|
||||||
|
tenantId: "",
|
||||||
|
resource: "https://management.azure.com/",
|
||||||
|
allowedServicePrincipalIds: "",
|
||||||
|
accessTokenTTL: "2592000",
|
||||||
|
accessTokenMaxTTL: "2592000",
|
||||||
|
accessTokenNumUsesLimit: "0",
|
||||||
|
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const onFormSubmit = async ({
|
||||||
|
tenantId,
|
||||||
|
resource,
|
||||||
|
allowedServicePrincipalIds,
|
||||||
|
accessTokenTTL,
|
||||||
|
accessTokenMaxTTL,
|
||||||
|
accessTokenNumUsesLimit,
|
||||||
|
accessTokenTrustedIps
|
||||||
|
}: FormData) => {
|
||||||
|
try {
|
||||||
|
if (!identityAuthMethodData) return;
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
await updateMutateAsync({
|
||||||
|
organizationId: orgId,
|
||||||
|
identityId: identityAuthMethodData.identityId,
|
||||||
|
tenantId,
|
||||||
|
resource,
|
||||||
|
allowedServicePrincipalIds,
|
||||||
|
accessTokenTTL: Number(accessTokenTTL),
|
||||||
|
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
||||||
|
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
||||||
|
accessTokenTrustedIps
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await addMutateAsync({
|
||||||
|
organizationId: orgId,
|
||||||
|
identityId: identityAuthMethodData.identityId,
|
||||||
|
tenantId: tenantId || "",
|
||||||
|
resource: resource || "",
|
||||||
|
allowedServicePrincipalIds: allowedServicePrincipalIds || "",
|
||||||
|
accessTokenTTL: Number(accessTokenTTL),
|
||||||
|
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
||||||
|
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
||||||
|
accessTokenTrustedIps
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePopUpToggle("identityAuthMethod", false);
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: `Successfully ${
|
||||||
|
identityAuthMethodData?.authMethod ? "updated" : "configured"
|
||||||
|
} auth method`,
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
|
||||||
|
reset();
|
||||||
|
} catch (err) {
|
||||||
|
createNotification({
|
||||||
|
text: `Failed to ${identityAuthMethodData?.authMethod ? "update" : "configure"} identity`,
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue="2592000"
|
||||||
|
name="tenantId"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Tenant ID"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
isRequired
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="" type="text" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="resource"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Resource / Audience"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="allowedServicePrincipalIds"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Allowed Service Principal IDs"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue="2592000"
|
||||||
|
name="accessTokenTTL"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Access Token TTL (seconds)"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue="2592000"
|
||||||
|
name="accessTokenMaxTTL"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Access Token Max TTL (seconds)"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue="0"
|
||||||
|
name="accessTokenNumUsesLimit"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Access Token Max Number of Uses"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||||
|
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
||||||
|
defaultValue="0.0.0.0/0"
|
||||||
|
render={({ field, fieldState: { error } }) => {
|
||||||
|
return (
|
||||||
|
<FormControl
|
||||||
|
className="mb-0 flex-grow"
|
||||||
|
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
value={field.value}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (subscription?.ipAllowlisting) {
|
||||||
|
field.onChange(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePopUpOpen("upgradePlan");
|
||||||
|
}}
|
||||||
|
placeholder="123.456.789.0"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => {
|
||||||
|
if (subscription?.ipAllowlisting) {
|
||||||
|
removeAccessTokenTrustedIp(index);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePopUpOpen("upgradePlan");
|
||||||
|
}}
|
||||||
|
size="lg"
|
||||||
|
colorSchema="danger"
|
||||||
|
variant="plain"
|
||||||
|
ariaLabel="update"
|
||||||
|
className="p-3"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="my-4 ml-1">
|
||||||
|
<Button
|
||||||
|
variant="outline_bg"
|
||||||
|
onClick={() => {
|
||||||
|
if (subscription?.ipAllowlisting) {
|
||||||
|
appendAccessTokenTrustedIp({
|
||||||
|
ipAddress: "0.0.0.0/0"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePopUpOpen("upgradePlan");
|
||||||
|
}}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
size="xs"
|
||||||
|
>
|
||||||
|
Add IP Address
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Button
|
||||||
|
className="mr-4"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
>
|
||||||
|
{identityAuthMethodData?.authMethod ? "Update" : "Configure"}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
variant="plain"
|
||||||
|
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||||
|
>
|
||||||
|
{identityAuthMethodData?.authMethod ? "Cancel" : "Skip"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user