Merge pull request #2606 from Infisical/daniel/multiple-auth-methods

feat: multiple auth methods for identities
This commit is contained in:
Maidul Islam
2024-11-02 12:17:37 -04:00
committed by GitHub
38 changed files with 2556 additions and 2318 deletions
+2 -2
View File
@@ -34,7 +34,7 @@ describe("Identity v1", async () => {
test("Create identity", async () => { test("Create identity", async () => {
const newIdentity = await createIdentity("mac1", OrgMembershipRole.Admin); const newIdentity = await createIdentity("mac1", OrgMembershipRole.Admin);
expect(newIdentity.name).toBe("mac1"); expect(newIdentity.name).toBe("mac1");
expect(newIdentity.authMethod).toBeNull(); expect(newIdentity.authMethods).toEqual([]);
await deleteIdentity(newIdentity.id); await deleteIdentity(newIdentity.id);
}); });
@@ -42,7 +42,7 @@ describe("Identity v1", async () => {
test("Update identity", async () => { test("Update identity", async () => {
const newIdentity = await createIdentity("mac1", OrgMembershipRole.Admin); const newIdentity = await createIdentity("mac1", OrgMembershipRole.Admin);
expect(newIdentity.name).toBe("mac1"); expect(newIdentity.name).toBe("mac1");
expect(newIdentity.authMethod).toBeNull(); expect(newIdentity.authMethods).toEqual([]);
const updatedIdentity = await testServer.inject({ const updatedIdentity = await testServer.inject({
method: "PATCH", method: "PATCH",
+1 -1
View File
@@ -51,7 +51,7 @@
"connect-redis": "^7.1.1", "connect-redis": "^7.1.1",
"cron": "^3.1.7", "cron": "^3.1.7",
"dotenv": "^16.4.1", "dotenv": "^16.4.1",
"fastify": "^4.26.0", "fastify": "^4.28.1",
"fastify-plugin": "^4.5.1", "fastify-plugin": "^4.5.1",
"google-auth-library": "^9.9.0", "google-auth-library": "^9.9.0",
"googleapis": "^137.1.0", "googleapis": "^137.1.0",
+2 -2
View File
@@ -44,7 +44,7 @@
"test:e2e-watch": "vitest -c vitest.e2e.config.ts --bail=1", "test:e2e-watch": "vitest -c vitest.e2e.config.ts --bail=1",
"test:e2e-coverage": "vitest run --coverage -c vitest.e2e.config.ts", "test:e2e-coverage": "vitest run --coverage -c vitest.e2e.config.ts",
"generate:component": "tsx ./scripts/create-backend-file.ts", "generate:component": "tsx ./scripts/create-backend-file.ts",
"generate:schema": "tsx ./scripts/generate-schema-types.ts", "generate:schema": "tsx ./scripts/generate-schema-types.ts && eslint --fix --ext ts ./src/db/schemas",
"auditlog-migration:latest": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:latest", "auditlog-migration:latest": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:latest",
"auditlog-migration:up": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:up", "auditlog-migration:up": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:up",
"auditlog-migration:down": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:down", "auditlog-migration:down": "knex --knexfile ./src/db/auditlog-knexfile.ts --client pg migrate:down",
@@ -156,7 +156,7 @@
"connect-redis": "^7.1.1", "connect-redis": "^7.1.1",
"cron": "^3.1.7", "cron": "^3.1.7",
"dotenv": "^16.4.1", "dotenv": "^16.4.1",
"fastify": "^4.26.0", "fastify": "^4.28.1",
"fastify-plugin": "^4.5.1", "fastify-plugin": "^4.5.1",
"google-auth-library": "^9.9.0", "google-auth-library": "^9.9.0",
"googleapis": "^137.1.0", "googleapis": "^137.1.0",
@@ -0,0 +1,73 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
const BATCH_SIZE = 10_000;
export async function up(knex: Knex): Promise<void> {
const hasAuthMethodColumnAccessToken = await knex.schema.hasColumn(TableName.IdentityAccessToken, "authMethod");
if (!hasAuthMethodColumnAccessToken) {
await knex.schema.alterTable(TableName.IdentityAccessToken, (t) => {
t.string("authMethod").nullable();
});
let nullableAccessTokens = await knex(TableName.IdentityAccessToken).whereNull("authMethod").limit(BATCH_SIZE);
let totalUpdated = 0;
do {
const batchIds = nullableAccessTokens.map((token) => token.id);
// ! Update the auth method column in batches for the current batch
// eslint-disable-next-line no-await-in-loop
await knex(TableName.IdentityAccessToken)
.whereIn("id", batchIds)
.update({
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore because generate schema happens after this
authMethod: knex(TableName.Identity)
.select("authMethod")
.whereRaw(`${TableName.IdentityAccessToken}."identityId" = ${TableName.Identity}.id`)
.whereNotNull("authMethod")
.first()
});
// eslint-disable-next-line no-await-in-loop
nullableAccessTokens = await knex(TableName.IdentityAccessToken).whereNull("authMethod").limit(BATCH_SIZE);
totalUpdated += batchIds.length;
console.log(`Updated ${batchIds.length} access tokens in batch <> Total updated: ${totalUpdated}`);
} while (nullableAccessTokens.length > 0);
// ! We delete all access tokens where the identity has no auth method set!
// ! Which means un-configured identities that for some reason have access tokens, will have their access tokens deleted.
await knex(TableName.IdentityAccessToken)
.whereNotExists((queryBuilder) => {
void queryBuilder
.select("id")
.from(TableName.Identity)
.whereRaw(`${TableName.IdentityAccessToken}."identityId" = ${TableName.Identity}.id`)
.whereNotNull("authMethod");
})
.delete();
// Finally we set the authMethod to notNullable after populating the column.
// This will fail if the data is not populated correctly, so it's safe.
await knex.schema.alterTable(TableName.IdentityAccessToken, (t) => {
t.string("authMethod").notNullable().alter();
});
}
// ! We aren't dropping the authMethod column from the Identity itself, because we wan't to be able to easily rollback for the time being.
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export async function down(knex: Knex): Promise<void> {
const hasAuthMethodColumnAccessToken = await knex.schema.hasColumn(TableName.IdentityAccessToken, "authMethod");
if (hasAuthMethodColumnAccessToken) {
await knex.schema.alterTable(TableName.IdentityAccessToken, (t) => {
t.dropColumn("authMethod");
});
}
}
@@ -20,7 +20,8 @@ export const IdentityAccessTokensSchema = z.object({
identityId: z.string().uuid(), identityId: z.string().uuid(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
name: z.string().nullable().optional() name: z.string().nullable().optional(),
authMethod: z.string()
}); });
export type TIdentityAccessTokens = z.infer<typeof IdentityAccessTokensSchema>; export type TIdentityAccessTokens = z.infer<typeof IdentityAccessTokensSchema>;
+1 -1
View File
@@ -189,7 +189,7 @@ export enum ProjectUpgradeStatus {
export enum IdentityAuthMethod { export enum IdentityAuthMethod {
TOKEN_AUTH = "token-auth", TOKEN_AUTH = "token-auth",
Univeral = "universal-auth", UNIVERSAL_AUTH = "universal-auth",
KUBERNETES_AUTH = "kubernetes-auth", KUBERNETES_AUTH = "kubernetes-auth",
GCP_AUTH = "gcp-auth", GCP_AUTH = "gcp-auth",
AWS_AUTH = "aws-auth", AWS_AUTH = "aws-auth",
+1 -1
View File
@@ -16,7 +16,7 @@ export async function seed(knex: Knex): Promise<void> {
// @ts-ignore // @ts-ignore
id: seedData1.machineIdentity.id, id: seedData1.machineIdentity.id,
name: seedData1.machineIdentity.name, name: seedData1.machineIdentity.name,
authMethod: IdentityAuthMethod.Univeral authMethod: IdentityAuthMethod.UNIVERSAL_AUTH
} }
]); ]);
const identityUa = await knex(TableName.IdentityUniversalAuth) const identityUa = await knex(TableName.IdentityUniversalAuth)
-7
View File
@@ -1087,7 +1087,6 @@ export const registerRoutes = async (
const identityTokenAuthService = identityTokenAuthServiceFactory({ const identityTokenAuthService = identityTokenAuthServiceFactory({
identityTokenAuthDAL, identityTokenAuthDAL,
identityDAL,
identityOrgMembershipDAL, identityOrgMembershipDAL,
identityAccessTokenDAL, identityAccessTokenDAL,
permissionService, permissionService,
@@ -1096,7 +1095,6 @@ export const registerRoutes = async (
const identityUaService = identityUaServiceFactory({ const identityUaService = identityUaServiceFactory({
identityOrgMembershipDAL, identityOrgMembershipDAL,
permissionService, permissionService,
identityDAL,
identityAccessTokenDAL, identityAccessTokenDAL,
identityUaClientSecretDAL, identityUaClientSecretDAL,
identityUaDAL, identityUaDAL,
@@ -1106,7 +1104,6 @@ export const registerRoutes = async (
identityKubernetesAuthDAL, identityKubernetesAuthDAL,
identityOrgMembershipDAL, identityOrgMembershipDAL,
identityAccessTokenDAL, identityAccessTokenDAL,
identityDAL,
orgBotDAL, orgBotDAL,
permissionService, permissionService,
licenseService licenseService
@@ -1115,7 +1112,6 @@ export const registerRoutes = async (
identityGcpAuthDAL, identityGcpAuthDAL,
identityOrgMembershipDAL, identityOrgMembershipDAL,
identityAccessTokenDAL, identityAccessTokenDAL,
identityDAL,
permissionService, permissionService,
licenseService licenseService
}); });
@@ -1124,7 +1120,6 @@ export const registerRoutes = async (
identityAccessTokenDAL, identityAccessTokenDAL,
identityAwsAuthDAL, identityAwsAuthDAL,
identityOrgMembershipDAL, identityOrgMembershipDAL,
identityDAL,
licenseService, licenseService,
permissionService permissionService
}); });
@@ -1133,7 +1128,6 @@ export const registerRoutes = async (
identityAzureAuthDAL, identityAzureAuthDAL,
identityOrgMembershipDAL, identityOrgMembershipDAL,
identityAccessTokenDAL, identityAccessTokenDAL,
identityDAL,
permissionService, permissionService,
licenseService licenseService
}); });
@@ -1142,7 +1136,6 @@ export const registerRoutes = async (
identityOidcAuthDAL, identityOidcAuthDAL,
identityOrgMembershipDAL, identityOrgMembershipDAL,
identityAccessTokenDAL, identityAccessTokenDAL,
identityDAL,
permissionService, permissionService,
licenseService, licenseService,
orgBotDAL orgBotDAL
@@ -37,7 +37,9 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
identity: IdentitiesSchema identity: IdentitiesSchema.extend({
authMethods: z.array(z.string())
})
}) })
} }
}, },
@@ -216,7 +218,9 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
permissions: true, permissions: true,
description: true description: true
}).optional(), }).optional(),
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true }) identity: IdentitiesSchema.pick({ name: true, id: true }).extend({
authMethods: z.array(z.string())
})
}) })
}) })
} }
@@ -261,7 +265,9 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
permissions: true, permissions: true,
description: true description: true
}).optional(), }).optional(),
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true }) identity: IdentitiesSchema.pick({ name: true, id: true }).extend({
authMethods: z.array(z.string())
})
}).array(), }).array(),
totalCount: z.number() totalCount: z.number()
}) })
@@ -319,7 +325,9 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
temporaryAccessEndTime: z.date().nullable().optional() temporaryAccessEndTime: z.date().nullable().optional()
}) })
), ),
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true }), identity: IdentitiesSchema.pick({ name: true, id: true }).extend({
authMethods: z.array(z.string())
}),
project: SanitizedProjectSchema.pick({ name: true, id: true }) project: SanitizedProjectSchema.pick({ name: true, id: true })
}) })
) )
@@ -58,7 +58,9 @@ export const registerIdentityOrgRouter = async (server: FastifyZodProvider) => {
permissions: true, permissions: true,
description: true description: true
}).optional(), }).optional(),
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true }) identity: IdentitiesSchema.pick({ name: true, id: true }).extend({
authMethods: z.array(z.string())
})
}) })
).array(), ).array(),
totalCount: z.number() totalCount: z.number()
@@ -264,7 +264,9 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
temporaryAccessEndTime: z.date().nullable().optional() temporaryAccessEndTime: z.date().nullable().optional()
}) })
), ),
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true }), identity: IdentitiesSchema.pick({ name: true, id: true }).extend({
authMethods: z.array(z.string())
}),
project: SanitizedProjectSchema.pick({ name: true, id: true }) project: SanitizedProjectSchema.pick({ name: true, id: true })
}) })
.array(), .array(),
@@ -285,6 +287,7 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
orderDirection: req.query.orderDirection, orderDirection: req.query.orderDirection,
search: req.query.search search: req.query.search
}); });
return { identityMemberships, totalCount }; return { identityMemberships, totalCount };
} }
}); });
@@ -328,7 +331,9 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
temporaryAccessEndTime: z.date().nullable().optional() temporaryAccessEndTime: z.date().nullable().optional()
}) })
), ),
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true }), identity: IdentitiesSchema.pick({ name: true, id: true }).extend({
authMethods: z.array(z.string())
}),
project: SanitizedProjectSchema.pick({ name: true, id: true }) project: SanitizedProjectSchema.pick({ name: true, id: true })
}) })
}) })
@@ -1,7 +1,7 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { IdentityAuthMethod, TableName, TIdentityAccessTokens } from "@app/db/schemas"; import { TableName, TIdentityAccessTokens } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex"; import { ormify, selectAllTableCols } from "@app/lib/knex";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
@@ -17,54 +17,27 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
const doc = await (tx || db.replicaNode())(TableName.IdentityAccessToken) const doc = await (tx || db.replicaNode())(TableName.IdentityAccessToken)
.where(filter) .where(filter)
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.IdentityAccessToken}.identityId`) .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.IdentityAccessToken}.identityId`)
.leftJoin(TableName.IdentityUaClientSecret, (qb) => { .leftJoin(
qb.on(`${TableName.Identity}.authMethod`, db.raw("?", [IdentityAuthMethod.Univeral])).andOn( TableName.IdentityUaClientSecret,
`${TableName.IdentityAccessToken}.identityUAClientSecretId`, `${TableName.IdentityAccessToken}.identityUAClientSecretId`,
`${TableName.IdentityUaClientSecret}.id` `${TableName.IdentityUaClientSecret}.id`
); )
}) .leftJoin(
.leftJoin(TableName.IdentityUniversalAuth, (qb) => { TableName.IdentityUniversalAuth,
qb.on(`${TableName.Identity}.authMethod`, db.raw("?", [IdentityAuthMethod.Univeral])).andOn( `${TableName.IdentityUaClientSecret}.identityUAId`,
`${TableName.IdentityUaClientSecret}.identityUAId`, `${TableName.IdentityUniversalAuth}.id`
`${TableName.IdentityUniversalAuth}.id` )
); .leftJoin(TableName.IdentityGcpAuth, `${TableName.Identity}.id`, `${TableName.IdentityGcpAuth}.identityId`)
}) .leftJoin(TableName.IdentityAwsAuth, `${TableName.Identity}.id`, `${TableName.IdentityAwsAuth}.identityId`)
.leftJoin(TableName.IdentityGcpAuth, (qb) => { .leftJoin(TableName.IdentityAzureAuth, `${TableName.Identity}.id`, `${TableName.IdentityAzureAuth}.identityId`)
qb.on(`${TableName.Identity}.authMethod`, db.raw("?", [IdentityAuthMethod.GCP_AUTH])).andOn( .leftJoin(
`${TableName.Identity}.id`, TableName.IdentityKubernetesAuth,
`${TableName.IdentityGcpAuth}.identityId` `${TableName.Identity}.id`,
); `${TableName.IdentityKubernetesAuth}.identityId`
}) )
.leftJoin(TableName.IdentityAwsAuth, (qb) => { .leftJoin(TableName.IdentityOidcAuth, `${TableName.Identity}.id`, `${TableName.IdentityOidcAuth}.identityId`)
qb.on(`${TableName.Identity}.authMethod`, db.raw("?", [IdentityAuthMethod.AWS_AUTH])).andOn( .leftJoin(TableName.IdentityTokenAuth, `${TableName.Identity}.id`, `${TableName.IdentityTokenAuth}.identityId`)
`${TableName.Identity}.id`,
`${TableName.IdentityAwsAuth}.identityId`
);
})
.leftJoin(TableName.IdentityAzureAuth, (qb) => {
qb.on(`${TableName.Identity}.authMethod`, db.raw("?", [IdentityAuthMethod.AZURE_AUTH])).andOn(
`${TableName.Identity}.id`,
`${TableName.IdentityAzureAuth}.identityId`
);
})
.leftJoin(TableName.IdentityKubernetesAuth, (qb) => {
qb.on(`${TableName.Identity}.authMethod`, db.raw("?", [IdentityAuthMethod.KUBERNETES_AUTH])).andOn(
`${TableName.Identity}.id`,
`${TableName.IdentityKubernetesAuth}.identityId`
);
})
.leftJoin(TableName.IdentityOidcAuth, (qb) => {
qb.on(`${TableName.Identity}.authMethod`, db.raw("?", [IdentityAuthMethod.OIDC_AUTH])).andOn(
`${TableName.Identity}.id`,
`${TableName.IdentityOidcAuth}.identityId`
);
})
.leftJoin(TableName.IdentityTokenAuth, (qb) => {
qb.on(`${TableName.Identity}.authMethod`, db.raw("?", [IdentityAuthMethod.TOKEN_AUTH])).andOn(
`${TableName.Identity}.id`,
`${TableName.IdentityTokenAuth}.identityId`
);
})
.select(selectAllTableCols(TableName.IdentityAccessToken)) .select(selectAllTableCols(TableName.IdentityAccessToken))
.select( .select(
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityUniversalAuth).as("accessTokenTrustedIpsUa"), db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityUniversalAuth).as("accessTokenTrustedIpsUa"),
@@ -82,14 +55,13 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
return { return {
...doc, ...doc,
accessTokenTrustedIps: trustedIpsUniversalAuth: doc.accessTokenTrustedIpsUa,
doc.accessTokenTrustedIpsUa || trustedIpsGcpAuth: doc.accessTokenTrustedIpsGcp,
doc.accessTokenTrustedIpsGcp || trustedIpsAwsAuth: doc.accessTokenTrustedIpsAws,
doc.accessTokenTrustedIpsAws || trustedIpsAzureAuth: doc.accessTokenTrustedIpsAzure,
doc.accessTokenTrustedIpsAzure || trustedIpsKubernetesAuth: doc.accessTokenTrustedIpsK8s,
doc.accessTokenTrustedIpsK8s || trustedIpsOidcAuth: doc.accessTokenTrustedIpsOidc,
doc.accessTokenTrustedIpsOidc || trustedIpsAccessTokenAuth: doc.accessTokenTrustedIpsToken
doc.accessTokenTrustedIpsToken
}; };
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "IdAccessTokenFindOne" }); throw new DatabaseError({ error, name: "IdAccessTokenFindOne" });
@@ -1,6 +1,6 @@
import jwt, { JwtPayload } from "jsonwebtoken"; import jwt, { JwtPayload } from "jsonwebtoken";
import { TableName, TIdentityAccessTokens } from "@app/db/schemas"; import { IdentityAuthMethod, TableName, TIdentityAccessTokens } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { checkIPAgainstBlocklist, TIp } from "@app/lib/ip"; import { checkIPAgainstBlocklist, TIp } from "@app/lib/ip";
@@ -164,10 +164,22 @@ export const identityAccessTokenServiceFactory = ({
message: "Failed to authorize revoked access token, access token is revoked" message: "Failed to authorize revoked access token, access token is revoked"
}); });
if (ipAddress && identityAccessToken) { const trustedIpsMap: Record<IdentityAuthMethod, unknown> = {
[IdentityAuthMethod.UNIVERSAL_AUTH]: identityAccessToken.trustedIpsUniversalAuth,
[IdentityAuthMethod.GCP_AUTH]: identityAccessToken.trustedIpsGcpAuth,
[IdentityAuthMethod.AWS_AUTH]: identityAccessToken.trustedIpsAwsAuth,
[IdentityAuthMethod.AZURE_AUTH]: identityAccessToken.trustedIpsAzureAuth,
[IdentityAuthMethod.KUBERNETES_AUTH]: identityAccessToken.trustedIpsKubernetesAuth,
[IdentityAuthMethod.OIDC_AUTH]: identityAccessToken.trustedIpsOidcAuth,
[IdentityAuthMethod.TOKEN_AUTH]: identityAccessToken.trustedIpsAccessTokenAuth
};
const trustedIps = trustedIpsMap[identityAccessToken.authMethod as IdentityAuthMethod];
if (ipAddress) {
checkIPAgainstBlocklist({ checkIPAgainstBlocklist({
ipAddress, ipAddress,
trustedIps: identityAccessToken?.accessTokenTrustedIps as TIp[] trustedIps: trustedIps as TIp[]
}); });
} }
@@ -13,7 +13,6 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedErro
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { ActorType, AuthTokenType } from "../auth/auth-type"; import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
@@ -33,7 +32,6 @@ type TIdentityAwsAuthServiceFactoryDep = {
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create">; identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create">;
identityAwsAuthDAL: Pick<TIdentityAwsAuthDALFactory, "findOne" | "transaction" | "create" | "updateById" | "delete">; identityAwsAuthDAL: Pick<TIdentityAwsAuthDALFactory, "findOne" | "transaction" | "create" | "updateById" | "delete">;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">; identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
identityDAL: Pick<TIdentityDALFactory, "updateById">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
}; };
@@ -44,7 +42,6 @@ export const identityAwsAuthServiceFactory = ({
identityAccessTokenDAL, identityAccessTokenDAL,
identityAwsAuthDAL, identityAwsAuthDAL,
identityOrgMembershipDAL, identityOrgMembershipDAL,
identityDAL,
licenseService, licenseService,
permissionService permissionService
}: TIdentityAwsAuthServiceFactoryDep) => { }: TIdentityAwsAuthServiceFactoryDep) => {
@@ -113,7 +110,8 @@ export const identityAwsAuthServiceFactory = ({
accessTokenTTL: identityAwsAuth.accessTokenTTL, accessTokenTTL: identityAwsAuth.accessTokenTTL,
accessTokenMaxTTL: identityAwsAuth.accessTokenMaxTTL, accessTokenMaxTTL: identityAwsAuth.accessTokenMaxTTL,
accessTokenNumUses: 0, accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityAwsAuth.accessTokenNumUsesLimit accessTokenNumUsesLimit: identityAwsAuth.accessTokenNumUsesLimit,
authMethod: IdentityAuthMethod.AWS_AUTH
}, },
tx tx
); );
@@ -155,10 +153,12 @@ export const identityAwsAuthServiceFactory = ({
}: TAttachAwsAuthDTO) => { }: TAttachAwsAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity.authMethod)
if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AWS_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "Failed to add AWS Auth to already configured identity" message: "Failed to add AWS Auth to already configured identity"
}); });
}
if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) { if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) {
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
@@ -206,13 +206,6 @@ export const identityAwsAuthServiceFactory = ({
}, },
tx tx
); );
await identityDAL.updateById(
identityMembershipOrg.identityId,
{
authMethod: IdentityAuthMethod.AWS_AUTH
},
tx
);
return doc; return doc;
}); });
return { ...identityAwsAuth, orgId: identityMembershipOrg.orgId }; return { ...identityAwsAuth, orgId: identityMembershipOrg.orgId };
@@ -234,10 +227,12 @@ export const identityAwsAuthServiceFactory = ({
}: TUpdateAwsAuthDTO) => { }: TUpdateAwsAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.AWS_AUTH)
throw new BadRequestError({ if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AWS_AUTH)) {
message: "Failed to update AWS Auth" throw new NotFoundError({
message: "The identity does not have AWS Auth attached"
}); });
}
const identityAwsAuth = await identityAwsAuthDAL.findOne({ identityId }); const identityAwsAuth = await identityAwsAuthDAL.findOne({ identityId });
@@ -293,10 +288,12 @@ export const identityAwsAuthServiceFactory = ({
const getAwsAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetAwsAuthDTO) => { const getAwsAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetAwsAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.AWS_AUTH)
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AWS_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "The identity does not have AWS Auth attached" message: "The identity does not have AWS Auth attached"
}); });
}
const awsIdentityAuth = await identityAwsAuthDAL.findOne({ identityId }); const awsIdentityAuth = await identityAwsAuthDAL.findOne({ identityId });
@@ -320,10 +317,11 @@ export const identityAwsAuthServiceFactory = ({
}: TRevokeAwsAuthDTO) => { }: TRevokeAwsAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.AWS_AUTH) if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AWS_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "The identity does not have aws auth" message: "The identity does not have aws auth"
}); });
}
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
@@ -348,7 +346,6 @@ export const identityAwsAuthServiceFactory = ({
const revokedIdentityAwsAuth = await identityAwsAuthDAL.transaction(async (tx) => { const revokedIdentityAwsAuth = await identityAwsAuthDAL.transaction(async (tx) => {
const deletedAwsAuth = await identityAwsAuthDAL.delete({ identityId }, tx); const deletedAwsAuth = await identityAwsAuthDAL.delete({ identityId }, tx);
await identityDAL.updateById(identityId, { authMethod: null }, tx);
return { ...deletedAwsAuth?.[0], orgId: identityMembershipOrg.orgId }; return { ...deletedAwsAuth?.[0], orgId: identityMembershipOrg.orgId };
}); });
return revokedIdentityAwsAuth; return revokedIdentityAwsAuth;
@@ -11,7 +11,6 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedErro
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { ActorType, AuthTokenType } from "../auth/auth-type"; import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
@@ -32,7 +31,6 @@ type TIdentityAzureAuthServiceFactoryDep = {
>; >;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">; identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create">; identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create">;
identityDAL: Pick<TIdentityDALFactory, "updateById">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
}; };
@@ -43,7 +41,6 @@ export const identityAzureAuthServiceFactory = ({
identityAzureAuthDAL, identityAzureAuthDAL,
identityOrgMembershipDAL, identityOrgMembershipDAL,
identityAccessTokenDAL, identityAccessTokenDAL,
identityDAL,
permissionService, permissionService,
licenseService licenseService
}: TIdentityAzureAuthServiceFactoryDep) => { }: TIdentityAzureAuthServiceFactoryDep) => {
@@ -84,7 +81,8 @@ export const identityAzureAuthServiceFactory = ({
accessTokenTTL: identityAzureAuth.accessTokenTTL, accessTokenTTL: identityAzureAuth.accessTokenTTL,
accessTokenMaxTTL: identityAzureAuth.accessTokenMaxTTL, accessTokenMaxTTL: identityAzureAuth.accessTokenMaxTTL,
accessTokenNumUses: 0, accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityAzureAuth.accessTokenNumUsesLimit accessTokenNumUsesLimit: identityAzureAuth.accessTokenNumUsesLimit,
authMethod: IdentityAuthMethod.AZURE_AUTH
}, },
tx tx
); );
@@ -126,11 +124,12 @@ export const identityAzureAuthServiceFactory = ({
}: TAttachAzureAuthDTO) => { }: TAttachAzureAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity.authMethod)
if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AZURE_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "Failed to add Azure Auth to already configured identity" message: "Failed to add Azure Auth to already configured identity"
}); });
}
if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) { if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) {
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
} }
@@ -176,13 +175,7 @@ export const identityAzureAuthServiceFactory = ({
}, },
tx tx
); );
await identityDAL.updateById(
identityMembershipOrg.identityId,
{
authMethod: IdentityAuthMethod.AZURE_AUTH
},
tx
);
return doc; return doc;
}); });
return { ...identityAzureAuth, orgId: identityMembershipOrg.orgId }; return { ...identityAzureAuth, orgId: identityMembershipOrg.orgId };
@@ -204,10 +197,11 @@ export const identityAzureAuthServiceFactory = ({
}: TUpdateAzureAuthDTO) => { }: TUpdateAzureAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.AZURE_AUTH) if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AZURE_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "Failed to update Azure Auth" message: "Failed to update Azure Auth"
}); });
}
const identityGcpAuth = await identityAzureAuthDAL.findOne({ identityId }); const identityGcpAuth = await identityAzureAuthDAL.findOne({ identityId });
@@ -266,10 +260,11 @@ export const identityAzureAuthServiceFactory = ({
const getAzureAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetAzureAuthDTO) => { const getAzureAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetAzureAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.AZURE_AUTH) if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AZURE_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "The identity does not have Azure Auth attached" message: "The identity does not have Azure Auth attached"
}); });
}
const identityAzureAuth = await identityAzureAuthDAL.findOne({ identityId }); const identityAzureAuth = await identityAzureAuthDAL.findOne({ identityId });
@@ -294,10 +289,11 @@ export const identityAzureAuthServiceFactory = ({
}: TRevokeAzureAuthDTO) => { }: TRevokeAzureAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.AZURE_AUTH) if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.AZURE_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "The identity does not have azure auth" message: "The identity does not have azure auth"
}); });
}
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
@@ -321,7 +317,6 @@ export const identityAzureAuthServiceFactory = ({
const revokedIdentityAzureAuth = await identityAzureAuthDAL.transaction(async (tx) => { const revokedIdentityAzureAuth = await identityAzureAuthDAL.transaction(async (tx) => {
const deletedAzureAuth = await identityAzureAuthDAL.delete({ identityId }, tx); const deletedAzureAuth = await identityAzureAuthDAL.delete({ identityId }, tx);
await identityDAL.updateById(identityId, { authMethod: null }, tx);
return { ...deletedAzureAuth?.[0], orgId: identityMembershipOrg.orgId }; return { ...deletedAzureAuth?.[0], orgId: identityMembershipOrg.orgId };
}); });
return revokedIdentityAzureAuth; return revokedIdentityAzureAuth;
@@ -11,7 +11,6 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedErro
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { ActorType, AuthTokenType } from "../auth/auth-type"; import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
@@ -30,7 +29,6 @@ type TIdentityGcpAuthServiceFactoryDep = {
identityGcpAuthDAL: Pick<TIdentityGcpAuthDALFactory, "findOne" | "transaction" | "create" | "updateById" | "delete">; identityGcpAuthDAL: Pick<TIdentityGcpAuthDALFactory, "findOne" | "transaction" | "create" | "updateById" | "delete">;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">; identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create">; identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create">;
identityDAL: Pick<TIdentityDALFactory, "updateById">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
}; };
@@ -41,7 +39,6 @@ export const identityGcpAuthServiceFactory = ({
identityGcpAuthDAL, identityGcpAuthDAL,
identityOrgMembershipDAL, identityOrgMembershipDAL,
identityAccessTokenDAL, identityAccessTokenDAL,
identityDAL,
permissionService, permissionService,
licenseService licenseService
}: TIdentityGcpAuthServiceFactoryDep) => { }: TIdentityGcpAuthServiceFactoryDep) => {
@@ -125,7 +122,8 @@ export const identityGcpAuthServiceFactory = ({
accessTokenTTL: identityGcpAuth.accessTokenTTL, accessTokenTTL: identityGcpAuth.accessTokenTTL,
accessTokenMaxTTL: identityGcpAuth.accessTokenMaxTTL, accessTokenMaxTTL: identityGcpAuth.accessTokenMaxTTL,
accessTokenNumUses: 0, accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityGcpAuth.accessTokenNumUsesLimit accessTokenNumUsesLimit: identityGcpAuth.accessTokenNumUsesLimit,
authMethod: IdentityAuthMethod.GCP_AUTH
}, },
tx tx
); );
@@ -168,10 +166,12 @@ export const identityGcpAuthServiceFactory = ({
}: TAttachGcpAuthDTO) => { }: TAttachGcpAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity.authMethod)
if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.GCP_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "Failed to add GCP Auth to already configured identity" message: "Failed to add GCP Auth to already configured identity"
}); });
}
if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) { if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) {
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
@@ -219,13 +219,6 @@ export const identityGcpAuthServiceFactory = ({
}, },
tx tx
); );
await identityDAL.updateById(
identityMembershipOrg.identityId,
{
authMethod: IdentityAuthMethod.GCP_AUTH
},
tx
);
return doc; return doc;
}); });
return { ...identityGcpAuth, orgId: identityMembershipOrg.orgId }; return { ...identityGcpAuth, orgId: identityMembershipOrg.orgId };
@@ -248,10 +241,12 @@ export const identityGcpAuthServiceFactory = ({
}: TUpdateGcpAuthDTO) => { }: TUpdateGcpAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.GCP_AUTH)
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.GCP_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "Failed to update GCP Auth" message: "Failed to update GCP Auth"
}); });
}
const identityGcpAuth = await identityGcpAuthDAL.findOne({ identityId }); const identityGcpAuth = await identityGcpAuthDAL.findOne({ identityId });
@@ -311,10 +306,12 @@ export const identityGcpAuthServiceFactory = ({
const getGcpAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetGcpAuthDTO) => { const getGcpAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetGcpAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.GCP_AUTH)
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.GCP_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "The identity does not have GCP Auth attached" message: "The identity does not have GCP Auth attached"
}); });
}
const identityGcpAuth = await identityGcpAuthDAL.findOne({ identityId }); const identityGcpAuth = await identityGcpAuthDAL.findOne({ identityId });
@@ -339,10 +336,12 @@ export const identityGcpAuthServiceFactory = ({
}: TRevokeGcpAuthDTO) => { }: TRevokeGcpAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.GCP_AUTH)
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.GCP_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "The identity does not have gcp auth" message: "The identity does not have gcp auth"
}); });
}
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
@@ -366,7 +365,6 @@ export const identityGcpAuthServiceFactory = ({
const revokedIdentityGcpAuth = await identityGcpAuthDAL.transaction(async (tx) => { const revokedIdentityGcpAuth = await identityGcpAuthDAL.transaction(async (tx) => {
const deletedGcpAuth = await identityGcpAuthDAL.delete({ identityId }, tx); const deletedGcpAuth = await identityGcpAuthDAL.delete({ identityId }, tx);
await identityDAL.updateById(identityId, { authMethod: null }, tx);
return { ...deletedGcpAuth?.[0], orgId: identityMembershipOrg.orgId }; return { ...deletedGcpAuth?.[0], orgId: identityMembershipOrg.orgId };
}); });
return revokedIdentityGcpAuth; return revokedIdentityGcpAuth;
@@ -22,7 +22,6 @@ import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal"; import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
import { ActorType, AuthTokenType } from "../auth/auth-type"; import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
@@ -44,7 +43,6 @@ type TIdentityKubernetesAuthServiceFactoryDep = {
>; >;
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create">; identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create">;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne" | "findById">; identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne" | "findById">;
identityDAL: Pick<TIdentityDALFactory, "updateById">;
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "transaction" | "create">; orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "transaction" | "create">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
@@ -56,7 +54,6 @@ export const identityKubernetesAuthServiceFactory = ({
identityKubernetesAuthDAL, identityKubernetesAuthDAL,
identityOrgMembershipDAL, identityOrgMembershipDAL,
identityAccessTokenDAL, identityAccessTokenDAL,
identityDAL,
orgBotDAL, orgBotDAL,
permissionService, permissionService,
licenseService licenseService
@@ -215,7 +212,8 @@ export const identityKubernetesAuthServiceFactory = ({
accessTokenTTL: identityKubernetesAuth.accessTokenTTL, accessTokenTTL: identityKubernetesAuth.accessTokenTTL,
accessTokenMaxTTL: identityKubernetesAuth.accessTokenMaxTTL, accessTokenMaxTTL: identityKubernetesAuth.accessTokenMaxTTL,
accessTokenNumUses: 0, accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityKubernetesAuth.accessTokenNumUsesLimit accessTokenNumUsesLimit: identityKubernetesAuth.accessTokenNumUsesLimit,
authMethod: IdentityAuthMethod.KUBERNETES_AUTH
}, },
tx tx
); );
@@ -260,10 +258,12 @@ export const identityKubernetesAuthServiceFactory = ({
}: TAttachKubernetesAuthDTO) => { }: TAttachKubernetesAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity.authMethod)
if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.KUBERNETES_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "Failed to add Kubernetes Auth to already configured identity" message: "Failed to add Kubernetes Auth to already configured identity"
}); });
}
if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) { if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) {
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
@@ -372,13 +372,6 @@ export const identityKubernetesAuthServiceFactory = ({
}, },
tx tx
); );
await identityDAL.updateById(
identityMembershipOrg.identityId,
{
authMethod: IdentityAuthMethod.KUBERNETES_AUTH
},
tx
);
return doc; return doc;
}); });
@@ -404,10 +397,12 @@ export const identityKubernetesAuthServiceFactory = ({
}: TUpdateKubernetesAuthDTO) => { }: TUpdateKubernetesAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.KUBERNETES_AUTH)
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.KUBERNETES_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "Failed to update Kubernetes Auth" message: "Failed to update Kubernetes Auth"
}); });
}
const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId }); const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId });
@@ -532,11 +527,12 @@ export const identityKubernetesAuthServiceFactory = ({
}: TGetKubernetesAuthDTO) => { }: TGetKubernetesAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.KUBERNETES_AUTH)
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.KUBERNETES_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "The identity does not have Kubernetes Auth attached" message: "The identity does not have Kubernetes Auth attached"
}); });
}
const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId }); const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId });
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
@@ -597,10 +593,12 @@ export const identityKubernetesAuthServiceFactory = ({
}: TRevokeKubernetesAuthDTO) => { }: TRevokeKubernetesAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.KUBERNETES_AUTH)
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.KUBERNETES_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "The identity does not have kubernetes auth" message: "The identity does not have kubernetes auth"
}); });
}
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
@@ -624,7 +622,6 @@ export const identityKubernetesAuthServiceFactory = ({
const revokedIdentityKubernetesAuth = await identityKubernetesAuthDAL.transaction(async (tx) => { const revokedIdentityKubernetesAuth = await identityKubernetesAuthDAL.transaction(async (tx) => {
const deletedKubernetesAuth = await identityKubernetesAuthDAL.delete({ identityId }, tx); const deletedKubernetesAuth = await identityKubernetesAuthDAL.delete({ identityId }, tx);
await identityDAL.updateById(identityId, { authMethod: null }, tx);
return { ...deletedKubernetesAuth?.[0], orgId: identityMembershipOrg.orgId }; return { ...deletedKubernetesAuth?.[0], orgId: identityMembershipOrg.orgId };
}); });
return revokedIdentityKubernetesAuth; return revokedIdentityKubernetesAuth;
@@ -22,7 +22,6 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedErro
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { ActorType, AuthTokenType } from "../auth/auth-type"; import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
@@ -41,7 +40,6 @@ type TIdentityOidcAuthServiceFactoryDep = {
identityOidcAuthDAL: TIdentityOidcAuthDALFactory; identityOidcAuthDAL: TIdentityOidcAuthDALFactory;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">; identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create">; identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create">;
identityDAL: Pick<TIdentityDALFactory, "updateById">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "transaction" | "create">; orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "transaction" | "create">;
@@ -52,7 +50,6 @@ export type TIdentityOidcAuthServiceFactory = ReturnType<typeof identityOidcAuth
export const identityOidcAuthServiceFactory = ({ export const identityOidcAuthServiceFactory = ({
identityOidcAuthDAL, identityOidcAuthDAL,
identityOrgMembershipDAL, identityOrgMembershipDAL,
identityDAL,
permissionService, permissionService,
licenseService, licenseService,
identityAccessTokenDAL, identityAccessTokenDAL,
@@ -61,7 +58,7 @@ export const identityOidcAuthServiceFactory = ({
const login = async ({ identityId, jwt: oidcJwt }: TLoginOidcAuthDTO) => { const login = async ({ identityId, jwt: oidcJwt }: TLoginOidcAuthDTO) => {
const identityOidcAuth = await identityOidcAuthDAL.findOne({ identityId }); const identityOidcAuth = await identityOidcAuthDAL.findOne({ identityId });
if (!identityOidcAuth) { if (!identityOidcAuth) {
throw new NotFoundError({ message: "GCP auth method not found for identity, did you configure GCP auth?" }); throw new NotFoundError({ message: "OIDC auth method not found for identity, did you configure OIDC auth?" });
} }
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ const identityMembershipOrg = await identityOrgMembershipDAL.findOne({
@@ -181,7 +178,8 @@ export const identityOidcAuthServiceFactory = ({
accessTokenTTL: identityOidcAuth.accessTokenTTL, accessTokenTTL: identityOidcAuth.accessTokenTTL,
accessTokenMaxTTL: identityOidcAuth.accessTokenMaxTTL, accessTokenMaxTTL: identityOidcAuth.accessTokenMaxTTL,
accessTokenNumUses: 0, accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityOidcAuth.accessTokenNumUsesLimit accessTokenNumUsesLimit: identityOidcAuth.accessTokenNumUsesLimit,
authMethod: IdentityAuthMethod.OIDC_AUTH
}, },
tx tx
); );
@@ -228,10 +226,11 @@ export const identityOidcAuthServiceFactory = ({
if (!identityMembershipOrg) { if (!identityMembershipOrg) {
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
} }
if (identityMembershipOrg.identity.authMethod) if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OIDC_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "Failed to add OIDC Auth to already configured identity" message: "Failed to add OIDC Auth to already configured identity"
}); });
}
if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) { if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) {
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
@@ -334,13 +333,6 @@ export const identityOidcAuthServiceFactory = ({
}, },
tx tx
); );
await identityDAL.updateById(
identityMembershipOrg.identityId,
{
authMethod: IdentityAuthMethod.OIDC_AUTH
},
tx
);
return doc; return doc;
}); });
return { ...identityOidcAuth, orgId: identityMembershipOrg.orgId, caCert }; return { ...identityOidcAuth, orgId: identityMembershipOrg.orgId, caCert };
@@ -364,11 +356,9 @@ export const identityOidcAuthServiceFactory = ({
actorOrgId actorOrgId
}: TUpdateOidcAuthDTO) => { }: TUpdateOidcAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) { if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
}
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.OIDC_AUTH) { if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OIDC_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "Failed to update OIDC Auth" message: "Failed to update OIDC Auth"
}); });
@@ -467,11 +457,9 @@ export const identityOidcAuthServiceFactory = ({
const getOidcAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetOidcAuthDTO) => { const getOidcAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetOidcAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) { if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
}
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.OIDC_AUTH) { if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OIDC_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "The identity does not have OIDC Auth attached" message: "The identity does not have OIDC Auth attached"
}); });
@@ -519,7 +507,7 @@ export const identityOidcAuthServiceFactory = ({
throw new NotFoundError({ message: "Failed to find identity" }); throw new NotFoundError({ message: "Failed to find identity" });
} }
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.OIDC_AUTH) { if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.OIDC_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "The identity does not have OIDC auth" message: "The identity does not have OIDC auth"
}); });
@@ -551,7 +539,6 @@ export const identityOidcAuthServiceFactory = ({
const revokedIdentityOidcAuth = await identityOidcAuthDAL.transaction(async (tx) => { const revokedIdentityOidcAuth = await identityOidcAuthDAL.transaction(async (tx) => {
const deletedOidcAuth = await identityOidcAuthDAL.delete({ identityId }, tx); const deletedOidcAuth = await identityOidcAuthDAL.delete({ identityId }, tx);
await identityDAL.updateById(identityId, { authMethod: null }, tx);
return { ...deletedOidcAuth?.[0], orgId: identityMembershipOrg.orgId }; return { ...deletedOidcAuth?.[0], orgId: identityMembershipOrg.orgId };
}); });
@@ -1,12 +1,24 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName, TIdentities } from "@app/db/schemas"; import {
TableName,
TIdentities,
TIdentityAwsAuths,
TIdentityAzureAuths,
TIdentityGcpAuths,
TIdentityKubernetesAuths,
TIdentityOidcAuths,
TIdentityTokenAuths,
TIdentityUniversalAuths
} from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
import { OrderByDirection } from "@app/lib/types"; import { OrderByDirection } from "@app/lib/types";
import { ProjectIdentityOrderBy, TListProjectIdentityDTO } from "@app/services/identity-project/identity-project-types"; import { ProjectIdentityOrderBy, TListProjectIdentityDTO } from "@app/services/identity-project/identity-project-types";
import { buildAuthMethods } from "../identity/identity-fns";
export type TIdentityProjectDALFactory = ReturnType<typeof identityProjectDALFactory>; export type TIdentityProjectDALFactory = ReturnType<typeof identityProjectDALFactory>;
export const identityProjectDALFactory = (db: TDbClient) => { export const identityProjectDALFactory = (db: TDbClient) => {
@@ -33,11 +45,48 @@ export const identityProjectDALFactory = (db: TDbClient) => {
`${TableName.IdentityProjectMembership}.id`, `${TableName.IdentityProjectMembership}.id`,
`${TableName.IdentityProjectAdditionalPrivilege}.projectMembershipId` `${TableName.IdentityProjectAdditionalPrivilege}.projectMembershipId`
) )
.leftJoin(
TableName.IdentityUniversalAuth,
`${TableName.IdentityProjectMembership}.identityId`,
`${TableName.IdentityUniversalAuth}.identityId`
)
.leftJoin(
TableName.IdentityGcpAuth,
`${TableName.IdentityProjectMembership}.identityId`,
`${TableName.IdentityGcpAuth}.identityId`
)
.leftJoin(
TableName.IdentityAwsAuth,
`${TableName.IdentityProjectMembership}.identityId`,
`${TableName.IdentityAwsAuth}.identityId`
)
.leftJoin(
TableName.IdentityKubernetesAuth,
`${TableName.IdentityProjectMembership}.identityId`,
`${TableName.IdentityKubernetesAuth}.identityId`
)
.leftJoin(
TableName.IdentityOidcAuth,
`${TableName.IdentityProjectMembership}.identityId`,
`${TableName.IdentityOidcAuth}.identityId`
)
.leftJoin(
TableName.IdentityAzureAuth,
`${TableName.IdentityProjectMembership}.identityId`,
`${TableName.IdentityAzureAuth}.identityId`
)
.leftJoin(
TableName.IdentityTokenAuth,
`${TableName.IdentityProjectMembership}.identityId`,
`${TableName.IdentityTokenAuth}.identityId`
)
.select( .select(
db.ref("id").withSchema(TableName.IdentityProjectMembership), db.ref("id").withSchema(TableName.IdentityProjectMembership),
db.ref("createdAt").withSchema(TableName.IdentityProjectMembership), db.ref("createdAt").withSchema(TableName.IdentityProjectMembership),
db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership), db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership),
db.ref("authMethod").as("identityAuthMethod").withSchema(TableName.Identity),
db.ref("id").as("identityId").withSchema(TableName.Identity), db.ref("id").as("identityId").withSchema(TableName.Identity),
db.ref("name").as("identityName").withSchema(TableName.Identity), db.ref("name").as("identityName").withSchema(TableName.Identity),
db.ref("id").withSchema(TableName.IdentityProjectMembership), db.ref("id").withSchema(TableName.IdentityProjectMembership),
@@ -52,12 +101,33 @@ export const identityProjectDALFactory = (db: TDbClient) => {
db.ref("temporaryAccessStartTime").withSchema(TableName.IdentityProjectMembershipRole), db.ref("temporaryAccessStartTime").withSchema(TableName.IdentityProjectMembershipRole),
db.ref("temporaryAccessEndTime").withSchema(TableName.IdentityProjectMembershipRole), db.ref("temporaryAccessEndTime").withSchema(TableName.IdentityProjectMembershipRole),
db.ref("projectId").withSchema(TableName.IdentityProjectMembership), db.ref("projectId").withSchema(TableName.IdentityProjectMembership),
db.ref("name").as("projectName").withSchema(TableName.Project) db.ref("name").as("projectName").withSchema(TableName.Project),
db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth),
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("oidcId").withSchema(TableName.IdentityOidcAuth),
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth)
); );
const members = sqlNestRelationships({ const members = sqlNestRelationships({
data: docs, data: docs,
parentMapper: ({ identityName, identityAuthMethod, id, createdAt, updatedAt, projectId, projectName }) => ({ parentMapper: ({
identityName,
uaId,
awsId,
gcpId,
kubernetesId,
oidcId,
azureId,
tokenId,
id,
createdAt,
updatedAt,
projectId,
projectName
}) => ({
id, id,
identityId, identityId,
createdAt, createdAt,
@@ -65,7 +135,15 @@ export const identityProjectDALFactory = (db: TDbClient) => {
identity: { identity: {
id: identityId, id: identityId,
name: identityName, name: identityName,
authMethod: identityAuthMethod authMethods: buildAuthMethods({
uaId,
awsId,
gcpId,
kubernetesId,
oidcId,
azureId,
tokenId
})
}, },
project: { project: {
id: projectId, id: projectId,
@@ -150,7 +228,7 @@ export const identityProjectDALFactory = (db: TDbClient) => {
}) })
.where((qb) => { .where((qb) => {
if (filter.identityId) { if (filter.identityId) {
void qb.where("identityId", filter.identityId); void qb.where(`${TableName.IdentityProjectMembership}.identityId`, filter.identityId);
} }
}) })
.join( .join(
@@ -168,6 +246,43 @@ export const identityProjectDALFactory = (db: TDbClient) => {
`${TableName.IdentityProjectMembership}.id`, `${TableName.IdentityProjectMembership}.id`,
`${TableName.IdentityProjectAdditionalPrivilege}.projectMembershipId` `${TableName.IdentityProjectAdditionalPrivilege}.projectMembershipId`
) )
.leftJoin<TIdentityUniversalAuths>(
TableName.IdentityUniversalAuth,
`${TableName.Identity}.id`,
`${TableName.IdentityUniversalAuth}.identityId`
)
.leftJoin<TIdentityGcpAuths>(
TableName.IdentityGcpAuth,
`${TableName.Identity}.id`,
`${TableName.IdentityGcpAuth}.identityId`
)
.leftJoin<TIdentityAwsAuths>(
TableName.IdentityAwsAuth,
`${TableName.Identity}.id`,
`${TableName.IdentityAwsAuth}.identityId`
)
.leftJoin<TIdentityKubernetesAuths>(
TableName.IdentityKubernetesAuth,
`${TableName.Identity}.id`,
`${TableName.IdentityKubernetesAuth}.identityId`
)
.leftJoin<TIdentityOidcAuths>(
TableName.IdentityOidcAuth,
`${TableName.Identity}.id`,
`${TableName.IdentityOidcAuth}.identityId`
)
.leftJoin<TIdentityAzureAuths>(
TableName.IdentityAzureAuth,
`${TableName.Identity}.id`,
`${TableName.IdentityAzureAuth}.identityId`
)
.leftJoin<TIdentityTokenAuths>(
TableName.IdentityTokenAuth,
`${TableName.Identity}.id`,
`${TableName.IdentityTokenAuth}.identityId`
)
.select( .select(
db.ref("id").withSchema(TableName.IdentityProjectMembership), db.ref("id").withSchema(TableName.IdentityProjectMembership),
db.ref("createdAt").withSchema(TableName.IdentityProjectMembership), db.ref("createdAt").withSchema(TableName.IdentityProjectMembership),
@@ -186,7 +301,14 @@ export const identityProjectDALFactory = (db: TDbClient) => {
db.ref("temporaryRange").withSchema(TableName.IdentityProjectMembershipRole), db.ref("temporaryRange").withSchema(TableName.IdentityProjectMembershipRole),
db.ref("temporaryAccessStartTime").withSchema(TableName.IdentityProjectMembershipRole), db.ref("temporaryAccessStartTime").withSchema(TableName.IdentityProjectMembershipRole),
db.ref("temporaryAccessEndTime").withSchema(TableName.IdentityProjectMembershipRole), db.ref("temporaryAccessEndTime").withSchema(TableName.IdentityProjectMembershipRole),
db.ref("name").as("projectName").withSchema(TableName.Project) db.ref("name").as("projectName").withSchema(TableName.Project),
db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth),
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("oidcId").withSchema(TableName.IdentityOidcAuth),
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth)
); );
// TODO: scott - joins seem to reorder identities so need to order again, for the sake of urgency will optimize at a later point // TODO: scott - joins seem to reorder identities so need to order again, for the sake of urgency will optimize at a later point
@@ -204,7 +326,21 @@ export const identityProjectDALFactory = (db: TDbClient) => {
const members = sqlNestRelationships({ const members = sqlNestRelationships({
data: docs, data: docs,
parentMapper: ({ identityId, identityName, identityAuthMethod, id, createdAt, updatedAt, projectName }) => ({ parentMapper: ({
identityId,
identityName,
uaId,
awsId,
gcpId,
kubernetesId,
oidcId,
azureId,
tokenId,
id,
createdAt,
updatedAt,
projectName
}) => ({
id, id,
identityId, identityId,
createdAt, createdAt,
@@ -212,7 +348,15 @@ export const identityProjectDALFactory = (db: TDbClient) => {
identity: { identity: {
id: identityId, id: identityId,
name: identityName, name: identityName,
authMethod: identityAuthMethod authMethods: buildAuthMethods({
uaId,
awsId,
gcpId,
kubernetesId,
oidcId,
azureId,
tokenId
})
}, },
project: { project: {
id: projectId, id: projectId,
@@ -11,7 +11,6 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { ActorType, AuthTokenType } from "../auth/auth-type"; import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
@@ -32,11 +31,10 @@ type TIdentityTokenAuthServiceFactoryDep = {
TIdentityTokenAuthDALFactory, TIdentityTokenAuthDALFactory,
"transaction" | "create" | "findOne" | "updateById" | "delete" "transaction" | "create" | "findOne" | "updateById" | "delete"
>; >;
identityDAL: Pick<TIdentityDALFactory, "updateById">;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">; identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
identityAccessTokenDAL: Pick< identityAccessTokenDAL: Pick<
TIdentityAccessTokenDALFactory, TIdentityAccessTokenDALFactory,
"create" | "find" | "update" | "findById" | "findOne" | "updateById" "create" | "find" | "update" | "findById" | "findOne" | "updateById" | "delete"
>; >;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
@@ -46,7 +44,7 @@ export type TIdentityTokenAuthServiceFactory = ReturnType<typeof identityTokenAu
export const identityTokenAuthServiceFactory = ({ export const identityTokenAuthServiceFactory = ({
identityTokenAuthDAL, identityTokenAuthDAL,
identityDAL, // identityDAL,
identityOrgMembershipDAL, identityOrgMembershipDAL,
identityAccessTokenDAL, identityAccessTokenDAL,
permissionService, permissionService,
@@ -65,10 +63,12 @@ export const identityTokenAuthServiceFactory = ({
}: TAttachTokenAuthDTO) => { }: TAttachTokenAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity.authMethod)
if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "Failed to add Token Auth to already configured identity" message: "Failed to add Token Auth to already configured identity"
}); });
}
if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) { if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) {
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
@@ -112,13 +112,6 @@ export const identityTokenAuthServiceFactory = ({
}, },
tx tx
); );
await identityDAL.updateById(
identityMembershipOrg.identityId,
{
authMethod: IdentityAuthMethod.TOKEN_AUTH
},
tx
);
return doc; return doc;
}); });
return { ...identityTokenAuth, orgId: identityMembershipOrg.orgId }; return { ...identityTokenAuth, orgId: identityMembershipOrg.orgId };
@@ -137,10 +130,12 @@ export const identityTokenAuthServiceFactory = ({
}: TUpdateTokenAuthDTO) => { }: TUpdateTokenAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.TOKEN_AUTH)
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "Failed to update Token Auth" message: "The identity does not have token auth"
}); });
}
const identityTokenAuth = await identityTokenAuthDAL.findOne({ identityId }); const identityTokenAuth = await identityTokenAuthDAL.findOne({ identityId });
@@ -197,10 +192,12 @@ export const identityTokenAuthServiceFactory = ({
const getTokenAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetTokenAuthDTO) => { const getTokenAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetTokenAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.TOKEN_AUTH)
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "The identity does not have Token Auth attached" message: "The identity does not have Token Auth attached"
}); });
}
const identityTokenAuth = await identityTokenAuthDAL.findOne({ identityId }); const identityTokenAuth = await identityTokenAuthDAL.findOne({ identityId });
@@ -225,10 +222,12 @@ export const identityTokenAuthServiceFactory = ({
}: TRevokeTokenAuthDTO) => { }: TRevokeTokenAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.TOKEN_AUTH)
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "The identity does not have Token Auth" message: "The identity does not have Token Auth"
}); });
}
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
@@ -254,7 +253,11 @@ export const identityTokenAuthServiceFactory = ({
const revokedIdentityTokenAuth = await identityTokenAuthDAL.transaction(async (tx) => { const revokedIdentityTokenAuth = await identityTokenAuthDAL.transaction(async (tx) => {
const deletedTokenAuth = await identityTokenAuthDAL.delete({ identityId }, tx); const deletedTokenAuth = await identityTokenAuthDAL.delete({ identityId }, tx);
await identityDAL.updateById(identityId, { authMethod: null }, tx); await identityAccessTokenDAL.delete({
identityId,
authMethod: IdentityAuthMethod.TOKEN_AUTH
});
return { ...deletedTokenAuth?.[0], orgId: identityMembershipOrg.orgId }; return { ...deletedTokenAuth?.[0], orgId: identityMembershipOrg.orgId };
}); });
return revokedIdentityTokenAuth; return revokedIdentityTokenAuth;
@@ -270,10 +273,12 @@ export const identityTokenAuthServiceFactory = ({
}: TCreateTokenAuthTokenDTO) => { }: TCreateTokenAuthTokenDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.TOKEN_AUTH)
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "The identity does not have Token Auth" message: "The identity does not have Token Auth"
}); });
}
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
@@ -307,7 +312,8 @@ export const identityTokenAuthServiceFactory = ({
accessTokenMaxTTL: identityTokenAuth.accessTokenMaxTTL, accessTokenMaxTTL: identityTokenAuth.accessTokenMaxTTL,
accessTokenNumUses: 0, accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityTokenAuth.accessTokenNumUsesLimit, accessTokenNumUsesLimit: identityTokenAuth.accessTokenNumUsesLimit,
name name,
authMethod: IdentityAuthMethod.TOKEN_AUTH
}, },
tx tx
); );
@@ -344,10 +350,12 @@ export const identityTokenAuthServiceFactory = ({
}: TGetTokenAuthTokensDTO) => { }: TGetTokenAuthTokensDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.TOKEN_AUTH)
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "The identity does not have Token Auth" message: "The identity does not have Token Auth"
}); });
}
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
@@ -359,7 +367,8 @@ export const identityTokenAuthServiceFactory = ({
const tokens = await identityAccessTokenDAL.find( const tokens = await identityAccessTokenDAL.find(
{ {
identityId identityId,
authMethod: IdentityAuthMethod.TOKEN_AUTH
}, },
{ offset, limit, sort: [["updatedAt", "desc"]] } { offset, limit, sort: [["updatedAt", "desc"]] }
); );
@@ -375,16 +384,21 @@ export const identityTokenAuthServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
}: TUpdateTokenAuthTokenDTO) => { }: TUpdateTokenAuthTokenDTO) => {
const foundToken = await identityAccessTokenDAL.findById(tokenId); const foundToken = await identityAccessTokenDAL.findOne({
id: tokenId,
authMethod: IdentityAuthMethod.TOKEN_AUTH
});
if (!foundToken) throw new NotFoundError({ message: `Token with ID ${tokenId} not found` }); if (!foundToken) throw new NotFoundError({ message: `Token with ID ${tokenId} not found` });
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: foundToken.identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: foundToken.identityId });
if (!identityMembershipOrg) { if (!identityMembershipOrg) {
throw new NotFoundError({ message: `Failed to find identity with ID ${foundToken.identityId}` }); throw new NotFoundError({ message: `Failed to find identity with ID ${foundToken.identityId}` });
} }
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.TOKEN_AUTH) if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TOKEN_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "The identity does not have Token Auth" message: "The identity does not have Token Auth"
}); });
}
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
@@ -409,6 +423,7 @@ export const identityTokenAuthServiceFactory = ({
const [token] = await identityAccessTokenDAL.update( const [token] = await identityAccessTokenDAL.update(
{ {
authMethod: IdentityAuthMethod.TOKEN_AUTH,
identityId: foundToken.identityId, identityId: foundToken.identityId,
id: tokenId id: tokenId
}, },
@@ -429,7 +444,8 @@ export const identityTokenAuthServiceFactory = ({
}: TRevokeTokenAuthTokenDTO) => { }: TRevokeTokenAuthTokenDTO) => {
const identityAccessToken = await identityAccessTokenDAL.findOne({ const identityAccessToken = await identityAccessTokenDAL.findOne({
[`${TableName.IdentityAccessToken}.id` as "id"]: tokenId, [`${TableName.IdentityAccessToken}.id` as "id"]: tokenId,
isAccessTokenRevoked: false isAccessTokenRevoked: false,
authMethod: IdentityAuthMethod.TOKEN_AUTH
}); });
if (!identityAccessToken) if (!identityAccessToken)
throw new NotFoundError({ throw new NotFoundError({
@@ -453,9 +469,15 @@ export const identityTokenAuthServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
const revokedToken = await identityAccessTokenDAL.updateById(identityAccessToken.id, { const [revokedToken] = await identityAccessTokenDAL.update(
isAccessTokenRevoked: true {
}); id: identityAccessToken.id,
authMethod: IdentityAuthMethod.TOKEN_AUTH
},
{
isAccessTokenRevoked: true
}
);
return { revokedToken }; return { revokedToken };
}; };
@@ -14,7 +14,6 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedErro
import { checkIPAgainstBlocklist, extractIPDetails, isValidIpOrCidr, TIp } from "@app/lib/ip"; import { checkIPAgainstBlocklist, extractIPDetails, isValidIpOrCidr, TIp } from "@app/lib/ip";
import { ActorType, AuthTokenType } from "../auth/auth-type"; import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
@@ -36,7 +35,6 @@ type TIdentityUaServiceFactoryDep = {
identityUaClientSecretDAL: TIdentityUaClientSecretDALFactory; identityUaClientSecretDAL: TIdentityUaClientSecretDALFactory;
identityAccessTokenDAL: TIdentityAccessTokenDALFactory; identityAccessTokenDAL: TIdentityAccessTokenDALFactory;
identityOrgMembershipDAL: TIdentityOrgDALFactory; identityOrgMembershipDAL: TIdentityOrgDALFactory;
identityDAL: Pick<TIdentityDALFactory, "updateById">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
}; };
@@ -48,7 +46,6 @@ export const identityUaServiceFactory = ({
identityUaClientSecretDAL, identityUaClientSecretDAL,
identityAccessTokenDAL, identityAccessTokenDAL,
identityOrgMembershipDAL, identityOrgMembershipDAL,
identityDAL,
permissionService, permissionService,
licenseService licenseService
}: TIdentityUaServiceFactoryDep) => { }: TIdentityUaServiceFactoryDep) => {
@@ -115,7 +112,8 @@ export const identityUaServiceFactory = ({
accessTokenTTL: identityUa.accessTokenTTL, accessTokenTTL: identityUa.accessTokenTTL,
accessTokenMaxTTL: identityUa.accessTokenMaxTTL, accessTokenMaxTTL: identityUa.accessTokenMaxTTL,
accessTokenNumUses: 0, accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityUa.accessTokenNumUsesLimit accessTokenNumUsesLimit: identityUa.accessTokenNumUsesLimit,
authMethod: IdentityAuthMethod.UNIVERSAL_AUTH
}, },
tx tx
); );
@@ -156,10 +154,12 @@ export const identityUaServiceFactory = ({
}: TAttachUaDTO) => { }: TAttachUaDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity.authMethod)
if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.UNIVERSAL_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "Failed to add universal auth to already configured identity" message: "Failed to add universal auth to already configured identity"
}); });
}
if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) { if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) {
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
@@ -221,13 +221,6 @@ export const identityUaServiceFactory = ({
}, },
tx tx
); );
await identityDAL.updateById(
identityMembershipOrg.identityId,
{
authMethod: IdentityAuthMethod.Univeral
},
tx
);
return doc; return doc;
}); });
return { ...identityUa, orgId: identityMembershipOrg.orgId }; return { ...identityUa, orgId: identityMembershipOrg.orgId };
@@ -247,10 +240,12 @@ export const identityUaServiceFactory = ({
}: TUpdateUaDTO) => { }: TUpdateUaDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral)
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.UNIVERSAL_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "Failed to updated universal auth" message: "The identity does not have universal auth"
}); });
}
const uaIdentityAuth = await identityUaDAL.findOne({ identityId }); const uaIdentityAuth = await identityUaDAL.findOne({ identityId });
@@ -321,10 +316,12 @@ export const identityUaServiceFactory = ({
const getIdentityUniversalAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetUaDTO) => { const getIdentityUniversalAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetUaDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral)
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.UNIVERSAL_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "The identity does not have universal auth" message: "The identity does not have universal auth"
}); });
}
const uaIdentityAuth = await identityUaDAL.findOne({ identityId }); const uaIdentityAuth = await identityUaDAL.findOne({ identityId });
@@ -348,10 +345,12 @@ export const identityUaServiceFactory = ({
}: TRevokeUaDTO) => { }: TRevokeUaDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral)
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.UNIVERSAL_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "The identity does not have universal auth" message: "The identity does not have universal auth"
}); });
}
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
@@ -375,7 +374,6 @@ export const identityUaServiceFactory = ({
const revokedIdentityUniversalAuth = await identityUaDAL.transaction(async (tx) => { const revokedIdentityUniversalAuth = await identityUaDAL.transaction(async (tx) => {
const deletedUniversalAuth = await identityUaDAL.delete({ identityId }, tx); const deletedUniversalAuth = await identityUaDAL.delete({ identityId }, tx);
await identityDAL.updateById(identityId, { authMethod: null }, tx);
return { ...deletedUniversalAuth?.[0], orgId: identityMembershipOrg.orgId }; return { ...deletedUniversalAuth?.[0], orgId: identityMembershipOrg.orgId };
}); });
return revokedIdentityUniversalAuth; return revokedIdentityUniversalAuth;
@@ -393,10 +391,13 @@ export const identityUaServiceFactory = ({
}: TCreateUaClientSecretDTO) => { }: TCreateUaClientSecretDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral)
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.UNIVERSAL_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "The identity does not have universal auth" message: "The identity does not have universal auth"
}); });
}
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
@@ -422,12 +423,11 @@ export const identityUaServiceFactory = ({
const appCfg = getConfig(); const appCfg = getConfig();
const clientSecret = crypto.randomBytes(32).toString("hex"); const clientSecret = crypto.randomBytes(32).toString("hex");
const clientSecretHash = await bcrypt.hash(clientSecret, appCfg.SALT_ROUNDS); const clientSecretHash = await bcrypt.hash(clientSecret, appCfg.SALT_ROUNDS);
const identityUniversalAuth = await identityUaDAL.findOne({
identityId const identityUaAuth = await identityUaDAL.findOne({ identityId: identityMembershipOrg.identityId });
});
const identityUaClientSecret = await identityUaClientSecretDAL.create({ const identityUaClientSecret = await identityUaClientSecretDAL.create({
identityUAId: identityUniversalAuth.id, identityUAId: identityUaAuth.id,
description, description,
clientSecretPrefix: clientSecret.slice(0, 4), clientSecretPrefix: clientSecret.slice(0, 4),
clientSecretHash, clientSecretHash,
@@ -439,7 +439,6 @@ export const identityUaServiceFactory = ({
return { return {
clientSecret, clientSecret,
clientSecretData: identityUaClientSecret, clientSecretData: identityUaClientSecret,
uaAuth: identityUniversalAuth,
orgId: identityMembershipOrg.orgId orgId: identityMembershipOrg.orgId
}; };
}; };
@@ -453,10 +452,12 @@ export const identityUaServiceFactory = ({
}: TGetUaClientSecretsDTO) => { }: TGetUaClientSecretsDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral)
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.UNIVERSAL_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "The identity does not have universal auth" message: "The identity does not have universal auth"
}); });
}
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
@@ -500,10 +501,13 @@ export const identityUaServiceFactory = ({
}: TGetUniversalAuthClientSecretByIdDTO) => { }: TGetUniversalAuthClientSecretByIdDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral)
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.UNIVERSAL_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "The identity does not have universal auth" message: "The identity does not have universal auth"
}); });
}
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
@@ -539,10 +543,13 @@ export const identityUaServiceFactory = ({
}: TRevokeUaClientSecretDTO) => { }: TRevokeUaClientSecretDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` }); if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral)
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.UNIVERSAL_AUTH)) {
throw new BadRequestError({ throw new BadRequestError({
message: "The identity does not have universal auth" message: "The identity does not have universal auth"
}); });
}
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
@@ -0,0 +1,29 @@
import { IdentityAuthMethod } from "@app/db/schemas";
export const buildAuthMethods = ({
uaId,
gcpId,
awsId,
kubernetesId,
oidcId,
azureId,
tokenId
}: {
uaId?: string;
gcpId?: string;
awsId?: string;
kubernetesId?: string;
oidcId?: string;
azureId?: string;
tokenId?: string;
}) => {
return [
...[uaId ? IdentityAuthMethod.UNIVERSAL_AUTH : null],
...[gcpId ? IdentityAuthMethod.GCP_AUTH : null],
...[awsId ? IdentityAuthMethod.AWS_AUTH : null],
...[kubernetesId ? IdentityAuthMethod.KUBERNETES_AUTH : null],
...[oidcId ? IdentityAuthMethod.OIDC_AUTH : null],
...[azureId ? IdentityAuthMethod.AZURE_AUTH : null],
...[tokenId ? IdentityAuthMethod.TOKEN_AUTH : null]
].filter((authMethod) => authMethod) as IdentityAuthMethod[];
};
+144 -13
View File
@@ -1,12 +1,25 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName, TIdentityOrgMemberships, TOrgRoles } from "@app/db/schemas"; import {
TableName,
TIdentityAwsAuths,
TIdentityAzureAuths,
TIdentityGcpAuths,
TIdentityKubernetesAuths,
TIdentityOidcAuths,
TIdentityOrgMemberships,
TIdentityTokenAuths,
TIdentityUniversalAuths,
TOrgRoles
} from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
import { OrderByDirection } from "@app/lib/types"; import { OrderByDirection } from "@app/lib/types";
import { OrgIdentityOrderBy, TListOrgIdentitiesByOrgIdDTO } from "@app/services/identity/identity-types"; import { OrgIdentityOrderBy, TListOrgIdentitiesByOrgIdDTO } from "@app/services/identity/identity-types";
import { buildAuthMethods } from "./identity-fns";
export type TIdentityOrgDALFactory = ReturnType<typeof identityOrgDALFactory>; export type TIdentityOrgDALFactory = ReturnType<typeof identityOrgDALFactory>;
export const identityOrgDALFactory = (db: TDbClient) => { export const identityOrgDALFactory = (db: TDbClient) => {
@@ -15,14 +28,73 @@ export const identityOrgDALFactory = (db: TDbClient) => {
const findOne = async (filter: Partial<TIdentityOrgMemberships>, tx?: Knex) => { const findOne = async (filter: Partial<TIdentityOrgMemberships>, tx?: Knex) => {
try { try {
const [data] = await (tx || db.replicaNode())(TableName.IdentityOrgMembership) const [data] = await (tx || db.replicaNode())(TableName.IdentityOrgMembership)
.where(filter) .where((queryBuilder) => {
Object.entries(filter).forEach(([key, value]) => {
void queryBuilder.where(`${TableName.IdentityOrgMembership}.${key}`, value);
});
})
.join(TableName.Identity, `${TableName.IdentityOrgMembership}.identityId`, `${TableName.Identity}.id`) .join(TableName.Identity, `${TableName.IdentityOrgMembership}.identityId`, `${TableName.Identity}.id`)
.select(selectAllTableCols(TableName.IdentityOrgMembership))
.select(db.ref("name").withSchema(TableName.Identity)) .leftJoin<TIdentityUniversalAuths>(
.select(db.ref("authMethod").withSchema(TableName.Identity)); TableName.IdentityUniversalAuth,
`${TableName.IdentityOrgMembership}.identityId`,
`${TableName.IdentityUniversalAuth}.identityId`
)
.leftJoin<TIdentityGcpAuths>(
TableName.IdentityGcpAuth,
`${TableName.IdentityOrgMembership}.identityId`,
`${TableName.IdentityGcpAuth}.identityId`
)
.leftJoin<TIdentityAwsAuths>(
TableName.IdentityAwsAuth,
`${TableName.IdentityOrgMembership}.identityId`,
`${TableName.IdentityAwsAuth}.identityId`
)
.leftJoin<TIdentityKubernetesAuths>(
TableName.IdentityKubernetesAuth,
`${TableName.IdentityOrgMembership}.identityId`,
`${TableName.IdentityKubernetesAuth}.identityId`
)
.leftJoin<TIdentityOidcAuths>(
TableName.IdentityOidcAuth,
`${TableName.IdentityOrgMembership}.identityId`,
`${TableName.IdentityOidcAuth}.identityId`
)
.leftJoin<TIdentityAzureAuths>(
TableName.IdentityAzureAuth,
`${TableName.IdentityOrgMembership}.identityId`,
`${TableName.IdentityAzureAuth}.identityId`
)
.leftJoin<TIdentityTokenAuths>(
TableName.IdentityTokenAuth,
`${TableName.IdentityOrgMembership}.identityId`,
`${TableName.IdentityTokenAuth}.identityId`
)
.select(
selectAllTableCols(TableName.IdentityOrgMembership),
db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth),
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("oidcId").withSchema(TableName.IdentityOidcAuth),
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth),
db.ref("name").withSchema(TableName.Identity)
);
if (data) { if (data) {
const { name, authMethod } = data; const { name } = data;
return { ...data, identity: { id: data.identityId, name, authMethod } }; return {
...data,
identity: {
id: data.identityId,
name,
authMethods: buildAuthMethods(data)
}
};
} }
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "FindOne" }); throw new DatabaseError({ error, name: "FindOne" });
@@ -51,8 +123,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
.orderBy(`${TableName.Identity}.${orderBy}`, orderDirection) .orderBy(`${TableName.Identity}.${orderBy}`, orderDirection)
.select( .select(
selectAllTableCols(TableName.IdentityOrgMembership), selectAllTableCols(TableName.IdentityOrgMembership),
db.ref("name").withSchema(TableName.Identity).as("identityName"), db.ref("name").withSchema(TableName.Identity).as("identityName")
db.ref("authMethod").withSchema(TableName.Identity).as("identityAuthMethod")
) )
.where(filter) .where(filter)
.as("paginatedIdentity"); .as("paginatedIdentity");
@@ -70,11 +141,49 @@ export const identityOrgDALFactory = (db: TDbClient) => {
const query = (tx || db.replicaNode()) const query = (tx || db.replicaNode())
.from<TSubquery[number], TSubquery>(paginatedIdentity) .from<TSubquery[number], TSubquery>(paginatedIdentity)
.leftJoin<TOrgRoles>(TableName.OrgRoles, `paginatedIdentity.roleId`, `${TableName.OrgRoles}.id`) .leftJoin<TOrgRoles>(TableName.OrgRoles, `paginatedIdentity.roleId`, `${TableName.OrgRoles}.id`)
.leftJoin(TableName.IdentityMetadata, (queryBuilder) => { .leftJoin(TableName.IdentityMetadata, (queryBuilder) => {
void queryBuilder void queryBuilder
.on(`paginatedIdentity.identityId`, `${TableName.IdentityMetadata}.identityId`) .on(`paginatedIdentity.identityId`, `${TableName.IdentityMetadata}.identityId`)
.andOn(`paginatedIdentity.orgId`, `${TableName.IdentityMetadata}.orgId`); .andOn(`paginatedIdentity.orgId`, `${TableName.IdentityMetadata}.orgId`);
}) })
.leftJoin<TIdentityUniversalAuths>(
TableName.IdentityUniversalAuth,
"paginatedIdentity.identityId",
`${TableName.IdentityUniversalAuth}.identityId`
)
.leftJoin<TIdentityGcpAuths>(
TableName.IdentityGcpAuth,
"paginatedIdentity.identityId",
`${TableName.IdentityGcpAuth}.identityId`
)
.leftJoin<TIdentityAwsAuths>(
TableName.IdentityAwsAuth,
"paginatedIdentity.identityId",
`${TableName.IdentityAwsAuth}.identityId`
)
.leftJoin<TIdentityKubernetesAuths>(
TableName.IdentityKubernetesAuth,
"paginatedIdentity.identityId",
`${TableName.IdentityKubernetesAuth}.identityId`
)
.leftJoin<TIdentityOidcAuths>(
TableName.IdentityOidcAuth,
"paginatedIdentity.identityId",
`${TableName.IdentityOidcAuth}.identityId`
)
.leftJoin<TIdentityAzureAuths>(
TableName.IdentityAzureAuth,
"paginatedIdentity.identityId",
`${TableName.IdentityAzureAuth}.identityId`
)
.leftJoin<TIdentityTokenAuths>(
TableName.IdentityTokenAuth,
"paginatedIdentity.identityId",
`${TableName.IdentityTokenAuth}.identityId`
)
.select( .select(
db.ref("id").withSchema("paginatedIdentity"), db.ref("id").withSchema("paginatedIdentity"),
db.ref("role").withSchema("paginatedIdentity"), db.ref("role").withSchema("paginatedIdentity"),
@@ -82,9 +191,16 @@ export const identityOrgDALFactory = (db: TDbClient) => {
db.ref("orgId").withSchema("paginatedIdentity"), db.ref("orgId").withSchema("paginatedIdentity"),
db.ref("createdAt").withSchema("paginatedIdentity"), db.ref("createdAt").withSchema("paginatedIdentity"),
db.ref("updatedAt").withSchema("paginatedIdentity"), db.ref("updatedAt").withSchema("paginatedIdentity"),
db.ref("identityId").withSchema("paginatedIdentity"), db.ref("identityId").withSchema("paginatedIdentity").as("identityId"),
db.ref("identityName").withSchema("paginatedIdentity"), db.ref("identityName").withSchema("paginatedIdentity"),
db.ref("identityAuthMethod").withSchema("paginatedIdentity")
db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth),
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("oidcId").withSchema(TableName.IdentityOidcAuth),
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth)
) )
// cr stands for custom role // cr stands for custom role
.select(db.ref("id").as("crId").withSchema(TableName.OrgRoles)) .select(db.ref("id").as("crId").withSchema(TableName.OrgRoles))
@@ -114,11 +230,17 @@ export const identityOrgDALFactory = (db: TDbClient) => {
crName, crName,
identityId, identityId,
identityName, identityName,
identityAuthMethod,
role, role,
roleId, roleId,
id, id,
orgId, orgId,
uaId,
awsId,
gcpId,
kubernetesId,
oidcId,
azureId,
tokenId,
createdAt, createdAt,
updatedAt updatedAt
}) => ({ }) => ({
@@ -126,6 +248,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
roleId, roleId,
identityId, identityId,
id, id,
orgId, orgId,
createdAt, createdAt,
updatedAt, updatedAt,
@@ -141,7 +264,15 @@ export const identityOrgDALFactory = (db: TDbClient) => {
identity: { identity: {
id: identityId, id: identityId,
name: identityName, name: identityName,
authMethod: identityAuthMethod as string authMethods: buildAuthMethods({
uaId,
awsId,
gcpId,
kubernetesId,
oidcId,
azureId,
tokenId
})
} }
}), }),
childrenMapper: [ childrenMapper: [
@@ -93,7 +93,7 @@ export const identityServiceFactory = ({
tx tx
); );
} }
return newIdentity; return { ...newIdentity, authMethods: [] };
}); });
await licenseService.updateSubscriptionOrgMemberCount(orgId); await licenseService.updateSubscriptionOrgMemberCount(orgId);
+1 -2
View File
@@ -127,8 +127,7 @@ export const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(
cursor-pointer select-none items-center overflow-hidden text-ellipsis whitespace-nowrap rounded-md py-2 cursor-pointer select-none items-center overflow-hidden text-ellipsis whitespace-nowrap rounded-md py-2
pl-10 pr-4 text-sm outline-none transition-all hover:bg-mineshaft-500 data-[highlighted]:bg-mineshaft-700/80`, pl-10 pr-4 text-sm outline-none transition-all hover:bg-mineshaft-500 data-[highlighted]:bg-mineshaft-700/80`,
isSelected && "bg-primary", isSelected && "bg-primary",
isDisabled && isDisabled && "cursor-not-allowed text-gray-600 opacity-80 hover:!bg-transparent",
"cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-mineshaft-600",
className className
)} )}
ref={forwardedRef} ref={forwardedRef}
+1 -1
View File
@@ -12,7 +12,7 @@ export type IdentityTrustedIp = {
export type Identity = { export type Identity = {
id: string; id: string;
name: string; name: string;
authMethod?: IdentityAuthMethod; authMethods: IdentityAuthMethod[];
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
}; };
@@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
import { useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { faChevronLeft, faEllipsis } from "@fortawesome/free-solid-svg-icons"; import { faChevronLeft, faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -19,12 +20,15 @@ import {
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context"; import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { withPermission } from "@app/hoc"; import { withPermission } from "@app/hoc";
import { import {
IdentityAuthMethod,
useDeleteIdentity, useDeleteIdentity,
useGetIdentityById, useGetIdentityById,
useRevokeIdentityTokenAuthToken, useRevokeIdentityTokenAuthToken,
useRevokeIdentityUniversalAuthClientSecret} from "@app/hooks/api"; useRevokeIdentityUniversalAuthClientSecret
} from "@app/hooks/api";
import { Identity } from "@app/hooks/api/identities/types";
import { usePopUp } from "@app/hooks/usePopUp"; import { usePopUp } from "@app/hooks/usePopUp";
import { TabSections } from"@app/views/Org/Types"; import { TabSections } from "@app/views/Org/Types";
import { IdentityAuthMethodModal } from "../MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal"; import { IdentityAuthMethodModal } from "../MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal";
import { IdentityModal } from "../MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal"; import { IdentityModal } from "../MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal";
@@ -49,6 +53,10 @@ export const IdentityPage = withPermission(
const { mutateAsync: revokeToken } = useRevokeIdentityTokenAuthToken(); const { mutateAsync: revokeToken } = useRevokeIdentityTokenAuthToken();
const { mutateAsync: revokeClientSecret } = useRevokeIdentityUniversalAuthClientSecret(); const { mutateAsync: revokeClientSecret } = useRevokeIdentityUniversalAuthClientSecret();
const [selectedAuthMethod, setSelectedAuthMethod] = useState<
Identity["authMethods"][number] | null
>(null);
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"identity", "identity",
"deleteIdentity", "deleteIdentity",
@@ -124,7 +132,7 @@ export const IdentityPage = withPermission(
const onDeleteClientSecretSubmit = async ({ clientSecretId }: { clientSecretId: string }) => { const onDeleteClientSecretSubmit = async ({ clientSecretId }: { clientSecretId: string }) => {
try { try {
if (!data?.identity.id) return; if (!data?.identity.id || selectedAuthMethod !== IdentityAuthMethod.UNIVERSAL_AUTH) return;
await revokeClientSecret({ await revokeClientSecret({
identityId: data?.identity.id, identityId: data?.identity.id,
@@ -208,12 +216,12 @@ export const IdentityPage = withPermission(
handlePopUpOpen("identityAuthMethod", { handlePopUpOpen("identityAuthMethod", {
identityId, identityId,
name: data.identity.name, name: data.identity.name,
authMethod: data.identity.authMethod allAuthMethods: data.identity.authMethods
}); });
}} }}
disabled={!isAllowed} disabled={!isAllowed}
> >
{`${data.identity.authMethod ? "Edit" : "Configure"} Auth Method`} Add new auth method
</DropdownMenuItem> </DropdownMenuItem>
)} )}
</OrgPermissionCan> </OrgPermissionCan>
@@ -247,6 +255,8 @@ export const IdentityPage = withPermission(
<div className="mr-4 w-96"> <div className="mr-4 w-96">
<IdentityDetailsSection identityId={identityId} handlePopUpOpen={handlePopUpOpen} /> <IdentityDetailsSection identityId={identityId} handlePopUpOpen={handlePopUpOpen} />
<IdentityAuthenticationSection <IdentityAuthenticationSection
selectedAuthMethod={selectedAuthMethod}
setSelectedAuthMethod={setSelectedAuthMethod}
identityId={identityId} identityId={identityId}
handlePopUpOpen={handlePopUpOpen} handlePopUpOpen={handlePopUpOpen}
/> />
@@ -1,15 +1,13 @@
import { faPencil } from "@fortawesome/free-solid-svg-icons"; import { useEffect } from "react";
import { faPencil, faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { OrgPermissionCan } from "@app/components/permissions"; import { OrgPermissionCan } from "@app/components/permissions";
import { import { Button, IconButton, Select, SelectItem, Tooltip } from "@app/components/v2";
IconButton,
// Button,
Tooltip
} from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { useGetIdentityById } from "@app/hooks/api"; import { useGetIdentityById } from "@app/hooks/api";
import { IdentityAuthMethod, identityAuthToNameMap } from "@app/hooks/api/identities"; import { IdentityAuthMethod, identityAuthToNameMap } from "@app/hooks/api/identities";
import { Identity } from "@app/hooks/api/identities/types";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
import { IdentityClientSecrets } from "./IdentityClientSecrets"; import { IdentityClientSecrets } from "./IdentityClientSecrets";
@@ -17,6 +15,8 @@ import { IdentityTokens } from "./IdentityTokens";
type Props = { type Props = {
identityId: string; identityId: string;
setSelectedAuthMethod: (authMethod: Identity["authMethods"][number] | null) => void;
selectedAuthMethod: Identity["authMethods"][number] | null;
handlePopUpOpen: ( handlePopUpOpen: (
popUpName: keyof UsePopUpState< popUpName: keyof UsePopUpState<
[ [
@@ -33,16 +33,34 @@ type Props = {
) => void; ) => void;
}; };
export const IdentityAuthenticationSection = ({ identityId, handlePopUpOpen }: Props) => { export const IdentityAuthenticationSection = ({
identityId,
setSelectedAuthMethod,
selectedAuthMethod,
handlePopUpOpen
}: Props) => {
const { data } = useGetIdentityById(identityId); const { data } = useGetIdentityById(identityId);
useEffect(() => {
if (!data?.identity) return;
if (data.identity.authMethods?.length) {
setSelectedAuthMethod(data.identity.authMethods[0]);
}
// eslint-disable-next-line consistent-return
return () => setSelectedAuthMethod(null);
}, [data?.identity]);
return data ? ( return data ? (
<div className="mt-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="mt-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4"> <div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">Authentication</h3> <h3 className="text-lg font-semibold text-mineshaft-100">Authentication</h3>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}> <OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}>
{(isAllowed) => { {(isAllowed) => {
return ( return (
<Tooltip content={`${data.identity.authMethod ? "Edit" : "Configure"} Auth Method`}> <Tooltip content="Add new auth method">
<IconButton <IconButton
isDisabled={!isAllowed} isDisabled={!isAllowed}
ariaLabel="copy icon" ariaLabel="copy icon"
@@ -52,32 +70,85 @@ export const IdentityAuthenticationSection = ({ identityId, handlePopUpOpen }: P
handlePopUpOpen("identityAuthMethod", { handlePopUpOpen("identityAuthMethod", {
identityId, identityId,
name: data.identity.name, name: data.identity.name,
authMethod: data.identity.authMethod allAuthMethods: data.identity.authMethods
}) })
} }
> >
<FontAwesomeIcon icon={faPencil} /> <FontAwesomeIcon icon={faPlus} />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
); );
}} }}
</OrgPermissionCan> </OrgPermissionCan>
</div> </div>
<div className="py-4"> {data.identity.authMethods.length > 0 ? (
<div className="flex justify-between"> <>
<p className="text-sm font-semibold text-mineshaft-300">Auth Method</p> <div className="py-4">
<div className="flex justify-between">
<p className="ml-px mb-0.5 text-sm font-semibold text-mineshaft-300">Auth Method</p>
</div>
<div className="flex items-center gap-2">
<div className="w-full">
<Select
className="w-full"
value={selectedAuthMethod as string}
onValueChange={(value) => setSelectedAuthMethod(value as IdentityAuthMethod)}
>
{(data.identity?.authMethods || []).map((authMethod) => (
<SelectItem key={authMethod || authMethod} value={authMethod}>
{identityAuthToNameMap[authMethod]}
</SelectItem>
))}
</Select>
</div>
<div>
<Tooltip content="Edit auth method">
<IconButton
onClick={() => {
handlePopUpOpen("identityAuthMethod", {
identityId,
name: data.identity.name,
authMethod: selectedAuthMethod,
allAuthMethods: data.identity.authMethods
});
}}
ariaLabel="copy icon"
variant="plain"
className="group relative"
>
<FontAwesomeIcon icon={faPencil} />
</IconButton>
</Tooltip>{" "}
</div>
</div>
</div>
{selectedAuthMethod === IdentityAuthMethod.UNIVERSAL_AUTH && (
<IdentityClientSecrets identityId={identityId} handlePopUpOpen={handlePopUpOpen} />
)}
{selectedAuthMethod === IdentityAuthMethod.TOKEN_AUTH && (
<IdentityTokens identityId={identityId} handlePopUpOpen={handlePopUpOpen} />
)}
</>
) : (
<div className="w-full space-y-2 pt-2">
<p className="text-sm text-mineshaft-300">
No authentication methods configured. Get started by creating a new auth method.
</p>
<Button
onClick={() => {
handlePopUpOpen("identityAuthMethod", {
identityId,
name: data.identity.name,
allAuthMethods: data.identity.authMethods
});
}}
variant="outline_bg"
className="w-full"
size="xs"
>
Create Auth Method
</Button>
</div> </div>
<p className="text-sm text-mineshaft-300">
{data.identity.authMethod
? identityAuthToNameMap[data.identity.authMethod]
: "Not configured"}
</p>
</div>
{data.identity.authMethod === IdentityAuthMethod.UNIVERSAL_AUTH && (
<IdentityClientSecrets identityId={identityId} handlePopUpOpen={handlePopUpOpen} />
)}
{data.identity.authMethod === IdentityAuthMethod.TOKEN_AUTH && (
<IdentityTokens identityId={identityId} handlePopUpOpen={handlePopUpOpen} />
)} )}
</div> </div>
) : ( ) : (
@@ -1,38 +1,10 @@
import { useEffect } from "react"; import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { createNotification } from "@app/components/notifications"; import { Modal, ModalContent } from "@app/components/v2";
import {
DeleteActionModal,
FormControl,
Modal,
ModalContent,
Select,
SelectItem,
UpgradePlanModal
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import {
useDeleteIdentityAwsAuth,
useDeleteIdentityAzureAuth,
useDeleteIdentityGcpAuth,
useDeleteIdentityKubernetesAuth,
useDeleteIdentityOidcAuth,
useDeleteIdentityTokenAuth,
useDeleteIdentityUniversalAuth
} from "@app/hooks/api";
import { IdentityAuthMethod, identityAuthToNameMap } from "@app/hooks/api/identities"; import { IdentityAuthMethod, identityAuthToNameMap } from "@app/hooks/api/identities";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
import { IdentityAwsAuthForm } from "./IdentityAwsAuthForm"; import { IdentityAuthMethodModalContent } from "./IdentityAuthMethodModalContent";
import { IdentityAzureAuthForm } from "./IdentityAzureAuthForm";
import { IdentityGcpAuthForm } from "./IdentityGcpAuthForm";
import { IdentityKubernetesAuthForm } from "./IdentityKubernetesAuthForm";
import { IdentityOidcAuthForm } from "./IdentityOidcAuthForm";
import { IdentityTokenAuthForm } from "./IdentityTokenAuthForm";
import { IdentityUniversalAuthForm } from "./IdentityUniversalAuthForm";
type Props = { type Props = {
popUp: UsePopUpState<["identityAuthMethod", "upgradePlan", "revokeAuthMethod"]>; popUp: UsePopUpState<["identityAuthMethod", "upgradePlan", "revokeAuthMethod"]>;
@@ -43,229 +15,13 @@ type Props = {
) => void; ) => void;
}; };
const identityAuthMethods = [
{ label: "Token Auth", value: IdentityAuthMethod.TOKEN_AUTH },
{ label: "Universal Auth", value: IdentityAuthMethod.UNIVERSAL_AUTH },
{ label: "Kubernetes Auth", value: IdentityAuthMethod.KUBERNETES_AUTH },
{ label: "GCP Auth", value: IdentityAuthMethod.GCP_AUTH },
{ label: "AWS Auth", value: IdentityAuthMethod.AWS_AUTH },
{ label: "Azure Auth", value: IdentityAuthMethod.AZURE_AUTH },
{ label: "OIDC Auth", value: IdentityAuthMethod.OIDC_AUTH }
];
const schema = yup
.object({
authMethod: yup
.mixed<IdentityAuthMethod>()
.oneOf(Object.values(IdentityAuthMethod))
.required("Auth method is required")
})
.required();
export type FormData = yup.InferType<typeof schema>;
export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Props) => { export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Props) => {
const { currentOrg } = useOrganization(); const [selectedAuthMethod, setSelectedAuthMethod] = useState<IdentityAuthMethod | null>(null);
const orgId = currentOrg?.id || "";
const { mutateAsync: revokeUniversalAuth } = useDeleteIdentityUniversalAuth();
const { mutateAsync: revokeTokenAuth } = useDeleteIdentityTokenAuth();
const { mutateAsync: revokeKubernetesAuth } = useDeleteIdentityKubernetesAuth();
const { mutateAsync: revokeGcpAuth } = useDeleteIdentityGcpAuth();
const { mutateAsync: revokeAwsAuth } = useDeleteIdentityAwsAuth();
const { mutateAsync: revokeAzureAuth } = useDeleteIdentityAzureAuth();
const { mutateAsync: revokeOidcAuth } = useDeleteIdentityOidcAuth();
const initialAuthMethod = popUp?.identityAuthMethod?.data?.authMethod; const initialAuthMethod = popUp?.identityAuthMethod?.data?.authMethod;
const { control, watch, setValue, reset } = useForm<FormData>({ const isSelectedAuthAlreadyConfigured =
resolver: yupResolver(schema), popUp?.identityAuthMethod?.data?.allAuthMethods?.includes(selectedAuthMethod);
defaultValues: {
authMethod: initialAuthMethod
}
});
useEffect(() => {
// reset form on open
if (popUp.identityAuthMethod.isOpen)
reset({ authMethod: popUp?.identityAuthMethod?.data?.authMethod });
}, [popUp.identityAuthMethod.isOpen]);
const identityAuthMethodData = {
identityId: popUp?.identityAuthMethod.data?.identityId,
name: popUp?.identityAuthMethod?.data?.name,
authMethod: watch("authMethod")
} as {
identityId: string;
name: string;
authMethod?: IdentityAuthMethod;
};
useEffect(() => {
if (identityAuthMethodData?.authMethod) {
setValue("authMethod", identityAuthMethodData.authMethod);
return;
}
setValue("authMethod", IdentityAuthMethod.UNIVERSAL_AUTH);
}, [identityAuthMethodData?.authMethod]);
const onRevokeAuthMethodSubmit = async (authMethod: IdentityAuthMethod) => {
if (!orgId || !authMethod) return;
try {
switch (authMethod) {
case IdentityAuthMethod.UNIVERSAL_AUTH: {
await revokeUniversalAuth({
identityId: identityAuthMethodData.identityId,
organizationId: orgId
});
break;
}
case IdentityAuthMethod.TOKEN_AUTH: {
await revokeTokenAuth({
identityId: identityAuthMethodData.identityId,
organizationId: orgId
});
break;
}
case IdentityAuthMethod.KUBERNETES_AUTH: {
await revokeKubernetesAuth({
identityId: identityAuthMethodData.identityId,
organizationId: orgId
});
break;
}
case IdentityAuthMethod.GCP_AUTH: {
await revokeGcpAuth({
identityId: identityAuthMethodData.identityId,
organizationId: orgId
});
break;
}
case IdentityAuthMethod.AWS_AUTH: {
await revokeAwsAuth({
identityId: identityAuthMethodData.identityId,
organizationId: orgId
});
break;
}
case IdentityAuthMethod.AZURE_AUTH: {
await revokeAzureAuth({
identityId: identityAuthMethodData.identityId,
organizationId: orgId
});
break;
}
case IdentityAuthMethod.OIDC_AUTH: {
await revokeOidcAuth({
identityId: identityAuthMethodData.identityId,
organizationId: orgId
});
break;
}
default:
break;
}
createNotification({
text: `Successfully removed ${identityAuthToNameMap[authMethod]} on ${identityAuthMethodData.name}`,
type: "success"
});
handlePopUpToggle("revokeAuthMethod", false);
handlePopUpToggle("identityAuthMethod", false);
} catch (err) {
console.error(err);
createNotification({
text: `Failed to remove ${identityAuthToNameMap[authMethod]} on ${identityAuthMethodData.name}`,
type: "error"
});
}
};
const renderIdentityAuthForm = () => {
switch (identityAuthMethodData.authMethod) {
case IdentityAuthMethod.AWS_AUTH: {
return (
<IdentityAwsAuthForm
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
identityAuthMethodData={identityAuthMethodData}
initialAuthMethod={initialAuthMethod!}
revokeAuth={onRevokeAuthMethodSubmit}
/>
);
}
case IdentityAuthMethod.KUBERNETES_AUTH: {
return (
<IdentityKubernetesAuthForm
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
identityAuthMethodData={identityAuthMethodData}
initialAuthMethod={initialAuthMethod!}
revokeAuth={onRevokeAuthMethodSubmit}
/>
);
}
case IdentityAuthMethod.GCP_AUTH: {
return (
<IdentityGcpAuthForm
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
identityAuthMethodData={identityAuthMethodData}
initialAuthMethod={initialAuthMethod!}
revokeAuth={onRevokeAuthMethodSubmit}
/>
);
}
case IdentityAuthMethod.AZURE_AUTH: {
return (
<IdentityAzureAuthForm
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
identityAuthMethodData={identityAuthMethodData}
initialAuthMethod={initialAuthMethod!}
revokeAuth={onRevokeAuthMethodSubmit}
/>
);
}
case IdentityAuthMethod.UNIVERSAL_AUTH: {
return (
<IdentityUniversalAuthForm
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
identityAuthMethodData={identityAuthMethodData}
initialAuthMethod={initialAuthMethod!}
revokeAuth={onRevokeAuthMethodSubmit}
/>
);
}
case IdentityAuthMethod.OIDC_AUTH: {
return (
<IdentityOidcAuthForm
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
identityAuthMethodData={identityAuthMethodData}
initialAuthMethod={initialAuthMethod!}
revokeAuth={onRevokeAuthMethodSubmit}
/>
);
}
case IdentityAuthMethod.TOKEN_AUTH: {
return (
<IdentityTokenAuthForm
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
identityAuthMethodData={identityAuthMethodData}
initialAuthMethod={initialAuthMethod!}
revokeAuth={onRevokeAuthMethodSubmit}
/>
);
}
default: {
return <div />;
}
}
};
return ( return (
<Modal <Modal
@@ -275,52 +31,23 @@ export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpTog
}} }}
> >
<ModalContent <ModalContent
title={`${ title={
identityAuthMethodData.authMethod === initialAuthMethod ? "Update" : "Configure" isSelectedAuthAlreadyConfigured
} Identity Auth Method for ${ ? `Edit ${identityAuthToNameMap[selectedAuthMethod!] ?? ""}`
identityAuthToNameMap[identityAuthMethodData.authMethod!] ?? "" : `Create new ${identityAuthToNameMap[selectedAuthMethod!] ?? ""}`
}`} }
> >
<Controller <IdentityAuthMethodModalContent
control={control} popUp={popUp}
name="authMethod" handlePopUpOpen={handlePopUpOpen}
defaultValue={IdentityAuthMethod.UNIVERSAL_AUTH} handlePopUpToggle={handlePopUpToggle}
render={({ field: { onChange, ...field }, fieldState: { error } }) => ( identity={{
<FormControl label="Auth Method" errorText={error?.message} isError={Boolean(error)}> name: popUp?.identityAuthMethod?.data?.name,
<Select authMethods: popUp?.identityAuthMethod?.data?.allAuthMethods,
defaultValue={field.value} id: popUp?.identityAuthMethod.data?.identityId
{...field} }}
onValueChange={(e) => { initialAuthMethod={initialAuthMethod}
onChange(e); setSelectedAuthMethod={setSelectedAuthMethod}
}}
className="w-full"
>
{identityAuthMethods.map(({ label, value }) => (
<SelectItem value={String(value || "")} key={label}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
{renderIdentityAuthForm()}
<UpgradePlanModal
isOpen={popUp?.upgradePlan?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can use IP allowlisting if you switch to Infisical's Pro plan."
/>
<DeleteActionModal
isOpen={popUp?.revokeAuthMethod?.isOpen}
title={`Are you sure want to remove ${
identityAuthMethodData?.authMethod
? identityAuthToNameMap[identityAuthMethodData.authMethod]
: "the auth method"
} on ${identityAuthMethodData?.name ?? ""}?`}
onChange={(isOpen) => handlePopUpToggle("revokeAuthMethod", isOpen)}
deleteKey="confirm"
buttonText="Remove"
onDeleteApproved={() => onRevokeAuthMethodSubmit(identityAuthMethodData.authMethod!)}
/> />
</ModalContent> </ModalContent>
</Modal> </Modal>
@@ -0,0 +1,317 @@
import { useCallback } from "react";
import { Controller, useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { createNotification } from "@app/components/notifications";
import {
Badge,
DeleteActionModal,
FormControl,
Select,
SelectItem,
Tooltip,
UpgradePlanModal
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import {
useDeleteIdentityAwsAuth,
useDeleteIdentityAzureAuth,
useDeleteIdentityGcpAuth,
useDeleteIdentityKubernetesAuth,
useDeleteIdentityOidcAuth,
useDeleteIdentityTokenAuth,
useDeleteIdentityUniversalAuth
} from "@app/hooks/api";
import { IdentityAuthMethod, identityAuthToNameMap } from "@app/hooks/api/identities";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { IdentityAwsAuthForm } from "./IdentityAwsAuthForm";
import { IdentityAzureAuthForm } from "./IdentityAzureAuthForm";
import { IdentityGcpAuthForm } from "./IdentityGcpAuthForm";
import { IdentityKubernetesAuthForm } from "./IdentityKubernetesAuthForm";
import { IdentityOidcAuthForm } from "./IdentityOidcAuthForm";
import { IdentityTokenAuthForm } from "./IdentityTokenAuthForm";
import { IdentityUniversalAuthForm } from "./IdentityUniversalAuthForm";
type Props = {
popUp: UsePopUpState<["identityAuthMethod", "upgradePlan", "revokeAuthMethod"]>;
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["identityAuthMethod", "upgradePlan", "revokeAuthMethod"]>,
state?: boolean
) => void;
identity: {
name: string;
id: string;
authMethods: IdentityAuthMethod[];
};
initialAuthMethod: IdentityAuthMethod;
setSelectedAuthMethod: (authMethod: IdentityAuthMethod) => void;
};
type TRevokeOptions = {
identityId: string;
organizationId: string;
};
type TRevokeMethods = {
revokeMethod: (revokeOptions: TRevokeOptions) => Promise<any>;
render: () => JSX.Element;
};
const identityAuthMethods = [
{ label: "Token Auth", value: IdentityAuthMethod.TOKEN_AUTH },
{ label: "Universal Auth", value: IdentityAuthMethod.UNIVERSAL_AUTH },
{ label: "Kubernetes Auth", value: IdentityAuthMethod.KUBERNETES_AUTH },
{ label: "GCP Auth", value: IdentityAuthMethod.GCP_AUTH },
{ label: "AWS Auth", value: IdentityAuthMethod.AWS_AUTH },
{ label: "Azure Auth", value: IdentityAuthMethod.AZURE_AUTH },
{ label: "OIDC Auth", value: IdentityAuthMethod.OIDC_AUTH }
];
const schema = yup
.object({
authMethod: yup
.mixed<IdentityAuthMethod>()
.oneOf(Object.values(IdentityAuthMethod))
.required("Auth method is required")
})
.required();
export type FormData = yup.InferType<typeof schema>;
export const IdentityAuthMethodModalContent = ({
popUp,
handlePopUpOpen,
handlePopUpToggle,
identity,
initialAuthMethod,
setSelectedAuthMethod
}: Props) => {
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
const { mutateAsync: revokeUniversalAuth } = useDeleteIdentityUniversalAuth();
const { mutateAsync: revokeTokenAuth } = useDeleteIdentityTokenAuth();
const { mutateAsync: revokeKubernetesAuth } = useDeleteIdentityKubernetesAuth();
const { mutateAsync: revokeGcpAuth } = useDeleteIdentityGcpAuth();
const { mutateAsync: revokeAwsAuth } = useDeleteIdentityAwsAuth();
const { mutateAsync: revokeAzureAuth } = useDeleteIdentityAzureAuth();
const { mutateAsync: revokeOidcAuth } = useDeleteIdentityOidcAuth();
const { control, watch } = useForm<FormData>({
resolver: yupResolver(schema),
defaultValues: async () => {
let authMethod = initialAuthMethod;
if (!authMethod) {
const firstAuthMethodNotConfiguredAuthMethod = identityAuthMethods.find(
({ value }) => !identity?.authMethods?.includes(value)
);
if (firstAuthMethodNotConfiguredAuthMethod) {
authMethod = firstAuthMethodNotConfiguredAuthMethod.value;
}
}
setSelectedAuthMethod(authMethod);
return {
authMethod
};
}
});
const watchedAuthMethod = watch("authMethod");
const identityAuthMethodData = {
identityId: identity.id,
name: identity.name,
authMethod: watch("authMethod"),
configuredAuthMethods: identity.authMethods
} as {
identityId: string;
name: string;
authMethod?: IdentityAuthMethod;
configuredAuthMethods?: IdentityAuthMethod[];
};
const isSelectedAuthAlreadyConfigured =
identityAuthMethodData?.configuredAuthMethods?.includes(watchedAuthMethod);
const methodMap: Record<IdentityAuthMethod, TRevokeMethods | undefined> = {
[IdentityAuthMethod.UNIVERSAL_AUTH]: {
revokeMethod: revokeUniversalAuth,
render: () => (
<IdentityUniversalAuthForm
identityAuthMethodData={identityAuthMethodData}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
)
},
[IdentityAuthMethod.OIDC_AUTH]: {
revokeMethod: revokeOidcAuth,
render: () => (
<IdentityOidcAuthForm
identityAuthMethodData={identityAuthMethodData}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
)
},
[IdentityAuthMethod.TOKEN_AUTH]: {
revokeMethod: revokeTokenAuth,
render: () => (
<IdentityTokenAuthForm
identityAuthMethodData={identityAuthMethodData}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
)
},
[IdentityAuthMethod.AZURE_AUTH]: {
revokeMethod: revokeAzureAuth,
render: () => (
<IdentityAzureAuthForm
identityAuthMethodData={identityAuthMethodData}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
)
},
[IdentityAuthMethod.GCP_AUTH]: {
revokeMethod: revokeGcpAuth,
render: () => (
<IdentityGcpAuthForm
identityAuthMethodData={identityAuthMethodData}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
)
},
[IdentityAuthMethod.KUBERNETES_AUTH]: {
revokeMethod: revokeKubernetesAuth,
render: () => (
<IdentityKubernetesAuthForm
identityAuthMethodData={identityAuthMethodData}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
)
},
[IdentityAuthMethod.AWS_AUTH]: {
revokeMethod: revokeAwsAuth,
render: () => (
<IdentityAwsAuthForm
identityAuthMethodData={identityAuthMethodData}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
)
}
};
const isAlreadyConfigured = useCallback((method: IdentityAuthMethod) => {
return identityAuthMethodData?.configuredAuthMethods?.includes(method);
}, []);
const selectedMethodItem = methodMap[identityAuthMethodData.authMethod!];
return (
<>
<Controller
control={control}
name="authMethod"
defaultValue={IdentityAuthMethod.UNIVERSAL_AUTH}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl label="Auth Method" errorText={error?.message} isError={Boolean(error)}>
<Select
isDisabled={isSelectedAuthAlreadyConfigured}
defaultValue={field.value}
{...field}
onValueChange={(e) => {
if (!isAlreadyConfigured(e as IdentityAuthMethod)) {
setSelectedAuthMethod(e as IdentityAuthMethod);
onChange(e);
}
}}
className="w-full"
>
{identityAuthMethods.map(({ label, value }) => {
const alreadyConfigured = isAlreadyConfigured(value);
return (
<Tooltip
key={`auth-method-${value}`}
content="Authentication method already configured"
isDisabled={!alreadyConfigured}
>
<SelectItem
isDisabled={alreadyConfigured}
value={String(value || "")}
key={label}
>
{label}{" "}
{alreadyConfigured && !isSelectedAuthAlreadyConfigured && (
<Badge>Configured</Badge>
)}
</SelectItem>
</Tooltip>
);
})}
</Select>
</FormControl>
)}
/>
{selectedMethodItem?.render ? selectedMethodItem.render() : <div />}
<UpgradePlanModal
isOpen={popUp?.upgradePlan?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can use IP allowlisting if you switch to Infisical's Pro plan."
/>
<DeleteActionModal
isOpen={popUp?.revokeAuthMethod?.isOpen}
title={`Are you sure want to remove ${
identityAuthMethodData?.authMethod
? identityAuthToNameMap[identityAuthMethodData.authMethod]
: "the auth method"
} on ${identityAuthMethodData?.name ?? ""}?`}
onChange={(isOpen) => handlePopUpToggle("revokeAuthMethod", isOpen)}
deleteKey="confirm"
buttonText="Remove"
onDeleteApproved={async () => {
if (!identityAuthMethodData.authMethod || !orgId || !selectedMethodItem) {
return;
}
try {
await selectedMethodItem.revokeMethod({
identityId: identityAuthMethodData.identityId,
organizationId: orgId
});
createNotification({
text: "Successfully removed auth method",
type: "success"
});
handlePopUpToggle("revokeAuthMethod", false);
handlePopUpToggle("identityAuthMethod", false);
} catch (err) {
createNotification({
text: "Failed to remove auth method",
type: "error"
});
}
}}
/>
</>
);
};
@@ -6,7 +6,7 @@ import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup"; import * as yup from "yup";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button, DeleteActionModal, FormControl, IconButton, Input } from "@app/components/v2"; import { Button, FormControl, IconButton, Input } from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context"; import { useOrganization, useSubscription } from "@app/context";
import { import {
useAddIdentityAwsAuth, useAddIdentityAwsAuth,
@@ -15,7 +15,7 @@ import {
} from "@app/hooks/api"; } from "@app/hooks/api";
import { IdentityAuthMethod } from "@app/hooks/api/identities"; import { IdentityAuthMethod } from "@app/hooks/api/identities";
import { IdentityTrustedIp } from "@app/hooks/api/identities/types"; import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
import { usePopUp, UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = yup const schema = yup
.object({ .object({
@@ -62,18 +62,15 @@ type Props = {
identityAuthMethodData: { identityAuthMethodData: {
identityId: string; identityId: string;
name: string; name: string;
configuredAuthMethods?: IdentityAuthMethod[];
authMethod?: IdentityAuthMethod; authMethod?: IdentityAuthMethod;
}; };
initialAuthMethod: IdentityAuthMethod;
revokeAuth: (authMethod: IdentityAuthMethod) => Promise<void>;
}; };
export const IdentityAwsAuthForm = ({ export const IdentityAwsAuthForm = ({
handlePopUpOpen, handlePopUpOpen,
handlePopUpToggle, handlePopUpToggle,
identityAuthMethodData, identityAuthMethodData
initialAuthMethod,
revokeAuth
}: Props) => { }: Props) => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || ""; const orgId = currentOrg?.id || "";
@@ -82,13 +79,13 @@ export const IdentityAwsAuthForm = ({
const { mutateAsync: addMutateAsync } = useAddIdentityAwsAuth(); const { mutateAsync: addMutateAsync } = useAddIdentityAwsAuth();
const { mutateAsync: updateMutateAsync } = useUpdateIdentityAwsAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityAwsAuth();
const isCurrentAuthMethod = identityAuthMethodData?.authMethod === initialAuthMethod; const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
identityAuthMethodData.authMethod! || ""
);
const { data } = useGetIdentityAwsAuth(identityAuthMethodData?.identityId ?? "", { const { data } = useGetIdentityAwsAuth(identityAuthMethodData?.identityId ?? "", {
enabled: isCurrentAuthMethod enabled: isUpdate
}); });
const internalPopUpState = usePopUp(["overwriteAuthMethod"] as const);
const { const {
control, control,
handleSubmit, handleSubmit,
@@ -184,230 +181,204 @@ export const IdentityAwsAuthForm = ({
handlePopUpToggle("identityAuthMethod", false); handlePopUpToggle("identityAuthMethod", false);
createNotification({ createNotification({
text: `Successfully ${isCurrentAuthMethod ? "updated" : "configured"} auth method`, text: `Successfully ${isUpdate ? "updated" : "configured"} auth method`,
type: "success" type: "success"
}); });
reset(); reset();
} catch (err) { } catch (err) {
createNotification({ createNotification({
text: `Failed to ${identityAuthMethodData?.authMethod ? "update" : "configure"} identity`, text: `Failed to ${isUpdate ? "update" : "configure"} identity`,
type: "error" type: "error"
}); });
} }
}; };
return ( return (
<> <form onSubmit={handleSubmit(onFormSubmit)}>
<form onSubmit={handleSubmit(onFormSubmit)}> <Controller
<Controller control={control}
control={control} defaultValue="2592000"
defaultValue="2592000" name="allowedPrincipalArns"
name="allowedPrincipalArns" render={({ field, fieldState: { error } }) => (
render={({ field, fieldState: { error } }) => ( <FormControl
<FormControl label="Allowed Principal ARNs"
label="Allowed Principal ARNs" isError={Boolean(error)}
isError={Boolean(error)} errorText={error?.message}
errorText={error?.message} >
> <Input
<Input {...field}
{...field} placeholder="arn:aws:iam::123456789012:role/MyRoleName, arn:aws:iam::123456789012:user/MyUserName..."
placeholder="arn:aws:iam::123456789012:role/MyRoleName, arn:aws:iam::123456789012:user/MyUserName..." type="text"
type="text"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="allowedAccountIds"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Allowed Account IDs"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="123456789012, ..." />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="https://sts.amazonaws.com/"
name="stsEndpoint"
render={({ field, fieldState: { error } }) => (
<FormControl label="STS Endpoint" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} placeholder="https://sts.amazonaws.com/" type="text" />
</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 </FormControl>
onClick={() => { )}
if (subscription?.ipAllowlisting) { />
removeAccessTokenTrustedIp(index); <Controller
return; control={control}
} name="allowedAccountIds"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Allowed Account IDs"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="123456789012, ..." />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="https://sts.amazonaws.com/"
name="stsEndpoint"
render={({ field, fieldState: { error } }) => (
<FormControl label="STS Endpoint" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} placeholder="https://sts.amazonaws.com/" type="text" />
</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"); handlePopUpOpen("upgradePlan");
}} }}
size="lg" placeholder="123.456.789.0"
colorSchema="danger" />
variant="plain" </FormControl>
ariaLabel="update" );
className="p-3" }}
> />
<FontAwesomeIcon icon={faXmark} /> <IconButton
</IconButton>
</div>
))}
<div className="my-4 ml-1">
<Button
variant="outline_bg"
onClick={() => { onClick={() => {
if (subscription?.ipAllowlisting) { if (subscription?.ipAllowlisting) {
appendAccessTokenTrustedIp({ removeAccessTokenTrustedIp(index);
ipAddress: "0.0.0.0/0"
});
return; return;
} }
handlePopUpOpen("upgradePlan"); handlePopUpOpen("upgradePlan");
}} }}
leftIcon={<FontAwesomeIcon icon={faPlus} />} size="lg"
size="xs" colorSchema="danger"
variant="plain"
ariaLabel="update"
className="p-3"
> >
Add IP Address <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 justify-between">
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{!isUpdate ? "Create" : "Edit"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button> </Button>
</div> </div>
<div className="flex justify-between"> {isUpdate && (
<div className="flex items-center"> <Button
{initialAuthMethod && identityAuthMethodData?.authMethod !== initialAuthMethod ? ( size="sm"
<Button colorSchema="danger"
className="mr-4" isLoading={isSubmitting}
size="sm" isDisabled={isSubmitting}
isLoading={isSubmitting} onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
isDisabled={isSubmitting} >
onClick={() => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", true)} Remove Auth Method
> </Button>
Overwrite )}
</Button> </div>
) : ( </form>
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Submit
</Button>
)}
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button>
</div>
{isCurrentAuthMethod && (
<Button
size="sm"
colorSchema="danger"
isLoading={isSubmitting}
isDisabled={isSubmitting}
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
>
Remove Auth Method
</Button>
)}
</div>
</form>
<DeleteActionModal
isOpen={internalPopUpState.popUp.overwriteAuthMethod?.isOpen}
title={`Are you sure want to overwrite ${initialAuthMethod || "the auth method"} on ${
identityAuthMethodData?.name ?? ""
}?`}
onChange={(isOpen) => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", isOpen)}
deleteKey="confirm"
buttonText="Overwrite"
onDeleteApproved={async () => {
await revokeAuth(initialAuthMethod);
handleSubmit(onFormSubmit)();
}}
/>
</>
); );
}; };
@@ -6,7 +6,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod"; import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button, DeleteActionModal, FormControl, IconButton, Input } from "@app/components/v2"; import { Button, FormControl, IconButton, Input } from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context"; import { useOrganization, useSubscription } from "@app/context";
import { import {
useAddIdentityAzureAuth, useAddIdentityAzureAuth,
@@ -15,7 +15,7 @@ import {
} from "@app/hooks/api"; } from "@app/hooks/api";
import { IdentityAuthMethod } from "@app/hooks/api/identities"; import { IdentityAuthMethod } from "@app/hooks/api/identities";
import { IdentityTrustedIp } from "@app/hooks/api/identities/types"; import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
import { usePopUp, UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = z const schema = z
.object({ .object({
@@ -50,18 +50,15 @@ type Props = {
identityAuthMethodData: { identityAuthMethodData: {
identityId: string; identityId: string;
name: string; name: string;
configuredAuthMethods?: IdentityAuthMethod[];
authMethod?: IdentityAuthMethod; authMethod?: IdentityAuthMethod;
}; };
initialAuthMethod: IdentityAuthMethod;
revokeAuth: (authMethod: IdentityAuthMethod) => Promise<void>;
}; };
export const IdentityAzureAuthForm = ({ export const IdentityAzureAuthForm = ({
handlePopUpOpen, handlePopUpOpen,
handlePopUpToggle, handlePopUpToggle,
identityAuthMethodData, identityAuthMethodData
initialAuthMethod,
revokeAuth
}: Props) => { }: Props) => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || ""; const orgId = currentOrg?.id || "";
@@ -70,18 +67,18 @@ export const IdentityAzureAuthForm = ({
const { mutateAsync: addMutateAsync } = useAddIdentityAzureAuth(); const { mutateAsync: addMutateAsync } = useAddIdentityAzureAuth();
const { mutateAsync: updateMutateAsync } = useUpdateIdentityAzureAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityAzureAuth();
const isCurrentAuthMethod = identityAuthMethodData?.authMethod === initialAuthMethod; const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
identityAuthMethodData.authMethod! || ""
);
const { data } = useGetIdentityAzureAuth(identityAuthMethodData?.identityId ?? "", { const { data } = useGetIdentityAzureAuth(identityAuthMethodData?.identityId ?? "", {
enabled: isCurrentAuthMethod enabled: isUpdate
}); });
const internalPopUpState = usePopUp(["overwriteAuthMethod"] as const);
const { const {
control, control,
handleSubmit, handleSubmit,
reset, reset,
trigger,
formState: { isSubmitting } formState: { isSubmitting }
} = useForm<FormData>({ } = useForm<FormData>({
resolver: zodResolver(schema), resolver: zodResolver(schema),
@@ -173,239 +170,204 @@ export const IdentityAzureAuthForm = ({
handlePopUpToggle("identityAuthMethod", false); handlePopUpToggle("identityAuthMethod", false);
createNotification({ createNotification({
text: `Successfully ${isCurrentAuthMethod ? "updated" : "configured"} auth method`, text: `Successfully ${isUpdate ? "updated" : "configured"} auth method`,
type: "success" type: "success"
}); });
reset(); reset();
} catch (err) { } catch (err) {
createNotification({ createNotification({
text: `Failed to ${identityAuthMethodData?.authMethod ? "update" : "configure"} identity`, text: `Failed to ${isUpdate ? "update" : "configure"} identity`,
type: "error" type: "error"
}); });
} }
}; };
return ( return (
<> <form onSubmit={handleSubmit(onFormSubmit)}>
<form onSubmit={handleSubmit(onFormSubmit)}> <Controller
<Controller control={control}
control={control} defaultValue="2592000"
defaultValue="2592000" name="tenantId"
name="tenantId" render={({ field, fieldState: { error } }) => (
render={({ field, fieldState: { error } }) => ( <FormControl
<FormControl label="Tenant ID"
label="Tenant ID" isError={Boolean(error)}
isError={Boolean(error)} errorText={error?.message}
errorText={error?.message} isRequired
isRequired >
> <Input {...field} placeholder="00000000-0000-0000-0000-000000000000" type="text" />
<Input {...field} placeholder="00000000-0000-0000-0000-000000000000" type="text" /> </FormControl>
</FormControl> )}
)} />
/> <Controller
<Controller control={control}
control={control} name="resource"
name="resource" render={({ field, fieldState: { error } }) => (
render={({ field, fieldState: { error } }) => ( <FormControl
<FormControl label="Resource / Audience"
label="Resource / Audience" isError={Boolean(error)}
isError={Boolean(error)} errorText={error?.message}
errorText={error?.message} >
> <Input {...field} placeholder="https://management.azure.com/" />
<Input {...field} placeholder="https://management.azure.com/" /> </FormControl>
</FormControl> )}
)} />
/> <Controller
<Controller control={control}
control={control} name="allowedServicePrincipalIds"
name="allowedServicePrincipalIds" render={({ field, fieldState: { error } }) => (
render={({ field, fieldState: { error } }) => ( <FormControl
<FormControl label="Allowed Service Principal IDs"
label="Allowed Service Principal IDs" isError={Boolean(error)}
isError={Boolean(error)} errorText={error?.message}
errorText={error?.message} >
> <Input {...field} placeholder="00000000-0000-0000-0000-000000000000, ..." />
<Input {...field} placeholder="00000000-0000-0000-0000-000000000000, ..." /> </FormControl>
</FormControl> )}
)} />
/> <Controller
<Controller control={control}
control={control} defaultValue="2592000"
defaultValue="2592000" name="accessTokenTTL"
name="accessTokenTTL" render={({ field, fieldState: { error } }) => (
render={({ field, fieldState: { error } }) => ( <FormControl
<FormControl label="Access Token TTL (seconds)"
label="Access Token TTL (seconds)" isError={Boolean(error)}
isError={Boolean(error)} errorText={error?.message}
errorText={error?.message} >
> <Input {...field} placeholder="2592000" type="number" min="1" step="1" />
<Input {...field} placeholder="2592000" type="number" min="1" step="1" /> </FormControl>
</FormControl> )}
)} />
/> <Controller
<Controller control={control}
control={control} defaultValue="2592000"
defaultValue="2592000" name="accessTokenMaxTTL"
name="accessTokenMaxTTL" render={({ field, fieldState: { error } }) => (
render={({ field, fieldState: { error } }) => ( <FormControl
<FormControl label="Access Token Max TTL (seconds)"
label="Access Token Max TTL (seconds)" isError={Boolean(error)}
isError={Boolean(error)} errorText={error?.message}
errorText={error?.message} >
> <Input {...field} placeholder="2592000" type="number" min="1" step="1" />
<Input {...field} placeholder="2592000" type="number" min="1" step="1" /> </FormControl>
</FormControl> )}
)} />
/> <Controller
<Controller control={control}
control={control} defaultValue="0"
defaultValue="0" name="accessTokenNumUsesLimit"
name="accessTokenNumUsesLimit" render={({ field, fieldState: { error } }) => (
render={({ field, fieldState: { error } }) => ( <FormControl
<FormControl label="Access Token Max Number of Uses"
label="Access Token Max Number of Uses" isError={Boolean(error)}
isError={Boolean(error)} errorText={error?.message}
errorText={error?.message} >
> <Input {...field} placeholder="0" type="number" min="0" step="1" />
<Input {...field} placeholder="0" type="number" min="0" step="1" /> </FormControl>
</FormControl> )}
)} />
/> {accessTokenTrustedIpsFields.map(({ id }, index) => (
{accessTokenTrustedIpsFields.map(({ id }, index) => ( <div className="mb-3 flex items-end space-x-2" key={id}>
<div className="mb-3 flex items-end space-x-2" key={id}> <Controller
<Controller control={control}
control={control} name={`accessTokenTrustedIps.${index}.ipAddress`}
name={`accessTokenTrustedIps.${index}.ipAddress`} defaultValue="0.0.0.0/0"
defaultValue="0.0.0.0/0" render={({ field, fieldState: { error } }) => {
render={({ field, fieldState: { error } }) => { return (
return ( <FormControl
<FormControl className="mb-0 flex-grow"
className="mb-0 flex-grow" label={index === 0 ? "Access Token Trusted IPs" : undefined}
label={index === 0 ? "Access Token Trusted IPs" : undefined} isError={Boolean(error)}
isError={Boolean(error)} errorText={error?.message}
errorText={error?.message} >
> <Input
<Input value={field.value}
value={field.value} onChange={(e) => {
onChange={(e) => { if (subscription?.ipAllowlisting) {
if (subscription?.ipAllowlisting) { field.onChange(e);
field.onChange(e); return;
return; }
}
handlePopUpOpen("upgradePlan"); handlePopUpOpen("upgradePlan");
}} }}
placeholder="123.456.789.0" placeholder="123.456.789.0"
/> />
</FormControl> </FormControl>
); );
}} }}
/> />
<IconButton <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={() => { onClick={() => {
if (subscription?.ipAllowlisting) { if (subscription?.ipAllowlisting) {
appendAccessTokenTrustedIp({ removeAccessTokenTrustedIp(index);
ipAddress: "0.0.0.0/0"
});
return; return;
} }
handlePopUpOpen("upgradePlan"); handlePopUpOpen("upgradePlan");
}} }}
leftIcon={<FontAwesomeIcon icon={faPlus} />} size="lg"
size="xs" colorSchema="danger"
variant="plain"
ariaLabel="update"
className="p-3"
> >
Add IP Address <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 justify-between">
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{!isUpdate ? "Create" : "Edit"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button> </Button>
</div> </div>
<div className="flex justify-between"> {isUpdate && (
<div className="flex items-center"> <Button
{initialAuthMethod && identityAuthMethodData?.authMethod !== initialAuthMethod ? ( size="sm"
<Button colorSchema="danger"
className="mr-4" isLoading={isSubmitting}
size="sm" isDisabled={isSubmitting}
isLoading={isSubmitting} onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
isDisabled={isSubmitting} >
onClick={() => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", true)} Remove Auth Method
> </Button>
Overwrite )}
</Button> </div>
) : ( </form>
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Submit
</Button>
)}
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button>
</div>
{isCurrentAuthMethod && (
<Button
size="sm"
colorSchema="danger"
isLoading={isSubmitting}
isDisabled={isSubmitting}
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
>
Remove Auth Method
</Button>
)}
</div>
</form>
<DeleteActionModal
isOpen={internalPopUpState.popUp.overwriteAuthMethod?.isOpen}
title={`Are you sure want to overwrite ${initialAuthMethod || "the auth method"} on ${
identityAuthMethodData?.name ?? ""
}?`}
onChange={(isOpen) => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", isOpen)}
deleteKey="confirm"
buttonText="Overwrite"
onDeleteApproved={async () => {
const result = await trigger();
if (result) {
await revokeAuth(initialAuthMethod);
handleSubmit(onFormSubmit)();
} else {
createNotification({
text: "Please fill in all required fields",
type: "error"
});
internalPopUpState.handlePopUpToggle("overwriteAuthMethod", false);
}
}}
/>
</>
); );
}; };
@@ -6,15 +6,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod"; import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { import { Button, FormControl, IconButton, Input, Select, SelectItem } from "@app/components/v2";
Button,
DeleteActionModal,
FormControl,
IconButton,
Input,
Select,
SelectItem
} from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context"; import { useOrganization, useSubscription } from "@app/context";
import { import {
useAddIdentityGcpAuth, useAddIdentityGcpAuth,
@@ -23,7 +15,7 @@ import {
} from "@app/hooks/api"; } from "@app/hooks/api";
import { IdentityAuthMethod } from "@app/hooks/api/identities"; import { IdentityAuthMethod } from "@app/hooks/api/identities";
import { IdentityTrustedIp } from "@app/hooks/api/identities/types"; import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
import { usePopUp, UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = z const schema = z
.object({ .object({
@@ -59,18 +51,15 @@ type Props = {
identityAuthMethodData: { identityAuthMethodData: {
identityId: string; identityId: string;
name: string; name: string;
configuredAuthMethods?: IdentityAuthMethod[];
authMethod?: IdentityAuthMethod; authMethod?: IdentityAuthMethod;
}; };
initialAuthMethod: IdentityAuthMethod;
revokeAuth: (authMethod: IdentityAuthMethod) => Promise<void>;
}; };
export const IdentityGcpAuthForm = ({ export const IdentityGcpAuthForm = ({
handlePopUpOpen, handlePopUpOpen,
handlePopUpToggle, handlePopUpToggle,
identityAuthMethodData, identityAuthMethodData
revokeAuth,
initialAuthMethod
}: Props) => { }: Props) => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || ""; const orgId = currentOrg?.id || "";
@@ -79,11 +68,12 @@ export const IdentityGcpAuthForm = ({
const { mutateAsync: addMutateAsync } = useAddIdentityGcpAuth(); const { mutateAsync: addMutateAsync } = useAddIdentityGcpAuth();
const { mutateAsync: updateMutateAsync } = useUpdateIdentityGcpAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityGcpAuth();
const isCurrentAuthMethod = identityAuthMethodData?.authMethod === initialAuthMethod; const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
identityAuthMethodData.authMethod! || ""
);
const { data } = useGetIdentityGcpAuth(identityAuthMethodData?.identityId ?? "", { const { data } = useGetIdentityGcpAuth(identityAuthMethodData?.identityId ?? "", {
enabled: isCurrentAuthMethod enabled: isUpdate
}); });
const internalPopUpState = usePopUp(["overwriteAuthMethod"] as const);
const { const {
control, control,
@@ -189,258 +179,228 @@ export const IdentityGcpAuthForm = ({
handlePopUpToggle("identityAuthMethod", false); handlePopUpToggle("identityAuthMethod", false);
createNotification({ createNotification({
text: `Successfully ${isCurrentAuthMethod ? "updated" : "configured"} auth method`, text: `Successfully ${isUpdate ? "updated" : "configured"} auth method`,
type: "success" type: "success"
}); });
reset(); reset();
} catch (err) { } catch (err) {
createNotification({ createNotification({
text: `Failed to ${identityAuthMethodData?.authMethod ? "update" : "configure"} identity`, text: `Failed to ${isUpdate ? "update" : "configure"} identity`,
type: "error" type: "error"
}); });
} }
}; };
return ( return (
<> <form onSubmit={handleSubmit(onFormSubmit)}>
<form onSubmit={handleSubmit(onFormSubmit)}> <Controller
<Controller control={control}
control={control} name="type"
name="type" render={({ field: { onChange, ...field }, fieldState: { error } }) => (
render={({ field: { onChange, ...field }, fieldState: { error } }) => ( <FormControl label="Type" isError={Boolean(error)} errorText={error?.message}>
<FormControl label="Type" isError={Boolean(error)} errorText={error?.message}> <Select
<Select defaultValue={field.value}
defaultValue={field.value} {...field}
{...field} onValueChange={(e) => onChange(e)}
onValueChange={(e) => onChange(e)} className="w-full"
className="w-full"
>
<SelectItem value="gce" key="gce">
GCP ID Token Auth (Recommended)
</SelectItem>
<SelectItem value="iam" key="iam">
GCP IAM Auth
</SelectItem>
</Select>
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="2592000"
name="allowedServiceAccounts"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Allowed Service Account Emails"
isError={Boolean(error)}
errorText={error?.message}
> >
<Input <SelectItem value="gce" key="gce">
{...field} GCP ID Token Auth (Recommended)
placeholder="[email protected], [email protected]" </SelectItem>
type="text" <SelectItem value="iam" key="iam">
/> GCP IAM Auth
</FormControl> </SelectItem>
)} </Select>
/> </FormControl>
{watchedType === "gce" && (
<Controller
control={control}
name="allowedProjects"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Allowed Projects"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="my-gcp-project, ..." />
</FormControl>
)}
/>
)} )}
{watchedType === "gce" && ( />
<Controller <Controller
control={control} control={control}
name="allowedZones" defaultValue="2592000"
render={({ field, fieldState: { error } }) => ( name="allowedServiceAccounts"
<FormControl render={({ field, fieldState: { error } }) => (
label="Allowed Zones" <FormControl
isError={Boolean(error)} label="Allowed Service Account Emails"
errorText={error?.message} isError={Boolean(error)}
> errorText={error?.message}
<Input {...field} placeholder="us-west2-a, us-central1-a, ..." /> >
</FormControl> <Input
)} {...field}
/> placeholder="[email protected], [email protected]"
)} type="text"
<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 </FormControl>
onClick={() => { )}
if (subscription?.ipAllowlisting) { />
removeAccessTokenTrustedIp(index); {watchedType === "gce" && (
return; <Controller
} control={control}
name="allowedProjects"
handlePopUpOpen("upgradePlan"); render={({ field, fieldState: { error } }) => (
}} <FormControl
size="lg" label="Allowed Projects"
colorSchema="danger" isError={Boolean(error)}
variant="plain" errorText={error?.message}
ariaLabel="update"
className="p-3"
> >
<FontAwesomeIcon icon={faXmark} /> <Input {...field} placeholder="my-gcp-project, ..." />
</IconButton> </FormControl>
</div> )}
))} />
<div className="my-4 ml-1"> )}
<Button {watchedType === "gce" && (
variant="outline_bg" <Controller
control={control}
name="allowedZones"
render={({ field, fieldState: { error } }) => (
<FormControl label="Allowed Zones" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} placeholder="us-west2-a, us-central1-a, ..." />
</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={() => { onClick={() => {
if (subscription?.ipAllowlisting) { if (subscription?.ipAllowlisting) {
appendAccessTokenTrustedIp({ removeAccessTokenTrustedIp(index);
ipAddress: "0.0.0.0/0"
});
return; return;
} }
handlePopUpOpen("upgradePlan"); handlePopUpOpen("upgradePlan");
}} }}
leftIcon={<FontAwesomeIcon icon={faPlus} />} size="lg"
size="xs" colorSchema="danger"
variant="plain"
ariaLabel="update"
className="p-3"
> >
Add IP Address <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 justify-between">
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{!isUpdate ? "Create" : "Edit"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button> </Button>
</div> </div>
<div className="flex justify-between"> {isUpdate && (
<div className="flex items-center"> <Button
{initialAuthMethod && identityAuthMethodData?.authMethod !== initialAuthMethod ? ( size="sm"
<Button colorSchema="danger"
className="mr-4" isLoading={isSubmitting}
size="sm" isDisabled={isSubmitting}
isLoading={isSubmitting} onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
isDisabled={isSubmitting} >
onClick={() => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", true)} Remove Auth Method
> </Button>
Overwrite )}
</Button> </div>
) : ( </form>
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Submit
</Button>
)}
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button>
</div>
{isCurrentAuthMethod && (
<Button
size="sm"
colorSchema="danger"
isLoading={isSubmitting}
isDisabled={isSubmitting}
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
>
Remove Auth Method
</Button>
)}
</div>
</form>
<DeleteActionModal
isOpen={internalPopUpState.popUp.overwriteAuthMethod?.isOpen}
title={`Are you sure want to overwrite ${initialAuthMethod || "the auth method"} on ${
identityAuthMethodData?.name ?? ""
}?`}
buttonText="Overwrite"
onChange={(isOpen) => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", isOpen)}
deleteKey="confirm"
onDeleteApproved={async () => {
await revokeAuth(initialAuthMethod);
handleSubmit(onFormSubmit)();
}}
/>
</>
); );
}; };
@@ -6,14 +6,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod"; import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { import { Button, FormControl, IconButton, Input, TextArea } from "@app/components/v2";
Button,
DeleteActionModal,
FormControl,
IconButton,
Input,
TextArea
} from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context"; import { useOrganization, useSubscription } from "@app/context";
import { import {
useAddIdentityKubernetesAuth, useAddIdentityKubernetesAuth,
@@ -22,7 +15,7 @@ import {
} from "@app/hooks/api"; } from "@app/hooks/api";
import { IdentityAuthMethod } from "@app/hooks/api/identities"; import { IdentityAuthMethod } from "@app/hooks/api/identities";
import { IdentityTrustedIp } from "@app/hooks/api/identities/types"; import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
import { usePopUp, UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = z const schema = z
.object({ .object({
@@ -60,18 +53,15 @@ type Props = {
identityAuthMethodData: { identityAuthMethodData: {
identityId: string; identityId: string;
name: string; name: string;
configuredAuthMethods?: IdentityAuthMethod[];
authMethod?: IdentityAuthMethod; authMethod?: IdentityAuthMethod;
}; };
initialAuthMethod: IdentityAuthMethod;
revokeAuth: (authMethod: IdentityAuthMethod) => Promise<void>;
}; };
export const IdentityKubernetesAuthForm = ({ export const IdentityKubernetesAuthForm = ({
handlePopUpOpen, handlePopUpOpen,
handlePopUpToggle, handlePopUpToggle,
identityAuthMethodData, identityAuthMethodData
initialAuthMethod,
revokeAuth
}: Props) => { }: Props) => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || ""; const orgId = currentOrg?.id || "";
@@ -80,17 +70,18 @@ export const IdentityKubernetesAuthForm = ({
const { mutateAsync: addMutateAsync } = useAddIdentityKubernetesAuth(); const { mutateAsync: addMutateAsync } = useAddIdentityKubernetesAuth();
const { mutateAsync: updateMutateAsync } = useUpdateIdentityKubernetesAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityKubernetesAuth();
const isCurrentAuthMethod = identityAuthMethodData?.authMethod === initialAuthMethod; const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
identityAuthMethodData.authMethod! || ""
);
const { data } = useGetIdentityKubernetesAuth(identityAuthMethodData?.identityId ?? "", { const { data } = useGetIdentityKubernetesAuth(identityAuthMethodData?.identityId ?? "", {
enabled: isCurrentAuthMethod enabled: isUpdate
}); });
const internalPopUpState = usePopUp(["overwriteAuthMethod"] as const);
const { const {
control, control,
handleSubmit, handleSubmit,
reset, reset,
trigger,
formState: { isSubmitting } formState: { isSubmitting }
} = useForm<FormData>({ } = useForm<FormData>({
resolver: zodResolver(schema), resolver: zodResolver(schema),
@@ -200,291 +191,256 @@ export const IdentityKubernetesAuthForm = ({
handlePopUpToggle("identityAuthMethod", false); handlePopUpToggle("identityAuthMethod", false);
createNotification({ createNotification({
text: `Successfully ${isCurrentAuthMethod ? "updated" : "configured"} auth method`, text: `Successfully ${isUpdate ? "updated" : "configured"} auth method`,
type: "success" type: "success"
}); });
reset(); reset();
} catch (err) { } catch (err) {
createNotification({ createNotification({
text: `Failed to ${identityAuthMethodData?.authMethod ? "update" : "configure"} identity`, text: `Failed to ${isUpdate ? "update" : "configure"} identity`,
type: "error" type: "error"
}); });
} }
}; };
return ( return (
<> <form onSubmit={handleSubmit(onFormSubmit)}>
<form onSubmit={handleSubmit(onFormSubmit)}> <Controller
<Controller control={control}
control={control} defaultValue="2592000"
defaultValue="2592000" name="kubernetesHost"
name="kubernetesHost" render={({ field, fieldState: { error } }) => (
render={({ field, fieldState: { error } }) => ( <FormControl
<FormControl label="Kubernetes Host / Base Kubernetes API URL "
label="Kubernetes Host / Base Kubernetes API URL " isError={Boolean(error)}
isError={Boolean(error)} errorText={error?.message}
errorText={error?.message} tooltipText="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'"
tooltipText="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'" isRequired
isRequired >
> <Input {...field} placeholder="https://my-example-k8s-api-host.com" type="text" />
<Input {...field} placeholder="https://my-example-k8s-api-host.com" type="text" /> </FormControl>
</FormControl> )}
)} />
/> <Controller
<Controller control={control}
control={control} name="tokenReviewerJwt"
name="tokenReviewerJwt" render={({ field, fieldState: { error } }) => (
render={({ field, fieldState: { error } }) => ( <FormControl
<FormControl label="Token Reviewer JWT"
label="Token Reviewer JWT" isError={Boolean(error)}
isError={Boolean(error)} errorText={error?.message}
errorText={error?.message} tooltipText="A long-lived service account JWT token for Infisical to access the TokenReview API to validate other service account JWT tokens submitted by applications/pods."
tooltipText="A long-lived service account JWT token for Infisical to access the TokenReview API to validate other service account JWT tokens submitted by applications/pods." isRequired
isRequired >
> <Input {...field} placeholder="" type="password" />
<Input {...field} placeholder="" type="password" /> </FormControl>
</FormControl> )}
)} />
/> <Controller
<Controller control={control}
control={control} name="allowedNames"
name="allowedNames" render={({ field, fieldState: { error } }) => (
render={({ field, fieldState: { error } }) => ( <FormControl
<FormControl label="Allowed Service Account Names"
label="Allowed Service Account Names" isError={Boolean(error)}
isError={Boolean(error)} tooltipText="An optional comma-separated list of trusted service account names that are allowed to authenticate with Infisical. Leave empty to allow any service account."
tooltipText="An optional comma-separated list of trusted service account names that are allowed to authenticate with Infisical. Leave empty to allow any service account." errorText={error?.message}
errorText={error?.message} >
> <Input {...field} placeholder="service-account-1-name, service-account-1-name" />
<Input {...field} placeholder="service-account-1-name, service-account-1-name" /> </FormControl>
</FormControl> )}
)} />
/> <Controller
<Controller control={control}
control={control} defaultValue=""
defaultValue="" name="allowedNamespaces"
name="allowedNamespaces" render={({ field, fieldState: { error } }) => (
render={({ field, fieldState: { error } }) => ( <FormControl
<FormControl label="Allowed Namespaces"
label="Allowed Namespaces" isError={Boolean(error)}
isError={Boolean(error)} errorText={error?.message}
errorText={error?.message} tooltipText="A comma-separated list of trusted namespaces that service accounts must belong to authenticate with Infisical."
tooltipText="A comma-separated list of trusted namespaces that service accounts must belong to authenticate with Infisical." >
> <Input {...field} placeholder="namespaceA, namespaceB" type="text" />
<Input {...field} placeholder="namespaceA, namespaceB" type="text" /> </FormControl>
</FormControl> )}
)} />
/> <Controller
<Controller control={control}
control={control} defaultValue=""
defaultValue="" name="allowedAudience"
name="allowedAudience" render={({ field, fieldState: { error } }) => (
render={({ field, fieldState: { error } }) => ( <FormControl
<FormControl label="Allowed Audience"
label="Allowed Audience" isError={Boolean(error)}
isError={Boolean(error)} errorText={error?.message}
errorText={error?.message} tooltipText="An optional audience claim that the service account JWT token must have to authenticate with Infisical. Leave empty to allow any audience claim."
tooltipText="An optional audience claim that the service account JWT token must have to authenticate with Infisical. Leave empty to allow any audience claim." >
> <Input {...field} placeholder="" type="text" />
<Input {...field} placeholder="" type="text" /> </FormControl>
</FormControl> )}
)} />
/> <Controller
<Controller control={control}
control={control} name="caCert"
name="caCert" render={({ field, fieldState: { error } }) => (
render={({ field, fieldState: { error } }) => ( <FormControl
<FormControl label="CA Certificate"
label="CA Certificate" errorText={error?.message}
errorText={error?.message} isError={Boolean(error)}
isError={Boolean(error)} tooltipText="An optional PEM-encoded CA cert for the Kubernetes API server. This is used by the TLS client for secure communication with the Kubernetes API server."
tooltipText="An optional PEM-encoded CA cert for the Kubernetes API server. This is used by the TLS client for secure communication with the Kubernetes API server." >
> <TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." /> </FormControl>
</FormControl> )}
)} />
/> <Controller
<Controller control={control}
control={control} defaultValue="2592000"
defaultValue="2592000" name="accessTokenTTL"
name="accessTokenTTL" render={({ field, fieldState: { error } }) => (
render={({ field, fieldState: { error } }) => ( <FormControl
<FormControl label="Access Token TTL (seconds)"
label="Access Token TTL (seconds)" tooltipText="The lifetime for an acccess token in seconds. This value will be referenced at renewal time."
tooltipText="The lifetime for an acccess token in seconds. This value will be referenced at renewal time." isError={Boolean(error)}
isError={Boolean(error)} errorText={error?.message}
errorText={error?.message} >
> <Input {...field} placeholder="2592000" type="number" min="1" step="1" />
<Input {...field} placeholder="2592000" type="number" min="1" step="1" /> </FormControl>
</FormControl> )}
)} />
/> <Controller
<Controller control={control}
control={control} defaultValue="2592000"
defaultValue="2592000" name="accessTokenMaxTTL"
name="accessTokenMaxTTL" render={({ field, fieldState: { error } }) => (
render={({ field, fieldState: { error } }) => ( <FormControl
<FormControl label="Access Token Max TTL (seconds)"
label="Access Token Max TTL (seconds)" isError={Boolean(error)}
isError={Boolean(error)} errorText={error?.message}
errorText={error?.message} tooltipText="The maximum lifetime for an access token in seconds. This value will be referenced at renewal time."
tooltipText="The maximum lifetime for an access token in seconds. This value will be referenced at renewal time." >
> <Input {...field} placeholder="2592000" type="number" min="1" step="1" />
<Input {...field} placeholder="2592000" type="number" min="1" step="1" /> </FormControl>
</FormControl> )}
)} />
/> <Controller
<Controller control={control}
control={control} defaultValue="0"
defaultValue="0" name="accessTokenNumUsesLimit"
name="accessTokenNumUsesLimit" render={({ field, fieldState: { error } }) => (
render={({ field, fieldState: { error } }) => ( <FormControl
<FormControl label="Access Token Max Number of Uses"
label="Access Token Max Number of Uses" isError={Boolean(error)}
isError={Boolean(error)} errorText={error?.message}
errorText={error?.message} tooltipText="The maximum number of times that an access token can be used; a value of 0 implies infinite number of uses."
tooltipText="The maximum number of times that an access token can be used; a value of 0 implies infinite number of uses." >
> <Input {...field} placeholder="0" type="number" min="0" step="1" />
<Input {...field} placeholder="0" type="number" min="0" step="1" /> </FormControl>
</FormControl> )}
)} />
/> {accessTokenTrustedIpsFields.map(({ id }, index) => (
{accessTokenTrustedIpsFields.map(({ id }, index) => ( <div className="mb-3 flex items-end space-x-2" key={id}>
<div className="mb-3 flex items-end space-x-2" key={id}> <Controller
<Controller control={control}
control={control} name={`accessTokenTrustedIps.${index}.ipAddress`}
name={`accessTokenTrustedIps.${index}.ipAddress`} defaultValue="0.0.0.0/0"
defaultValue="0.0.0.0/0" render={({ field, fieldState: { error } }) => {
render={({ field, fieldState: { error } }) => { return (
return ( <FormControl
<FormControl className="mb-0 flex-grow"
className="mb-0 flex-grow" label={index === 0 ? "Access Token Trusted IPs" : undefined}
label={index === 0 ? "Access Token Trusted IPs" : undefined} isError={Boolean(error)}
isError={Boolean(error)} errorText={error?.message}
errorText={error?.message} tooltipText="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."
tooltipText="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." >
> <Input
<Input value={field.value}
value={field.value} onChange={(e) => {
onChange={(e) => { if (subscription?.ipAllowlisting) {
if (subscription?.ipAllowlisting) { field.onChange(e);
field.onChange(e); return;
return; }
}
handlePopUpOpen("upgradePlan"); handlePopUpOpen("upgradePlan");
}} }}
placeholder="123.456.789.0" placeholder="123.456.789.0"
/> />
</FormControl> </FormControl>
); );
}} }}
/> />
<IconButton <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={() => { onClick={() => {
if (subscription?.ipAllowlisting) { if (subscription?.ipAllowlisting) {
appendAccessTokenTrustedIp({ removeAccessTokenTrustedIp(index);
ipAddress: "0.0.0.0/0"
});
return; return;
} }
handlePopUpOpen("upgradePlan"); handlePopUpOpen("upgradePlan");
}} }}
leftIcon={<FontAwesomeIcon icon={faPlus} />} size="lg"
size="xs" colorSchema="danger"
variant="plain"
ariaLabel="update"
className="p-3"
> >
Add IP Address <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 justify-between">
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{isUpdate ? "Update" : "Create"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button> </Button>
</div> </div>
<div className="flex justify-between"> {isUpdate && (
<div className="flex items-center"> <Button
{initialAuthMethod && identityAuthMethodData?.authMethod !== initialAuthMethod ? ( size="sm"
<Button colorSchema="danger"
className="mr-4" isLoading={isSubmitting}
size="sm" isDisabled={isSubmitting}
isLoading={isSubmitting} onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
isDisabled={isSubmitting} >
onClick={() => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", true)} Remove Auth Method
> </Button>
Overwrite )}
</Button> </div>
) : ( </form>
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Submit
</Button>
)}
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button>
</div>
{isCurrentAuthMethod && (
<Button
size="sm"
colorSchema="danger"
isLoading={isSubmitting}
isDisabled={isSubmitting}
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
>
Remove Auth Method
</Button>
)}
</div>
</form>
<DeleteActionModal
isOpen={internalPopUpState.popUp.overwriteAuthMethod?.isOpen}
title={`Are you sure want to overwrite ${initialAuthMethod || "the auth method"} on ${
identityAuthMethodData?.name ?? ""
}?`}
onChange={(isOpen) => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", isOpen)}
deleteKey="confirm"
buttonText="Overwrite"
onDeleteApproved={async () => {
const result = await trigger();
if (result) {
await revokeAuth(initialAuthMethod);
handleSubmit(onFormSubmit)();
} else {
createNotification({
text: "Please fill in all required fields",
type: "error"
});
internalPopUpState.handlePopUpToggle("overwriteAuthMethod", false);
}
}}
/>
</>
); );
}; };
@@ -154,12 +154,6 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
handlePopUpToggle("identity", false); handlePopUpToggle("identity", false);
router.push(`/org/${orgId}/identities/${createdId}`); router.push(`/org/${orgId}/identities/${createdId}`);
// handlePopUpOpen("identityAuthMethod", {
// identityId: createdId,
// name: createdName,
// authMethod
// });
} }
createNotification({ createNotification({
@@ -7,21 +7,13 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod"; import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { import { Button, FormControl, IconButton, Input, TextArea, Tooltip } from "@app/components/v2";
Button,
DeleteActionModal,
FormControl,
IconButton,
Input,
TextArea,
Tooltip
} from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context"; import { useOrganization, useSubscription } from "@app/context";
import { useAddIdentityOidcAuth, useUpdateIdentityOidcAuth } from "@app/hooks/api"; import { useAddIdentityOidcAuth, useUpdateIdentityOidcAuth } from "@app/hooks/api";
import { IdentityAuthMethod } from "@app/hooks/api/identities"; import { IdentityAuthMethod } from "@app/hooks/api/identities";
import { useGetIdentityOidcAuth } from "@app/hooks/api/identities/queries"; import { useGetIdentityOidcAuth } from "@app/hooks/api/identities/queries";
import { IdentityTrustedIp } from "@app/hooks/api/identities/types"; import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
import { usePopUp, UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = z.object({ const schema = z.object({
accessTokenTrustedIps: z accessTokenTrustedIps: z
@@ -62,18 +54,15 @@ type Props = {
identityAuthMethodData: { identityAuthMethodData: {
identityId: string; identityId: string;
name: string; name: string;
configuredAuthMethods?: IdentityAuthMethod[];
authMethod?: IdentityAuthMethod; authMethod?: IdentityAuthMethod;
}; };
initialAuthMethod: IdentityAuthMethod;
revokeAuth: (authMethod: IdentityAuthMethod) => Promise<void>;
}; };
export const IdentityOidcAuthForm = ({ export const IdentityOidcAuthForm = ({
handlePopUpOpen, handlePopUpOpen,
handlePopUpToggle, handlePopUpToggle,
identityAuthMethodData, identityAuthMethodData
initialAuthMethod,
revokeAuth
}: Props) => { }: Props) => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || ""; const orgId = currentOrg?.id || "";
@@ -82,17 +71,17 @@ export const IdentityOidcAuthForm = ({
const { mutateAsync: addMutateAsync } = useAddIdentityOidcAuth(); const { mutateAsync: addMutateAsync } = useAddIdentityOidcAuth();
const { mutateAsync: updateMutateAsync } = useUpdateIdentityOidcAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityOidcAuth();
const isCurrentAuthMethod = identityAuthMethodData?.authMethod === initialAuthMethod; const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
identityAuthMethodData.authMethod! || ""
);
const { data } = useGetIdentityOidcAuth(identityAuthMethodData?.identityId ?? "", { const { data } = useGetIdentityOidcAuth(identityAuthMethodData?.identityId ?? "", {
enabled: isCurrentAuthMethod enabled: isUpdate
}); });
const internalPopUpState = usePopUp(["overwriteAuthMethod"] as const);
const { const {
control, control,
handleSubmit, handleSubmit,
reset, reset,
trigger,
formState: { isSubmitting } formState: { isSubmitting }
} = useForm<FormData>({ } = useForm<FormData>({
resolver: zodResolver(schema), resolver: zodResolver(schema),
@@ -210,364 +199,329 @@ export const IdentityOidcAuthForm = ({
handlePopUpToggle("identityAuthMethod", false); handlePopUpToggle("identityAuthMethod", false);
createNotification({ createNotification({
text: `Successfully ${isCurrentAuthMethod ? "updated" : "configured"} auth method`, text: `Successfully ${isUpdate ? "updated" : "configured"} auth method`,
type: "success" type: "success"
}); });
reset(); reset();
} catch (err) { } catch (err) {
createNotification({ createNotification({
text: `Failed to ${identityAuthMethodData?.authMethod ? "update" : "configure"} identity`, text: `Failed to ${isUpdate ? "update" : "configure"} identity`,
type: "error" type: "error"
}); });
} }
}; };
return ( return (
<> <form onSubmit={handleSubmit(onFormSubmit)}>
<form onSubmit={handleSubmit(onFormSubmit)}> <Controller
<Controller control={control}
control={control} name="oidcDiscoveryUrl"
name="oidcDiscoveryUrl" render={({ field, fieldState: { error } }) => (
render={({ field, fieldState: { error } }) => ( <FormControl
<FormControl isRequired
isRequired label="OIDC Discovery URL"
label="OIDC Discovery URL" isError={Boolean(error)}
isError={Boolean(error)} errorText={error?.message}
errorText={error?.message}
>
<Input
{...field}
placeholder="https://token.actions.githubusercontent.com"
type="text"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="boundIssuer"
render={({ field, fieldState: { error } }) => (
<FormControl
isRequired
label="Issuer"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
type="text"
placeholder="https://token.actions.githubusercontent.com"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="caCert"
render={({ field, fieldState: { error } }) => (
<FormControl label="CA Certificate" errorText={error?.message} isError={Boolean(error)}>
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
</FormControl>
)}
/>
<Controller
control={control}
name="boundSubject"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Subject"
isError={Boolean(error)}
errorText={error?.message}
icon={
<Tooltip
className="text-center"
content={<span>This field supports glob patterns</span>}
>
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
</Tooltip>
}
>
<Input {...field} type="text" />
</FormControl>
)}
/>
<Controller
control={control}
name="boundAudiences"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Audiences"
isError={Boolean(error)}
errorText={error?.message}
icon={
<Tooltip
className="text-center"
content={<span>This field supports glob patterns</span>}
>
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
</Tooltip>
}
>
<Input {...field} type="text" placeholder="service1, service2" />
</FormControl>
)}
/>
{boundClaimsFields.map(({ id }, index) => (
<div className="mb-3 flex items-end space-x-2" key={id}>
<Controller
control={control}
name={`boundClaims.${index}.key`}
render={({ field, fieldState: { error } }) => {
return (
<FormControl
className="mb-0 flex-grow"
label={index === 0 ? "Claims" : undefined}
icon={
index === 0 ? (
<Tooltip
className="text-center"
content={<span>This field supports glob patterns</span>}
>
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
</Tooltip>
) : undefined
}
isError={Boolean(error)}
errorText={error?.message}
>
<Input
value={field.value}
onChange={(e) => field.onChange(e)}
placeholder="property"
/>
</FormControl>
);
}}
/>
<Controller
control={control}
name={`boundClaims.${index}.value`}
render={({ field, fieldState: { error } }) => {
return (
<FormControl
className="mb-0 flex-grow"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
value={field.value}
onChange={(e) => field.onChange(e)}
placeholder="value1, value2"
/>
</FormControl>
);
}}
/>
<IconButton
onClick={() => removeBoundClaimField(index)}
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={() =>
appendBoundClaimField({
key: "",
value: ""
})
}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
> >
Add Claims <Input
</Button> {...field}
</div> placeholder="https://token.actions.githubusercontent.com"
<Controller type="text"
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 </FormControl>
onClick={() => { )}
if (subscription?.ipAllowlisting) { />
removeAccessTokenTrustedIp(index); <Controller
return; control={control}
} name="boundIssuer"
render={({ field, fieldState: { error } }) => (
<FormControl
isRequired
label="Issuer"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
type="text"
placeholder="https://token.actions.githubusercontent.com"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="caCert"
render={({ field, fieldState: { error } }) => (
<FormControl label="CA Certificate" errorText={error?.message} isError={Boolean(error)}>
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
</FormControl>
)}
/>
<Controller
control={control}
name="boundSubject"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Subject"
isError={Boolean(error)}
errorText={error?.message}
icon={
<Tooltip
className="text-center"
content={<span>This field supports glob patterns</span>}
>
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
</Tooltip>
}
>
<Input {...field} type="text" />
</FormControl>
)}
/>
<Controller
control={control}
name="boundAudiences"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Audiences"
isError={Boolean(error)}
errorText={error?.message}
icon={
<Tooltip
className="text-center"
content={<span>This field supports glob patterns</span>}
>
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
</Tooltip>
}
>
<Input {...field} type="text" placeholder="service1, service2" />
</FormControl>
)}
/>
{boundClaimsFields.map(({ id }, index) => (
<div className="mb-3 flex items-end space-x-2" key={id}>
<Controller
control={control}
name={`boundClaims.${index}.key`}
render={({ field, fieldState: { error } }) => {
return (
<FormControl
className="mb-0 flex-grow"
label={index === 0 ? "Claims" : undefined}
icon={
index === 0 ? (
<Tooltip
className="text-center"
content={<span>This field supports glob patterns</span>}
>
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
</Tooltip>
) : undefined
}
isError={Boolean(error)}
errorText={error?.message}
>
<Input
value={field.value}
onChange={(e) => field.onChange(e)}
placeholder="property"
/>
</FormControl>
);
}}
/>
<Controller
control={control}
name={`boundClaims.${index}.value`}
render={({ field, fieldState: { error } }) => {
return (
<FormControl
className="mb-0 flex-grow"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
value={field.value}
onChange={(e) => field.onChange(e)}
placeholder="value1, value2"
/>
</FormControl>
);
}}
/>
handlePopUpOpen("upgradePlan"); <IconButton
}} onClick={() => removeBoundClaimField(index)}
size="lg" size="lg"
colorSchema="danger" colorSchema="danger"
variant="plain" variant="plain"
ariaLabel="update" ariaLabel="update"
className="p-3" className="p-3"
> >
<FontAwesomeIcon icon={faXmark} /> <FontAwesomeIcon icon={faXmark} />
</IconButton> </IconButton>
</div> </div>
))} ))}
<div className="my-4 ml-1"> <div className="my-4 ml-1">
<Button <Button
variant="outline_bg" variant="outline_bg"
onClick={() =>
appendBoundClaimField({
key: "",
value: ""
})
}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
>
Add Claims
</Button>
</div>
<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={() => { onClick={() => {
if (subscription?.ipAllowlisting) { if (subscription?.ipAllowlisting) {
appendAccessTokenTrustedIp({ removeAccessTokenTrustedIp(index);
ipAddress: "0.0.0.0/0"
});
return; return;
} }
handlePopUpOpen("upgradePlan"); handlePopUpOpen("upgradePlan");
}} }}
leftIcon={<FontAwesomeIcon icon={faPlus} />} size="lg"
size="xs" colorSchema="danger"
variant="plain"
ariaLabel="update"
className="p-3"
> >
Add IP Address <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 justify-between">
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{isUpdate ? "Update" : "Create"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button> </Button>
</div> </div>
<div className="flex justify-between"> {isUpdate && (
<div className="flex items-center"> <Button
{initialAuthMethod && identityAuthMethodData?.authMethod !== initialAuthMethod ? ( size="sm"
<Button colorSchema="danger"
className="mr-4" isLoading={isSubmitting}
size="sm" isDisabled={isSubmitting}
isLoading={isSubmitting} onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
isDisabled={isSubmitting} >
onClick={() => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", true)} Remove Auth Method
> </Button>
Overwrite )}
</Button> </div>
) : ( </form>
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Submit
</Button>
)}
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button>
</div>
{isCurrentAuthMethod && (
<Button
size="sm"
colorSchema="danger"
isLoading={isSubmitting}
isDisabled={isSubmitting}
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
>
Remove Auth Method
</Button>
)}
</div>
</form>
<DeleteActionModal
isOpen={internalPopUpState.popUp.overwriteAuthMethod?.isOpen}
title={`Are you sure want to overwrite ${initialAuthMethod || "the auth method"} on ${
identityAuthMethodData?.name ?? ""
}?`}
onChange={(isOpen) => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", isOpen)}
deleteKey="confirm"
buttonText="Overwrite"
onDeleteApproved={async () => {
const result = await trigger();
if (result) {
await revokeAuth(initialAuthMethod);
handleSubmit(onFormSubmit)();
} else {
createNotification({
text: "Please fill in all required fields",
type: "error"
});
internalPopUpState.handlePopUpToggle("overwriteAuthMethod", false);
}
}}
/>
</>
); );
}; };
@@ -5,7 +5,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod"; import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button, DeleteActionModal, FormControl, IconButton, Input } from "@app/components/v2"; import { Button, FormControl, IconButton, Input } from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context"; import { useOrganization, useSubscription } from "@app/context";
import { import {
useAddIdentityTokenAuth, useAddIdentityTokenAuth,
@@ -13,7 +13,7 @@ import {
useUpdateIdentityTokenAuth useUpdateIdentityTokenAuth
} from "@app/hooks/api"; } from "@app/hooks/api";
import { IdentityAuthMethod } from "@app/hooks/api/identities"; import { IdentityAuthMethod } from "@app/hooks/api/identities";
import { usePopUp, UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = z const schema = z
.object({ .object({
@@ -42,21 +42,18 @@ type Props = {
popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>, popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>,
state?: boolean state?: boolean
) => void; ) => void;
identityAuthMethodData: { identityAuthMethodData?: {
identityId: string; identityId: string;
name: string; name: string;
configuredAuthMethods?: IdentityAuthMethod[];
authMethod?: IdentityAuthMethod; authMethod?: IdentityAuthMethod;
}; };
initialAuthMethod: IdentityAuthMethod;
revokeAuth: (authMethod: IdentityAuthMethod) => Promise<void>;
}; };
export const IdentityTokenAuthForm = ({ export const IdentityTokenAuthForm = ({
handlePopUpOpen, handlePopUpOpen,
handlePopUpToggle, handlePopUpToggle,
identityAuthMethodData, identityAuthMethodData
initialAuthMethod,
revokeAuth
}: Props) => { }: Props) => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || ""; const orgId = currentOrg?.id || "";
@@ -65,11 +62,13 @@ export const IdentityTokenAuthForm = ({
const { mutateAsync: addMutateAsync } = useAddIdentityTokenAuth(); const { mutateAsync: addMutateAsync } = useAddIdentityTokenAuth();
const { mutateAsync: updateMutateAsync } = useUpdateIdentityTokenAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityTokenAuth();
const isCurrentAuthMethod = identityAuthMethodData?.authMethod === initialAuthMethod; const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
identityAuthMethodData.authMethod! || ""
);
const { data } = useGetIdentityTokenAuth(identityAuthMethodData?.identityId ?? "", { const { data } = useGetIdentityTokenAuth(identityAuthMethodData?.identityId ?? "", {
enabled: isCurrentAuthMethod enabled: isUpdate
}); });
const internalPopUpState = usePopUp(["overwriteAuthMethod"] as const);
const { const {
control, control,
@@ -124,189 +123,163 @@ export const IdentityTokenAuthForm = ({
handlePopUpToggle("identityAuthMethod", false); handlePopUpToggle("identityAuthMethod", false);
createNotification({ createNotification({
text: `Successfully ${isCurrentAuthMethod ? "updated" : "configured"} auth method`, text: `Successfully ${isUpdate ? "updated" : "configured"} auth method`,
type: "success" type: "success"
}); });
reset(); reset();
} catch (err) { } catch (err) {
createNotification({ createNotification({
text: `Failed to ${isCurrentAuthMethod ? "update" : "configure"} identity`, text: `Failed to ${isUpdate ? "update" : "configure"} identity`,
type: "error" type: "error"
}); });
} }
}; };
return ( return (
<> <form onSubmit={handleSubmit(onFormSubmit)}>
<form onSubmit={handleSubmit(onFormSubmit)}> <Controller
<Controller control={control}
control={control} defaultValue="2592000"
defaultValue="2592000" name="accessTokenTTL"
name="accessTokenTTL" render={({ field, fieldState: { error } }) => (
render={({ field, fieldState: { error } }) => ( <FormControl
<FormControl label="Access Token TTL (seconds)"
label="Access Token TTL (seconds)" isError={Boolean(error)}
isError={Boolean(error)} errorText={error?.message}
errorText={error?.message} >
> <Input {...field} placeholder="2592000" type="number" min="1" step="1" />
<Input {...field} placeholder="2592000" type="number" min="1" step="1" /> </FormControl>
</FormControl> )}
)} />
/> <Controller
<Controller control={control}
control={control} defaultValue="2592000"
defaultValue="2592000" name="accessTokenMaxTTL"
name="accessTokenMaxTTL" render={({ field, fieldState: { error } }) => (
render={({ field, fieldState: { error } }) => ( <FormControl
<FormControl label="Access Token Max TTL (seconds)"
label="Access Token Max TTL (seconds)" isError={Boolean(error)}
isError={Boolean(error)} errorText={error?.message}
errorText={error?.message} >
> <Input {...field} placeholder="2592000" type="number" min="1" step="1" />
<Input {...field} placeholder="2592000" type="number" min="1" step="1" /> </FormControl>
</FormControl> )}
)} />
/> <Controller
<Controller control={control}
control={control} defaultValue="0"
defaultValue="0" name="accessTokenNumUsesLimit"
name="accessTokenNumUsesLimit" render={({ field, fieldState: { error } }) => (
render={({ field, fieldState: { error } }) => ( <FormControl
<FormControl label="Access Token Max Number of Uses"
label="Access Token Max Number of Uses" isError={Boolean(error)}
isError={Boolean(error)} errorText={error?.message}
errorText={error?.message} >
> <Input {...field} placeholder="0" type="number" min="0" step="1" />
<Input {...field} placeholder="0" type="number" min="0" step="1" /> </FormControl>
</FormControl> )}
)} />
/> {accessTokenTrustedIpsFields.map(({ id }, index) => (
{accessTokenTrustedIpsFields.map(({ id }, index) => ( <div className="mb-3 flex items-end space-x-2" key={id}>
<div className="mb-3 flex items-end space-x-2" key={id}> <Controller
<Controller control={control}
control={control} name={`accessTokenTrustedIps.${index}.ipAddress`}
name={`accessTokenTrustedIps.${index}.ipAddress`} defaultValue="0.0.0.0/0"
defaultValue="0.0.0.0/0" render={({ field, fieldState: { error } }) => {
render={({ field, fieldState: { error } }) => { return (
return ( <FormControl
<FormControl className="mb-0 flex-grow"
className="mb-0 flex-grow" label={index === 0 ? "Access Token Trusted IPs" : undefined}
label={index === 0 ? "Access Token Trusted IPs" : undefined} isError={Boolean(error)}
isError={Boolean(error)} errorText={error?.message}
errorText={error?.message} >
> <Input
<Input value={field.value}
value={field.value} onChange={(e) => {
onChange={(e) => { if (subscription?.ipAllowlisting) {
if (subscription?.ipAllowlisting) { field.onChange(e);
field.onChange(e); return;
return; }
}
handlePopUpOpen("upgradePlan"); handlePopUpOpen("upgradePlan");
}} }}
placeholder="123.456.789.0" placeholder="123.456.789.0"
/> />
</FormControl> </FormControl>
); );
}} }}
/> />
<IconButton <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={() => { onClick={() => {
if (subscription?.ipAllowlisting) { if (subscription?.ipAllowlisting) {
appendAccessTokenTrustedIp({ removeAccessTokenTrustedIp(index);
ipAddress: "0.0.0.0/0"
});
return; return;
} }
handlePopUpOpen("upgradePlan"); handlePopUpOpen("upgradePlan");
}} }}
leftIcon={<FontAwesomeIcon icon={faPlus} />} size="lg"
size="xs" colorSchema="danger"
variant="plain"
ariaLabel="update"
className="p-3"
> >
Add IP Address <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 justify-between">
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{isUpdate ? "Update" : "Create"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button> </Button>
</div> </div>
<div className="flex justify-between"> {isUpdate && (
<div className="flex items-center"> <Button
{initialAuthMethod && identityAuthMethodData?.authMethod !== initialAuthMethod ? ( size="sm"
<Button colorSchema="danger"
className="mr-4" isLoading={isSubmitting}
size="sm" isDisabled={isSubmitting}
isLoading={isSubmitting} onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
isDisabled={isSubmitting} >
onClick={() => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", true)} Remove Auth Method
> </Button>
Overwrite )}
</Button> </div>
) : ( </form>
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Submit
</Button>
)}
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button>
</div>
{isCurrentAuthMethod && (
<Button
size="sm"
colorSchema="danger"
isLoading={isSubmitting}
isDisabled={isSubmitting}
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
>
Remove Auth Method
</Button>
)}
</div>
</form>
<DeleteActionModal
isOpen={internalPopUpState.popUp.overwriteAuthMethod?.isOpen}
title={`Are you sure want to overwrite ${initialAuthMethod || "the auth method"} on ${
identityAuthMethodData?.name ?? ""
}?`}
onChange={(isOpen) => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", isOpen)}
deleteKey="confirm"
buttonText="Overwrite"
onDeleteApproved={async () => {
await revokeAuth(initialAuthMethod);
handleSubmit(onFormSubmit)();
}}
/>
</>
); );
}; };
@@ -6,7 +6,7 @@ import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup"; import * as yup from "yup";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button, DeleteActionModal, FormControl, IconButton, Input } from "@app/components/v2"; import { Button, FormControl, IconButton, Input } from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context"; import { useOrganization, useSubscription } from "@app/context";
import { import {
useAddIdentityUniversalAuth, useAddIdentityUniversalAuth,
@@ -15,7 +15,7 @@ import {
} from "@app/hooks/api"; } from "@app/hooks/api";
import { IdentityAuthMethod } from "@app/hooks/api/identities"; import { IdentityAuthMethod } from "@app/hooks/api/identities";
import { IdentityTrustedIp } from "@app/hooks/api/identities/types"; import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
import { usePopUp, UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = yup const schema = yup
.object({ .object({
@@ -65,21 +65,18 @@ type Props = {
popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>, popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>,
state?: boolean state?: boolean
) => void; ) => void;
identityAuthMethodData: { identityAuthMethodData?: {
identityId: string; identityId: string;
name: string; name: string;
configuredAuthMethods?: IdentityAuthMethod[];
authMethod?: IdentityAuthMethod; authMethod?: IdentityAuthMethod;
}; };
initialAuthMethod: IdentityAuthMethod;
revokeAuth: (authMethod: IdentityAuthMethod) => Promise<void>;
}; };
export const IdentityUniversalAuthForm = ({ export const IdentityUniversalAuthForm = ({
handlePopUpOpen, handlePopUpOpen,
handlePopUpToggle, handlePopUpToggle,
identityAuthMethodData, identityAuthMethodData
initialAuthMethod,
revokeAuth
}: Props) => { }: Props) => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || ""; const orgId = currentOrg?.id || "";
@@ -87,11 +84,13 @@ export const IdentityUniversalAuthForm = ({
const { mutateAsync: addMutateAsync } = useAddIdentityUniversalAuth(); const { mutateAsync: addMutateAsync } = useAddIdentityUniversalAuth();
const { mutateAsync: updateMutateAsync } = useUpdateIdentityUniversalAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityUniversalAuth();
const isCurrentAuthMethod = identityAuthMethodData?.authMethod === initialAuthMethod; const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
identityAuthMethodData.authMethod! || ""
);
const { data } = useGetIdentityUniversalAuth(identityAuthMethodData?.identityId ?? "", { const { data } = useGetIdentityUniversalAuth(identityAuthMethodData?.identityId ?? "", {
enabled: isCurrentAuthMethod enabled: isUpdate
}); });
const internalPopUpState = usePopUp(["overwriteAuthMethod"] as const);
const { const {
control, control,
@@ -190,7 +189,7 @@ export const IdentityUniversalAuthForm = ({
handlePopUpToggle("identityAuthMethod", false); handlePopUpToggle("identityAuthMethod", false);
createNotification({ createNotification({
text: `Successfully ${isCurrentAuthMethod ? "updated" : "configured"} auth method`, text: `Successfully ${isUpdate ? "updated" : "created"} auth method`,
type: "success" type: "success"
}); });
@@ -199,8 +198,7 @@ export const IdentityUniversalAuthForm = ({
console.error(err); console.error(err);
const error = err as any; const error = err as any;
const text = const text =
error?.response?.data?.message ?? error?.response?.data?.message ?? `Failed to ${isUpdate ? "update" : "configure"} identity`;
`Failed to ${identityAuthMethodData?.authMethod ? "update" : "configure"} identity`;
createNotification({ createNotification({
text, text,
@@ -210,243 +208,216 @@ export const IdentityUniversalAuthForm = ({
}; };
return ( return (
<> <form onSubmit={handleSubmit(onFormSubmit)}>
<form onSubmit={handleSubmit(onFormSubmit)}> <Controller
<Controller control={control}
control={control} defaultValue="2592000"
defaultValue="2592000" name="accessTokenTTL"
name="accessTokenTTL" render={({ field, fieldState: { error } }) => (
render={({ field, fieldState: { error } }) => ( <FormControl
<FormControl label="Access Token TTL (seconds)"
label="Access Token TTL (seconds)" isError={Boolean(error)}
isError={Boolean(error)} errorText={error?.message}
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>
)}
/>
{clientSecretTrustedIpsFields.map(({ id }, index) => (
<div className="mb-3 flex items-end space-x-2" key={id}>
<Controller
control={control}
name={`clientSecretTrustedIps.${index}.ipAddress`}
defaultValue="0.0.0.0/0"
render={({ field, fieldState: { error } }) => {
return (
<FormControl
className="mb-0 flex-grow"
label={index === 0 ? "Client Secret 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) {
removeClientSecretTrustedIp(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) {
appendClientSecretTrustedIp({
ipAddress: "0.0.0.0/0"
});
return;
}
handlePopUpOpen("upgradePlan");
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
> >
Add IP Address <Input {...field} placeholder="2592000" type="number" min="1" step="1" />
</Button> </FormControl>
</div> )}
{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 justify-between">
<div className="flex items-center">
{initialAuthMethod && identityAuthMethodData?.authMethod !== initialAuthMethod ? (
<Button
className="mr-4"
size="sm"
isLoading={isSubmitting}
isDisabled={isSubmitting}
onClick={() => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", true)}
>
Overwrite
</Button>
) : (
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Submit
</Button>
)}
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button>
</div>
{isCurrentAuthMethod && (
<Button
size="sm"
colorSchema="danger"
isLoading={isSubmitting}
isDisabled={isSubmitting}
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
>
Remove Auth Method
</Button>
)}
</div>
</form>
<DeleteActionModal
isOpen={internalPopUpState.popUp.overwriteAuthMethod?.isOpen}
title={`Are you sure want to overwrite ${initialAuthMethod || "the auth method"} on ${
identityAuthMethodData?.name ?? ""
}?`}
onChange={(isOpen) => internalPopUpState.handlePopUpToggle("overwriteAuthMethod", isOpen)}
deleteKey="confirm"
buttonText="Overwrite"
onDeleteApproved={async () => {
await revokeAuth(initialAuthMethod);
handleSubmit(onFormSubmit)();
}}
/> />
</> <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>
)}
/>
{clientSecretTrustedIpsFields.map(({ id }, index) => (
<div className="mb-3 flex items-end space-x-2" key={id}>
<Controller
control={control}
name={`clientSecretTrustedIps.${index}.ipAddress`}
defaultValue="0.0.0.0/0"
render={({ field, fieldState: { error } }) => {
return (
<FormControl
className="mb-0 flex-grow"
label={index === 0 ? "Client Secret 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) {
removeClientSecretTrustedIp(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) {
appendClientSecretTrustedIp({
ipAddress: "0.0.0.0/0"
});
return;
}
handlePopUpOpen("upgradePlan");
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
>
Add IP Address
</Button>
</div>
{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 justify-between">
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{isUpdate ? "Edit" : "Create"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button>
</div>
{isUpdate && (
<Button
size="sm"
colorSchema="danger"
isLoading={isSubmitting}
isDisabled={isSubmitting}
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
>
Delete Auth Method
</Button>
)}
</div>
</form>
); );
}; };