From 50ef23e8a03a0523f29095e512e4c4f2e59c5d90 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Sat, 9 Dec 2023 22:18:38 +0700 Subject: [PATCH] Restructure MIs to more generic Identity --- backend/src/controllers/v1/authController.ts | 795 ++++++++++++++- .../controllers/v2/organizationsController.ts | 18 +- .../src/controllers/v2/workspaceController.ts | 145 +-- .../src/controllers/v3/secretsController.ts | 2 +- .../ee/controllers/v1/identitiesController.ts | 324 ++++++ backend/src/ee/controllers/v1/index.ts | 4 +- .../v1/machineIdentitiesController.ts | 934 ------------------ .../ee/controllers/v1/workspaceController.ts | 28 +- backend/src/ee/models/auditLog/auditLog.ts | 2 +- backend/src/ee/models/auditLog/enums.ts | 23 +- backend/src/ee/models/auditLog/types.ts | 119 ++- backend/src/ee/routes/v1/identities.ts | 31 + backend/src/ee/routes/v1/index.ts | 4 +- backend/src/ee/routes/v1/machineIdentities.ts | 66 -- backend/src/ee/services/EEAuditLogService.ts | 34 +- backend/src/ee/services/ProjectRoleService.ts | 48 +- backend/src/ee/services/RoleService.ts | 46 +- backend/src/helpers/organization.ts | 12 +- backend/src/helpers/workspace.ts | 4 +- backend/src/index.ts | 6 +- backend/src/interfaces/middleware/index.ts | 12 +- backend/src/middleware/requireAuth.ts | 2 +- backend/src/models/identity.ts | 38 + backend/src/models/identityAccessToken.ts | 45 +- ...ineMembership.ts => identityMembership.ts} | 12 +- ...bershipOrg.ts => identityMembershipOrg.ts} | 12 +- ...neIdentity.ts => identityUniversalAuth.ts} | 72 +- ...s => identityUniversalAuthClientSecret.ts} | 24 +- backend/src/models/index.ts | 12 +- backend/src/routes/v1/auth.ts | 60 ++ backend/src/routes/v2/organizations.ts | 4 +- backend/src/routes/v2/serviceTokenData.ts | 6 +- backend/src/routes/v2/workspace.ts | 17 +- backend/src/routes/v3/secrets.ts | 20 +- .../{machineIdentity.ts => identity.ts} | 43 +- .../utils/authn/authModeValidators/index.ts | 2 +- .../utils/authn/helpers/authDataExtractors.ts | 6 +- backend/src/utils/authn/helpers/index.ts | 23 +- backend/src/validation/auth.ts | 93 ++ backend/src/validation/identities.ts | 26 + backend/src/validation/index.ts | 2 +- backend/src/validation/integrationAuth.ts | 4 +- backend/src/validation/machineIdentity.ts | 97 -- backend/src/validation/organization.ts | 8 +- backend/src/validation/workspace.ts | 18 +- backend/src/variables/authentication.ts | 5 +- .../src/context/OrgPermissionContext/types.ts | 4 +- .../context/ProjectPermissionContext/types.ts | 4 +- .../src/hooks/api/auditLogs/constants.tsx | 14 +- frontend/src/hooks/api/auditLogs/enums.tsx | 15 +- frontend/src/hooks/api/auditLogs/types.tsx | 131 ++- .../src/hooks/api/identities/constants.tsx | 5 + frontend/src/hooks/api/identities/enums.tsx | 3 + frontend/src/hooks/api/identities/index.tsx | 14 + .../src/hooks/api/identities/mutations.tsx | 163 +++ frontend/src/hooks/api/identities/queries.tsx | 40 + .../types.ts | 177 ++-- frontend/src/hooks/api/index.tsx | 2 +- .../src/hooks/api/machineIdentities/index.tsx | 9 - .../hooks/api/machineIdentities/mutations.tsx | 119 --- .../hooks/api/machineIdentities/queries.tsx | 26 - frontend/src/hooks/api/organization/index.ts | 5 +- .../src/hooks/api/organization/queries.tsx | 16 +- frontend/src/hooks/api/workspace/index.tsx | 8 +- frontend/src/hooks/api/workspace/queries.tsx | 67 +- .../src/views/Org/MembersPage/MembersPage.tsx | 13 +- .../OrgIdentityTab.tsx} | 8 +- .../IdentityAuthMethodModal.tsx | 108 ++ .../IdentitySection/IdentityModal.tsx | 239 +++++ .../IdentitySection/IdentitySection.tsx | 118 +++ .../IdentitySection/IdentityTable.tsx | 267 +++++ ...dentityUniversalAuthClientSecretModal.tsx} | 77 +- .../IdentityUniversalAuthForm.tsx | 428 ++++++++ .../components/IdentitySection/index.tsx | 1 + .../OrgIdentityTab/components/index.tsx | 1 + .../components/OrgIdentityTab/index.tsx | 1 + .../AddMachineIdentityModal.tsx | 596 ----------- .../MachineIdentitySection.tsx | 104 -- .../MachineIdentityTable.tsx | 284 ------ .../MachineIdentitySection/index.tsx | 1 - .../components/index.tsx | 1 - .../OrgMachineIdentityTab/index.tsx | 1 - .../OrgRoleModifySection.tsx | 4 +- .../OrgRoleModifySection.utils.ts | 2 +- .../Org/MembersPage/components/index.tsx | 2 +- .../AuditLogsPage/components/LogsFilter.tsx | 6 +- .../AuditLogsPage/components/LogsTableRow.tsx | 14 +- .../views/Project/MembersPage/MembersPage.tsx | 10 +- .../IdentityTab.tsx} | 8 +- .../IdentitySection/IdentityModal.tsx} | 78 +- .../IdentitySection/IdentitySection.tsx} | 62 +- .../IdentitySection/IdentityTable.tsx} | 53 +- .../components/IdentitySection/index.tsx | 1 + .../IdentityTab/components/index.tsx | 1 + .../components/IdentityTab/index.tsx | 1 + .../MachineIdentitySection/index.tsx | 1 - .../MachineIdentityTab/components/index.tsx | 1 - .../components/MachineIdentityTab/index.tsx | 1 - .../ProjectRoleModifySection.tsx | 4 +- .../ProjectRoleModifySection.utils.ts | 2 +- .../SingleProjectPermission.tsx | 2 +- .../Project/MembersPage/components/index.tsx | 2 +- 102 files changed, 3652 insertions(+), 2981 deletions(-) create mode 100644 backend/src/ee/controllers/v1/identitiesController.ts delete mode 100644 backend/src/ee/controllers/v1/machineIdentitiesController.ts create mode 100644 backend/src/ee/routes/v1/identities.ts delete mode 100644 backend/src/ee/routes/v1/machineIdentities.ts create mode 100644 backend/src/models/identity.ts rename backend/src/models/{machineMembership.ts => identityMembership.ts} (69%) rename backend/src/models/{machineMembershipOrg.ts => identityMembershipOrg.ts} (66%) rename backend/src/models/{machineIdentity.ts => identityUniversalAuth.ts} (80%) rename backend/src/models/{machineIdentityClientSecret.ts => identityUniversalAuthClientSecret.ts} (70%) rename backend/src/utils/authn/authModeValidators/{machineIdentity.ts => identity.ts} (65%) create mode 100644 backend/src/validation/identities.ts delete mode 100644 backend/src/validation/machineIdentity.ts create mode 100644 frontend/src/hooks/api/identities/constants.tsx create mode 100644 frontend/src/hooks/api/identities/enums.tsx create mode 100644 frontend/src/hooks/api/identities/index.tsx create mode 100644 frontend/src/hooks/api/identities/mutations.tsx create mode 100644 frontend/src/hooks/api/identities/queries.tsx rename frontend/src/hooks/api/{machineIdentities => identities}/types.ts (56%) delete mode 100644 frontend/src/hooks/api/machineIdentities/index.tsx delete mode 100644 frontend/src/hooks/api/machineIdentities/mutations.tsx delete mode 100644 frontend/src/hooks/api/machineIdentities/queries.tsx rename frontend/src/views/Org/MembersPage/components/{OrgMachineIdentityTab/OrgMachineIdentityTab.tsx => OrgIdentityTab/OrgIdentityTab.tsx} (71%) create mode 100644 frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal.tsx create mode 100644 frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx create mode 100644 frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx create mode 100644 frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx rename frontend/src/views/Org/MembersPage/components/{OrgMachineIdentityTab/components/MachineIdentitySection/CreateClientSecretModal.tsx => OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthClientSecretModal.tsx} (83%) create mode 100644 frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthForm.tsx create mode 100644 frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/index.tsx create mode 100644 frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/index.tsx create mode 100644 frontend/src/views/Org/MembersPage/components/OrgIdentityTab/index.tsx delete mode 100644 frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/AddMachineIdentityModal.tsx delete mode 100644 frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/MachineIdentitySection.tsx delete mode 100644 frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/MachineIdentityTable.tsx delete mode 100644 frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/index.tsx delete mode 100644 frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/index.tsx delete mode 100644 frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/index.tsx rename frontend/src/views/Project/MembersPage/components/{MachineIdentityTab/MachineIdentityTab.tsx => IdentityTab/IdentityTab.tsx} (69%) rename frontend/src/views/Project/MembersPage/components/{MachineIdentityTab/components/MachineIdentitySection/AddMachineIdentityModal.tsx => IdentityTab/components/IdentitySection/IdentityModal.tsx} (70%) rename frontend/src/views/Project/MembersPage/components/{MachineIdentityTab/components/MachineIdentitySection/MachineIdentitySection.tsx => IdentityTab/components/IdentitySection/IdentitySection.tsx} (61%) rename frontend/src/views/Project/MembersPage/components/{MachineIdentityTab/components/MachineIdentitySection/MachineIdentityTable.tsx => IdentityTab/components/IdentitySection/IdentityTable.tsx} (83%) create mode 100644 frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentitySection/index.tsx create mode 100644 frontend/src/views/Project/MembersPage/components/IdentityTab/components/index.tsx create mode 100644 frontend/src/views/Project/MembersPage/components/IdentityTab/index.tsx delete mode 100644 frontend/src/views/Project/MembersPage/components/MachineIdentityTab/components/MachineIdentitySection/index.tsx delete mode 100644 frontend/src/views/Project/MembersPage/components/MachineIdentityTab/components/index.tsx delete mode 100644 frontend/src/views/Project/MembersPage/components/MachineIdentityTab/index.tsx diff --git a/backend/src/controllers/v1/authController.ts b/backend/src/controllers/v1/authController.ts index 363d9eee0..e41f517ee 100644 --- a/backend/src/controllers/v1/authController.ts +++ b/backend/src/controllers/v1/authController.ts @@ -1,21 +1,54 @@ import { Request, Response } from "express"; +import { Types } from "mongoose"; import jwt from "jsonwebtoken"; +import crypto from "crypto"; +import bcrypt from "bcrypt"; import * as bigintConversion from "bigint-conversion"; // eslint-disable-next-line @typescript-eslint/no-var-requires const jsrp = require("jsrp"); -import { LoginSRPDetail, TokenVersion, User } from "../../models"; +import { + IIdentity, + IIdentityTrustedIp, + IIdentityUniversalAuthClientSecret, + Identity, + IdentityAccessToken, + IdentityAuthMethod, + IdentityMembershipOrg, + IdentityUniversalAuth, + IdentityUniversalAuthClientSecret, + LoginSRPDetail, + TokenVersion, + User +} from "../../models"; import { clearTokens, createToken, issueAuthTokens } from "../../helpers/auth"; import { checkUserDevice } from "../../helpers/user"; import { AuthTokenType } from "../../variables"; -import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors"; +import { + BadRequestError, + ForbiddenRequestError, + ResourceNotFoundError, + UnauthorizedRequestError +} from "../../utils/errors"; import { getAuthSecret, getHttpsEnabled, - getJwtAuthLifetime + getJwtAuthLifetime, + getSaltRounds } from "../../config"; -import { ActorType } from "../../ee/models"; +import { ActorType, EventType, IRole } from "../../ee/models"; import { validateRequest } from "../../helpers/validation"; import * as reqValidator from "../../validation/auth"; +import { checkIPAgainstBlocklist, extractIPDetails, isValidIpOrCidr } from "../../utils/ip"; +import { getUserAgentType } from "../../utils/posthog"; +import { EEAuditLogService, EELicenseService } from "../../ee/services"; +import { + OrgPermissionActions, + OrgPermissionSubjects, + getAuthDataOrgPermissions, + getOrgRolePermissions, + isAtLeastAsPrivilegedOrg +} from "../../ee/services/RoleService"; +import { ForbiddenError } from "@casl/ability"; declare module "jsonwebtoken" { export interface AuthnJwtPayload extends jwt.JwtPayload { @@ -25,10 +58,10 @@ declare module "jsonwebtoken" { userId: string; refreshVersion?: number; } - export interface MachineAccessTokenJwtPayload extends jwt.JwtPayload { + export interface IdentityAccessTokenJwtPayload extends jwt.JwtPayload { _id: string; clientSecretId: string; - machineAccessTokenId: string; + identityAccessTokenId: string; authTokenType: string; } } @@ -268,3 +301,753 @@ export const getNewToken = async (req: Request, res: Response) => { export const handleAuthProviderCallback = (req: Request, res: Response) => { res.redirect(`/login/provider/success?token=${encodeURIComponent(req.providerAuthToken)}`); }; + +// ---- new IDENTITY logic + +const packageUniversalAuthClientSecretData = (identityUniversalAuthClientSecret: IIdentityUniversalAuthClientSecret) => ({ + _id: identityUniversalAuthClientSecret._id, + identityUniversalAuth: identityUniversalAuthClientSecret.identityUniversalAuth, + isClientSecretRevoked: identityUniversalAuthClientSecret.isClientSecretRevoked, + description: identityUniversalAuthClientSecret.description, + clientSecretPrefix: identityUniversalAuthClientSecret.clientSecretPrefix, + clientSecretNumUses: identityUniversalAuthClientSecret.clientSecretNumUses, + clientSecretNumUsesLimit: identityUniversalAuthClientSecret.clientSecretNumUsesLimit, + clientSecretTTL: identityUniversalAuthClientSecret.clientSecretTTL, + createdAt: identityUniversalAuthClientSecret.createdAt, + updatedAt: identityUniversalAuthClientSecret.updatedAt +}); + +/** + * Renews an access token by its TTL + * @param req + * @param res + */ + export const renewAccessToken = async (req: Request, res: Response) => { + const { + body: { + accessToken + } + } = await validateRequest(reqValidator.RenewAccessTokenV1, req); + + const decodedToken = ( + jwt.verify(accessToken, await getAuthSecret()) + ); + + if (decodedToken.authTokenType !== AuthTokenType.IDENTITY_ACCESS_TOKEN) throw UnauthorizedRequestError(); + + const identityAccessToken = await IdentityAccessToken.findOne({ + _id: decodedToken.identityAccessTokenId, + isAccessTokenRevoked: false + }); + + if (!identityAccessToken) throw UnauthorizedRequestError(); + + const { + accessTokenTTL, + accessTokenLastRenewedAt, + accessTokenMaxTTL, + createdAt: accessTokenCreatedAt + } = identityAccessToken; + + if (accessTokenTTL === accessTokenMaxTTL) throw UnauthorizedRequestError({ + message: "Failed to renew non-renewable access token" + }); + + // ttl check + if (accessTokenTTL > 0) { + const currentDate = new Date(); + if (accessTokenLastRenewedAt) { + // access token has been renewed + const accessTokenRenewed = new Date(accessTokenLastRenewedAt); + const ttlInMilliseconds = accessTokenTTL * 1000; + const expirationDate = new Date(accessTokenRenewed.getTime() + ttlInMilliseconds); + + if (currentDate > expirationDate) throw UnauthorizedRequestError({ + message: "Failed to renew MI access token due to TTL expiration" + }); + } else { + // access token has never been renewed + const accessTokenCreated = new Date(accessTokenCreatedAt); + const ttlInMilliseconds = accessTokenTTL * 1000; + const expirationDate = new Date(accessTokenCreated.getTime() + ttlInMilliseconds); + + if (currentDate > expirationDate) throw UnauthorizedRequestError({ + message: "Failed to renew MI access token due to TTL expiration" + }); + } + } + + // max ttl checks + if (accessTokenMaxTTL > 0) { + const accessTokenCreated = new Date(accessTokenCreatedAt); + const ttlInMilliseconds = accessTokenMaxTTL * 1000; + const currentDate = new Date(); + const expirationDate = new Date(accessTokenCreated.getTime() + ttlInMilliseconds); + + if (currentDate > expirationDate) throw UnauthorizedRequestError({ + message: "Failed to renew MI access token due to Max TTL expiration" + }); + + const extendToDate = new Date(currentDate.getTime() + accessTokenTTL); + if (extendToDate > expirationDate) throw UnauthorizedRequestError({ + message: "Failed to renew MI access token past its Max TTL expiration" + }); + } + + await IdentityAccessToken.findByIdAndUpdate( + identityAccessToken._id, + { + accessTokenLastRenewedAt: new Date() + } + ); + + return res.status(200).send({ + accessToken, + expiresIn: identityAccessToken.accessTokenTTL, + tokenType: "Bearer" + }); +} + +/** + * Return access token for identity with client id [clientId] + * and client secret [clientSecret] + * @param req + * @param res + */ + export const loginIdentityUniversalAuth = async (req: Request, res: Response) => { + const { + body: { + clientId, + clientSecret + } + } = await validateRequest(reqValidator.LoginUniversalAuthV1, req); + + const identityUniversalAuth = await IdentityUniversalAuth.findOne({ + clientId + }).populate<{ identity: IIdentity }>("identity"); + + if (!identityUniversalAuth) throw UnauthorizedRequestError(); + + checkIPAgainstBlocklist({ + ipAddress: req.realIP, + trustedIps: identityUniversalAuth.clientSecretTrustedIps + }); + + const clientSecretData = await IdentityUniversalAuthClientSecret.find({ + identity: identityUniversalAuth.identity, + isClientSecretRevoked: false + }); + + let validatedClientSecretDatum: IIdentityUniversalAuthClientSecret | undefined; + + for (const clientSecretDatum of clientSecretData) { + const isSecretValid = await bcrypt.compare( + clientSecret, + clientSecretDatum.clientSecretHash + ); + + if (isSecretValid) { + validatedClientSecretDatum = clientSecretDatum; + break; + } + } + + if (!validatedClientSecretDatum) throw UnauthorizedRequestError(); + + const { + clientSecretTTL, + clientSecretNumUses, + clientSecretNumUsesLimit, + } = validatedClientSecretDatum; + + if (clientSecretTTL > 0) { + const clientSecretCreated = new Date(validatedClientSecretDatum.createdAt) + const ttlInMilliseconds = clientSecretTTL * 1000; + const currentDate = new Date(); + const expirationTime = new Date(clientSecretCreated.getTime() + ttlInMilliseconds); + + if (currentDate > expirationTime) { + await IdentityUniversalAuthClientSecret.findByIdAndUpdate( + validatedClientSecretDatum._id, + { + isClientSecretRevoked: true + } + ); + + throw UnauthorizedRequestError({ + message: "Failed to authenticate identity credentials due to expired client secret" + }); + } + } + + if (clientSecretNumUsesLimit > 0 && clientSecretNumUses === clientSecretNumUsesLimit) { + // number of times client secret can be used for + // a login operation reached + await IdentityUniversalAuthClientSecret.findByIdAndUpdate( + validatedClientSecretDatum._id, + { + isClientSecretRevoked: true + }, + { + new: true + } + ); + + throw UnauthorizedRequestError({ + message: "Failed to authenticate identity credentials due to client secret number of uses limit reached" + }); + } + + // increment usage count by 1 + await IdentityUniversalAuthClientSecret + .findByIdAndUpdate( + validatedClientSecretDatum._id, + { + clientSecretLastUsedAt: new Date(), + $inc: { clientSecretNumUses: 1 } + }, + { + new: true + } + ); + + const identityAccessToken = await new IdentityAccessToken({ + identity: identityUniversalAuth.identity, + identityUniversalAuthClientSecret: validatedClientSecretDatum._id, + accessTokenNumUses: 0, + accessTokenNumUsesLimit: identityUniversalAuth.accessTokenNumUsesLimit, + accessTokenTTL: identityUniversalAuth.accessTokenTTL, + accessTokenMaxTTL: identityUniversalAuth.accessTokenMaxTTL, + accessTokenTrustedIps: identityUniversalAuth.accessTokenTrustedIps, + isAccessTokenRevoked: false + }).save(); + + // token version + const accessToken = createToken({ + payload: { + identityId: identityUniversalAuth.identity.toString(), + clientSecretId: validatedClientSecretDatum._id.toString(), + identityAccessTokenId: identityAccessToken._id.toString(), + authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN + }, + secret: await getAuthSecret() + }); + + const userAgent = req.headers["user-agent"] ?? ""; + + await EEAuditLogService.createAuditLog( + { + actor: { + type: ActorType.IDENTITY, + metadata: { + identityId: identityUniversalAuth.identity._id.toString(), + name: identityUniversalAuth.identity.name + } + }, + authPayload: identityUniversalAuth.identity, + ipAddress: req.realIP, + userAgent, + userAgentType: getUserAgentType(userAgent) + }, + { + type: EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH, + metadata: { + identityId: identityUniversalAuth.identity._id.toString(), + clientSecretId: validatedClientSecretDatum._id.toString(), + identityAccessTokenId: identityAccessToken._id.toString() + } + } + ); + + return res.status(200).send({ + accessToken, + expiresIn: identityUniversalAuth.accessTokenTTL, + tokenType: "Bearer" + }); +} + +export const addIdentityUniversalAuth = async (req: Request, res: Response) => { + const { + params: { identityId }, + body: { + clientSecretTrustedIps, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps, + } + } = await validateRequest(reqValidator.AddUniversalAuthToIdentityV1, req); + + const identityMembershipOrg = await IdentityMembershipOrg + .findOne({ + identity: new Types.ObjectId(identityId) + }) + .populate<{ + identity: IIdentity, + customRole: IRole + }>("identity customRole"); + + if (!identityMembershipOrg) throw ResourceNotFoundError({ + message: `Failed to find identity with id ${identityId}` + }); + + if (identityMembershipOrg.identity?.authMethod) throw BadRequestError({ + message: "Failed to add universal auth to already-configured identity" + }); + + if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) { + throw BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }) + } + + const { permission } = await getAuthDataOrgPermissions({ + authData: req.authData, + organizationId: identityMembershipOrg.organization + }); + + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Create, + OrgPermissionSubjects.Identity + ); + + const plan = await EELicenseService.getPlan(identityMembershipOrg.organization); + + // validate trusted ips + const reformattedClientSecretTrustedIps = clientSecretTrustedIps.map((clientSecretTrustedIp) => { + if (!plan.ipAllowlisting && clientSecretTrustedIp.ipAddress !== "0.0.0.0/0") return res.status(400).send({ + message: "Failed to add IP access range to service token due to plan restriction. Upgrade plan to add IP access range." + }); + + const isValidIPOrCidr = isValidIpOrCidr(clientSecretTrustedIp.ipAddress); + + if (!isValidIPOrCidr) return res.status(400).send({ + message: "The IP is not a valid IPv4, IPv6, or CIDR block" + }); + + return extractIPDetails(clientSecretTrustedIp.ipAddress); + }); + + const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { + if (!plan.ipAllowlisting && accessTokenTrustedIp.ipAddress !== "0.0.0.0/0") return res.status(400).send({ + message: "Failed to add IP access range to service token due to plan restriction. Upgrade plan to add IP access range." + }); + + const isValidIPOrCidr = isValidIpOrCidr(accessTokenTrustedIp.ipAddress); + + if (!isValidIPOrCidr) return res.status(400).send({ + message: "The IP is not a valid IPv4, IPv6, or CIDR block" + }); + + return extractIPDetails(accessTokenTrustedIp.ipAddress); + }); + + const identityUniversalAuth = await new IdentityUniversalAuth({ + identity: identityMembershipOrg.identity._id, + clientId: crypto.randomUUID(), + clientSecretTrustedIps: reformattedClientSecretTrustedIps, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps: reformattedAccessTokenTrustedIps, + }).save(); + + await Identity.findByIdAndUpdate( + identityMembershipOrg.identity._id, + { + authMethod: IdentityAuthMethod.UNIVERSAL_AUTH + } + ); + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.ADD_IDENTITY_UNIVERSAL_AUTH, + metadata: { + identityId: identityMembershipOrg.identity._id.toString(), + clientSecretTrustedIps: reformattedClientSecretTrustedIps as Array, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps: reformattedAccessTokenTrustedIps as Array + } + } + ); + + return res.status(200).send({ + identityUniversalAuth + }); +} + +export const updateIdentityUniversalAuth = async (req: Request, res: Response) => { + const { + params: { identityId }, + body: { + clientSecretTrustedIps, + accessTokenTTL, // TODO: validate this and max TTL + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps, + } + } = await validateRequest(reqValidator.UpdateUniversalAuthToIdentityV1, req); + + const identityMembershipOrg = await IdentityMembershipOrg + .findOne({ + identity: new Types.ObjectId(identityId) + }) + .populate<{ + identity: IIdentity, + customRole: IRole + }>("identity customRole"); + + if (!identityMembershipOrg) throw ResourceNotFoundError({ + message: `Failed to find identity with id ${identityId}` + }); + + if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.UNIVERSAL_AUTH) throw BadRequestError({ + message: "Failed to add universal auth to already-configured identity" + }); + + const { permission } = await getAuthDataOrgPermissions({ + authData: req.authData, + organizationId: identityMembershipOrg.organization + }); + + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Edit, + OrgPermissionSubjects.Identity + ); + + const plan = await EELicenseService.getPlan(identityMembershipOrg.organization); + + // validate trusted ips + let reformattedClientSecretTrustedIps; + if (clientSecretTrustedIps) { + reformattedClientSecretTrustedIps = clientSecretTrustedIps.map((clientSecretTrustedIp) => { + if (!plan.ipAllowlisting && clientSecretTrustedIp.ipAddress !== "0.0.0.0/0") return res.status(400).send({ + message: "Failed to add IP access range to service token due to plan restriction. Upgrade plan to add IP access range." + }); + + const isValidIPOrCidr = isValidIpOrCidr(clientSecretTrustedIp.ipAddress); + + if (!isValidIPOrCidr) return res.status(400).send({ + message: "The IP is not a valid IPv4, IPv6, or CIDR block" + }); + + return extractIPDetails(clientSecretTrustedIp.ipAddress); + }); + } + + let reformattedAccessTokenTrustedIps; + if (accessTokenTrustedIps) { + reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { + if (!plan.ipAllowlisting && accessTokenTrustedIp.ipAddress !== "0.0.0.0/0") return res.status(400).send({ + message: "Failed to add IP access range to service token due to plan restriction. Upgrade plan to add IP access range." + }); + + const isValidIPOrCidr = isValidIpOrCidr(accessTokenTrustedIp.ipAddress); + + if (!isValidIPOrCidr) return res.status(400).send({ + message: "The IP is not a valid IPv4, IPv6, or CIDR block" + }); + + return extractIPDetails(accessTokenTrustedIp.ipAddress); + }); + } + + const identityUniversalAuth = await IdentityUniversalAuth.findOneAndUpdate( + { + identity: identityMembershipOrg.identity._id, + }, + { + clientSecretTrustedIps: reformattedClientSecretTrustedIps, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps: reformattedAccessTokenTrustedIps, + }, + { + new: true + } + ); + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.UPDATE_IDENTITY_UNIVERSAL_AUTH, + metadata: { + identityId: identityMembershipOrg.identity._id.toString(), + clientSecretTrustedIps: reformattedClientSecretTrustedIps as Array, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps: reformattedAccessTokenTrustedIps as Array + } + } + ); + + return res.status(200).send({ + identityUniversalAuth + }); +} + +export const getIdentityUniversalAuth = async (req: Request, res: Response) => { + const { + params: { identityId } + } = await validateRequest(reqValidator.GetUniversalAuthForIdentityV1, req); + + const identityMembershipOrg = await IdentityMembershipOrg + .findOne({ + identity: new Types.ObjectId(identityId) + }) + .populate<{ + identity: IIdentity, + customRole: IRole + }>("identity customRole"); + + if (!identityMembershipOrg) throw ResourceNotFoundError({ + message: `Failed to find identity with id ${identityId}` + }); + + const { permission } = await getAuthDataOrgPermissions({ + authData: req.authData, + organizationId: identityMembershipOrg.organization + }); + + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Read, + OrgPermissionSubjects.Identity + ); + + if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.UNIVERSAL_AUTH) throw BadRequestError({ + message: "The identity does not have universal auth configured" + }); + + const identityUniversalAuth = await IdentityUniversalAuth.findOne({ + identity: identityMembershipOrg.identity._id, + }); + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.GET_IDENTITY_UNIVERSAL_AUTH, + metadata: { + identityId: identityMembershipOrg.identity._id.toString(), + } + } + ); + + return res.status(200).send({ + identityUniversalAuth + }); +} + +export const createUniversalAuthClientSecret = async (req: Request, res: Response) => { + const { + params: { identityId }, + body: { + description, + numUsesLimit, + ttl + } + } = await validateRequest(reqValidator.CreateUniversalAuthClientSecretV1, req); + + const identityMembershipOrg = await IdentityMembershipOrg.findOne({ + identity: new Types.ObjectId(identityId) + }).populate<{ + identity: IIdentity, + customRole: IRole + }>("identity customRole"); + + if (!identityMembershipOrg) throw ResourceNotFoundError({ + message: `Failed to find identity with id ${identityId}` + }); + + const { permission } = await getAuthDataOrgPermissions({ + authData: req.authData, + organizationId: identityMembershipOrg.organization + }); + + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Create, + OrgPermissionSubjects.Identity + ); + + if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.UNIVERSAL_AUTH) throw BadRequestError({ + message: "The identity does not have universal auth configured" + }); + + const rolePermission = await getOrgRolePermissions( + identityMembershipOrg?.customRole?.slug ?? identityMembershipOrg.role, + identityMembershipOrg.organization.toString() + ); + const hasRequiredPrivileges = isAtLeastAsPrivilegedOrg(permission, rolePermission); + + if (!hasRequiredPrivileges) throw ForbiddenRequestError({ + message: "Failed to create client secret for more privileged identity" + }); + + const clientSecret = crypto.randomBytes(32).toString("hex"); + const clientSecretHash = await bcrypt.hash(clientSecret, await getSaltRounds()); + + const identityUniversalAuth = await IdentityUniversalAuth.findOne({ + identity: identityMembershipOrg.identity._id + }); + + if (!identityUniversalAuth) throw ResourceNotFoundError(); + + const identityUniversalAuthClientSecret = await new IdentityUniversalAuthClientSecret({ + identity: identityMembershipOrg.identity._id, + identityUniversalAuth: identityUniversalAuth._id, + description, + clientSecretPrefix: clientSecret.slice(0, 4), + clientSecretHash, + clientSecretNumUses: 0, + clientSecretNumUsesLimit: numUsesLimit, + clientSecretTTL: ttl, + isClientSecretRevoked: false + }).save(); + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET, + metadata: { + identityId: identityMembershipOrg.identity._id.toString(), + clientSecretId: identityUniversalAuthClientSecret._id.toString() + } + } + ); + + return res.status(200).send({ + clientSecret, + clientSecretData: packageUniversalAuthClientSecretData(identityUniversalAuthClientSecret) + }); +} + +export const getUniversalAuthClientSecrets = async (req: Request, res: Response) => { + const { + params: { identityId } + } = await validateRequest(reqValidator.GetUniversalAuthClientSecretsV1, req); + + const identityMembershipOrg = await IdentityMembershipOrg.findOne({ + identity: new Types.ObjectId(identityId) + }).populate<{ + identity: IIdentity, + customRole: IRole + }>("identity customRole"); + + if (!identityMembershipOrg) throw ResourceNotFoundError(); + + const { permission } = await getAuthDataOrgPermissions({ + authData: req.authData, + organizationId: identityMembershipOrg.organization + }); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Read, + OrgPermissionSubjects.Identity + ); + + if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.UNIVERSAL_AUTH) throw BadRequestError({ + message: "The identity does not have universal auth configured" + }); + + const rolePermission = await getOrgRolePermissions( + identityMembershipOrg?.customRole?.slug ?? identityMembershipOrg.role, + identityMembershipOrg.organization.toString() + ); + const hasRequiredPrivileges = isAtLeastAsPrivilegedOrg(permission, rolePermission); + + if (!hasRequiredPrivileges) throw ForbiddenRequestError({ + message: "Failed to get client secrets for more privileged MI" + }); + + const clientSecretData = await IdentityUniversalAuthClientSecret + .find({ + identity: identityMembershipOrg.identity, + isClientSecretRevoked: false + }) + .sort({ createdAt: -1 }) + .limit(5); + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS, + metadata: { + identityId: identityMembershipOrg.identity._id.toString() + } + } + ); + + return res.status(200).send({ + clientSecretData: clientSecretData.map((clientSecretDatum) => packageUniversalAuthClientSecretData(clientSecretDatum)) + }); +} + +export const revokeUniversalAuthClientSecret = async (req: Request, res: Response) => { + const { + params: { identityId, clientSecretId } + } = await validateRequest(reqValidator.RevokeUniversalAuthClientSecretV1, req); + + const identityMembershipOrg = await IdentityMembershipOrg + .findOne({ + identity: new Types.ObjectId(identityId) + }) + .populate<{ + identity: IIdentity, + customRole: IRole + }>("identity customRole"); + + if (!identityMembershipOrg) throw ResourceNotFoundError({ + message: `Failed to find identity with id ${identityId}` + }); + + const { permission } = await getAuthDataOrgPermissions({ + authData: req.authData, + organizationId: identityMembershipOrg.organization + }); + + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Delete, + OrgPermissionSubjects.Identity + ); + + const rolePermission = await getOrgRolePermissions( + identityMembershipOrg?.customRole?.slug ?? identityMembershipOrg.role, + identityMembershipOrg.organization.toString() + ); + const hasRequiredPrivileges = isAtLeastAsPrivilegedOrg(permission, rolePermission); + + if (!hasRequiredPrivileges) throw ForbiddenRequestError({ + message: "Failed to delete client secrets for more privileged identity" + }); + + const clientSecretData = await IdentityUniversalAuthClientSecret.findOneAndUpdate( + { + _id: new Types.ObjectId(clientSecretId), + identity: identityMembershipOrg.identity._id + }, + { + isClientSecretRevoked: true + }, + { + new: true + } + ); + + if (!clientSecretData) throw ResourceNotFoundError(); + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET, + metadata: { + identityId: identityMembershipOrg.identity._id.toString(), + clientSecretId: clientSecretId + } + } + ); + + return res.status(200).send({ + clientSecretData: packageUniversalAuthClientSecretData(clientSecretData) + }) +} \ No newline at end of file diff --git a/backend/src/controllers/v2/organizationsController.ts b/backend/src/controllers/v2/organizationsController.ts index c06934520..f8ffd46c8 100644 --- a/backend/src/controllers/v2/organizationsController.ts +++ b/backend/src/controllers/v2/organizationsController.ts @@ -1,7 +1,7 @@ import { Request, Response } from "express"; import { Types } from "mongoose"; import { - MachineMembershipOrg, + IdentityMembershipOrg, Membership, MembershipOrg, Workspace @@ -419,15 +419,15 @@ export const deleteOrganizationById = async (req: Request, res: Response) => { }; /** - * Return list of service memberships for organization with id [organizationId] + * Return list of identity memberships for organization with id [organizationId] * @param req * @param res * @returns */ -export const getOrganizationMachineMemberships = async (req: Request, res: Response) => { + export const getOrganizationIdentityMemberships = async (req: Request, res: Response) => { const { params: { organizationId } - } = await validateRequest(reqValidator.GetOrgServiceMembersV2, req); + } = await validateRequest(reqValidator.GetOrgIdentityMembershipsV2, req); const { permission } = await getAuthDataOrgPermissions({ authData: req.authData, @@ -435,14 +435,14 @@ export const getOrganizationMachineMemberships = async (req: Request, res: Respo }); ForbiddenError.from(permission).throwUnlessCan( OrgPermissionActions.Read, - OrgPermissionSubjects.MachineIdentity + OrgPermissionSubjects.Identity ); - - const machineMemberships = await MachineMembershipOrg.find({ + + const identityMemberships = await IdentityMembershipOrg.find({ organization: new Types.ObjectId(organizationId) - }).populate("machineIdentity customRole"); + }).populate("identity customRole"); return res.status(200).send({ - machineMemberships + identityMemberships }); } \ No newline at end of file diff --git a/backend/src/controllers/v2/workspaceController.ts b/backend/src/controllers/v2/workspaceController.ts index e400b3de5..3ab41a38b 100644 --- a/backend/src/controllers/v2/workspaceController.ts +++ b/backend/src/controllers/v2/workspaceController.ts @@ -1,10 +1,10 @@ import { Request, Response } from "express"; import { Types } from "mongoose"; import { - IMachineIdentity, + IIdentity, + IdentityMembership, + IdentityMembershipOrg, Key, - MachineIdentity, - MachineMembership, Membership, ServiceTokenData, Workspace @@ -506,18 +506,18 @@ export const toggleAutoCapitalization = async (req: Request, res: Response) => { }; /** - * Add machine identity with id [machineId] to workspace + * Add identity with id [identityId] to workspace * with id [workspaceId] * @param req * @param res */ -export const addMachineToWorkspace = async (req: Request, res: Response) => { + export const addIdentityToWorkspace = async (req: Request, res: Response) => { const { - params: { workspaceId, machineId }, + params: { workspaceId, identityId }, body: { role } - } = await validateRequest(reqValidator.AddMachineToWorkspaceV2, req); + } = await validateRequest(reqValidator.AddIdentityToWorkspaceV2, req); const { permission } = await getAuthDataProjectPermissions({ authData: req.authData, @@ -526,35 +526,40 @@ export const addMachineToWorkspace = async (req: Request, res: Response) => { ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, - ProjectPermissionSub.MachineIdentity + ProjectPermissionSub.Identity ); - - let machineMembership = await MachineMembership.findOne({ - machineIdentity: new Types.ObjectId(machineId), + + let identityMembership = await IdentityMembership.findOne({ + identity: new Types.ObjectId(identityId), workspace: new Types.ObjectId(workspaceId) }); - if (machineMembership) throw BadRequestError({ - message: `Machine identity with id ${machineId} already exists in project with id ${workspaceId}` + if (identityMembership) throw BadRequestError({ + message: `Identity with id ${identityId} already exists in project with id ${workspaceId}` }); - const machineIdentity = await MachineIdentity.findById(machineId); - if (!machineIdentity) throw ResourceNotFoundError({ - message: `Failed to find machine identity with id ${machineId}` - }); const workspace = await Workspace.findById(workspaceId); if (!workspace) throw ResourceNotFoundError(); + + const identityMembershipOrg = await IdentityMembershipOrg.findOne({ + identity: new Types.ObjectId(identityId), + organization: workspace.organization + }); + + if (!identityMembershipOrg) throw ResourceNotFoundError({ + message: `Failed to find identity with id ${identityId}` + }); - if (!machineIdentity.organization.equals(workspace.organization)) throw BadRequestError({ - message: "Failed to add machine identity to project in another organization" + if (!identityMembershipOrg.organization.equals(workspace.organization)) throw BadRequestError({ + message: "Failed to add identity to project in another organization" }); const rolePermission = await getWorkspaceRolePermissions(role, workspaceId); const isAsPrivilegedAsIntendedRole = isAtLeastAsPrivilegedWorkspace(permission, rolePermission); if (!isAsPrivilegedAsIntendedRole) throw ForbiddenRequestError({ - message: "Failed to add MI to project with more privileged role" + message: "Failed to add identity to project with more privileged role" }); let customRole; @@ -571,31 +576,31 @@ export const addMachineToWorkspace = async (req: Request, res: Response) => { } } - machineMembership = await new MachineMembership({ - machineIdentity: machineIdentity._id, + identityMembership = await new IdentityMembership({ + identity: identityMembershipOrg.identity, workspace: new Types.ObjectId(workspaceId), role: customRole ? CUSTOM : role, customRole }).save(); return res.status(200).send({ - machineMembership + identityMembership }); } /** - * Update role of machine identity with id [machineId] in workspace + * Update role of identity with id [identityId] in workspace * with id [workspaceId] to [role] * @param req * @param res */ - export const updateMachineWorkspaceRole = async (req: Request, res: Response) => { + export const updateIdentityWorkspaceRole = async (req: Request, res: Response) => { const { - params: { workspaceId, machineId }, + params: { workspaceId, identityId }, body: { role } - } = await validateRequest(reqValidator.UpdateMachineWorkspaceRoleV2, req); + } = await validateRequest(reqValidator.UpdateIdentityWorkspaceRoleV2, req); const { permission } = await getAuthDataProjectPermissions({ authData: req.authData, @@ -604,37 +609,37 @@ export const addMachineToWorkspace = async (req: Request, res: Response) => { ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Edit, - ProjectPermissionSub.MachineIdentity + ProjectPermissionSub.Identity ); - let machineMembership = await MachineMembership + let identityMembership = await IdentityMembership .findOne({ - machineIdentity: new Types.ObjectId(machineId), + identity: new Types.ObjectId(identityId), workspace: new Types.ObjectId(workspaceId) }) .populate<{ - machineIdentity: IMachineIdentity, + identity: IIdentity, customRole: IRole - }>("machineIdentity customRole"); + }>("identity customRole"); - if (!machineMembership) throw BadRequestError({ - message: `Machine identity with id ${machineId} does not exist in project with id ${workspaceId}` + if (!identityMembership) throw BadRequestError({ + message: `Identity with id ${identityId} does not exist in project with id ${workspaceId}` }); - const machineIdentityRolePermission = await getWorkspaceRolePermissions( - machineMembership?.customRole?.slug ?? machineMembership.role, - machineMembership.workspace.toString() + const identityRolePermission = await getWorkspaceRolePermissions( + identityMembership?.customRole?.slug ?? identityMembership.role, + identityMembership.workspace.toString() ); - const isAsPrivilegedAsMachine = isAtLeastAsPrivilegedWorkspace(permission, machineIdentityRolePermission); - if (!isAsPrivilegedAsMachine) throw ForbiddenRequestError({ - message: "Failed to update role of more privileged MI" + const isAsPrivilegedAsIdentity = isAtLeastAsPrivilegedWorkspace(permission, identityRolePermission); + if (!isAsPrivilegedAsIdentity) throw ForbiddenRequestError({ + message: "Failed to update role of more privileged identity" }); const rolePermission = await getWorkspaceRolePermissions(role, workspaceId); const isAsPrivilegedAsIntendedRole = isAtLeastAsPrivilegedWorkspace(permission, rolePermission); if (!isAsPrivilegedAsIntendedRole) throw ForbiddenRequestError({ - message: "Failed to update MI to a more privileged role" + message: "Failed to update identity to a more privileged role" }); let customRole; @@ -651,9 +656,9 @@ export const addMachineToWorkspace = async (req: Request, res: Response) => { } } - machineMembership = await MachineMembership.findOneAndUpdate( + identityMembership = await IdentityMembership.findOneAndUpdate( { - machineIdentity: machineMembership.machineIdentity, + identity: identityMembership.identity._id, workspace: new Types.ObjectId(workspaceId), }, { @@ -666,20 +671,20 @@ export const addMachineToWorkspace = async (req: Request, res: Response) => { ); return res.status(200).send({ - machineMembership + identityMembership }); } /** - * Delete machine identity with id [machineId] to workspace + * Delete identity with id [identityId] to workspace * with id [workspaceId] * @param req * @param res */ - export const deleteMachineFromWorkspace = async (req: Request, res: Response) => { + export const deleteIdentityFromWorkspace = async (req: Request, res: Response) => { const { - params: { workspaceId, machineId } - } = await validateRequest(reqValidator.DeleteMachineFromWorkspaceV2, req); + params: { workspaceId, identityId } + } = await validateRequest(reqValidator.DeleteIdentityFromWorkspaceV2, req); const { permission } = await getAuthDataProjectPermissions({ authData: req.authData, @@ -688,49 +693,49 @@ export const addMachineToWorkspace = async (req: Request, res: Response) => { ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, - ProjectPermissionSub.MachineIdentity + ProjectPermissionSub.Identity ); - const machineMembership = await MachineMembership + const identityMembership = await IdentityMembership .findOne({ - machineIdentity: new Types.ObjectId(machineId), + identity: new Types.ObjectId(identityId), workspace: new Types.ObjectId(workspaceId) }) .populate<{ - machineIdentity: IMachineIdentity, + identity: IIdentity, customRole: IRole - }>("machineIdentity customRole"); + }>("identity customRole"); - if (!machineMembership) throw ResourceNotFoundError({ - message: `Machine with id ${machineId} does not exist in project with id ${workspaceId}` + if (!identityMembership) throw ResourceNotFoundError({ + message: `Identity with id ${identityId} does not exist in project with id ${workspaceId}` }); - const machineIdentityRolePermission = await getWorkspaceRolePermissions( - machineMembership?.customRole?.slug ?? machineMembership.role, - machineMembership.workspace.toString() + const identityRolePermission = await getWorkspaceRolePermissions( + identityMembership?.customRole?.slug ?? identityMembership.role, + identityMembership.workspace.toString() ); - const isAsPrivilegedAsMachine = isAtLeastAsPrivilegedWorkspace(permission, machineIdentityRolePermission); - if (!isAsPrivilegedAsMachine) throw ForbiddenRequestError({ - message: "Failed to remove more privileged MI from project" + const isAsPrivilegedAsIdentity = isAtLeastAsPrivilegedWorkspace(permission, identityRolePermission); + if (!isAsPrivilegedAsIdentity) throw ForbiddenRequestError({ + message: "Failed to remove more privileged identity from project" }); - await MachineMembership.findByIdAndDelete(machineMembership._id); + await IdentityMembership.findByIdAndDelete(identityMembership._id); return res.status(200).send({ - machineMembership + identityMembership }); } /** - * Return list of machine identity memberships for workspace with id [workspaceId] + * Return list of identity memberships for workspace with id [workspaceId] * @param req * @param res * @returns */ - export const getWorkspaceMachineMemberships = async (req: Request, res: Response) => { + export const getWorkspaceIdentityMemberships = async (req: Request, res: Response) => { const { params: { workspaceId } - } = await validateRequest(reqValidator.GetWorkspaceMachineMembersV2, req); + } = await validateRequest(reqValidator.GetWorkspaceIdentityMembersV2, req); const { permission } = await getAuthDataProjectPermissions({ authData: req.authData, @@ -739,14 +744,14 @@ export const addMachineToWorkspace = async (req: Request, res: Response) => { ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, - ProjectPermissionSub.MachineIdentity + ProjectPermissionSub.Identity ); - const machineMemberships = await MachineMembership.find({ + const identityMemberships = await IdentityMembership.find({ workspace: new Types.ObjectId(workspaceId) - }).populate("machineIdentity customRole"); + }).populate("identity customRole"); return res.status(200).send({ - machineMemberships + identityMemberships }); } \ No newline at end of file diff --git a/backend/src/controllers/v3/secretsController.ts b/backend/src/controllers/v3/secretsController.ts index 5359fa09b..88d5ab8d9 100644 --- a/backend/src/controllers/v3/secretsController.ts +++ b/backend/src/controllers/v3/secretsController.ts @@ -94,7 +94,7 @@ const checkSecretsPermission = async ({ }); return { authVerifier: () => true }; } - case ActorType.MACHINE: { + case ActorType.IDENTITY: { const { permission } = await getAuthDataProjectPermissions({ authData, workspaceId: new Types.ObjectId(workspaceId) diff --git a/backend/src/ee/controllers/v1/identitiesController.ts b/backend/src/ee/controllers/v1/identitiesController.ts new file mode 100644 index 000000000..c4e88f7c2 --- /dev/null +++ b/backend/src/ee/controllers/v1/identitiesController.ts @@ -0,0 +1,324 @@ +import { Request, Response } from "express"; +import { Types } from "mongoose"; +import { + IIdentity, + Identity, + IdentityAccessToken, + IdentityMembership, + IdentityMembershipOrg, + IdentityUniversalAuth, + IdentityUniversalAuthClientSecret, + Organization +} from "../../../models"; +import { + EventType, + IRole, + Role +} from "../../models"; +import { validateRequest } from "../../../helpers/validation"; +import * as reqValidator from "../../../validation/identities"; +import { + getAuthDataOrgPermissions, + getOrgRolePermissions, + isAtLeastAsPrivilegedOrg +} from "../../services/RoleService"; +import { + BadRequestError, + ForbiddenRequestError, + ResourceNotFoundError, +} from "../../../utils/errors"; +import { ADMIN, CUSTOM, MEMBER, NO_ACCESS } from "../../../variables"; +import { + OrgPermissionActions, + OrgPermissionSubjects +} from "../../services/RoleService"; +import { EEAuditLogService } from "../../services"; +import { ForbiddenError } from "@casl/ability"; + +/** + * Create identity + * @param req + * @param res + * @returns + */ +export const createIdentity = async (req: Request, res: Response) => { + const { + body: { + name, + organizationId, + role + } + } = await validateRequest(reqValidator.CreateIdentityV1, req); + + const { permission } = await getAuthDataOrgPermissions({ + authData: req.authData, + organizationId: new Types.ObjectId(organizationId) + }); + + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Create, + OrgPermissionSubjects.Identity + ); + + const rolePermission = await getOrgRolePermissions(role, organizationId); + const hasRequiredPrivileges = isAtLeastAsPrivilegedOrg(permission, rolePermission); + + if (!hasRequiredPrivileges) throw ForbiddenRequestError({ + message: "Failed to create a more privileged identity" + }); + + const organization = await Organization.findById(organizationId); + if (!organization) throw BadRequestError({ message: `Organization with id ${organizationId} not found` }); + + const isCustomRole = ![ADMIN, MEMBER, NO_ACCESS].includes(role); + + let customRole; + if (isCustomRole) { + customRole = await Role.findOne({ + slug: role, + isOrgRole: true, + organization: new Types.ObjectId(organizationId) + }); + + if (!customRole) throw BadRequestError({ message: "Role not found" }); + } + + const identity = await new Identity({ + name + }).save(); + + await new IdentityMembershipOrg({ + identity: identity._id, + organization: new Types.ObjectId(organizationId), + role: isCustomRole ? CUSTOM : role, + customRole + }).save(); + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.CREATE_IDENTITY, + metadata: { + identityId: identity._id.toString(), + name + } + }, + { + organizationId: new Types.ObjectId(organizationId) + } + ); + + return res.status(200).send({ + identity + }); +} + +/** + * Update identity with id [identityId] + * @param req + * @param res + * @returns + */ + export const updateIdentity = async (req: Request, res: Response) => { + const { + params: { identityId }, + body: { + name, + role + } + } = await validateRequest(reqValidator.UpdateIdentityV1, req); + + const identityMembershipOrg = await IdentityMembershipOrg + .findOne({ + identity: new Types.ObjectId(identityId) + }) + .populate<{ + identity: IIdentity, + customRole: IRole + }>("identity customRole"); + + if (!identityMembershipOrg) throw ResourceNotFoundError({ + message: `Failed to find identity with id ${identityId}` + }); + + const { permission } = await getAuthDataOrgPermissions({ + authData: req.authData, + organizationId: identityMembershipOrg.organization + }); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Edit, + OrgPermissionSubjects.Identity + ); + + const identityRolePermission = await getOrgRolePermissions( + identityMembershipOrg?.customRole?.slug ?? identityMembershipOrg.role, + identityMembershipOrg.organization.toString() + ); + const hasRequiredPrivileges = isAtLeastAsPrivilegedOrg(permission, identityRolePermission); + if (!hasRequiredPrivileges) throw ForbiddenRequestError({ + message: "Failed to update more privileged identity" + }); + + if (role) { + const rolePermission = await getOrgRolePermissions(role, identityMembershipOrg.organization.toString()); + const hasRequiredPrivileges = isAtLeastAsPrivilegedOrg(permission, rolePermission); + + if (!hasRequiredPrivileges) throw ForbiddenRequestError({ + message: "Failed to update identity to a more privileged role" + }); + } + + let customRole; + if (role) { + const isCustomRole = ![ADMIN, MEMBER, NO_ACCESS].includes(role); + if (isCustomRole) { + customRole = await Role.findOne({ + slug: role, + isOrgRole: true, + organization: identityMembershipOrg.organization + }); + + if (!customRole) throw BadRequestError({ message: "Role not found" }); + } + } + + const identity = await Identity.findByIdAndUpdate( + identityId, + { + name, + }, + { + new: true + } + ); + + if (!identity) throw BadRequestError({ + message: `Failed to update identity with id ${identityId}` + }); + + await IdentityMembershipOrg.findOneAndUpdate( + { + identity: identity._id + }, + { + role: customRole ? CUSTOM : role, + ...(customRole ? { + customRole + } : {}), + ...(role && !customRole ? { // non-custom role + $unset: { + customRole: 1 + } + } : {}) + }, + { + new: true + } + ); + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.UPDATE_IDENTITY, + metadata: { + identityId: identity._id.toString(), + name: identity.name, + } + }, + { + organizationId: identityMembershipOrg.organization + } + ); + + return res.status(200).send({ + identity + }); +} + +/** + * Delete identity with id [identityId] + * @param req + * @param res + * @returns + */ + export const deleteIdentity = async (req: Request, res: Response) => { + const { + params: { identityId } + } = await validateRequest(reqValidator.DeleteIdentityV1, req); + + const identityMembershipOrg = await IdentityMembershipOrg + .findOne({ + identity: new Types.ObjectId(identityId) + }) + .populate<{ + identity: IIdentity, + customRole: IRole + }>("identity customRole"); + + if (!identityMembershipOrg) throw ResourceNotFoundError({ + message: `Failed to find identity with id ${identityId}` + }); + + const { permission } = await getAuthDataOrgPermissions({ + authData: req.authData, + organizationId: identityMembershipOrg.organization + }); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Delete, + OrgPermissionSubjects.Identity + ); + + const identityRolePermission = await getOrgRolePermissions( + identityMembershipOrg?.customRole?.slug ?? identityMembershipOrg.role, + identityMembershipOrg.organization.toString() + ); + const hasRequiredPrivileges = isAtLeastAsPrivilegedOrg(permission, identityRolePermission); + if (!hasRequiredPrivileges) throw ForbiddenRequestError({ + message: "Failed to delete more privileged identity" + }); + + const identity = await Identity.findByIdAndDelete(identityMembershipOrg.identity); + if (!identity) throw ResourceNotFoundError({ + message: `Identity with id ${identityId} not found` + }); + + await IdentityMembershipOrg.findByIdAndDelete(identityMembershipOrg._id); + + await IdentityMembership.deleteMany({ + identity: identityMembershipOrg.identity + }); + + await IdentityUniversalAuth.deleteMany({ + identity: identityMembershipOrg.identity + }); + + await IdentityUniversalAuthClientSecret.deleteMany({ + identity: identityMembershipOrg.identity + }); + + await IdentityAccessToken.deleteMany({ + identity: identityMembershipOrg.identity + }); + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.DELETE_IDENTITY, + metadata: { + identityId: identity._id.toString() + } + }, + { + organizationId: identityMembershipOrg.organization + } + ); + + return res.status(200).send({ + identity + }); +} + + + + + diff --git a/backend/src/ee/controllers/v1/index.ts b/backend/src/ee/controllers/v1/index.ts index 0e3f18e29..0d17e5a06 100644 --- a/backend/src/ee/controllers/v1/index.ts +++ b/backend/src/ee/controllers/v1/index.ts @@ -1,4 +1,4 @@ -import * as machineIdentitiesController from "./machineIdentitiesController"; +import * as identitiesController from "./identitiesController"; import * as secretController from "./secretController"; import * as secretSnapshotController from "./secretSnapshotController"; import * as organizationsController from "./organizationsController"; @@ -14,7 +14,7 @@ import * as secretRotationProviderController from "./secretRotationProviderContr import * as secretRotationController from "./secretRotationController"; export { - machineIdentitiesController, + identitiesController, secretController, secretSnapshotController, organizationsController, diff --git a/backend/src/ee/controllers/v1/machineIdentitiesController.ts b/backend/src/ee/controllers/v1/machineIdentitiesController.ts deleted file mode 100644 index c0f54e3c2..000000000 --- a/backend/src/ee/controllers/v1/machineIdentitiesController.ts +++ /dev/null @@ -1,934 +0,0 @@ -import jwt from "jsonwebtoken"; -import bcrypt from "bcrypt"; -import crypto from "crypto"; -import { Request, Response } from "express"; -import { Types } from "mongoose"; -import { - IMachineIdentity, - IMachineIdentityClientSecret, - IMachineIdentityTrustedIp, - IdentityAccessToken, - MachineIdentity, - MachineIdentityClientSecret, - MachineMembership, - MachineMembershipOrg, - Organization, -} from "../../../models"; -import { - ActorType, - EventType, - IRole, - Role -} from "../../models"; -import { validateRequest } from "../../../helpers/validation"; -import * as reqValidator from "../../../validation/machineIdentity"; -import { createToken } from "../../../helpers/auth"; -import { - getAuthDataOrgPermissions, - getOrgRolePermissions, - isAtLeastAsPrivilegedOrg -} from "../../services/RoleService"; -import { - BadRequestError, - ForbiddenRequestError, - ResourceNotFoundError, - UnauthorizedRequestError -} from "../../../utils/errors"; -import { extractIPDetails, isValidIpOrCidr } from "../../../utils/ip"; -import { EEAuditLogService, EELicenseService } from "../../services"; -import { getAuthSecret, getSaltRounds } from "../../../config"; -import { ADMIN, AuthTokenType, CUSTOM, MEMBER, NO_ACCESS } from "../../../variables"; -import { - OrgPermissionActions, - OrgPermissionSubjects -} from "../../services/RoleService"; -import { ForbiddenError } from "@casl/ability"; -import { checkIPAgainstBlocklist } from "../../../utils/ip"; -import { getUserAgentType } from "../../../utils/posthog"; - -const packageClientSecretData = (machineIdentityClientSecret: IMachineIdentityClientSecret) => ({ - _id: machineIdentityClientSecret._id, - machineIdentity: machineIdentityClientSecret.machineIdentity, - isClientSecretRevoked: machineIdentityClientSecret.isClientSecretRevoked, - description: machineIdentityClientSecret.description, - clientSecretPrefix: machineIdentityClientSecret.clientSecretPrefix, - clientSecretNumUses: machineIdentityClientSecret.clientSecretNumUses, - clientSecretNumUsesLimit: machineIdentityClientSecret.clientSecretNumUsesLimit, - clientSecretTTL: machineIdentityClientSecret.clientSecretTTL, - createdAt: machineIdentityClientSecret.createdAt, - updatedAt: machineIdentityClientSecret.updatedAt -}); - -/** - * Return client secrets for machine with id [machineId] - * @param req - * @param res - */ -export const getMIClientSecrets = async (req: Request, res: Response) => { - const { - params: { - machineId - } - } = await validateRequest(reqValidator.GetClientSecretsV1, req); - - const machineMembershipOrg = await MachineMembershipOrg.findOne({ - machineIdentity: new Types.ObjectId(machineId) - }).populate<{ - machineIdentity: IMachineIdentity, - customRole: IRole - }>("machineIdentity customRole"); - - if (!machineMembershipOrg) throw ResourceNotFoundError(); - - const { permission } = await getAuthDataOrgPermissions({ - authData: req.authData, - organizationId: machineMembershipOrg.organization - }); - ForbiddenError.from(permission).throwUnlessCan( - OrgPermissionActions.Read, - OrgPermissionSubjects.MachineIdentity - ); - - const rolePermission = await getOrgRolePermissions( - machineMembershipOrg?.customRole?.slug ?? machineMembershipOrg.role, - machineMembershipOrg.organization.toString() - ); - const hasRequiredPrivileges = isAtLeastAsPrivilegedOrg(permission, rolePermission); - - if (!hasRequiredPrivileges) throw ForbiddenRequestError({ - message: "Failed to get client secrets for more privileged MI" - }); - - const clientSecretData = await MachineIdentityClientSecret - .find({ - machineIdentity: machineMembershipOrg.machineIdentity, - isClientSecretRevoked: false - }) - .sort({ createdAt: -1 }) - .limit(5); - - await EEAuditLogService.createAuditLog( - req.authData, - { - type: EventType.GET_MACHINE_IDENTITY_CLIENT_SECRETS, - metadata: { - machineId: machineMembershipOrg.machineIdentity._id.toString() - } - }, - { - organizationId: machineMembershipOrg.organization - } - ); - - return res.status(200).send({ - clientSecretData: clientSecretData.map((clientSecretDatum) => packageClientSecretData(clientSecretDatum)) - }); -} - -/** - * Create a new client secret for machine with id [machineId] - * @param req - * @param res - */ -export const createMIClientSecret = async (req: Request, res: Response) => { - const { - params: { - machineId - }, - body: { - description, - ttl, - numUsesLimit - } - } = await validateRequest(reqValidator.CreateClientSecretV1, req); - - const machineMembershipOrg = await MachineMembershipOrg.findOne({ - machineIdentity: new Types.ObjectId(machineId) - }).populate<{ - machineIdentity: IMachineIdentity, - customRole: IRole - }>("machineIdentity customRole"); - - if (!machineMembershipOrg) throw ResourceNotFoundError(); - - const { permission } = await getAuthDataOrgPermissions({ - authData: req.authData, - organizationId: machineMembershipOrg.organization - }); - - ForbiddenError.from(permission).throwUnlessCan( - OrgPermissionActions.Create, - OrgPermissionSubjects.MachineIdentity - ); - - const rolePermission = await getOrgRolePermissions( - machineMembershipOrg?.customRole?.slug ?? machineMembershipOrg.role, - machineMembershipOrg.organization.toString() - ); - const hasRequiredPrivileges = isAtLeastAsPrivilegedOrg(permission, rolePermission); - - if (!hasRequiredPrivileges) throw ForbiddenRequestError({ - message: "Failed to create client secret for more privileged MI" - }); - - const clientSecret = crypto.randomBytes(32).toString("hex"); - const clientSecretHash = await bcrypt.hash(clientSecret, await getSaltRounds()); - - const machineIdentityClientSecret = await new MachineIdentityClientSecret({ - machineIdentity: machineMembershipOrg.machineIdentity, - description, - clientSecretPrefix: clientSecret.slice(0, 4), - clientSecretHash, - clientSecretNumUses: 0, - clientSecretNumUsesLimit: numUsesLimit, - clientSecretTTL: ttl, - isClientSecretRevoked: false - }).save(); - - await EEAuditLogService.createAuditLog( - req.authData, - { - type: EventType.CREATE_MACHINE_IDENTITY_CLIENT_SECRET, - metadata: { - machineId: machineMembershipOrg.machineIdentity._id.toString(), - clientSecretId: machineIdentityClientSecret._id.toString() - } - }, - { - organizationId: machineMembershipOrg.organization - } - ); - - return res.status(200).send({ - clientSecret, - clientSecretData: packageClientSecretData(machineIdentityClientSecret) - }); -} - -/** - * Delete client secret with id [clientSecretId] - * @param req - * @param res - */ -export const revokeMIClientSecret = async (req: Request, res: Response) => { - const { - params: { - machineId, - clientSecretId - } - } = await validateRequest(reqValidator.DeleteClientSecretV1, req); - - const machineMembershipOrg = await MachineMembershipOrg - .findOne({ - machineIdentity: new Types.ObjectId(machineId) - }) - .populate<{ - machineIdentity: IMachineIdentity, - customRole: IRole - }>("machineIdentity customRole"); - - if (!machineMembershipOrg) throw ResourceNotFoundError({ - message: `Failed to find machine identity with id ${machineId}` - }); - - const { permission } = await getAuthDataOrgPermissions({ - authData: req.authData, - organizationId: machineMembershipOrg.organization - }); - - ForbiddenError.from(permission).throwUnlessCan( - OrgPermissionActions.Delete, - OrgPermissionSubjects.MachineIdentity - ); - - const rolePermission = await getOrgRolePermissions( - machineMembershipOrg?.customRole?.slug ?? machineMembershipOrg.role, - machineMembershipOrg.organization.toString() - ); - const hasRequiredPrivileges = isAtLeastAsPrivilegedOrg(permission, rolePermission); - - if (!hasRequiredPrivileges) throw ForbiddenRequestError({ - message: "Failed to delete client secrets for more privileged MI" - }); - - const machineIdentityClientSecret = await MachineIdentityClientSecret.findOneAndUpdate( - { - _id: clientSecretId, - machineIdentity: machineId - }, - { - isClientSecretRevoked: true - }, - { - new: true - } - ); - - if (!machineIdentityClientSecret) throw ResourceNotFoundError(); - - await EEAuditLogService.createAuditLog( - req.authData, - { - type: EventType.REVOKE_MACHINE_IDENTITY_CLIENT_SECRET, - metadata: { - machineId: machineMembershipOrg.machineIdentity._id.toString(), - clientSecretId: clientSecretId - } - }, - { - organizationId: machineMembershipOrg.organization - } - ); - - return res.status(200).send({ - clientSecretData: packageClientSecretData(machineIdentityClientSecret) - }) -} - -/** - * Return access token for machine identity with client id [clientId] - * and client secret [clientSecret] - * @param req - * @param res - */ -export const loginMI = async (req: Request, res: Response) => { - const { - body: { - clientId, - clientSecret - } - } = await validateRequest(reqValidator.LoginMachineIdentityV1, req); - - const machineIdentity = await MachineIdentity.findOne({ - clientId - }); - - if (!machineIdentity) throw UnauthorizedRequestError(); - - checkIPAgainstBlocklist({ - ipAddress: req.realIP, - trustedIps: machineIdentity.clientSecretTrustedIps - }); - - const clientSecretData = await MachineIdentityClientSecret.find({ - machineIdentity: machineIdentity._id, - isClientSecretRevoked: false - }); - - let validatedClientSecretDatum: IMachineIdentityClientSecret | undefined; - - for (const clientSecretDatum of clientSecretData) { - const isSecretValid = await bcrypt.compare( - clientSecret, - clientSecretDatum.clientSecretHash - ); - - if (isSecretValid) { - validatedClientSecretDatum = clientSecretDatum; - break; - } - } - - if (!validatedClientSecretDatum) throw UnauthorizedRequestError(); - - const { - clientSecretTTL, - clientSecretNumUses, - clientSecretNumUsesLimit, - } = validatedClientSecretDatum; - - if (clientSecretTTL > 0) { - const clientSecretCreated = new Date(validatedClientSecretDatum.createdAt) - const ttlInMilliseconds = clientSecretTTL * 1000; - const currentDate = new Date(); - const expirationTime = new Date(clientSecretCreated.getTime() + ttlInMilliseconds); - - if (currentDate > expirationTime) { - await MachineIdentityClientSecret.findByIdAndUpdate( - validatedClientSecretDatum._id, - { - isClientSecretRevoked: true - } - ); - - throw UnauthorizedRequestError({ - message: "Failed to authenticate MI credentials due to expired client secret" - }); - } - } - - if (clientSecretNumUsesLimit > 0 && clientSecretNumUses === clientSecretNumUsesLimit) { - // number of times client secret can be used for - // a login operation reached - await MachineIdentityClientSecret.findByIdAndUpdate( - validatedClientSecretDatum._id, - { - isClientSecretRevoked: true - }, - { - new: true - } - ); - - throw UnauthorizedRequestError({ - message: "Failed to authenticate MI credentials due to client secret number of uses limit reached" - }); - } - - // increment usage count by 1 - await MachineIdentityClientSecret.findByIdAndUpdate( - validatedClientSecretDatum._id, - { - clientSecretLastUsedAt: new Date(), - $inc: { clientSecretNumUses: 1 } - }, - { - new: true - } - ); - - const identityAccessToken = await new IdentityAccessToken({ - machineIdentity: machineIdentity._id, - machineIdentityClientSecret: validatedClientSecretDatum._id, - accessTokenNumUses: 0, - accessTokenNumUsesLimit: machineIdentity.accessTokenNumUsesLimit, - accessTokenTTL: machineIdentity.accessTokenTTL, - accessTokenMaxTTL: machineIdentity.accessTokenMaxTTL, - isAccessTokenRevoked: false - }).save(); - - // token version - const accessToken = createToken({ - payload: { - machineId: machineIdentity._id.toString(), - clientSecretId: validatedClientSecretDatum._id.toString(), - identityAccessTokenId: identityAccessToken._id.toString(), - authTokenType: AuthTokenType.MACHINE_ACCESS_TOKEN - }, - expiresIn: machineIdentity.accessTokenTTL, - secret: await getAuthSecret() - }); - - const userAgent = req.headers["user-agent"] ?? ""; - - await EEAuditLogService.createAuditLog( - { - actor: { - type: ActorType.MACHINE, - metadata: { - machineId: machineIdentity._id.toString(), - name: machineIdentity.name - } - }, - authPayload: machineIdentity, - ipAddress: req.realIP, - userAgent, - userAgentType: getUserAgentType(userAgent) - }, - { - type: EventType.LOGIN_MACHINE_IDENTITY, - metadata: { - machineId: machineIdentity._id.toString(), - machineAccessTokenId: identityAccessToken._id.toString(), - clientSecretId: validatedClientSecretDatum._id.toString(), - identityAccessTokenId: identityAccessToken._id.toString() - } - }, - { - organizationId: machineIdentity.organization - } - ); - - return res.status(200).send({ - accessToken, - expiresIn: machineIdentity.accessTokenTTL, - tokenType: "Bearer" - }); -} - -/** - * Renews an access token by its TTL - * @param req - * @param res - */ -export const renewAccessToken = async (req: Request, res: Response) => { - const { - body: { - accessToken - } - } = await validateRequest(reqValidator.RenewAccessTokenV1, req); - - const decodedToken = ( - jwt.verify(accessToken, await getAuthSecret()) - ); - - if (decodedToken.authTokenType !== AuthTokenType.MACHINE_ACCESS_TOKEN) throw UnauthorizedRequestError(); - - const machineIdentityAccessToken = await IdentityAccessToken.findOne({ - _id: decodedToken.identityAccessTokenId, - isAccessTokenRevoked: false - }); - - if (!machineIdentityAccessToken) throw UnauthorizedRequestError(); - - const { - accessTokenTTL, - accessTokenLastRenewedAt, - accessTokenMaxTTL, - createdAt: accessTokenCreatedAt - } = machineIdentityAccessToken; - - if (accessTokenTTL === accessTokenMaxTTL) throw UnauthorizedRequestError({ - message: "Failed to renew non-renewable access token" - }); - - // ttl check - if (accessTokenTTL > 0) { - const currentDate = new Date(); - if (accessTokenLastRenewedAt) { - // access token has been renewed - const accessTokenRenewed = new Date(accessTokenLastRenewedAt); - const ttlInMilliseconds = accessTokenTTL * 1000; - const expirationDate = new Date(accessTokenRenewed.getTime() + ttlInMilliseconds); - - if (currentDate > expirationDate) throw UnauthorizedRequestError({ - message: "Failed to renew MI access token due to TTL expiration" - }); - } else { - // access token has never been renewed - const accessTokenCreated = new Date(accessTokenCreatedAt); - const ttlInMilliseconds = accessTokenTTL * 1000; - const expirationDate = new Date(accessTokenCreated.getTime() + ttlInMilliseconds); - - if (currentDate > expirationDate) throw UnauthorizedRequestError({ - message: "Failed to renew MI access token due to TTL expiration" - }); - } - } - - // max ttl checks - if (accessTokenMaxTTL > 0) { - const accessTokenCreated = new Date(accessTokenCreatedAt); - const ttlInMilliseconds = accessTokenMaxTTL * 1000; - const currentDate = new Date(); - const expirationDate = new Date(accessTokenCreated.getTime() + ttlInMilliseconds); - - if (currentDate > expirationDate) throw UnauthorizedRequestError({ - message: "Failed to renew MI access token due to Max TTL expiration" - }); - - const extendToDate = new Date(currentDate.getTime() + accessTokenTTL); - if (extendToDate > expirationDate) throw UnauthorizedRequestError({ - message: "Failed to renew MI access token past its Max TTL expiration" - }); - } - - await IdentityAccessToken.findByIdAndUpdate( - machineIdentityAccessToken._id, - { - accessTokenLastRenewedAt: new Date() - } - ); - - return res.status(200).send({ - accessToken, - expiresIn: machineIdentityAccessToken.accessTokenTTL, - tokenType: "Bearer" - }); -} - -/** - * Create machine identity - * @param req - * @param res - * @returns - */ -export const createMachineIdentity = async (req: Request, res: Response) => { - const { - body: { - name, - organizationId, - role, - clientSecretTrustedIps, - accessTokenTrustedIps, - accessTokenTTL, - accessTokenMaxTTL, - accessTokenNumUsesLimit - } - } = await validateRequest(reqValidator.CreateMachineIdentityV1, req); - - if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) { - throw BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }) - } - - const { permission } = await getAuthDataOrgPermissions({ - authData: req.authData, - organizationId: new Types.ObjectId(organizationId) - }); - - ForbiddenError.from(permission).throwUnlessCan( - OrgPermissionActions.Create, - OrgPermissionSubjects.MachineIdentity - ); - - const rolePermission = await getOrgRolePermissions(role, organizationId); - const hasRequiredPrivileges = isAtLeastAsPrivilegedOrg(permission, rolePermission); - - if (!hasRequiredPrivileges) throw ForbiddenRequestError({ - message: "Failed to create a more privileged MI" - }); - - const organization = await Organization.findById(organizationId); - if (!organization) throw BadRequestError({ message: `Organization with id ${organizationId} not found` }); - - const isCustomRole = ![ADMIN, MEMBER, NO_ACCESS].includes(role); - - let customRole; - if (isCustomRole) { - customRole = await Role.findOne({ - slug: role, - isOrgRole: true, - organization: new Types.ObjectId(organizationId) - }); - - if (!customRole) throw BadRequestError({ message: "Role not found" }); - } - - const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId)); - - // validate trusted ips - const reformattedClientSecretTrustedIps = clientSecretTrustedIps.map((clientSecretTrustedIp) => { - if (!plan.ipAllowlisting && clientSecretTrustedIp.ipAddress !== "0.0.0.0/0") return res.status(400).send({ - message: "Failed to add IP access range to service token due to plan restriction. Upgrade plan to add IP access range." - }); - - const isValidIPOrCidr = isValidIpOrCidr(clientSecretTrustedIp.ipAddress); - - if (!isValidIPOrCidr) return res.status(400).send({ - message: "The IP is not a valid IPv4, IPv6, or CIDR block" - }); - - return extractIPDetails(clientSecretTrustedIp.ipAddress); - }); - - const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { - if (!plan.ipAllowlisting && accessTokenTrustedIp.ipAddress !== "0.0.0.0/0") return res.status(400).send({ - message: "Failed to add IP access range to service token due to plan restriction. Upgrade plan to add IP access range." - }); - - const isValidIPOrCidr = isValidIpOrCidr(accessTokenTrustedIp.ipAddress); - - if (!isValidIPOrCidr) return res.status(400).send({ - message: "The IP is not a valid IPv4, IPv6, or CIDR block" - }); - - return extractIPDetails(accessTokenTrustedIp.ipAddress); - }); - - const machineIdentity = await new MachineIdentity({ - clientId: crypto.randomUUID(), - name, - organization: new Types.ObjectId(organizationId), - accessTokenTTL, - accessTokenMaxTTL, - accessTokenNumUses: 0, - accessTokenNumUsesLimit, - clientSecretTrustedIps: reformattedClientSecretTrustedIps, - accessTokenTrustedIps: reformattedAccessTokenTrustedIps, - }).save(); - - await new MachineMembershipOrg({ - machineIdentity: machineIdentity._id, - organization: machineIdentity.organization, - role: isCustomRole ? CUSTOM : role, - customRole - }).save(); - - await EEAuditLogService.createAuditLog( - req.authData, - { - type: EventType.CREATE_MACHINE_IDENTITY, - metadata: { - name, - role, - clientSecretTrustedIps: reformattedClientSecretTrustedIps as Array, - accessTokenTrustedIps: reformattedAccessTokenTrustedIps as Array - } - }, - { - organizationId: new Types.ObjectId(organizationId) - } - ); - - return res.status(200).send({ - machineIdentity - }); -} - -/** - * Update machine identity with id [machineId] - * @param req - * @param res - * @returns - */ -export const updateMachineIdentity = async (req: Request, res: Response) => { - const { - params: { machineId }, - body: { - name, - role, - clientSecretTrustedIps, - accessTokenTrustedIps, - accessTokenTTL, - accessTokenNumUsesLimit, - accessTokenMaxTTL - } - } = await validateRequest(reqValidator.UpdateMachineIdentityV1, req); - - if (accessTokenTTL && accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) { - throw BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }) - } - - const machineMembershipOrg = await MachineMembershipOrg - .findOne({ - machineIdentity: new Types.ObjectId(machineId) - }) - .populate<{ - machineIdentity: IMachineIdentity, - customRole: IRole - }>("machineIdentity customRole"); - - if (!machineMembershipOrg) throw ResourceNotFoundError({ - message: `Failed to find machine identity with id ${machineId}` - }); - - const { permission } = await getAuthDataOrgPermissions({ - authData: req.authData, - organizationId: machineMembershipOrg.organization - }); - ForbiddenError.from(permission).throwUnlessCan( - OrgPermissionActions.Edit, - OrgPermissionSubjects.MachineIdentity - ); - - const machineIdentityRolePermission = await getOrgRolePermissions( - machineMembershipOrg?.customRole?.slug ?? machineMembershipOrg.role, - machineMembershipOrg.organization.toString() - ); - const hasRequiredPrivileges = isAtLeastAsPrivilegedOrg(permission, machineIdentityRolePermission); - if (!hasRequiredPrivileges) throw ForbiddenRequestError({ - message: "Failed to update more privileged MI" - }); - - if (role) { - const rolePermission = await getOrgRolePermissions(role, machineMembershipOrg.organization.toString()); - const hasRequiredPrivileges = isAtLeastAsPrivilegedOrg(permission, rolePermission); - - if (!hasRequiredPrivileges) throw ForbiddenRequestError({ - message: "Failed to update MI to a more privileged role" - }); - } - - let customRole; - if (role) { - const isCustomRole = ![ADMIN, MEMBER, NO_ACCESS].includes(role); - if (isCustomRole) { - customRole = await Role.findOne({ - slug: role, - isOrgRole: true, - organization: machineMembershipOrg.organization - }); - - if (!customRole) throw BadRequestError({ message: "Role not found" }); - } - } - - const plan = await EELicenseService.getPlan(machineMembershipOrg.organization); - - // validate client secret trusted ips - let reformattedClientSecretTrustedIps; - if (clientSecretTrustedIps) { - reformattedClientSecretTrustedIps = clientSecretTrustedIps.map((clientSecretTrustedIp) => { - if (!plan.ipAllowlisting && clientSecretTrustedIp.ipAddress !== "0.0.0.0/0") return res.status(400).send({ - message: "Failed to update IP access range to service token due to plan restriction. Upgrade plan to update IP access range." - }); - - const isValidIPOrCidr = isValidIpOrCidr(clientSecretTrustedIp.ipAddress); - - if (!isValidIPOrCidr) return res.status(400).send({ - message: "The IP is not a valid IPv4, IPv6, or CIDR block" - }); - - return extractIPDetails(clientSecretTrustedIp.ipAddress); - }); - } - - // validate access token trusted ips - let reformattedAccessTokenTrustedIps; - if (accessTokenTrustedIps) { - reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { - if (!plan.ipAllowlisting && accessTokenTrustedIp.ipAddress !== "0.0.0.0/0") return res.status(400).send({ - message: "Failed to update IP access range to service token due to plan restriction. Upgrade plan to update IP access range." - }); - - const isValidIPOrCidr = isValidIpOrCidr(accessTokenTrustedIp.ipAddress); - - if (!isValidIPOrCidr) return res.status(400).send({ - message: "The IP is not a valid IPv4, IPv6, or CIDR block" - }); - - return extractIPDetails(accessTokenTrustedIp.ipAddress); - }); - } - - const machineIdentity = await MachineIdentity.findByIdAndUpdate( - machineId, - { - name, - clientSecretTrustedIps: reformattedClientSecretTrustedIps, - accessTokenTrustedIps: reformattedAccessTokenTrustedIps, - accessTokenTTL, - accessTokenNumUsesLimit, - accessTokenMaxTTL - }, - { - new: true - } - ); - - if (!machineIdentity) throw BadRequestError({ - message: `Failed to update machine identity with id ${machineId}` - }); - - await MachineMembershipOrg.findOneAndUpdate( - { - machineIdentity: machineIdentity._id - }, - { - role: customRole ? CUSTOM : role, - ...(customRole ? { - customRole - } : {}), - ...(role && !customRole ? { // non-custom role - $unset: { - customRole: 1 - } - } : {}) - }, - { - new: true - } - ); - - await EEAuditLogService.createAuditLog( - req.authData, - { - type: EventType.UPDATE_MACHINE_IDENTITY, - metadata: { - name: machineIdentity.name, - role, - clientSecretTrustedIps: reformattedClientSecretTrustedIps as Array, - accessTokenTrustedIps: reformattedAccessTokenTrustedIps as Array - } - }, - { - organizationId: machineIdentity.organization - } - ); - - return res.status(200).send({ - machineIdentity - }); -} - -/** - * Delete machine identity with id [machineId] - * @param req - * @param res - * @returns - */ -export const deleteMachineIdentity = async (req: Request, res: Response) => { - const { - params: { machineId } - } = await validateRequest(reqValidator.DeleteMachineIdentityV1, req); - - const machineMembershipOrg = await MachineMembershipOrg - .findOne({ - machineIdentity: new Types.ObjectId(machineId) - }) - .populate<{ - machineIdentity: IMachineIdentity, - customRole: IRole - }>("machineIdentity customRole"); - - if (!machineMembershipOrg) throw ResourceNotFoundError({ - message: `Failed to find machine identity with id ${machineId}` - }); - - const { permission } = await getAuthDataOrgPermissions({ - authData: req.authData, - organizationId: machineMembershipOrg.organization - }); - ForbiddenError.from(permission).throwUnlessCan( - OrgPermissionActions.Delete, - OrgPermissionSubjects.MachineIdentity - ); - - const machineIdentityRolePermission = await getOrgRolePermissions( - machineMembershipOrg?.customRole?.slug ?? machineMembershipOrg.role, - machineMembershipOrg.organization.toString() - ); - const hasRequiredPrivileges = isAtLeastAsPrivilegedOrg(permission, machineIdentityRolePermission); - if (!hasRequiredPrivileges) throw ForbiddenRequestError({ - message: "Failed to delete more privileged MI" - }); - - const machineIdentity = await MachineIdentity.findByIdAndDelete(machineMembershipOrg.machineIdentity); - if (!machineIdentity) throw ResourceNotFoundError({ - message: `Machine identity with id ${machineId} not found` - }); - - await MachineMembershipOrg.findByIdAndDelete(machineMembershipOrg._id); - - if (!machineMembershipOrg) throw BadRequestError({ - message: `Failed to delete machine identity with id ${machineId}` - }); - - await MachineMembership.deleteMany({ - machineIdentity: machineMembershipOrg.machineIdentity - }); - - const machineIdentityClientSecretIds = await MachineIdentityClientSecret.distinct("_id", { - machineIdentity: machineMembershipOrg.machineIdentity - }); - - await MachineIdentityClientSecret.deleteMany({ - machineIdentity: machineMembershipOrg.machineIdentity - }); - - await IdentityAccessToken.deleteMany({ - machineIdentityClientSecret: { - $in: machineIdentityClientSecretIds - } - }); - - await EEAuditLogService.createAuditLog( - req.authData, - { - type: EventType.DELETE_MACHINE_IDENTITY, - metadata: { - name: machineIdentity.name, - role: machineMembershipOrg.role, - clientSecretTrustedIps: machineIdentity.clientSecretTrustedIps as Array, - accessTokenTrustedIps: machineIdentity.accessTokenTrustedIps as Array, - } - }, - { - organizationId: machineIdentity.organization - } - ); - - return res.status(200).send({ - machineIdentity - }); -} \ No newline at end of file diff --git a/backend/src/ee/controllers/v1/workspaceController.ts b/backend/src/ee/controllers/v1/workspaceController.ts index a44c1793a..9f63de3d9 100644 --- a/backend/src/ee/controllers/v1/workspaceController.ts +++ b/backend/src/ee/controllers/v1/workspaceController.ts @@ -2,8 +2,8 @@ import { Request, Response } from "express"; import { PipelineStage, Types } from "mongoose"; import { Folder, - MachineIdentity, - MachineMembership, + Identity, + IdentityMembership, Membership, Secret, ServiceTokenData, @@ -18,7 +18,7 @@ import { FolderVersion, IPType, ISecretVersion, - MachineActor, + IdentityActor, SecretSnapshot, SecretVersion, ServiceActor, @@ -679,8 +679,8 @@ export const getWorkspaceAuditLogs = async (req: Request, res: Response) => { case ActorType.SERVICE: actorMetadataQuery = "actor.metadata.serviceId"; break; - case ActorType.MACHINE: - actorMetadataQuery = "actor.metadata.machineId"; + case ActorType.IDENTITY: + actorMetadataQuery = "actor.metadata.identityId"; break; } } @@ -772,25 +772,25 @@ export const getWorkspaceAuditLogActorFilterOpts = async (req: Request, res: Res } })); - const machineIds = await MachineMembership.distinct("machineIdentity", { + const identityIds = await IdentityMembership.distinct("identity", { workspace: new Types.ObjectId(workspaceId) }); - const machineActors: MachineActor[] = ( - await MachineIdentity.find({ + const identityActors: IdentityActor[] = ( + await Identity.find({ _id: { - $in: machineIds + $in: identityIds } }) - ).map((machineIdentity) => ({ - type: ActorType.MACHINE, + ).map((identity) => ({ + type: ActorType.IDENTITY, metadata: { - machineId: machineIdentity._id.toString(), - name: machineIdentity.name + identityId: identity._id.toString(), + name: identity.name } })); - const actors = [...userActors, ...serviceActors, ...machineActors]; + const actors = [...userActors, ...serviceActors, ...identityActors]; return res.status(200).send({ actors diff --git a/backend/src/ee/models/auditLog/auditLog.ts b/backend/src/ee/models/auditLog/auditLog.ts index c2c5aba68..824e2f89f 100644 --- a/backend/src/ee/models/auditLog/auditLog.ts +++ b/backend/src/ee/models/auditLog/auditLog.ts @@ -10,7 +10,7 @@ export interface IAuditLog { event: Event; userAgent: string; userAgentType: UserAgentType; - expiresAt: Date; + expiresAt?: Date; } const auditLogSchema = new Schema( diff --git a/backend/src/ee/models/auditLog/enums.ts b/backend/src/ee/models/auditLog/enums.ts index 1710d6357..0a12c4415 100644 --- a/backend/src/ee/models/auditLog/enums.ts +++ b/backend/src/ee/models/auditLog/enums.ts @@ -1,7 +1,7 @@ export enum ActorType { // would extend to AWS, Azure, ... - USER = "user", // userIdentity - SERVICE = "service", - MACHINE = "machine" // machineIdentity + USER = "user", // userIdentity + SERVICE = "service", + IDENTITY = "identity" } export enum UserAgentType { @@ -31,13 +31,16 @@ export enum EventType { DELETE_TRUSTED_IP = "delete-trusted-ip", CREATE_SERVICE_TOKEN = "create-service-token", // v2 DELETE_SERVICE_TOKEN = "delete-service-token", // v2 - CREATE_MACHINE_IDENTITY = "create-machine-identity", - UPDATE_MACHINE_IDENTITY = "update-machine-identity", - DELETE_MACHINE_IDENTITY = "delete-machine-identity", - LOGIN_MACHINE_IDENTITY = "login-machine-identity", - CREATE_MACHINE_IDENTITY_CLIENT_SECRET = "create-machine-identity-secret", - REVOKE_MACHINE_IDENTITY_CLIENT_SECRET = "revoke-machine-identity-secret", - GET_MACHINE_IDENTITY_CLIENT_SECRETS = "get-machine-identity-secrets", + CREATE_IDENTITY = "create-identity", + UPDATE_IDENTITY = "update-identity", + DELETE_IDENTITY = "delete-identity", + LOGIN_IDENTITY_UNIVERSAL_AUTH = "login-identity-universal-auth", + ADD_IDENTITY_UNIVERSAL_AUTH = "add-identity-universal-auth", + UPDATE_IDENTITY_UNIVERSAL_AUTH = "update-identity-universal-auth", + GET_IDENTITY_UNIVERSAL_AUTH = "get-identity-universal-auth", + CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "create-identity-universal-auth-client-secret", + REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "revoke-identity-universal-auth-client-secret", + GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS = "get-identity-universal-auth-client-secret", CREATE_ENVIRONMENT = "create-environment", UPDATE_ENVIRONMENT = "update-environment", DELETE_ENVIRONMENT = "delete-environment", diff --git a/backend/src/ee/models/auditLog/types.ts b/backend/src/ee/models/auditLog/types.ts index 049b95d54..3741d5ce7 100644 --- a/backend/src/ee/models/auditLog/types.ts +++ b/backend/src/ee/models/auditLog/types.ts @@ -1,5 +1,5 @@ import { ActorType, EventType } from "./enums"; -import { IMachineIdentityTrustedIp } from "../../../models/machineIdentity"; +import { IIdentityTrustedIp } from "../../../models"; interface UserActorMetadata { userId: string; @@ -11,8 +11,8 @@ interface ServiceActorMetadata { name: string; } -interface MachineActorMetadata { - machineId: string; +interface IdentityActorMetadata { + identityId: string; name: string; } @@ -26,16 +26,12 @@ export interface ServiceActor { metadata: ServiceActorMetadata; } -export interface MachineActor { - type: ActorType.MACHINE; - metadata: MachineActorMetadata; +export interface IdentityActor { + type: ActorType.IDENTITY; + metadata: IdentityActorMetadata; } -// export interface MachineActor { -// type: ActorType.Machine; -// } - -export type Actor = UserActor | ServiceActor | MachineActor; +export type Actor = UserActor | ServiceActor | IdentityActor; interface GetSecretsEvent { type: EventType.GET_SECRETS; @@ -225,66 +221,90 @@ interface DeleteServiceTokenEvent { }; } -interface CreateMachineIdentityEvent { - type: EventType.CREATE_MACHINE_IDENTITY; +interface CreateIdentityEvent { // note: currently not logging org-role + type: EventType.CREATE_IDENTITY; metadata: { + identityId: string; name: string; - role: string; - clientSecretTrustedIps: Array; - accessTokenTrustedIps: Array; }; } -interface UpdateMachineIdentityEvent { - type: EventType.UPDATE_MACHINE_IDENTITY; +interface UpdateIdentityEvent { + type: EventType.UPDATE_IDENTITY; metadata: { + identityId: string; name?: string; - role?: string; - clientSecretTrustedIps?: Array; - accessTokenTrustedIps?: Array; }; } -interface DeleteMachineIdentityEvent { - type: EventType.DELETE_MACHINE_IDENTITY; +interface DeleteIdentityEvent { + type: EventType.DELETE_IDENTITY; metadata: { - name: string; - role: string; - clientSecretTrustedIps: Array; - accessTokenTrustedIps: Array; + identityId: string; }; } -interface LoginMachineIdentityEvent { - type: EventType.LOGIN_MACHINE_IDENTITY ; +interface LoginIdentityUniversalAuthEvent { + type: EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH ; metadata: { - machineId: string; - machineAccessTokenId: string; + identityId: string; clientSecretId: string; identityAccessTokenId: string; }; } -interface CreateMachineIdentitySecretEvent { - type: EventType.CREATE_MACHINE_IDENTITY_CLIENT_SECRET ; +interface AddIdentityUniversalAuthEvent { + type: EventType.ADD_IDENTITY_UNIVERSAL_AUTH; metadata: { - machineId: string; + identityId: string; + clientSecretTrustedIps: Array; + accessTokenTTL: number; + accessTokenMaxTTL: number; + accessTokenNumUsesLimit: number; + accessTokenTrustedIps: Array; + }; +} + +interface UpdateIdentityUniversalAuthEvent { + type: EventType.UPDATE_IDENTITY_UNIVERSAL_AUTH; + metadata: { + identityId: string; + clientSecretTrustedIps?: Array; + accessTokenTTL?: number; + accessTokenMaxTTL?: number; + accessTokenNumUsesLimit?: number; + accessTokenTrustedIps?: Array; + }; +} + +interface GetIdentityUniversalAuthEvent { + type: EventType.GET_IDENTITY_UNIVERSAL_AUTH; + metadata: { + identityId: string; + }; +} + +interface CreateIdentityUniversalAuthClientSecretEvent { + type: EventType.CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET ; + metadata: { + identityId: string; clientSecretId: string; }; } -interface DeleteMachineIdentitySecretEvent { - type: EventType.REVOKE_MACHINE_IDENTITY_CLIENT_SECRET ; +interface GetIdentityUniversalAuthClientSecretsEvent { + type: EventType.GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS; metadata: { - machineId: string; - clientSecretId: string; + identityId: string; }; } -interface GetMachineIdentitySecretsEvent { - type: EventType.GET_MACHINE_IDENTITY_CLIENT_SECRETS ; + +interface RevokeIdentityUniversalAuthClientSecretEvent { + type: EventType.REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET ; metadata: { - machineId: string; + identityId: string; + clientSecretId: string; }; } @@ -530,13 +550,16 @@ export type Event = | DeleteTrustedIPEvent | CreateServiceTokenEvent | DeleteServiceTokenEvent - | CreateMachineIdentityEvent - | UpdateMachineIdentityEvent - | DeleteMachineIdentityEvent - | CreateMachineIdentitySecretEvent - | DeleteMachineIdentitySecretEvent - | LoginMachineIdentityEvent - | GetMachineIdentitySecretsEvent + | CreateIdentityEvent + | UpdateIdentityEvent + | DeleteIdentityEvent + | LoginIdentityUniversalAuthEvent + | AddIdentityUniversalAuthEvent + | UpdateIdentityUniversalAuthEvent + | GetIdentityUniversalAuthEvent + | CreateIdentityUniversalAuthClientSecretEvent + | GetIdentityUniversalAuthClientSecretsEvent + | RevokeIdentityUniversalAuthClientSecretEvent | CreateEnvironmentEvent | UpdateEnvironmentEvent | DeleteEnvironmentEvent diff --git a/backend/src/ee/routes/v1/identities.ts b/backend/src/ee/routes/v1/identities.ts new file mode 100644 index 000000000..c78a7d8c8 --- /dev/null +++ b/backend/src/ee/routes/v1/identities.ts @@ -0,0 +1,31 @@ +import express from "express"; +const router = express.Router(); +import { requireAuth } from "../../../middleware"; +import { AuthMode } from "../../../variables"; +import { identitiesController } from "../../controllers/v1"; + +router.post( + "/", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN] + }), + identitiesController.createIdentity +); + +router.patch( + "/:identityId", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + identitiesController.updateIdentity +); + +router.delete( + "/:identityId", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + identitiesController.deleteIdentity +); + +export default router; \ No newline at end of file diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index f04fb2767..b22c61629 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -1,4 +1,4 @@ -import machineIdentities from "./machineIdentities"; +import identities from "./identities"; import secret from "./secret"; import secretSnapshot from "./secretSnapshot"; import organizations from "./organizations"; @@ -14,7 +14,7 @@ import secretRotationProvider from "./secretRotationProvider"; import secretRotation from "./secretRotation"; export { - machineIdentities, + identities, secret, secretSnapshot, organizations, diff --git a/backend/src/ee/routes/v1/machineIdentities.ts b/backend/src/ee/routes/v1/machineIdentities.ts deleted file mode 100644 index a00cf56ea..000000000 --- a/backend/src/ee/routes/v1/machineIdentities.ts +++ /dev/null @@ -1,66 +0,0 @@ -import express from "express"; -const router = express.Router(); -import { requireAuth } from "../../../middleware"; -import { AuthMode } from "../../../variables"; -import { machineIdentitiesController } from "../../controllers/v1"; - -router.get( - "/:machineId/client-secrets", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT] - }), - machineIdentitiesController.getMIClientSecrets -); - -router.post( - "/:machineId/client-secrets", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT] - }), - machineIdentitiesController.createMIClientSecret -); - -router.post( - "/:machineId/client-secrets/:clientSecretId/revoke", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT] - }), - machineIdentitiesController.revokeMIClientSecret -); - -router.post( - "/login", - machineIdentitiesController.loginMI -); - -// note: currently this is machine-identity specific -router.post( - "/access-token/renew", - machineIdentitiesController.renewAccessToken -); - -router.post( - "/", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.MACHINE_ACCESS_TOKEN] - }), - machineIdentitiesController.createMachineIdentity -); - -router.patch( - "/:machineId", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT] - }), - machineIdentitiesController.updateMachineIdentity -); - -router.delete( - "/:machineId", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT] - }), - machineIdentitiesController.deleteMachineIdentity -); - -export default router; \ No newline at end of file diff --git a/backend/src/ee/services/EEAuditLogService.ts b/backend/src/ee/services/EEAuditLogService.ts index eb5c1bbb3..9d220feee 100644 --- a/backend/src/ee/services/EEAuditLogService.ts +++ b/backend/src/ee/services/EEAuditLogService.ts @@ -3,7 +3,6 @@ import { AuditLog, Event } from "../models"; import { AuthData } from "../../interfaces/middleware"; import EELicenseService from "./EELicenseService"; import { Workspace } from "../../models"; -import { OrganizationNotFoundError } from "../../utils/errors"; interface EventScope { workspaceId?: Types.ObjectId; @@ -14,31 +13,42 @@ type ValidEventScope = | Required> | Required> | Required + | Record; export default class EEAuditLogService { - static async createAuditLog(authData: AuthData, event: Event, eventScope: ValidEventScope, shouldSave = true) { + static async createAuditLog(authData: AuthData, event: Event, eventScope: ValidEventScope = {}, shouldSave = true) { const MS_IN_DAY = 24 * 60 * 60 * 1000; - const organizationId = ("organizationId" in eventScope) - ? eventScope.organizationId - : (await Workspace.findById(eventScope.workspaceId).select("organization").lean())?.organization; + let organizationId; + if ("organizationId" in eventScope) { + organizationId = eventScope.organizationId; + } - if (!organizationId) throw OrganizationNotFoundError({ - message: "createAuditLog: Failed to create audit log due to missing organizationId" - }); - - const ttl = (await EELicenseService.getPlan(organizationId)).auditLogsRetentionDays * MS_IN_DAY; + let workspaceId; + if ("workspaceId" in eventScope) { + workspaceId = eventScope.workspaceId; + + if (!organizationId) { + organizationId = (await Workspace.findById(workspaceId).select("organization").lean())?.organization; + } + } + + let expiresAt; + if (organizationId) { + const ttl = (await EELicenseService.getPlan(organizationId)).auditLogsRetentionDays * MS_IN_DAY; + expiresAt = new Date(Date.now() + ttl); + } const auditLog = await new AuditLog({ actor: authData.actor, organization: organizationId, - workspace: ("workspaceId" in eventScope) ? eventScope.workspaceId : undefined, + workspace: workspaceId, ipAddress: authData.ipAddress, event, userAgent: authData.userAgent, userAgentType: authData.userAgentType, - expiresAt: new Date(Date.now() + ttl) + expiresAt }); if (shouldSave) { diff --git a/backend/src/ee/services/ProjectRoleService.ts b/backend/src/ee/services/ProjectRoleService.ts index bfba10b1d..18511948b 100644 --- a/backend/src/ee/services/ProjectRoleService.ts +++ b/backend/src/ee/services/ProjectRoleService.ts @@ -13,13 +13,12 @@ import picomatch from "picomatch"; import { AuthData } from "../../interfaces/middleware"; import { ActorType, IRole, Role } from "../models"; import { - IMachineIdentity, - MachineMembership, + IIdentity, + IdentityMembership, Membership, ServiceTokenData } from "../../models"; import { ADMIN, CUSTOM, MEMBER, NO_ACCESS, VIEWER } from "../../variables"; -import { checkIPAgainstBlocklist } from "../../utils/ip"; import { BadRequestError } from "../../utils/errors"; const $glob: FieldInstruction = { @@ -62,7 +61,7 @@ export enum ProjectPermissionSub { SecretRollback = "secret-rollback", SecretApproval = "secret-approval", SecretRotation = "secret-rotation", - MachineIdentity = "machine-identity" + Identity = "identity" } type SubjectFields = { @@ -87,7 +86,7 @@ export type ProjectPermissionSet = | [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens] | [ProjectPermissionActions, ProjectPermissionSub.SecretApproval] | [ProjectPermissionActions, ProjectPermissionSub.SecretRotation] - | [ProjectPermissionActions, ProjectPermissionSub.MachineIdentity] + | [ProjectPermissionActions, ProjectPermissionSub.Identity] | [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace] | [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace] | [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback] @@ -134,10 +133,10 @@ const buildAdminPermission = () => { can(ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks); can(ProjectPermissionActions.Delete, ProjectPermissionSub.Webhooks); - can(ProjectPermissionActions.Read, ProjectPermissionSub.MachineIdentity); - can(ProjectPermissionActions.Create, ProjectPermissionSub.MachineIdentity); - can(ProjectPermissionActions.Edit, ProjectPermissionSub.MachineIdentity); - can(ProjectPermissionActions.Delete, ProjectPermissionSub.MachineIdentity); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Identity); + can(ProjectPermissionActions.Create, ProjectPermissionSub.Identity); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.Identity); can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens); can(ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens); @@ -204,10 +203,10 @@ const buildMemberPermission = () => { can(ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks); can(ProjectPermissionActions.Delete, ProjectPermissionSub.Webhooks); - can(ProjectPermissionActions.Read, ProjectPermissionSub.MachineIdentity); - can(ProjectPermissionActions.Create, ProjectPermissionSub.MachineIdentity); - can(ProjectPermissionActions.Edit, ProjectPermissionSub.MachineIdentity); - can(ProjectPermissionActions.Delete, ProjectPermissionSub.MachineIdentity); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Identity); + can(ProjectPermissionActions.Create, ProjectPermissionSub.Identity); + can(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.Identity); can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens); can(ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens); @@ -249,7 +248,7 @@ const buildViewerPermission = () => { can(ProjectPermissionActions.Read, ProjectPermissionSub.Role); can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks); - can(ProjectPermissionActions.Read, ProjectPermissionSub.MachineIdentity); + can(ProjectPermissionActions.Read, ProjectPermissionSub.Identity); can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens); can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings); can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments); @@ -310,28 +309,23 @@ export const getAuthDataProjectPermissions = async ({ role = "viewer"; break; } - case ActorType.MACHINE: { - const machineMembership = await MachineMembership.findOne({ - machineIdentity: authData.authPayload._id, + case ActorType.IDENTITY: { + const identityMembership = await IdentityMembership.findOne({ + identity: authData.authPayload._id, workspace: workspaceId }) .populate<{ customRole: IRole & { permissions: RawRuleOf>[] }; - machineIdentity: IMachineIdentity - }>("customRole machineIdentity") + identity: IIdentity + }>("customRole identity") .exec(); - if (!machineMembership || (machineMembership.role === "custom" && !machineMembership.customRole)) { + if (!identityMembership || (identityMembership.role === "custom" && !identityMembership.customRole)) { throw UnauthorizedRequestError(); } - - checkIPAgainstBlocklist({ - ipAddress: authData.ipAddress, - trustedIps: machineMembership.machineIdentity.accessTokenTrustedIps - }); - role = machineMembership.role; - customRole = machineMembership.customRole; + role = identityMembership.role; + customRole = identityMembership.customRole; break; } diff --git a/backend/src/ee/services/RoleService.ts b/backend/src/ee/services/RoleService.ts index d68075121..1822c1d71 100644 --- a/backend/src/ee/services/RoleService.ts +++ b/backend/src/ee/services/RoleService.ts @@ -1,13 +1,12 @@ import { Types } from "mongoose"; import { AbilityBuilder, MongoAbility, RawRuleOf, createMongoAbility } from "@casl/ability"; import { - IMachineIdentity, - MachineMembershipOrg, + IIdentity, + IdentityMembershipOrg, MembershipOrg } from "../../models"; import { ActorType, IRole, Role } from "../models"; import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors"; -import { checkIPAgainstBlocklist } from "../../utils/ip"; import { ACCEPTED, ADMIN, CUSTOM, MEMBER, NO_ACCESS} from "../../variables"; import { conditionsMatcher } from "./ProjectRoleService"; import { AuthData } from "../../interfaces/middleware"; @@ -28,7 +27,7 @@ export enum OrgPermissionSubjects { Sso = "sso", Billing = "billing", SecretScanning = "secret-scanning", - MachineIdentity = "machine-identity" + Identity = "identity" } export type OrgPermissionSet = @@ -41,7 +40,7 @@ export type OrgPermissionSet = | [OrgPermissionActions, OrgPermissionSubjects.Sso] | [OrgPermissionActions, OrgPermissionSubjects.SecretScanning] | [OrgPermissionActions, OrgPermissionSubjects.Billing] - | [OrgPermissionActions, OrgPermissionSubjects.MachineIdentity]; + | [OrgPermissionActions, OrgPermissionSubjects.Identity]; const buildAdminPermission = () => { const { can, build } = new AbilityBuilder>(createMongoAbility); @@ -84,10 +83,10 @@ const buildAdminPermission = () => { can(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing); can(OrgPermissionActions.Delete, OrgPermissionSubjects.Billing); - can(OrgPermissionActions.Read, OrgPermissionSubjects.MachineIdentity); - can(OrgPermissionActions.Create, OrgPermissionSubjects.MachineIdentity); - can(OrgPermissionActions.Edit, OrgPermissionSubjects.MachineIdentity); - can(OrgPermissionActions.Delete, OrgPermissionSubjects.MachineIdentity); + can(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); + can(OrgPermissionActions.Create, OrgPermissionSubjects.Identity); + can(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + can(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity); return build({ conditionsMatcher }); }; @@ -112,10 +111,10 @@ const buildMemberPermission = () => { can(OrgPermissionActions.Edit, OrgPermissionSubjects.SecretScanning); can(OrgPermissionActions.Delete, OrgPermissionSubjects.SecretScanning); - can(OrgPermissionActions.Read, OrgPermissionSubjects.MachineIdentity); - can(OrgPermissionActions.Create, OrgPermissionSubjects.MachineIdentity); - can(OrgPermissionActions.Edit, OrgPermissionSubjects.MachineIdentity); - can(OrgPermissionActions.Delete, OrgPermissionSubjects.MachineIdentity); + can(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); + can(OrgPermissionActions.Create, OrgPermissionSubjects.Identity); + can(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + can(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity); return build({ conditionsMatcher }); }; @@ -203,28 +202,23 @@ export const getUserOrgPermissions = async (userId: string, orgId: string) => { message: "Failed to access organization-level resources with service token" }); } - case ActorType.MACHINE: { - const machineMembershipOrg = await MachineMembershipOrg.findOne({ - machineIdentity: authData.authPayload._id, + case ActorType.IDENTITY: { + const identityMembershipOrg = await IdentityMembershipOrg.findOne({ + identity: authData.authPayload._id, organization: organizationId }) .populate<{ customRole: IRole & { permissions: RawRuleOf>[] }; - machineIdentity: IMachineIdentity - }>("customRole machineIdentity") + identity: IIdentity + }>("customRole identity") .exec(); - if (!machineMembershipOrg || (machineMembershipOrg.role === "custom" && !machineMembershipOrg.customRole)) { + if (!identityMembershipOrg || (identityMembershipOrg.role === "custom" && !identityMembershipOrg.customRole)) { throw UnauthorizedRequestError(); } - - checkIPAgainstBlocklist({ - ipAddress: authData.ipAddress, - trustedIps: machineMembershipOrg.machineIdentity.accessTokenTrustedIps - }); - role = machineMembershipOrg.role; - customRole = machineMembershipOrg.customRole; + role = identityMembershipOrg.role; + customRole = identityMembershipOrg.customRole; break; } default: diff --git a/backend/src/helpers/organization.ts b/backend/src/helpers/organization.ts index d0d494890..e591ef78a 100644 --- a/backend/src/helpers/organization.ts +++ b/backend/src/helpers/organization.ts @@ -4,13 +4,13 @@ import { BotKey, BotOrg, Folder, + Identity, + IdentityMembership, + IdentityMembershipOrg, IncidentContactOrg, Integration, IntegrationAuth, Key, - MachineIdentity, - MachineMembership, - MachineMembershipOrg, Membership, MembershipOrg, Organization, @@ -125,11 +125,11 @@ export const deleteOrganization = async ({ organization: organization._id }); - await MachineIdentity.deleteMany({ + await Identity.deleteMany({ organization: organization._id }); - await MachineMembershipOrg.deleteMany({ + await IdentityMembershipOrg.deleteMany({ organization: organization._id }); @@ -277,7 +277,7 @@ export const deleteOrganization = async ({ } }); - await MachineMembership.deleteMany({ + await IdentityMembership.deleteMany({ workspace: { $in: workspaceIds } diff --git a/backend/src/helpers/workspace.ts b/backend/src/helpers/workspace.ts index 77d1eea3d..3a031a066 100644 --- a/backend/src/helpers/workspace.ts +++ b/backend/src/helpers/workspace.ts @@ -3,10 +3,10 @@ import { Bot, BotKey, Folder, + IdentityMembership, Integration, IntegrationAuth, Key, - MachineMembership, Membership, Secret, SecretBlindIndexData, @@ -178,7 +178,7 @@ export const deleteWorkspace = async ({ workspace: workspace._id }); - await MachineMembership.deleteMany({ + await IdentityMembership.deleteMany({ workspace: workspace._id }); diff --git a/backend/src/index.ts b/backend/src/index.ts index 35104edc3..9d5302f01 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -25,7 +25,7 @@ import { secretSnapshot as eeSecretSnapshotRouter, users as eeUsersRouter, workspace as eeWorkspaceRouter, - machineIdentities as v1MachineIdentitiesRouter, + identities as v1IdentitiesRouter, roles as v1RoleRouter, secretApprovalPolicy as v1SecretApprovalPolicyRouter, secretApprovalRequest as v1SecretApprovalRequestRouter, @@ -198,7 +198,7 @@ const main = async () => { } // (EE) routes - app.use("/api/v1/machine-identities", v1MachineIdentitiesRouter); + app.use("/api/v1/identities", v1IdentitiesRouter); app.use("/api/v1/secret", eeSecretRouter); app.use("/api/v1/secret-snapshot", eeSecretSnapshotRouter); app.use("/api/v1/users", eeUsersRouter); @@ -212,7 +212,7 @@ const main = async () => { // v1 routes app.use("/api/v1/signup", v1SignupRouter); - app.use("/api/v1/auth", v1AuthRouter); + app.use("/api/v1/auth", v1AuthRouter); // note: updated for identities app.use("/api/v1/admin", v1AdminRouter); app.use("/api/v1/bot", v1BotRouter); app.use("/api/v1/user", v1UserRouter); diff --git a/backend/src/interfaces/middleware/index.ts b/backend/src/interfaces/middleware/index.ts index 8f72c6668..acd992162 100644 --- a/backend/src/interfaces/middleware/index.ts +++ b/backend/src/interfaces/middleware/index.ts @@ -1,6 +1,6 @@ import { Types } from "mongoose"; -import { IMachineIdentity, IServiceTokenData, IUser } from "../../models"; -import { MachineActor, ServiceActor, UserActor, UserAgentType } from "../../ee/models"; +import { IIdentity, IServiceTokenData, IUser } from "../../models"; +import { IdentityActor, ServiceActor, UserActor, UserAgentType } from "../../ee/models"; interface BaseAuthData { ipAddress: string; @@ -14,9 +14,9 @@ export interface UserAuthData extends BaseAuthData { authPayload: IUser; } -export interface MachineIdentityAuthData extends BaseAuthData { - actor: MachineActor; - authPayload: IMachineIdentity; +export interface IdentityAuthData extends BaseAuthData { + actor: IdentityActor; + authPayload: IIdentity; } export interface ServiceTokenAuthData extends BaseAuthData { @@ -24,4 +24,4 @@ export interface ServiceTokenAuthData extends BaseAuthData { authPayload: IServiceTokenData; } -export type AuthData = UserAuthData | MachineIdentityAuthData | ServiceTokenAuthData; +export type AuthData = UserAuthData | IdentityAuthData | ServiceTokenAuthData; \ No newline at end of file diff --git a/backend/src/middleware/requireAuth.ts b/backend/src/middleware/requireAuth.ts index 07dc93a38..e68de3133 100644 --- a/backend/src/middleware/requireAuth.ts +++ b/backend/src/middleware/requireAuth.ts @@ -50,7 +50,7 @@ const requireAuth = ({ case AuthMode.SERVICE_TOKEN: req.serviceTokenData = authData.authPayload; break; - case AuthMode.MACHINE_ACCESS_TOKEN: + case AuthMode.IDENTITY_ACCESS_TOKEN: req.serviceTokenData = authData.authPayload; break; case AuthMode.API_KEY: diff --git a/backend/src/models/identity.ts b/backend/src/models/identity.ts new file mode 100644 index 000000000..ec4948e1b --- /dev/null +++ b/backend/src/models/identity.ts @@ -0,0 +1,38 @@ +import { Document, Schema, Types, model } from "mongoose"; +import { IPType } from "../ee/models"; + +export interface IIdentityTrustedIp { + ipAddress: string; + type: IPType; + prefix: number; +} + +export enum IdentityAuthMethod { + UNIVERSAL_AUTH = "universal-auth" +} + +export interface IIdentity extends Document { + _id: Types.ObjectId; + name: string; + authMethod?: IdentityAuthMethod; +} + +const identitySchema = new Schema( + { + name: { + type: String, + required: true + }, + authMethod: { + type: String, + enum: IdentityAuthMethod, + required: false, + }, + + }, + { + timestamps: true + } +); + +export const Identity = model("Identity", identitySchema); diff --git a/backend/src/models/identityAccessToken.ts b/backend/src/models/identityAccessToken.ts index e75fdaf2f..7acdb2dbe 100644 --- a/backend/src/models/identityAccessToken.ts +++ b/backend/src/models/identityAccessToken.ts @@ -1,15 +1,18 @@ import { Document, Schema, Types, model } from "mongoose"; +import { IIdentityTrustedIp } from "./identity"; +import { IPType } from "../ee/models/trustedIp"; export interface IIdentityAccessToken extends Document { _id: Types.ObjectId; - machineIdentity?: Types.ObjectId; - machineIdentityClientSecret?: Types.ObjectId; + identity: Types.ObjectId; + identityUniversalAuthClientSecret?: Types.ObjectId; accessTokenLastUsedAt?: Date; accessTokenLastRenewedAt?: Date; accessTokenNumUses: number; accessTokenNumUsesLimit: number; accessTokenTTL: number; accessTokenMaxTTL: number; + accessTokenTrustedIps: Array; isAccessTokenRevoked: boolean; updatedAt: Date; createdAt: Date; @@ -17,14 +20,14 @@ export interface IIdentityAccessToken extends Document { const identityAccessTokenSchema = new Schema( { - machineIdentity: { + identity: { type: Schema.Types.ObjectId, - ref: "MachineIdentity", + ref: "Identity", required: false }, - machineIdentityClientSecret: { + identityUniversalAuthClientSecret: { type: Schema.Types.ObjectId, - ref: "MachineIdentityClientSecret", + ref: "IdentityUniversalAuthClientSecret", required: false }, accessTokenLastUsedAt: { @@ -59,6 +62,34 @@ const identityAccessTokenSchema = new Schema( default: 7200, required: true }, + accessTokenTrustedIps: { + type: [ + { + ipAddress: { + type: String, + required: true + }, + type: { + type: String, + enum: [ + IPType.IPV4, + IPType.IPV6 + ], + required: true + }, + prefix: { + type: Number, + required: false + } + } + ], + default: [{ + ipAddress: "0.0.0.0", + type: IPType.IPV4.toString(), + prefix: 0 + }], + required: true + }, isAccessTokenRevoked: { type: Boolean, default: false, @@ -70,4 +101,4 @@ const identityAccessTokenSchema = new Schema( } ); -export const IdentityAccessToken = model("IdentityAccessToken", identityAccessTokenSchema); \ No newline at end of file +export const IdentityAccessToken = model("IdentityAccessToken", identityAccessTokenSchema); diff --git a/backend/src/models/machineMembership.ts b/backend/src/models/identityMembership.ts similarity index 69% rename from backend/src/models/machineMembership.ts rename to backend/src/models/identityMembership.ts index 2463009b3..4fedfe909 100644 --- a/backend/src/models/machineMembership.ts +++ b/backend/src/models/identityMembership.ts @@ -1,19 +1,19 @@ import { Schema, Types, model } from "mongoose"; import { ADMIN, CUSTOM, MEMBER, NO_ACCESS, VIEWER } from "../variables"; -export interface IMachineMembership { +export interface IIdentityMembership { _id: Types.ObjectId; - machineIdentity: Types.ObjectId; + identity: Types.ObjectId; workspace: Types.ObjectId; role: "admin" | "member" | "viewer" | "no-access" | "custom"; customRole: Types.ObjectId; } -const machineMembershipSchema = new Schema( +const identityMembershipSchema = new Schema( { - machineIdentity: { + identity: { type: Schema.Types.ObjectId, - ref: "MachineIdentity" + ref: "Identity" }, workspace: { type: Schema.Types.ObjectId, @@ -36,4 +36,4 @@ const machineMembershipSchema = new Schema( } ); -export const MachineMembership = model("MachineMembership", machineMembershipSchema); \ No newline at end of file +export const IdentityMembership = model("IdentityMembership", identityMembershipSchema); \ No newline at end of file diff --git a/backend/src/models/machineMembershipOrg.ts b/backend/src/models/identityMembershipOrg.ts similarity index 66% rename from backend/src/models/machineMembershipOrg.ts rename to backend/src/models/identityMembershipOrg.ts index 2a36618da..8da8693c4 100644 --- a/backend/src/models/machineMembershipOrg.ts +++ b/backend/src/models/identityMembershipOrg.ts @@ -1,19 +1,19 @@ import { Schema, Types, model } from "mongoose"; import { ADMIN, CUSTOM, MEMBER, NO_ACCESS} from "../variables"; -export interface IMachineMembershipOrg { +export interface IIdentityMembershipOrg { _id: Types.ObjectId; - machineIdentity: Types.ObjectId; + identity: Types.ObjectId; organization: Types.ObjectId; role: "admin" | "member" | "no-access" | "custom"; customRole: Types.ObjectId; } -const machineMembershipOrgSchema = new Schema( +const identityMembershipOrgSchema = new Schema( { - machineIdentity: { + identity: { type: Schema.Types.ObjectId, - ref: "MachineIdentity" + ref: "Identity" }, organization: { type: Schema.Types.ObjectId, @@ -34,4 +34,4 @@ const machineMembershipOrgSchema = new Schema( } ); -export const MachineMembershipOrg = model("MachineMembershipOrg", machineMembershipOrgSchema); \ No newline at end of file +export const IdentityMembershipOrg = model("IdentityMembershipOrg", identityMembershipOrgSchema); \ No newline at end of file diff --git a/backend/src/models/machineIdentity.ts b/backend/src/models/identityUniversalAuth.ts similarity index 80% rename from backend/src/models/machineIdentity.ts rename to backend/src/models/identityUniversalAuth.ts index 2a579d5ca..89fb46a95 100644 --- a/backend/src/models/machineIdentity.ts +++ b/backend/src/models/identityUniversalAuth.ts @@ -1,57 +1,29 @@ import { Document, Schema, Types, model } from "mongoose"; import { IPType } from "../ee/models"; +import { IIdentityTrustedIp } from "./identity"; -export interface IMachineIdentityTrustedIp { - ipAddress: string; - type: IPType; - prefix: number; -} - -export interface IMachineIdentity extends Document { +export interface IIdentityUniversalAuth extends Document { _id: Types.ObjectId; + identity: Types.ObjectId; clientId: string; - name: string; - organization: Types.ObjectId; + clientSecretTrustedIps: Array; accessTokenTTL: number; accessTokenMaxTTL: number; accessTokenNumUsesLimit: number; - clientSecretTrustedIps: Array; - accessTokenTrustedIps: Array; + accessTokenTrustedIps: Array; } -const machineIdentitySchema = new Schema( +const identityUniversalAuthSchema = new Schema( { + identity: { + type: Schema.Types.ObjectId, + ref: "Identity", + required: true + }, clientId: { type: String, required: true }, - name: { - type: String, - required: true - }, - organization: { - type: Schema.Types.ObjectId, - ref: "Organization", - required: true - }, - accessTokenTTL: { // seconds - // incremental lifetime - type: Number, - default: 7200, - required: true - }, - accessTokenMaxTTL: { // seconds - // max lifetime - type: Number, - default: 7200, - required: true - }, - accessTokenNumUsesLimit: { - // number of times access token can be used for - type: Number, - default: 0, // default: used as many times as needed - required: true - }, clientSecretTrustedIps: { type: [ { @@ -80,6 +52,24 @@ const machineIdentitySchema = new Schema( }], required: true }, + accessTokenTTL: { // seconds + // incremental lifetime + type: Number, + default: 7200, + required: true + }, + accessTokenMaxTTL: { // seconds + // max lifetime + type: Number, + default: 7200, + required: true + }, + accessTokenNumUsesLimit: { + // number of times access token can be used for + type: Number, + default: 0, // default: used as many times as needed + required: true + }, accessTokenTrustedIps: { type: [ { @@ -114,6 +104,4 @@ const machineIdentitySchema = new Schema( } ); -machineIdentitySchema.index({ clientId: 1 }) - -export const MachineIdentity = model("MachineIdentity", machineIdentitySchema); \ No newline at end of file +export const IdentityUniversalAuth = model("IdentityUniversalAuth", identityUniversalAuthSchema); \ No newline at end of file diff --git a/backend/src/models/machineIdentityClientSecret.ts b/backend/src/models/identityUniversalAuthClientSecret.ts similarity index 70% rename from backend/src/models/machineIdentityClientSecret.ts rename to backend/src/models/identityUniversalAuthClientSecret.ts index 6245efabe..af9cc08a4 100644 --- a/backend/src/models/machineIdentityClientSecret.ts +++ b/backend/src/models/identityUniversalAuthClientSecret.ts @@ -1,8 +1,9 @@ import { Document, Schema, Types, model } from "mongoose"; -export interface IMachineIdentityClientSecret extends Document { +export interface IIdentityUniversalAuthClientSecret extends Document { _id: Types.ObjectId; - machineIdentity: Types.ObjectId; + identity: Types.ObjectId; + identityUniversalAuth : Types.ObjectId; description: string; clientSecretPrefix: string; clientSecretHash: string; @@ -15,11 +16,16 @@ export interface IMachineIdentityClientSecret extends Document { isClientSecretRevoked: boolean; } -const machineIdentityClientSecretSchema = new Schema( +const identityUniversalAuthClientSecretSchema = new Schema( { - machineIdentity: { + identity: { type: Schema.Types.ObjectId, - ref: "MachineIdentity", + ref: "Identity", + required: true + }, + identityUniversalAuth: { + type: Schema.Types.ObjectId, + ref: "IdentityUniversalAuth", required: true }, description: { @@ -68,8 +74,8 @@ const machineIdentityClientSecretSchema = new Schema( } ); -machineIdentityClientSecretSchema.index( - { machineIdentity: 1, isClientSecretRevoked: 1 } -) +identityUniversalAuthClientSecretSchema.index( + { identityUniversalAuth: 1, isClientSecretRevoked: 1 } +); -export const MachineIdentityClientSecret = model("MachineIdentityClientSecret", machineIdentityClientSecretSchema); \ No newline at end of file +export const IdentityUniversalAuthClientSecret = model("IdentityUniversalAuthClientSecret", identityUniversalAuthClientSecretSchema); \ No newline at end of file diff --git a/backend/src/models/index.ts b/backend/src/models/index.ts index e7a66114d..9d20ea67a 100644 --- a/backend/src/models/index.ts +++ b/backend/src/models/index.ts @@ -20,11 +20,15 @@ export * from "./user"; export * from "./userAction"; export * from "./workspace"; export * from "./serviceTokenData"; // TODO: deprecate -export * from "./machineIdentity"; -export * from "./machineIdentityClientSecret"; + +// new +export * from "./identity"; +export * from "./identityMembership"; +export * from "./identityMembershipOrg"; +export * from "./identityUniversalAuth"; +export * from "./identityUniversalAuthClientSecret"; export * from "./identityAccessToken"; -export * from "./machineMembershipOrg"; -export * from "./machineMembership"; + export * from "./apiKeyData"; // TODO: deprecate export * from "./apiKeyDataV2"; export * from "./loginSRPDetail"; diff --git a/backend/src/routes/v1/auth.ts b/backend/src/routes/v1/auth.ts index a3037f311..93c9fe985 100644 --- a/backend/src/routes/v1/auth.ts +++ b/backend/src/routes/v1/auth.ts @@ -48,4 +48,64 @@ router.delete( authController.revokeAllSessions ); +// --- identity endpoints + +router.post( + "/token/renew", + authController.renewAccessToken +); + +router.post( + "/universal-auth/login", + authController.loginIdentityUniversalAuth +); + +router.post( + "/universal-auth/identities/:identityId", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + authController.addIdentityUniversalAuth +); + +router.patch( + "/universal-auth/identities/:identityId", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + authController.updateIdentityUniversalAuth +); + +router.get( + "/universal-auth/identities/:identityId", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + authController.getIdentityUniversalAuth +); + +router.post( + "/universal-auth/identities/:identityId/client-secrets", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + authController.createUniversalAuthClientSecret +); + +router.get( + "/universal-auth/identities/:identityId/client-secrets", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + authController.getUniversalAuthClientSecrets +); + +router.delete( + "/universal-auth/identities/:identityId/client-secrets/:clientSecretId", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + authController.revokeUniversalAuthClientSecret +); + export default router; diff --git a/backend/src/routes/v2/organizations.ts b/backend/src/routes/v2/organizations.ts index f367d4bce..cfe1c4d6e 100644 --- a/backend/src/routes/v2/organizations.ts +++ b/backend/src/routes/v2/organizations.ts @@ -55,11 +55,11 @@ router.delete( ); router.get( - "/:organizationId/machine-memberships", + "/:organizationId/identity-memberships", requireAuth({ acceptedAuthModes: [AuthMode.JWT] }), - organizationsController.getOrganizationMachineMemberships + organizationsController.getOrganizationIdentityMemberships ); export default router; diff --git a/backend/src/routes/v2/serviceTokenData.ts b/backend/src/routes/v2/serviceTokenData.ts index 7b3dcaa06..2a0760f50 100644 --- a/backend/src/routes/v2/serviceTokenData.ts +++ b/backend/src/routes/v2/serviceTokenData.ts @@ -6,7 +6,7 @@ import { import { AuthMode } from "../../variables"; import { serviceTokenDataController } from "../../controllers/v2"; -router.get( // TODO: deprecate (moving to machine identity) +router.get( // TODO: deprecate (moving to identity) "/", requireAuth({ acceptedAuthModes: [AuthMode.SERVICE_TOKEN] @@ -14,7 +14,7 @@ router.get( // TODO: deprecate (moving to machine identity) serviceTokenDataController.getServiceTokenData ); -router.post( // TODO: deprecate (moving to machine identity) +router.post( // TODO: deprecate (moving to identity) "/", requireAuth({ acceptedAuthModes: [AuthMode.JWT] @@ -22,7 +22,7 @@ router.post( // TODO: deprecate (moving to machine identity) serviceTokenDataController.createServiceTokenData ); -router.delete( // TODO: deprecate (moving to machine identity) +router.delete( // TODO: deprecate (moving to identity) "/:serviceTokenDataId", requireAuth({ acceptedAuthModes: [AuthMode.JWT] diff --git a/backend/src/routes/v2/workspace.ts b/backend/src/routes/v2/workspace.ts index d4dc87c37..59b7647e1 100644 --- a/backend/src/routes/v2/workspace.ts +++ b/backend/src/routes/v2/workspace.ts @@ -94,35 +94,36 @@ router.patch( ); router.post( - "/:workspaceId/machine-memberships/:machineId", + "/:workspaceId/identity-memberships/:identityId", requireAuth({ acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] }), - workspaceController.addMachineToWorkspace + workspaceController.addIdentityToWorkspace ); router.patch( - "/:workspaceId/machine-memberships/:machineId", + "/:workspaceId/identity-memberships/:identityId", requireAuth({ acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] }), - workspaceController.updateMachineWorkspaceRole + workspaceController.updateIdentityWorkspaceRole ); router.delete( - "/:workspaceId/machine-memberships/:machineId", + "/:workspaceId/identity-memberships/:identityId", requireAuth({ acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] }), - workspaceController.deleteMachineFromWorkspace + workspaceController.deleteIdentityFromWorkspace ); router.get( - "/:workspaceId/machine-memberships", + "/:workspaceId/identity-memberships", requireAuth({ acceptedAuthModes: [AuthMode.JWT] }), - workspaceController.getWorkspaceMachineMemberships + workspaceController.getWorkspaceIdentityMemberships ); + export default router; diff --git a/backend/src/routes/v3/secrets.ts b/backend/src/routes/v3/secrets.ts index d19445258..be6daba2c 100644 --- a/backend/src/routes/v3/secrets.ts +++ b/backend/src/routes/v3/secrets.ts @@ -7,7 +7,7 @@ import { AuthMode } from "../../variables"; router.get( "/raw", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.MACHINE_ACCESS_TOKEN] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN] }), secretsController.getSecretsRaw ); @@ -15,7 +15,7 @@ router.get( router.get( "/raw/:secretName", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.MACHINE_ACCESS_TOKEN] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN] }), requireBlindIndicesEnabled({ locationWorkspaceId: "query" @@ -29,7 +29,7 @@ router.get( router.post( "/raw/:secretName", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.MACHINE_ACCESS_TOKEN] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN] }), requireBlindIndicesEnabled({ locationWorkspaceId: "body" @@ -43,7 +43,7 @@ router.post( router.patch( "/raw/:secretName", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.MACHINE_ACCESS_TOKEN] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN] }), requireBlindIndicesEnabled({ locationWorkspaceId: "body" @@ -57,7 +57,7 @@ router.patch( router.delete( "/raw/:secretName", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.MACHINE_ACCESS_TOKEN] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN] }), requireBlindIndicesEnabled({ locationWorkspaceId: "body" @@ -71,7 +71,7 @@ router.delete( router.get( "/", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.MACHINE_ACCESS_TOKEN] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN] }), requireBlindIndicesEnabled({ locationWorkspaceId: "query" @@ -116,7 +116,7 @@ router.delete( router.post( "/:secretName", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.MACHINE_ACCESS_TOKEN] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN] }), requireBlindIndicesEnabled({ locationWorkspaceId: "body" @@ -127,7 +127,7 @@ router.post( router.get( "/:secretName", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.MACHINE_ACCESS_TOKEN] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN] }), requireBlindIndicesEnabled({ locationWorkspaceId: "query" @@ -138,7 +138,7 @@ router.get( router.patch( "/:secretName", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.MACHINE_ACCESS_TOKEN] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN] }), requireBlindIndicesEnabled({ locationWorkspaceId: "body" @@ -149,7 +149,7 @@ router.patch( router.delete( "/:secretName", requireAuth({ - acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.MACHINE_ACCESS_TOKEN] + acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN] }), requireBlindIndicesEnabled({ locationWorkspaceId: "body" diff --git a/backend/src/utils/authn/authModeValidators/machineIdentity.ts b/backend/src/utils/authn/authModeValidators/identity.ts similarity index 65% rename from backend/src/utils/authn/authModeValidators/machineIdentity.ts rename to backend/src/utils/authn/authModeValidators/identity.ts index 00c6fbe63..c85227e32 100644 --- a/backend/src/utils/authn/authModeValidators/machineIdentity.ts +++ b/backend/src/utils/authn/authModeValidators/identity.ts @@ -1,33 +1,33 @@ import jwt from "jsonwebtoken"; -import { - IMachineIdentity, - IdentityAccessToken, -} from "../../../models"; +import { IIdentity, IdentityAccessToken } from "../../../models"; import { getAuthSecret } from "../../../config"; import { AuthTokenType } from "../../../variables"; import { UnauthorizedRequestError } from "../../errors"; +import { checkIPAgainstBlocklist } from "../../../utils/ip"; -interface ValidateMachineIdentityParams { +interface ValidateIdentityParams { authTokenValue: string; + ipAddress: string; } -export const validateMachineIdentity = async ({ - authTokenValue -}: ValidateMachineIdentityParams) => { - const decodedToken = ( +export const validateIdentity = async ({ + authTokenValue, + ipAddress +}: ValidateIdentityParams) => { + const decodedToken = ( jwt.verify(authTokenValue, await getAuthSecret()) ); - if (decodedToken.authTokenType !== AuthTokenType.MACHINE_ACCESS_TOKEN) throw UnauthorizedRequestError(); + if (decodedToken.authTokenType !== AuthTokenType.IDENTITY_ACCESS_TOKEN) throw UnauthorizedRequestError(); - const machineIdentityAccessToken = await IdentityAccessToken + const identityAccessToken = await IdentityAccessToken .findOne({ _id: decodedToken.identityAccessTokenId, isAccessTokenRevoked: false }) - .populate<{ machineIdentity: IMachineIdentity }>("machineIdentity"); + .populate<{ identity: IIdentity }>("identity"); - if (!machineIdentityAccessToken || !machineIdentityAccessToken?.machineIdentity) throw UnauthorizedRequestError(); + if (!identityAccessToken || !identityAccessToken?.identity) throw UnauthorizedRequestError(); const { accessTokenNumUsesLimit, @@ -36,7 +36,12 @@ export const validateMachineIdentity = async ({ accessTokenLastRenewedAt, accessTokenMaxTTL, createdAt: accessTokenCreatedAt - } = machineIdentityAccessToken; + } = identityAccessToken; + + checkIPAgainstBlocklist({ + ipAddress, + trustedIps: identityAccessToken.accessTokenTrustedIps + }); // ttl check if (accessTokenTTL > 0) { @@ -48,7 +53,7 @@ export const validateMachineIdentity = async ({ const expirationDate = new Date(accessTokenRenewed.getTime() + ttlInMilliseconds); if (currentDate > expirationDate) throw UnauthorizedRequestError({ - message: "Failed to authenticate MI access token due to TTL expiration" + message: "Failed to authenticate identity access token due to TTL expiration" }); } else { // access token has never been renewed @@ -57,7 +62,7 @@ export const validateMachineIdentity = async ({ const expirationDate = new Date(accessTokenCreated.getTime() + ttlInMilliseconds); if (currentDate > expirationDate) throw UnauthorizedRequestError({ - message: "Failed to authenticate MI access token due to TTL expiration" + message: "Failed to authenticate identity access token due to TTL expiration" }); } } @@ -70,7 +75,7 @@ export const validateMachineIdentity = async ({ const expirationDate = new Date(accessTokenCreated.getTime() + ttlInMilliseconds); if (currentDate > expirationDate) throw UnauthorizedRequestError({ - message: "Failed to authenticate MI access token due to Max TTL expiration" + message: "Failed to authenticate identity access token due to Max TTL expiration" }); } @@ -85,7 +90,7 @@ export const validateMachineIdentity = async ({ } await IdentityAccessToken.findByIdAndUpdate( - machineIdentityAccessToken._id, + identityAccessToken._id, { accessTokenLastUsedAt: new Date(), $inc: { accessTokenNumUses: 1 } @@ -95,5 +100,5 @@ export const validateMachineIdentity = async ({ } ); - return machineIdentityAccessToken.machineIdentity; + return identityAccessToken.identity; } \ No newline at end of file diff --git a/backend/src/utils/authn/authModeValidators/index.ts b/backend/src/utils/authn/authModeValidators/index.ts index 6a8ab3ada..170a8ce59 100644 --- a/backend/src/utils/authn/authModeValidators/index.ts +++ b/backend/src/utils/authn/authModeValidators/index.ts @@ -2,4 +2,4 @@ export * from "./apiKey"; export * from "./apiKeyV2"; export * from "./jwt"; export * from "./serviceTokenV2"; -export * from "./machineIdentity"; \ No newline at end of file +export * from "./identity"; \ No newline at end of file diff --git a/backend/src/utils/authn/helpers/authDataExtractors.ts b/backend/src/utils/authn/helpers/authDataExtractors.ts index 97cb9b1f4..e928108c3 100644 --- a/backend/src/utils/authn/helpers/authDataExtractors.ts +++ b/backend/src/utils/authn/helpers/authDataExtractors.ts @@ -1,6 +1,6 @@ import { AuthData } from "../../../interfaces/middleware"; import { - MachineIdentity, + Identity, ServiceTokenData, User } from "../../../models"; @@ -19,7 +19,7 @@ import { return { serviceTokenDataId: authData.authPayload._id }; } - if (authData.authPayload instanceof MachineIdentity) { + if (authData.authPayload instanceof Identity) { return { serviceTokenDataId: authData.authPayload._id }; } }; @@ -38,7 +38,7 @@ export const getAuthDataPayloadUserObj = (authData: AuthData) => { return { user: authData.authPayload.user }; } - if (authData.authPayload instanceof MachineIdentity) { + if (authData.authPayload instanceof Identity) { return {}; } } \ No newline at end of file diff --git a/backend/src/utils/authn/helpers/index.ts b/backend/src/utils/authn/helpers/index.ts index 54a58ae33..7f3998e57 100644 --- a/backend/src/utils/authn/helpers/index.ts +++ b/backend/src/utils/authn/helpers/index.ts @@ -7,8 +7,8 @@ import { UnauthorizedRequestError } from "../../errors"; import { validateAPIKey, validateAPIKeyV2, + validateIdentity, validateJWT, - validateMachineIdentity, validateServiceTokenV2 } from "../authModeValidators"; import { getUserAgentType } from "../../posthog"; @@ -36,7 +36,7 @@ interface GetAuthDataParams { * - SERVICE_TOKEN * - API_KEY * - JWT - * - MACHINE_ACCESS_TOKEN (from machine identity) + * - IDENTITY_ACCESS_TOKEN (from identity) * - API_KEY_V2 * @param {Object} params * @param {Object.} params.headers - The HTTP request headers, usually from Express's `req.headers`. @@ -77,8 +77,8 @@ export const extractAuthMode = async ({ return { authMode: AuthMode.JWT, authTokenValue }; case AuthTokenType.API_KEY: return { authMode: AuthMode.API_KEY_V2, authTokenValue }; - case AuthTokenType.MACHINE_ACCESS_TOKEN: - return { authMode: AuthMode.MACHINE_ACCESS_TOKEN, authTokenValue }; + case AuthTokenType.IDENTITY_ACCESS_TOKEN: + return { authMode: AuthMode.IDENTITY_ACCESS_TOKEN, authTokenValue }; default: throw UnauthorizedRequestError({ message: "Failed to authenticate unknown authentication method" @@ -115,20 +115,21 @@ export const getAuthData = async ({ userAgentType } } - case AuthMode.MACHINE_ACCESS_TOKEN: { - const machineIdentity = await validateMachineIdentity({ - authTokenValue + case AuthMode.IDENTITY_ACCESS_TOKEN: { + const identity = await validateIdentity({ + authTokenValue, + ipAddress }); return { actor: { - type: ActorType.MACHINE, + type: ActorType.IDENTITY, metadata: { - machineId: machineIdentity._id.toString(), - name: machineIdentity.name + identityId: identity._id.toString(), + name: identity.name } }, - authPayload: machineIdentity, + authPayload: identity, ipAddress, userAgent, userAgentType diff --git a/backend/src/validation/auth.ts b/backend/src/validation/auth.ts index 071ad5e49..84fea54b6 100644 --- a/backend/src/validation/auth.ts +++ b/backend/src/validation/auth.ts @@ -84,6 +84,99 @@ export const ResetPasswordV1 = z.object({ }) }); +export const RenewAccessTokenV1 = z.object({ + body: z.object({ + accessToken: z.string().trim(), + }) +}); + +export const LoginUniversalAuthV1 = z.object({ + body: z.object({ + clientId: z.string().trim(), + clientSecret: z.string().trim() + }) +}); + +export const AddUniversalAuthToIdentityV1 = z.object({ + params: z.object({ + identityId: z.string().trim() + }), + body: z.object({ + clientSecretTrustedIps: z + .object({ + ipAddress: z.string().trim(), + }) + .array() + .min(1) + .default([{ ipAddress: "0.0.0.0/0" }]), + accessTokenTrustedIps: z + .object({ + ipAddress: z.string().trim(), + }) + .array() + .min(1) + .default([{ ipAddress: "0.0.0.0/0" }]), + accessTokenTTL: z.number().int().min(0).default(7200), + accessTokenMaxTTL: z.number().int().min(0).default(0), + accessTokenNumUsesLimit: z.number().int().min(0).default(0) + }) +}); + +export const UpdateUniversalAuthToIdentityV1 = z.object({ + params: z.object({ + identityId: z.string() + }), + body: z.object({ + clientSecretTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .optional(), + accessTokenTrustedIps: z + .object({ + ipAddress: z.string().trim(), + }) + .array() + .min(1) + .optional(), + accessTokenTTL: z.number().int().min(0).optional(), + accessTokenNumUsesLimit: z.number().int().min(0).optional(), + accessTokenMaxTTL: z.number().int().min(0).default(0), + }), +}); + +export const GetUniversalAuthForIdentityV1 = z.object({ + params: z.object({ + identityId: z.string().trim() + }) +}); + +export const CreateUniversalAuthClientSecretV1 = z.object({ + params: z.object({ + identityId: z.string() + }), + body: z.object({ + description: z.string().trim().default(""), + numUsesLimit: z.number().min(0).default(0), + ttl: z.number().min(0).default(0), + }), +}); + +export const GetUniversalAuthClientSecretsV1 = z.object({ + params: z.object({ + identityId: z.string() + }) +}); + +export const RevokeUniversalAuthClientSecretV1 = z.object({ + params: z.object({ + identityId: z.string(), + clientSecretId: z.string() + }) +}); + export const VerifyMfaTokenV2 = z.object({ body: z.object({ mfaToken: z.string().trim() diff --git a/backend/src/validation/identities.ts b/backend/src/validation/identities.ts new file mode 100644 index 000000000..fa22fde34 --- /dev/null +++ b/backend/src/validation/identities.ts @@ -0,0 +1,26 @@ +import { z } from "zod"; +import { NO_ACCESS } from "../variables"; + +export const CreateIdentityV1 = z.object({ + body: z.object({ + name: z.string().trim(), + organizationId: z.string().trim(), + role: z.string().trim().min(1).default(NO_ACCESS) + }) +}); + +export const UpdateIdentityV1 = z.object({ + params: z.object({ + identityId: z.string() + }), + body: z.object({ + name: z.string().trim().optional(), + role: z.string().trim().min(1).optional() + }), +}); + +export const DeleteIdentityV1 = z.object({ + params: z.object({ + identityId: z.string() + }), +}); diff --git a/backend/src/validation/index.ts b/backend/src/validation/index.ts index a9b8a963f..e2027d41b 100644 --- a/backend/src/validation/index.ts +++ b/backend/src/validation/index.ts @@ -8,5 +8,5 @@ export * from "./membershipOrg"; export * from "./organization"; export * from "./secrets"; export * from "./serviceTokenData"; -export * from "./machineIdentity"; +export * from "./identities"; export * from "./apiKeyDataV3"; diff --git a/backend/src/validation/integrationAuth.ts b/backend/src/validation/integrationAuth.ts index d4a6b42a5..7488109d0 100644 --- a/backend/src/validation/integrationAuth.ts +++ b/backend/src/validation/integrationAuth.ts @@ -58,9 +58,9 @@ const validateClientForIntegrationAuth = async ({ throw UnauthorizedRequestError({ message: "Failed service token authorization for integration authorization" }); - case ActorType.MACHINE: + case ActorType.IDENTITY: throw UnauthorizedRequestError({ - message: "Failed machine authorization for integration authorization" + message: "Failed identity authorization for integration authorization" }); } }; diff --git a/backend/src/validation/machineIdentity.ts b/backend/src/validation/machineIdentity.ts deleted file mode 100644 index 0f40cff6c..000000000 --- a/backend/src/validation/machineIdentity.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { z } from "zod"; -import { NO_ACCESS } from "../variables"; - -export const GetClientSecretsV1 = z.object({ - params: z.object({ - machineId: z.string() - }) -}); - -export const CreateClientSecretV1 = z.object({ - params: z.object({ - machineId: z.string() - }), - body: z.object({ - description: z.string().trim().default(""), - numUsesLimit: z.number().min(0).default(0), - ttl: z.number().min(0).default(0), - }), -}); - -export const DeleteClientSecretV1 = z.object({ - params: z.object({ - machineId: z.string(), - clientSecretId: z.string() - }) -}); - -export const LoginMachineIdentityV1 = z.object({ - body: z.object({ - clientId: z.string().trim(), - clientSecret: z.string().trim() - }) -}); - -export const RenewAccessTokenV1 = z.object({ - body: z.object({ - accessToken: z.string().trim() - }) -}); - -export const CreateMachineIdentityV1 = z.object({ - body: z.object({ - name: z.string().trim(), - organizationId: z.string().trim(), - role: z.string().trim().min(1).default(NO_ACCESS), - clientSecretTrustedIps: z - .object({ - ipAddress: z.string().trim(), - }) - .array() - .min(1) - .default([{ ipAddress: "0.0.0.0/0" }]), - accessTokenTrustedIps: z - .object({ - ipAddress: z.string().trim(), - }) - .array() - .min(1) - .default([{ ipAddress: "0.0.0.0/0" }]), - accessTokenTTL: z.number().int().min(0).default(7200), - accessTokenMaxTTL: z.number().int().min(0).default(0), - accessTokenNumUsesLimit: z.number().int().min(0).default(0) - }) -}); - -export const UpdateMachineIdentityV1 = z.object({ - params: z.object({ - machineId: z.string() - }), - body: z.object({ - name: z.string().trim().optional(), - role: z.string().trim().min(1).optional(), - clientSecretTrustedIps: z - .object({ - ipAddress: z.string().trim() - }) - .array() - .min(1) - .optional(), - accessTokenTrustedIps: z - .object({ - ipAddress: z.string().trim(), - }) - .array() - .min(1) - .optional(), - accessTokenTTL: z.number().int().min(0).optional(), - accessTokenNumUsesLimit: z.number().int().min(0).optional(), - accessTokenMaxTTL: z.number().int().min(0).default(0), - }), -}); - -export const DeleteMachineIdentityV1 = z.object({ - params: z.object({ - machineId: z.string() - }), -}); \ No newline at end of file diff --git a/backend/src/validation/organization.ts b/backend/src/validation/organization.ts index 20e2f1567..d0ab37057 100644 --- a/backend/src/validation/organization.ts +++ b/backend/src/validation/organization.ts @@ -46,9 +46,9 @@ export const validateClientForOrganization = async ({ throw UnauthorizedRequestError({ message: "Failed service token authorization for organization" }); - case ActorType.MACHINE: + case ActorType.IDENTITY: throw UnauthorizedRequestError({ - message: "Failed machine authorization for organization" + message: "Failed identity authorization for organization" }); } }; @@ -216,4 +216,8 @@ export const DeleteOrgv2 = z.object({ export const GetOrgServiceMembersV2 = z.object({ params: z.object({ organizationId: z.string().trim() }) +}); + +export const GetOrgIdentityMembershipsV2 = z.object({ + params: z.object({ organizationId: z.string().trim() }) }); \ No newline at end of file diff --git a/backend/src/validation/workspace.ts b/backend/src/validation/workspace.ts index 4f1d017ae..4c9a2183d 100644 --- a/backend/src/validation/workspace.ts +++ b/backend/src/validation/workspace.ts @@ -60,9 +60,9 @@ export const validateClientForWorkspace = async ({ requiredPermissions }); return { membership, workspace }; - case ActorType.MACHINE: + case ActorType.IDENTITY: throw UnauthorizedRequestError({ - message: "Failed machine authorization for organization" + message: "Failed identity authorization for organization" }); } }; @@ -280,34 +280,34 @@ export const ToggleAutoCapitalizationV2 = z.object({ }) }); -export const AddMachineToWorkspaceV2 = z.object({ +export const AddIdentityToWorkspaceV2 = z.object({ params: z.object({ workspaceId: z.string().trim(), - machineId: z.string().trim() + identityId: z.string().trim() }), body: z.object({ role: z.string().trim().min(1).default(NO_ACCESS), }) }); -export const UpdateMachineWorkspaceRoleV2 = z.object({ +export const UpdateIdentityWorkspaceRoleV2 = z.object({ params: z.object({ workspaceId: z.string().trim(), - machineId: z.string().trim() + identityId: z.string().trim() }), body: z.object({ role: z.string().trim().min(1).default(NO_ACCESS), }) }); -export const DeleteMachineFromWorkspaceV2 = z.object({ +export const DeleteIdentityFromWorkspaceV2 = z.object({ params: z.object({ workspaceId: z.string().trim(), - machineId: z.string().trim() + identityId: z.string().trim() }) }); -export const GetWorkspaceMachineMembersV2 = z.object({ +export const GetWorkspaceIdentityMembersV2 = z.object({ params: z.object({ workspaceId: z.string().trim() }), diff --git a/backend/src/variables/authentication.ts b/backend/src/variables/authentication.ts index c474eb309..5eec0ba22 100644 --- a/backend/src/variables/authentication.ts +++ b/backend/src/variables/authentication.ts @@ -7,14 +7,13 @@ export enum AuthTokenType { MFA_TOKEN = "mfaToken", // TODO: remove in favor of claim PROVIDER_TOKEN = "providerToken", // TODO: remove in favor of claim API_KEY = "apiKey", - MACHINE_ACCESS_TOKEN = "machineAccessToken", - MACHINE_REFRESH_TOKEN = "machineRefreshToken" + IDENTITY_ACCESS_TOKEN = "identityAccessToken", } export enum AuthMode { JWT = "jwt", SERVICE_TOKEN = "serviceToken", - MACHINE_ACCESS_TOKEN = "machineAccessToken", + IDENTITY_ACCESS_TOKEN = "identityAccessToken", API_KEY = "apiKey", API_KEY_V2 = "apiKeyV2" } diff --git a/frontend/src/context/OrgPermissionContext/types.ts b/frontend/src/context/OrgPermissionContext/types.ts index 65315cb16..f0fbd23eb 100644 --- a/frontend/src/context/OrgPermissionContext/types.ts +++ b/frontend/src/context/OrgPermissionContext/types.ts @@ -16,7 +16,7 @@ export enum OrgPermissionSubjects { Sso = "sso", Billing = "billing", SecretScanning = "secret-scanning", - MachineIdentity = "machine-identity" + Identity = "identity" } export type OrgPermissionSet = @@ -29,6 +29,6 @@ export type OrgPermissionSet = | [OrgPermissionActions, OrgPermissionSubjects.Sso] | [OrgPermissionActions, OrgPermissionSubjects.SecretScanning] | [OrgPermissionActions, OrgPermissionSubjects.Billing] - | [OrgPermissionActions, OrgPermissionSubjects.MachineIdentity]; + | [OrgPermissionActions, OrgPermissionSubjects.Identity]; export type TOrgPermission = MongoAbility; diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index 60d33afbd..608419f4f 100644 --- a/frontend/src/context/ProjectPermissionContext/types.ts +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -23,7 +23,7 @@ export enum ProjectPermissionSub { SecretRollback = "secret-rollback", SecretApproval = "secret-approval", SecretRotation = "secret-rotation", - MachineIdentity = "machine-identity" + Identity = "identity" } type SubjectFields = { @@ -45,7 +45,7 @@ export type ProjectPermissionSet = | [ProjectPermissionActions, ProjectPermissionSub.Environments] | [ProjectPermissionActions, ProjectPermissionSub.IpAllowList] | [ProjectPermissionActions, ProjectPermissionSub.Settings] - | [ProjectPermissionActions, ProjectPermissionSub.MachineIdentity] + | [ProjectPermissionActions, ProjectPermissionSub.Identity] | [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens] | [ProjectPermissionActions, ProjectPermissionSub.SecretApproval] | [ProjectPermissionActions, ProjectPermissionSub.SecretRotation] diff --git a/frontend/src/hooks/api/auditLogs/constants.tsx b/frontend/src/hooks/api/auditLogs/constants.tsx index be43a2112..01674d2ee 100644 --- a/frontend/src/hooks/api/auditLogs/constants.tsx +++ b/frontend/src/hooks/api/auditLogs/constants.tsx @@ -16,9 +16,17 @@ export const eventToNameMap: { [K in EventType]: string } = { [EventType.DELETE_TRUSTED_IP]: "Delete trusted IP", [EventType.CREATE_SERVICE_TOKEN]: "Create service token", [EventType.DELETE_SERVICE_TOKEN]: "Delete service token", - [EventType.CREATE_MACHINE_IDENTITY]: "Create machine identity", - [EventType.UPDATE_MACHINE_IDENTITY]: "Update machine identity", - [EventType.DELETE_MACHINE_IDENTITY]: "Delete machine identity", + [EventType.CREATE_IDENTITY]: "Create identity", + [EventType.UPDATE_IDENTITY]: "Update identity", + [EventType.DELETE_IDENTITY]: "Delete identity", + [EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH]: "Login via universal auth", + [EventType.ADD_IDENTITY_UNIVERSAL_AUTH]: "Add universal auth", + [EventType.UPDATE_IDENTITY_UNIVERSAL_AUTH]: "Update universal auth", + [EventType.GET_IDENTITY_UNIVERSAL_AUTH]: "Get universal auth", + [EventType.CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET]: "Create universal auth client secret", + [EventType.REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET]: "Revoke universal auth client secret", + [EventType.GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS]: "Get universal auth client secrets", + [EventType.GET_IDENTITY_UNIVERSAL_AUTH]: "Get universal auth", [EventType.CREATE_ENVIRONMENT]: "Create environment", [EventType.UPDATE_ENVIRONMENT]: "Update environment", [EventType.DELETE_ENVIRONMENT]: "Delete environment", diff --git a/frontend/src/hooks/api/auditLogs/enums.tsx b/frontend/src/hooks/api/auditLogs/enums.tsx index 9aefd34a1..ea1affa87 100644 --- a/frontend/src/hooks/api/auditLogs/enums.tsx +++ b/frontend/src/hooks/api/auditLogs/enums.tsx @@ -1,7 +1,7 @@ export enum ActorType { USER = "user", SERVICE = "service", - MACHINE = "machine" + IDENTITY = "identity" } export enum UserAgentType { @@ -27,9 +27,16 @@ export enum EventType { DELETE_TRUSTED_IP = "delete-trusted-ip", CREATE_SERVICE_TOKEN = "create-service-token", // v2 DELETE_SERVICE_TOKEN = "delete-service-token", // v2 - CREATE_MACHINE_IDENTITY = "create-machine-identity", - UPDATE_MACHINE_IDENTITY = "update-machine-identity", - DELETE_MACHINE_IDENTITY = "delete-machine-identity", + CREATE_IDENTITY = "create-identity", + UPDATE_IDENTITY = "update-identity", + DELETE_IDENTITY = "delete-identity", + LOGIN_IDENTITY_UNIVERSAL_AUTH = "login-identity-universal-auth", + ADD_IDENTITY_UNIVERSAL_AUTH = "add-identity-universal-auth", + UPDATE_IDENTITY_UNIVERSAL_AUTH = "update-identity-universal-auth", + GET_IDENTITY_UNIVERSAL_AUTH = "get-identity-universal-auth", + CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "create-identity-universal-auth-client-secret", + REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "revoke-identity-universal-auth-client-secret", + GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS = "get-identity-universal-auth-client-secret", CREATE_ENVIRONMENT = "create-environment", UPDATE_ENVIRONMENT = "update-environment", DELETE_ENVIRONMENT = "delete-environment", diff --git a/frontend/src/hooks/api/auditLogs/types.tsx b/frontend/src/hooks/api/auditLogs/types.tsx index ce17d490e..63efda532 100644 --- a/frontend/src/hooks/api/auditLogs/types.tsx +++ b/frontend/src/hooks/api/auditLogs/types.tsx @@ -1,3 +1,4 @@ +import { IdentityTrustedIp } from "../identities/types"; import { ActorType, EventType, UserAgentType } from "./enums"; interface UserActorMetadata { @@ -10,8 +11,8 @@ interface ServiceActorMetadata { name: string; } -interface MachineActorMetadata { - machineId: string; +interface IdentityActorMetadata { + identityId: string; name: string; } @@ -25,12 +26,12 @@ export interface ServiceActor { metadata: ServiceActorMetadata; } -export interface MachineActor { - type: ActorType.MACHINE; - metadata: MachineActorMetadata; +export interface IdentityActor { + type: ActorType.IDENTITY; + metadata: IdentityActorMetadata; } -export type Actor = UserActor | ServiceActor | MachineActor; +export type Actor = UserActor | ServiceActor | IdentityActor; interface GetSecretsEvent { type: EventType.GET_SECRETS; @@ -193,34 +194,91 @@ interface DeleteServiceTokenEvent { }; } -interface CreateMachineIdentityEvent { - type: EventType.CREATE_MACHINE_IDENTITY; - metadata: { - name: string; - isActive: boolean; - role: string; - expiresAt?: Date; - } +interface CreateIdentityEvent { // note: currently not logging org-role + type: EventType.CREATE_IDENTITY; + metadata: { + identityId: string; + name: string; + }; } -interface UpdateMachineIdentityEvent { - type: EventType.UPDATE_MACHINE_IDENTITY; - metadata: { - name?: string; - isActive?: boolean; - role?: string; - expiresAt?: Date; - } +interface UpdateIdentityEvent { + type: EventType.UPDATE_IDENTITY; + metadata: { + identityId: string; + name?: string; + }; } -interface DeleteMachineIdentityEvent { - type: EventType.DELETE_MACHINE_IDENTITY; - metadata: { - name: string; - isActive: boolean; - role?: string; - expiresAt?: Date; - } +interface DeleteIdentityEvent { + type: EventType.DELETE_IDENTITY; + metadata: { + identityId: string; + }; +} + +interface LoginIdentityUniversalAuthEvent { + type: EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH ; + metadata: { + identityId: string; + clientSecretId: string; + identityAccessTokenId: string; + }; +} + +interface AddIdentityUniversalAuthEvent { + type: EventType.ADD_IDENTITY_UNIVERSAL_AUTH; + metadata: { + identityId: string; + clientSecretTrustedIps: Array; + accessTokenTTL: number; + accessTokenMaxTTL: number; + accessTokenNumUsesLimit: number; + accessTokenTrustedIps: Array; + }; +} + +interface UpdateIdentityUniversalAuthEvent { + type: EventType.UPDATE_IDENTITY_UNIVERSAL_AUTH; + metadata: { + identityId: string; + clientSecretTrustedIps?: Array; + accessTokenTTL?: number; + accessTokenMaxTTL?: number; + accessTokenNumUsesLimit?: number; + accessTokenTrustedIps?: Array; + }; +} + +interface GetIdentityUniversalAuthEvent { + type: EventType.GET_IDENTITY_UNIVERSAL_AUTH; + metadata: { + identityId: string; + }; +} + +interface CreateIdentityUniversalAuthClientSecretEvent { + type: EventType.CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET ; + metadata: { + identityId: string; + clientSecretId: string; + }; +} + +interface GetIdentityUniversalAuthClientSecretsEvent { + type: EventType.GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS; + metadata: { + identityId: string; + }; +} + + +interface RevokeIdentityUniversalAuthClientSecretEvent { + type: EventType.REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET ; + metadata: { + identityId: string; + clientSecretId: string; + }; } interface CreateEnvironmentEvent { @@ -419,9 +477,16 @@ export type Event = | DeleteTrustedIPEvent | CreateServiceTokenEvent | DeleteServiceTokenEvent - | CreateMachineIdentityEvent - | UpdateMachineIdentityEvent - | DeleteMachineIdentityEvent + | CreateIdentityEvent + | UpdateIdentityEvent + | DeleteIdentityEvent + | LoginIdentityUniversalAuthEvent + | AddIdentityUniversalAuthEvent + | UpdateIdentityUniversalAuthEvent + | GetIdentityUniversalAuthEvent + | CreateIdentityUniversalAuthClientSecretEvent + | GetIdentityUniversalAuthClientSecretsEvent + | RevokeIdentityUniversalAuthClientSecretEvent | CreateEnvironmentEvent | UpdateEnvironmentEvent | DeleteEnvironmentEvent diff --git a/frontend/src/hooks/api/identities/constants.tsx b/frontend/src/hooks/api/identities/constants.tsx new file mode 100644 index 000000000..6cce84b53 --- /dev/null +++ b/frontend/src/hooks/api/identities/constants.tsx @@ -0,0 +1,5 @@ +import { IdentityAuthMethod } from "./enums"; + +export const identityAuthToNameMap: { [I in IdentityAuthMethod]: string } = { + [IdentityAuthMethod.UNIVERSAL_AUTH]: "Universal Auth" +}; \ No newline at end of file diff --git a/frontend/src/hooks/api/identities/enums.tsx b/frontend/src/hooks/api/identities/enums.tsx new file mode 100644 index 000000000..71ef886d6 --- /dev/null +++ b/frontend/src/hooks/api/identities/enums.tsx @@ -0,0 +1,3 @@ +export enum IdentityAuthMethod { + UNIVERSAL_AUTH = "universal-auth" +} \ No newline at end of file diff --git a/frontend/src/hooks/api/identities/index.tsx b/frontend/src/hooks/api/identities/index.tsx new file mode 100644 index 000000000..b893ff99d --- /dev/null +++ b/frontend/src/hooks/api/identities/index.tsx @@ -0,0 +1,14 @@ +export { identityAuthToNameMap } from "./constants"; +export { IdentityAuthMethod } from "./enums"; +export { + useAddIdentityUniversalAuth, + useCreateIdentity, + useCreateIdentityUniversalAuthClientSecret, + useDeleteIdentity, + useDeleteIdentityUniversalAuthClientSecret, + useUpdateIdentity, + useUpdateIdentityUniversalAuth} from "./mutations"; +export { + useGetIdentityUniversalAuth, + useGetIdentityUniversalAuthClientSecrets +} from "./queries"; \ No newline at end of file diff --git a/frontend/src/hooks/api/identities/mutations.tsx b/frontend/src/hooks/api/identities/mutations.tsx new file mode 100644 index 000000000..044983f8f --- /dev/null +++ b/frontend/src/hooks/api/identities/mutations.tsx @@ -0,0 +1,163 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { organizationKeys } from "../organization/queries"; +import { identitiesKeys } from "./queries"; +import { + AddIdentityUniversalAuthDTO, + CreateIdentityDTO, + CreateIdentityUniversalAuthClientSecretDTO, + CreateIdentityUniversalAuthClientSecretRes, + DeleteIdentityDTO, + DeleteIdentityUniversalAuthClientSecretDTO, + Identity, + IdentityUniversalAuth, + UpdateIdentityDTO, + UpdateIdentityUniversalAuthDTO} from "./types"; + +export const useCreateIdentity = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (body) => { + const { data: { identity } } = await apiRequest.post("/api/v1/identities/", body); + return identity; + }, + onSuccess: (_, { organizationId }) => { + queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId)); + } + }); +}; + +export const useUpdateIdentity = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + identityId, + name, + role + }) => { + + const { data: { identity } } = await apiRequest.patch(`/api/v1/identities/${identityId}`, { + name, + role + }); + + return identity; + }, + onSuccess: (_, { organizationId }) => { + queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId)); + } + }); +} + +export const useDeleteIdentity = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + identityId, + }) => { + const { data: { identity } } = await apiRequest.delete(`/api/v1/identities/${identityId}`); + return identity; + }, + onSuccess: (_, { organizationId }) => { + queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId)); + } + }); +}; + +// TODO: move these to /auth + +export const useAddIdentityUniversalAuth = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + identityId, + clientSecretTrustedIps, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps, + }) => { + const { data: { identityUniversalAuth } } = await apiRequest.post(`/api/v1/auth/universal-auth/identities/${identityId}`, + { + clientSecretTrustedIps, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps, + } + ); + return identityUniversalAuth; + }, + onSuccess: (_, { organizationId }) => { + queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId)); + } + }); +}; + +export const useUpdateIdentityUniversalAuth = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + identityId, + clientSecretTrustedIps, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps, + }) => { + const { data: { identityUniversalAuth } } = await apiRequest.patch(`/api/v1/auth/universal-auth/identities/${identityId}`, + { + clientSecretTrustedIps, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps, + } + ); + return identityUniversalAuth; + }, + onSuccess: (_, { organizationId }) => { + queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId)); + } + }); +}; + +export const useCreateIdentityUniversalAuthClientSecret = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + identityId, + description, + ttl, + numUsesLimit + }) => { + const { data } = await apiRequest.post(`/api/v1/auth/universal-auth/identities/${identityId}/client-secrets`, { + description, + ttl, + numUsesLimit + }); + return data; + }, + onSuccess: (_, { identityId }) => { + queryClient.invalidateQueries(identitiesKeys.getIdentityUniversalAuthClientSecrets(identityId)); + } + }); +}; + +export const useDeleteIdentityUniversalAuthClientSecret = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + identityId, + clientSecretId + }) => { + const { data: { identity } } = await apiRequest.delete(`/api/v1/auth/universal-auth/identities/${identityId}/client-secrets/${clientSecretId}`); + return identity; + }, + onSuccess: (_, { identityId }) => { + queryClient.invalidateQueries(identitiesKeys.getIdentityUniversalAuthClientSecrets(identityId)); + } + }); +}; \ No newline at end of file diff --git a/frontend/src/hooks/api/identities/queries.tsx b/frontend/src/hooks/api/identities/queries.tsx new file mode 100644 index 000000000..aa14c1e60 --- /dev/null +++ b/frontend/src/hooks/api/identities/queries.tsx @@ -0,0 +1,40 @@ +import { useQuery } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { ClientSecretData , IdentityUniversalAuth } from "./types"; + +export const identitiesKeys = { + getIdentityUniversalAuth: (identityId: string) => [{ identityId }, "identity-universal-auth"] as const, + getIdentityUniversalAuthClientSecrets: (identityId: string) => [{ identityId }, "identity-universal-auth-client-secrets"] as const +} + +export const useGetIdentityUniversalAuth = (identityId: string) => { + return useQuery({ + queryKey: identitiesKeys.getIdentityUniversalAuth(identityId), + queryFn: async () => { + if (identityId === "") return undefined; + + const { data: { identityUniversalAuth } } = await apiRequest.get<{ identityUniversalAuth: IdentityUniversalAuth }>( + `/api/v1/auth/universal-auth/identities/${identityId}` + ); + + return identityUniversalAuth; + } + }); +} + +export const useGetIdentityUniversalAuthClientSecrets = (identityId: string) => { + return useQuery({ + queryKey: identitiesKeys.getIdentityUniversalAuthClientSecrets(identityId), + queryFn: async () => { + if (identityId === "") return []; + + const { data: { clientSecretData } } = await apiRequest.get<{ clientSecretData: ClientSecretData[] }>( + `/api/v1/auth/universal-auth/identities/${identityId}/client-secrets` + ); + + return clientSecretData; + } + }); +} \ No newline at end of file diff --git a/frontend/src/hooks/api/machineIdentities/types.ts b/frontend/src/hooks/api/identities/types.ts similarity index 56% rename from frontend/src/hooks/api/machineIdentities/types.ts rename to frontend/src/hooks/api/identities/types.ts index 2c5579fd9..0a9a41fc3 100644 --- a/frontend/src/hooks/api/machineIdentities/types.ts +++ b/frontend/src/hooks/api/identities/types.ts @@ -1,29 +1,108 @@ import { TRole } from "../roles/types"; +import { IdentityAuthMethod } from "./enums"; -export type MachineTrustedIp = { +export type IdentityTrustedIp = { _id: string; ipAddress: string; type: "ipv4" | "ipv6"; prefix?: number; } -export type MachineIdentity = { +export type Identity = { _id: string; - clientId: string; name: string; - organization: string; - accessTokenTTL: number; - accessTokenMaxTTL: number; - accessTokenNumUsesLimit: number; - clientSecretTrustedIps: MachineTrustedIp[]; - accessTokenTrustedIps: MachineTrustedIp[]; + authMethod?: IdentityAuthMethod; createdAt: string; updatedAt: string; }; -export type MachineIdentityClientSecret = { +export type IdentityMembershipOrg = { _id: string; - machineIdentity: string; + identity: Identity; + organization: string; + role: "admin" | "member" | "viewer" | "no-access" | "custom"; + customRole?: TRole; + createdAt: string; + updatedAt: string; +} + +export type IdentityMembership = { + _id: string; + identity: Identity; + organization: string; + role: "admin" | "member" | "viewer" | "no-access" | "custom"; + customRole?: TRole; + createdAt: string; + updatedAt: string; +} + +export type CreateIdentityDTO = { + name: string; + organizationId: string; + role?: string; +} + +export type UpdateIdentityDTO = { + identityId: string; + name?: string; + role?: string; + organizationId: string; +} + +export type DeleteIdentityDTO = { + identityId: string; + organizationId: string; +} + +export type IdentityUniversalAuth = { + identityId: string; + clientId: string; + clientSecretTrustedIps: IdentityTrustedIp[]; + accessTokenTTL: number; + accessTokenMaxTTL: number; + accessTokenNumUsesLimit: number; + accessTokenTrustedIps: IdentityTrustedIp[]; +} + +export type AddIdentityUniversalAuthDTO = { + organizationId: string; + identityId: string; + clientSecretTrustedIps: { + ipAddress: string; + }[]; + accessTokenTTL: number; + accessTokenMaxTTL: number; + accessTokenNumUsesLimit: number; + accessTokenTrustedIps: { + ipAddress: string; + }[]; +} + +export type UpdateIdentityUniversalAuthDTO = { + organizationId: string; + identityId: string; + clientSecretTrustedIps?: { + ipAddress: string; + }[]; + accessTokenTTL?: number; + accessTokenMaxTTL?: number; + accessTokenNumUsesLimit?: number; + accessTokenTrustedIps?: { + ipAddress: string; + }[]; +} + +export type CreateIdentityUniversalAuthClientSecretDTO = { + identityId: string; + description?: string; + ttl?: number; + numUsesLimit?: number; +} + +export type ClientSecretData = { + _id: string; + identityUniversalAuth: string; + isClientSecretRevoked: boolean; description: string; clientSecretPrefix: string; clientSecretNumUses: number; @@ -31,80 +110,14 @@ export type MachineIdentityClientSecret = { clientSecretTTL: number; createdAt: string; updatedAt: string; - isClientSecretRevoked: boolean; } -export type MachineMembershipOrg = { - _id: string; - machineIdentity: MachineIdentity; - organization: string; - role: "admin" | "member" | "viewer" | "no-access" | "custom"; - customRole?: TRole; - createdAt: string; - updatedAt: string; -} - -export type MachineMembership = { - _id: string; - machineIdentity: MachineIdentity; - organization: string; - role: "admin" | "member" | "viewer" | "no-access" | "custom"; - customRole?: TRole; - createdAt: string; - updatedAt: string; -} - -export type CreateMachineIdentityDTO = { - name: string; - organizationId: string; - role?: string; - clientSecretTrustedIps: { - ipAddress: string; - }[]; - accessTokenTrustedIps: { - ipAddress: string; - }[]; - accessTokenTTL: number; - accessTokenMaxTTL: number; - accessTokenNumUsesLimit: number; -} - -export type CreateMachineIdentityClientSecretDTO = { - machineId: string; - description?: string; - ttl?: number; - numUsesLimit?: number; -} - -export type CreateMachineIdentityClientSecretRes = { +export type CreateIdentityUniversalAuthClientSecretRes = { clientSecret: string; - machineIdentity: string; - isActive: boolean; - description: string; - clientSecretNumUses: number; - clientSecretNumUsesLimit: number; - expiresAt?: Date; + clientSecretData: ClientSecretData; } -export type CreateMachineIdentityRes = { - machineIdentity: MachineIdentity; -} - -export type UpdateMachineIdentityDTO = { - machineId: string; - name?: string; - role?: string; - clientSecretTrustedIps?: { - ipAddress: string; - }[]; - accessTokenTrustedIps?: { - ipAddress: string; - }[]; - accessTokenTTL?: number; - accessTokenMaxTTL?: number; - accessTokenNumUsesLimit?: number; -} - -export type DeleteMachineIdentityDTO = { - machineId: string; +export type DeleteIdentityUniversalAuthClientSecretDTO = { + identityId: string; + clientSecretId: string; } \ No newline at end of file diff --git a/frontend/src/hooks/api/index.tsx b/frontend/src/hooks/api/index.tsx index f3f1e4eee..4caeaea48 100644 --- a/frontend/src/hooks/api/index.tsx +++ b/frontend/src/hooks/api/index.tsx @@ -3,11 +3,11 @@ export * from "./apiKeys"; export * from "./auditLogs"; export * from "./auth"; export * from "./bots"; +export * from "./identities"; export * from "./incidentContacts"; export * from "./integrationAuth"; export * from "./integrations"; export * from "./keys"; -export * from "./machineIdentities"; export * from "./organization"; export * from "./roles"; export * from "./secretApproval"; diff --git a/frontend/src/hooks/api/machineIdentities/index.tsx b/frontend/src/hooks/api/machineIdentities/index.tsx deleted file mode 100644 index 71acfab8e..000000000 --- a/frontend/src/hooks/api/machineIdentities/index.tsx +++ /dev/null @@ -1,9 +0,0 @@ -export { - useCreateMachineIdentity, - useCreateMachineIdentityClientSecret, - useDeleteMachineIdentity, - useDeleteMachineIdentityClientSecret, - useUpdateMachineIdentity} from "./mutations"; -export { - useGetMachineIdentityClientSecrets -} from "./queries"; \ No newline at end of file diff --git a/frontend/src/hooks/api/machineIdentities/mutations.tsx b/frontend/src/hooks/api/machineIdentities/mutations.tsx deleted file mode 100644 index 20508a5d7..000000000 --- a/frontend/src/hooks/api/machineIdentities/mutations.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import { useMutation, useQueryClient } from "@tanstack/react-query"; - -import { apiRequest } from "@app/config/request"; - -import { organizationKeys } from "../organization/queries"; -import { machineIdentityKeys } from "./queries"; -import { - CreateMachineIdentityClientSecretDTO, - CreateMachineIdentityClientSecretRes, - CreateMachineIdentityDTO, - CreateMachineIdentityRes, - DeleteMachineIdentityDTO, - MachineIdentity, - UpdateMachineIdentityDTO, -} from "./types"; - -export const useCreateMachineIdentity = () => { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: async (body) => { - const { data } = await apiRequest.post("/api/v1/machine-identities/", body); - return data; - }, - onSuccess: ({ machineIdentity }) => { - queryClient.invalidateQueries(organizationKeys.getOrgServiceMemberships(machineIdentity.organization)); - } - }); -}; - -export const useCreateMachineIdentityClientSecret = () => { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: async ({ - machineId, - description, - ttl, - numUsesLimit - }) => { - - const { data } = await apiRequest.post(`/api/v1/machine-identities/${machineId}/client-secrets`, { - machineId, - description, - ttl, - numUsesLimit - }); - - return data; - }, - onSuccess: (_, { machineId }) => { - queryClient.invalidateQueries(machineIdentityKeys.getMachineIdentityClientSecrets(machineId)); - } - }); -}; - -export const useDeleteMachineIdentityClientSecret = () => { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: async ({ - machineId, - clientSecretId - }: { - machineId:string; - clientSecretId: string; - }) => { - const { data } = await apiRequest.post(`/api/v1/machine-identities/${machineId}/client-secrets/${clientSecretId}/revoke`); - return data; - }, - onSuccess: (_, { machineId }) => { - queryClient.invalidateQueries(machineIdentityKeys.getMachineIdentityClientSecrets(machineId)); - } - }); -}; - -export const useUpdateMachineIdentity = () => { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: async ({ - machineId, - name, - role, - clientSecretTrustedIps, - accessTokenTrustedIps, - accessTokenTTL, - accessTokenNumUsesLimit, - accessTokenMaxTTL - }) => { - - const { data: { machineIdentity } } = await apiRequest.patch(`/api/v1/machine-identities/${machineId}`, { - name, - role, - clientSecretTrustedIps, - accessTokenTrustedIps, - accessTokenTTL, - accessTokenNumUsesLimit, - accessTokenMaxTTL - }); - - return machineIdentity; - }, - onSuccess: ({ organization }) => { - queryClient.invalidateQueries(organizationKeys.getOrgServiceMemberships(organization)); - } - }); -}; - -export const useDeleteMachineIdentity = () => { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: async ({ - machineId - }) => { - const { data: { machineIdentity } } = await apiRequest.delete(`/api/v1/machine-identities/${machineId}`); - return machineIdentity; - }, - onSuccess: ({ organization }) => { - queryClient.invalidateQueries(organizationKeys.getOrgServiceMemberships(organization)); - } - }); -}; \ No newline at end of file diff --git a/frontend/src/hooks/api/machineIdentities/queries.tsx b/frontend/src/hooks/api/machineIdentities/queries.tsx deleted file mode 100644 index 347ae9661..000000000 --- a/frontend/src/hooks/api/machineIdentities/queries.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; - -import { apiRequest } from "@app/config/request"; - -import { - MachineIdentityClientSecret -} from "./types"; - -export const machineIdentityKeys = { - getMachineIdentityClientSecrets: (machineId: string) => [{ machineId }, "machine-identity-client-secrets"] as const -} - -export const useGetMachineIdentityClientSecrets = (machineId: string) => { - return useQuery({ - queryKey: machineIdentityKeys.getMachineIdentityClientSecrets(machineId), - queryFn: async () => { - if (machineId === "") return []; - - const { data: { clientSecretData } } = await apiRequest.get<{ clientSecretData: MachineIdentityClientSecret[] }>( - `/api/v1/machine-identities/${machineId}/client-secrets` - ); - - return clientSecretData; - } - }); -} \ No newline at end of file diff --git a/frontend/src/hooks/api/organization/index.ts b/frontend/src/hooks/api/organization/index.ts index dcc5b7daf..c8a284836 100644 --- a/frontend/src/hooks/api/organization/index.ts +++ b/frontend/src/hooks/api/organization/index.ts @@ -6,7 +6,7 @@ export { useDeleteOrgById, useDeleteOrgPmtMethod, useDeleteOrgTaxId, - useGetMachineMembershipOrgs, + useGetIdentityMembershipOrgs, useGetOrganizations, useGetOrgBillingDetails, useGetOrgInvoices, @@ -18,4 +18,5 @@ export { useGetOrgTaxIds, useGetOrgTrialUrl, useRenameOrg, - useUpdateOrgBillingDetails} from "./queries"; + useUpdateOrgBillingDetails +} from "./queries"; diff --git a/frontend/src/hooks/api/organization/queries.tsx b/frontend/src/hooks/api/organization/queries.tsx index 1e8f06e8e..5b8b145c2 100644 --- a/frontend/src/hooks/api/organization/queries.tsx +++ b/frontend/src/hooks/api/organization/queries.tsx @@ -2,7 +2,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; -import { MachineMembershipOrg } from "../machineIdentities/types"; +import { IdentityMembershipOrg } from "../identities/types"; import { BillingDetails, Invoice, @@ -27,7 +27,7 @@ export const organizationKeys = { getOrgTaxIds: (orgId: string) => [{ orgId }, "organization-tax-ids"] as const, getOrgInvoices: (orgId: string) => [{ orgId }, "organization-invoices"] as const, getOrgLicenses: (orgId: string) => [{ orgId }, "organization-licenses"] as const, - getOrgServiceMemberships: (orgId: string) => [{ orgId }, "organization-service-memberships"] as const, + getOrgIdentityMemberships: (orgId: string) => [{ orgId }, "organization-identity-memberships"] as const, }; export const fetchOrganizations = async () => { @@ -351,17 +351,17 @@ export const useGetOrgLicenses = (organizationId: string) => { }); }; -export const useGetMachineMembershipOrgs = (organizationId: string) => { +export const useGetIdentityMembershipOrgs = (organizationId: string) => { return useQuery({ - queryKey: organizationKeys.getOrgServiceMemberships(organizationId), + queryKey: organizationKeys.getOrgIdentityMemberships(organizationId), queryFn: async () => { const { - data: { machineMemberships } - } = await apiRequest.get<{ machineMemberships: MachineMembershipOrg[] }>( - `/api/v2/organizations/${organizationId}/machine-memberships` + data: { identityMemberships } + } = await apiRequest.get<{ identityMemberships: IdentityMembershipOrg[] }>( + `/api/v2/organizations/${organizationId}/identity-memberships` ); - return machineMemberships; + return identityMemberships; }, enabled: true }); diff --git a/frontend/src/hooks/api/workspace/index.tsx b/frontend/src/hooks/api/workspace/index.tsx index 494cee176..6a678a432 100644 --- a/frontend/src/hooks/api/workspace/index.tsx +++ b/frontend/src/hooks/api/workspace/index.tsx @@ -1,9 +1,9 @@ export { - useAddMachineToWorkspace, + useAddIdentityToWorkspace, useAddUserToWorkspace, useCreateWorkspace, useCreateWsEnvironment, - useDeleteMachineFromWorkspace, + useDeleteIdentityFromWorkspace, useDeleteUserFromWorkspace, useDeleteWorkspace, useDeleteWsEnvironment, @@ -11,15 +11,15 @@ export { useGetUserWorkspaces, useGetWorkspaceAuthorizations, useGetWorkspaceById, + useGetWorkspaceIdentityMemberships, useGetWorkspaceIndexStatus, useGetWorkspaceIntegrations, - useGetWorkspaceMachineMemberships, useGetWorkspaceSecrets, useGetWorkspaceUsers, useNameWorkspaceSecrets, useRenameWorkspace, useReorderWsEnvironment, useToggleAutoCapitalization, - useUpdateMachineWorkspaceRole, + useUpdateIdentityWorkspaceRole, useUpdateUserWorkspaceRole, useUpdateWsEnvironment} from "./queries"; diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx index e6217fbab..86f5206c7 100644 --- a/frontend/src/hooks/api/workspace/queries.tsx +++ b/frontend/src/hooks/api/workspace/queries.tsx @@ -2,9 +2,9 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; +import { IdentityMembership } from "../identities/types"; import { IntegrationAuth } from "../integrationAuth/types"; import { TIntegration } from "../integrations/types"; -import { MachineMembership } from "../machineIdentities/types"; import { EncryptedSecret } from "../secrets/types"; import { TWorkspaceUser } from "../users/types"; import { @@ -31,7 +31,7 @@ export const workspaceKeys = { getAllUserWorkspace: ["workspaces"] as const, getWorkspaceAuditLogs: (workspaceId: string) => [{ workspaceId }] as const, getWorkspaceUsers: (workspaceId: string) => [{ workspaceId }] as const, - getWorkspaceMachineMemberships: (workspaceId: string) => [{ workspaceId }, "workspace-machine-memberships"] as const + getWorkspaceIdentityMemberships: (workspaceId: string) => [{ workspaceId }, "workspace-identity-memberships"] as const }; const fetchWorkspaceById = async (workspaceId: string) => { @@ -356,97 +356,92 @@ export const useUpdateUserWorkspaceRole = () => { }); }; -export const useAddMachineToWorkspace = () => { +export const useAddIdentityToWorkspace = () => { const queryClient = useQueryClient(); return useMutation({ mutationFn: async ({ - machineId, + identityId, workspaceId, role }: { - machineId: string; + identityId: string; workspaceId: string; role?: string; }) => { const { - data: { serviceMembership } - } = await apiRequest.post(`/api/v2/workspace/${workspaceId}/machine-memberships/${machineId}`, { + data: { identityMembership } + } = await apiRequest.post(`/api/v2/workspace/${workspaceId}/identity-memberships/${identityId}`, { role }); - return serviceMembership; + return identityMembership; }, onSuccess: (_, { workspaceId }) => { - queryClient.invalidateQueries(workspaceKeys.getWorkspaceMachineMemberships(workspaceId)); + queryClient.invalidateQueries(workspaceKeys.getWorkspaceIdentityMemberships(workspaceId)); } }); }; -export const useUpdateMachineWorkspaceRole = () => { +export const useUpdateIdentityWorkspaceRole = () => { const queryClient = useQueryClient(); return useMutation({ mutationFn: async ({ - machineId, + identityId, workspaceId, role }: { - machineId: string; + identityId: string; workspaceId: string; role?: string; }) => { const { - data: { serviceMembership } - } = await apiRequest.patch(`/api/v2/workspace/${workspaceId}/machine-memberships/${machineId}`, { + data: { identityMembership } + } = await apiRequest.patch(`/api/v2/workspace/${workspaceId}/identity-memberships/${identityId}`, { role }); - - return serviceMembership; + + return identityMembership; }, onSuccess: (_, { workspaceId }) => { - queryClient.invalidateQueries(workspaceKeys.getWorkspaceMachineMemberships(workspaceId)); + queryClient.invalidateQueries(workspaceKeys.getWorkspaceIdentityMemberships(workspaceId)); } }); }; -export const useDeleteMachineFromWorkspace = () => { +export const useDeleteIdentityFromWorkspace = () => { const queryClient = useQueryClient(); return useMutation({ mutationFn: async ({ - machineId, + identityId, workspaceId, }: { - machineId: string; + identityId: string; workspaceId: string; }) => { - const { - data: { serviceMembership } - } = await apiRequest.delete(`/api/v2/workspace/${workspaceId}/machine-memberships/${machineId}`); - - return serviceMembership; + data: { identityMembership } + } = await apiRequest.delete(`/api/v2/workspace/${workspaceId}/identity-memberships/${identityId}`); + return identityMembership; }, onSuccess: (_, { workspaceId }) => { - queryClient.invalidateQueries(workspaceKeys.getWorkspaceMachineMemberships(workspaceId)); + queryClient.invalidateQueries(workspaceKeys.getWorkspaceIdentityMemberships(workspaceId)); } }); }; - -export const useGetWorkspaceMachineMemberships = (workspaceId: string) => { +export const useGetWorkspaceIdentityMemberships = (workspaceId: string) => { return useQuery({ - queryKey: workspaceKeys.getWorkspaceMachineMemberships(workspaceId), + queryKey: workspaceKeys.getWorkspaceIdentityMemberships(workspaceId), queryFn: async () => { - const { - data: { machineMemberships } - } = await apiRequest.get<{ machineMemberships: MachineMembership[] }>( - `/api/v2/workspace/${workspaceId}/machine-memberships` + data: { identityMemberships } + } = await apiRequest.get<{ identityMemberships: IdentityMembership[] }>( + `/api/v2/workspace/${workspaceId}/identity-memberships` ); - - return machineMemberships; + return identityMemberships; }, enabled: true }); -}; +}; \ No newline at end of file diff --git a/frontend/src/views/Org/MembersPage/MembersPage.tsx b/frontend/src/views/Org/MembersPage/MembersPage.tsx index 3a768b8bb..867b0a61c 100644 --- a/frontend/src/views/Org/MembersPage/MembersPage.tsx +++ b/frontend/src/views/Org/MembersPage/MembersPage.tsx @@ -4,14 +4,15 @@ import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; import { withPermission } from "@app/hoc"; import { - OrgMachineIdentityTab, + OrgIdentityTab, OrgMembersTab, - OrgRoleTabSection} from "./components"; + OrgRoleTabSection +} from "./components"; enum TabSections { Member = "members", Roles = "roles", - MachineIdentities = "machine-identities" + Identities = "identities" } export const MembersPage = withPermission( @@ -25,7 +26,7 @@ export const MembersPage = withPermission( People - +

Machine Identities

@@ -38,8 +39,8 @@ export const MembersPage = withPermission( - - + + diff --git a/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/OrgMachineIdentityTab.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/OrgIdentityTab.tsx similarity index 71% rename from frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/OrgMachineIdentityTab.tsx rename to frontend/src/views/Org/MembersPage/components/OrgIdentityTab/OrgIdentityTab.tsx index 6c3973c3b..74b710374 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/OrgMachineIdentityTab.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/OrgIdentityTab.tsx @@ -1,8 +1,10 @@ import { motion } from "framer-motion"; -import { MachineIdentitySection } from "./components"; +import { + IdentitySection +} from "./components"; -export const OrgMachineIdentityTab = () => { +export const OrgIdentityTab = () => { return ( { animate={{ opacity: 1, translateX: 0 }} exit={{ opacity: 0, translateX: 30 }} > - + ); } \ No newline at end of file diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal.tsx new file mode 100644 index 000000000..c10760496 --- /dev/null +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal.tsx @@ -0,0 +1,108 @@ +import { Controller, useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; + +import { + FormControl, + Modal, + ModalContent, + Select, + SelectItem, + UpgradePlanModal +} from "@app/components/v2"; +import { IdentityAuthMethod } from "@app/hooks/api/identities"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +import { IdentityUniversalAuthForm } from "./IdentityUniversalAuthForm"; + +type Props = { + popUp: UsePopUpState<["identityAuthMethod", "upgradePlan"]>; + handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void; + handlePopUpToggle: (popUpName: keyof UsePopUpState<["identityAuthMethod", "upgradePlan"]>, state?: boolean) => void; +} + +const identityAuthMethods = [ + { label: "Universal Auth", value: IdentityAuthMethod.UNIVERSAL_AUTH } +]; + +const schema = yup.object({ + authMethod: yup.string().required("Auth method is required"), // TODO: better enforcement here +}).required(); + +export type FormData = yup.InferType; + +export const IdentityAuthMethodModal = ({ + popUp, + handlePopUpOpen, + handlePopUpToggle +}: Props) => { + + const { + control, + // watch, + } = useForm({ + resolver: yupResolver(schema) + }); + + const identityAuthMethodData = popUp?.identityAuthMethod?.data as { + identityId: string; + name: string; + authMethod?: IdentityAuthMethod; + } + + // const authMethod = watch("authMethod"); + + const renderIdentityAuthForm = () => { + return ( + + ); + } + + return ( + { + handlePopUpToggle("identityAuthMethod", isOpen); + }} + > + + ( + + + + )} + /> + {renderIdentityAuthForm()} + handlePopUpToggle("upgradePlan", isOpen)} + text="You can use IP allowlisting if you switch to Infisical's Pro plan." + /> + + + ); +} + diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx new file mode 100644 index 000000000..28a4e4dfe --- /dev/null +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx @@ -0,0 +1,239 @@ +import { useEffect } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; + +import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; +import { + Button, + FormControl, + Input, + Modal, + ModalContent, + Select, + SelectItem +} from "@app/components/v2"; +import { useOrganization } from "@app/context"; +import { + useCreateIdentity, + useGetRoles, + useUpdateIdentity +} from "@app/hooks/api"; +import { IdentityAuthMethod } from "@app/hooks/api/identities"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +const schema = yup.object({ + name: yup.string().required("MI name is required"), + role: yup.string(), +}).required(); + +export type FormData = yup.InferType; + +type Props = { + popUp: UsePopUpState<["identity"]>; + handlePopUpOpen: ( + popUpName: keyof UsePopUpState<["identityAuthMethod"]>, + data: { + identityId: string; + name: string; + authMethod?: IdentityAuthMethod; + } + ) => void; + handlePopUpToggle: (popUpName: keyof UsePopUpState<["identity"]>, state?: boolean) => void; +}; + +export const IdentityModal = ({ + popUp, + handlePopUpOpen, + handlePopUpToggle +}: Props) => { + const { createNotification } = useNotificationContext(); + + const { currentOrg } = useOrganization(); + const orgId = currentOrg?._id || ""; + + const { data: roles } = useGetRoles({ + orgId + }); + + const { mutateAsync: createMutateAsync } = useCreateIdentity(); + const { mutateAsync: updateMutateAsync } = useUpdateIdentity(); + + const { + control, + handleSubmit, + reset, + formState: { isSubmitting } + } = useForm({ + resolver: yupResolver(schema), + defaultValues: { + name: "" + } + }); + + useEffect(() => { + + const identity = popUp?.identity?.data as { + identityId: string; + name: string; + role: string; + customRole: { + name: string; + slug: string; + }; + }; + + if (!roles?.length) return; + + if (identity) { + reset({ + name: identity.name, + role: identity?.customRole?.slug ?? identity.role + }); + } else { + reset({ + name: "", + role: roles[0].slug + }); + } + }, [popUp?.identity?.data, roles]); + + const onFormSubmit = async ({ + name, + role, + }: FormData) => { + try { + + const identity = popUp?.identity?.data as { + identityId: string; + name: string; + role: string; + }; + + if (identity) { + // update + + await updateMutateAsync({ + identityId: identity.identityId, + name, + role: role || undefined, + organizationId: orgId + }); + + handlePopUpToggle("identity", false); + } else { + // create + + const { + _id: createdId, + name: createdName, + authMethod + } = await createMutateAsync({ + name, + role: role || undefined, + organizationId: orgId + }); + + handlePopUpToggle("identity", false); + handlePopUpOpen("identityAuthMethod", { + identityId: createdId, + name: createdName, + authMethod + }); + } + + createNotification({ + text: `Successfully ${popUp?.identity?.data ? "updated" : "created"} identity`, + type: "success" + }); + + reset(); + } catch (err) { + console.error(err); + const error = err as any; + const text = error?.response?.data?.message + ?? `Failed to ${popUp?.identity?.data ? "updated" : "created"} identity`; + + createNotification({ + text, + type: "error" + }); + } + } + + return ( + { + handlePopUpToggle("identity", isOpen); + reset(); + }} + > + +
+ ( + + + + )} + /> + ( + + + + )} + /> +
+ + +
+ +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx new file mode 100644 index 000000000..0359a3911 --- /dev/null +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx @@ -0,0 +1,118 @@ +import { faPlus } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; +import { OrgPermissionCan } from "@app/components/permissions"; +import { + Button, + DeleteActionModal +} from "@app/components/v2"; +import { + OrgPermissionActions, + OrgPermissionSubjects, + useOrganization} from "@app/context"; +import { withPermission } from "@app/hoc"; +import { useDeleteIdentity } from "@app/hooks/api"; +import { usePopUp } from "@app/hooks/usePopUp"; + +import { IdentityAuthMethodModal } from "./IdentityAuthMethodModal"; +import { IdentityModal } from "./IdentityModal"; +import { IdentityTable } from "./IdentityTable"; +import { IdentityUniversalAuthClientSecretModal } from "./IdentityUniversalAuthClientSecretModal"; + +export const IdentitySection = withPermission( + () => { + const { currentOrg } = useOrganization(); + const orgId = currentOrg?._id || ""; + + const { createNotification } = useNotificationContext(); + const { mutateAsync: deleteMutateAsync } = useDeleteIdentity(); + const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ + "identity", + "identityAuthMethod", + "deleteIdentity", + "universalAuthClientSecret", + "deleteUniversalAuthClientSecret", + "upgradePlan" + ] as const); + + const onDeleteIdentitySubmit = async (identityId: string) => { + try { + + await deleteMutateAsync({ + identityId, + organizationId: orgId + }); + + createNotification({ + text: "Successfully deleted identity", + type: "success" + }); + + handlePopUpClose("deleteIdentity"); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to delete identity", + type: "error" + }); + } + } + + return ( +
+
+

+ Identities +

+ + {(isAllowed) => ( + + )} + +
+ + + + + handlePopUpToggle("deleteIdentity", isOpen)} + deleteKey="confirm" + onDeleteApproved={() => + onDeleteIdentitySubmit( + (popUp?.deleteIdentity?.data as { identityId: string })?.identityId + ) + } + /> +
+ ); + }, + { action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.Identity } +); diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx new file mode 100644 index 000000000..b9dca1ad1 --- /dev/null +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx @@ -0,0 +1,267 @@ +import { faKey, faLock,faPencil, faServer, faXmark } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; +import { OrgPermissionCan } from "@app/components/permissions"; +import { + EmptyState, + IconButton, + Select, + SelectItem, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tooltip, + Tr +} from "@app/components/v2"; +import { + OrgPermissionActions, + OrgPermissionSubjects, + useOrganization} from "@app/context"; +import { + useGetIdentityMembershipOrgs, + useGetRoles, + useUpdateIdentity} from "@app/hooks/api"; +import { IdentityAuthMethod, identityAuthToNameMap } from "@app/hooks/api/identities"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +// TODO: some kind of map + +type Props = { + handlePopUpOpen: ( + popUpName: keyof UsePopUpState<["deleteIdentity", "identity", "universalAuthClientSecret", "identityAuthMethod"]>, + data?: { + identityId?: string; + name?: string; + authMethod?: string; + role?: string; + customRole?: { + name: string; + slug: string; + }; + } + ) => void; + }; + +export const IdentityTable = ({ + handlePopUpOpen +}: Props) => { + const { createNotification } = useNotificationContext(); + const { currentOrg } = useOrganization(); + const orgId = currentOrg?._id || ""; + + const { mutateAsync: updateMutateAsync } = useUpdateIdentity(); + const { data, isLoading } = useGetIdentityMembershipOrgs(orgId); + + const { data: roles } = useGetRoles({ + orgId + }); + + const handleChangeRole = async ({ + identityId, + role + }: { + identityId: string; + role: string; + }) => { + try { + + await updateMutateAsync({ + identityId, + role, + organizationId: orgId + }); + + createNotification({ + text: "Successfully updated identity role", + type: "success" + }); + } catch (err) { + console.error(err); + const error = err as any; + const text = error?.response?.data?.message ?? "Failed to update identity role" + + createNotification({ + text, + type: "error" + }); + } + } + + return ( + + + + + + + + + + + {isLoading && } + {!isLoading && + data && + data.length > 0 && + data.map(({ + identity: { + _id, + name, + authMethod + }, + role, + customRole + }) => { + return ( + + + + + + + ); + })} + {!isLoading && data && data?.length === 0 && ( + + + + )} + +
NameRoleAuth Method +
{name} + + {(isAllowed) => { + return ( + + ); + }} + + {authMethod ? identityAuthToNameMap[authMethod] : "Not configured"} +
+ {authMethod === IdentityAuthMethod.UNIVERSAL_AUTH && ( + + { + handlePopUpOpen("universalAuthClientSecret", { + identityId: _id, + name + }); + }} + size="lg" + colorSchema="primary" + variant="plain" + ariaLabel="update" + // isDisabled={!isAllowed} + > + + + + )} + + {(isAllowed) => ( + + { + handlePopUpOpen("identityAuthMethod", { + identityId: _id, + name, + authMethod + }); + }} + size="lg" + colorSchema="primary" + variant="plain" + ariaLabel="update" + className="ml-4" + isDisabled={!isAllowed} + > + + + + )} + + + {(isAllowed) => ( + { + handlePopUpOpen("identity", { + identityId: _id, + name, + role, + customRole + }); + }} + size="lg" + colorSchema="primary" + variant="plain" + ariaLabel="update" + className="ml-4" + isDisabled={!isAllowed} + > + + + )} + + + {(isAllowed) => ( + { + handlePopUpOpen("deleteIdentity", { + identityId: _id, + name + }); + }} + size="lg" + colorSchema="danger" + variant="plain" + ariaLabel="update" + className="ml-4" + isDisabled={!isAllowed} + > + + + )} + +
+
+ +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/CreateClientSecretModal.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthClientSecretModal.tsx similarity index 83% rename from frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/CreateClientSecretModal.tsx rename to frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthClientSecretModal.tsx index 9d7a57730..c6c276133 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/CreateClientSecretModal.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthClientSecretModal.tsx @@ -28,9 +28,10 @@ import { } from "@app/components/v2"; import { useToggle } from "@app/hooks"; import { - useCreateMachineIdentityClientSecret, - useDeleteMachineIdentityClientSecret, - useGetMachineIdentityClientSecrets} from "@app/hooks/api"; + useCreateIdentityUniversalAuthClientSecret, + useDeleteIdentityUniversalAuthClientSecret, + useGetIdentityUniversalAuth, + useGetIdentityUniversalAuthClientSecrets} from "@app/hooks/api"; import { UsePopUpState } from "@app/hooks/usePopUp"; const schema = yup.object({ @@ -42,18 +43,18 @@ const schema = yup.object({ export type FormData = yup.InferType; type Props = { - popUp: UsePopUpState<["clientSecret", "deleteClientSecret"]>; + popUp: UsePopUpState<["universalAuthClientSecret", "deleteUniversalAuthClientSecret"]>; handlePopUpOpen: ( - popUpName: keyof UsePopUpState<["deleteClientSecret"]>, + popUpName: keyof UsePopUpState<["deleteUniversalAuthClientSecret"]>, data?: { clientSecretPrefix: string; clientSecretId: string; } ) => void; - handlePopUpToggle: (popUpName: keyof UsePopUpState<["clientSecret", "deleteClientSecret"]>, state?: boolean) => void; + handlePopUpToggle: (popUpName: keyof UsePopUpState<["universalAuthClientSecret", "deleteUniversalAuthClientSecret"]>, state?: boolean) => void; }; -export const CreateClientSecretModal = ({ +export const IdentityUniversalAuthClientSecretModal = ({ popUp, handlePopUpOpen, handlePopUpToggle @@ -63,15 +64,16 @@ export const CreateClientSecretModal = ({ const [token, setToken] = useState(""); const [isTokenCopied, setIsTokenCopied] = useToggle(false); - const popUpData = (popUp?.clientSecret?.data as { - machineId?: string; + const popUpData = (popUp?.universalAuthClientSecret?.data as { + identityId?: string; name?: string; }); - const { data, isLoading } = useGetMachineIdentityClientSecrets(popUpData?.machineId ?? ""); - - const { mutateAsync: createClientSecretMutateAsync } = useCreateMachineIdentityClientSecret(); - const { mutateAsync: deleteClientSecretMutateAsync } = useDeleteMachineIdentityClientSecret(); + const { data, isLoading } = useGetIdentityUniversalAuthClientSecrets(popUpData?.identityId ?? ""); + const { data: identityUniversalAuth } = useGetIdentityUniversalAuth(popUpData?.identityId ?? ""); + + const { mutateAsync: createClientSecretMutateAsync } = useCreateIdentityUniversalAuthClientSecret(); + const { mutateAsync: deleteClientSecretMutateAsync } = useDeleteIdentityUniversalAuthClientSecret(); const { control, @@ -108,10 +110,10 @@ export const CreateClientSecretModal = ({ }: FormData) => { try { - if (!popUpData?.machineId) return; - + if (!popUpData?.identityId) return; + const { clientSecret } = await createClientSecretMutateAsync({ - machineId: popUpData.machineId, + identityId: popUpData.identityId, description, ttl: Number(ttl), numUsesLimit: Number(numUsesLimit) @@ -141,19 +143,19 @@ export const CreateClientSecretModal = ({ }) => { try { - if (!popUpData?.machineId) return; + if (!popUpData?.identityId) return; await deleteClientSecretMutateAsync({ - machineId: popUpData.machineId, + identityId: popUpData.identityId, clientSecretId }); - + if (token.startsWith(clientSecretPrefix)) { reset(); setToken(""); } - handlePopUpToggle("deleteClientSecret", false); + handlePopUpToggle("deleteUniversalAuthClientSecret", false); createNotification({ text: "Successfully deleted client secret", @@ -173,14 +175,29 @@ export const CreateClientSecretModal = ({ return ( { - handlePopUpToggle("clientSecret", isOpen); + handlePopUpToggle("universalAuthClientSecret", isOpen); reset(); setToken(""); }} > - + +

Client ID

+
+

{identityUniversalAuth?.clientId ?? ""}

+ navigator.clipboard.writeText(identityUniversalAuth?.clientId ?? "")} + > + + + {t("common.click-to-copy")} + + +

New Client Secret

{hasToken ? (
@@ -306,7 +323,7 @@ export const CreateClientSecretModal = ({ - {isLoading && } + {isLoading && } {!isLoading && data && data.length > 0 && @@ -333,7 +350,7 @@ export const CreateClientSecretModal = ({ { - handlePopUpOpen("deleteClientSecret", { + handlePopUpOpen("deleteUniversalAuthClientSecret", { clientSecretPrefix, clientSecretId: _id }); @@ -352,7 +369,7 @@ export const CreateClientSecretModal = ({ {!isLoading && data && data?.length === 0 && ( - + )} @@ -360,14 +377,14 @@ export const CreateClientSecretModal = ({ handlePopUpToggle("deleteClientSecret", isOpen)} + onChange={(isOpen) => handlePopUpToggle("deleteUniversalAuthClientSecret", isOpen)} deleteKey="confirm" onDeleteApproved={() => { - const deleteClientSecretData = (popUp?.deleteClientSecret?.data as { + const deleteClientSecretData = (popUp?.deleteUniversalAuthClientSecret?.data as { clientSecretId: string; clientSecretPrefix: string; }); diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthForm.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthForm.tsx new file mode 100644 index 000000000..a1d0a0bd2 --- /dev/null +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthForm.tsx @@ -0,0 +1,428 @@ +import { useEffect } from "react"; +import { Controller, useFieldArray, useForm } from "react-hook-form"; +import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; + +import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; +import { + Button, + FormControl, + IconButton, + Input, +} from "@app/components/v2"; +import { + useOrganization, + useSubscription +} from "@app/context"; +import { + useAddIdentityUniversalAuth, + useGetIdentityUniversalAuth, + useUpdateIdentityUniversalAuth +} from "@app/hooks/api"; +import { IdentityAuthMethod } from "@app/hooks/api/identities"; +import { IdentityTrustedIp } from "@app/hooks/api/identities/types"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +const schema = yup.object({ + accessTokenTTL: yup + .string() + .required("Access Token TTL is required"), + accessTokenMaxTTL: yup + .string() + .required("Access Max Token TTL is required"), + accessTokenNumUsesLimit: yup + .string() + .required("Access Token Max Number of Uses is required"), + clientSecretTrustedIps: yup + .array( + yup.object({ + ipAddress: yup.string().max(50).required().label("IP Address") + }) + ) + .min(1) + .required() + .label("Client Secret Trusted IP"), + accessTokenTrustedIps: yup + .array( + yup.object({ + ipAddress: yup.string().max(50).required().label("IP Address") + }) + ) + .min(1) + .required() + .label("Access Token Trusted IP") +}).required(); + +export type FormData = yup.InferType; + +type Props = { + handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void; + handlePopUpToggle: (popUpName: keyof UsePopUpState<["identityAuthMethod"]>, state?: boolean) => void; + identityAuthMethodData: { + identityId: string; + name: string; + authMethod?: IdentityAuthMethod; + } +} + +export const IdentityUniversalAuthForm = ({ + handlePopUpOpen, + handlePopUpToggle, + identityAuthMethodData +}: Props) => { + const { createNotification } = useNotificationContext(); + const { currentOrg } = useOrganization(); + const orgId = currentOrg?._id || ""; + const { subscription } = useSubscription(); + const { mutateAsync: addMutateAsync } = useAddIdentityUniversalAuth(); + const { mutateAsync: updateMutateAsync } = useUpdateIdentityUniversalAuth(); + const { data } = useGetIdentityUniversalAuth(identityAuthMethodData?.identityId ?? ""); + + const { + control, + handleSubmit, + reset, + formState: { isSubmitting } + } = useForm({ + resolver: yupResolver(schema), + defaultValues: { + accessTokenTTL: "7200", + accessTokenMaxTTL: "0", + accessTokenNumUsesLimit: "0", + clientSecretTrustedIps: [{ + ipAddress: "0.0.0.0/0" + }], + accessTokenTrustedIps: [{ + ipAddress: "0.0.0.0/0" + }], + } + }); + + const { + fields: clientSecretTrustedIpsFields, + append: appendClientSecretTrustedIp, + remove: removeClientSecretTrustedIp + } = useFieldArray({ control, name: "clientSecretTrustedIps" }); + const { + fields: accessTokenTrustedIpsFields, + append: appendAccessTokenTrustedIp, + remove: removeAccessTokenTrustedIp + } = useFieldArray({ control, name: "accessTokenTrustedIps" }); + + useEffect(() => { + if (data) { // TODO: fix data type + reset({ + accessTokenTTL: String(data.accessTokenTTL), + accessTokenMaxTTL: String(data.accessTokenMaxTTL), + accessTokenNumUsesLimit: String(data.accessTokenNumUsesLimit), + clientSecretTrustedIps: data.clientSecretTrustedIps.map(({ + ipAddress, + prefix + }: IdentityTrustedIp) => { + return ({ + ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}` + }); + }), + accessTokenTrustedIps: data.accessTokenTrustedIps.map(({ + ipAddress, + prefix + }: IdentityTrustedIp) => { + return ({ + ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}` + }); + }) + }); + } else { + reset({ + accessTokenTTL: "7200", + accessTokenMaxTTL: "0", + accessTokenNumUsesLimit: "0", + clientSecretTrustedIps: [{ + ipAddress: "0.0.0.0/0" + }], + accessTokenTrustedIps: [{ + ipAddress: "0.0.0.0/0" + }] + }); + } + }, [data]); + + const onFormSubmit = async ({ + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + clientSecretTrustedIps, + accessTokenTrustedIps + }: FormData) => { + try { + + if (!identityAuthMethodData) return; + + if (data) { + // update universal auth configuration + await updateMutateAsync({ + organizationId: orgId, + identityId: identityAuthMethodData.identityId, + clientSecretTrustedIps, + accessTokenTTL: Number(accessTokenTTL), + accessTokenMaxTTL: Number(accessTokenMaxTTL), + accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), + accessTokenTrustedIps, + }); + + } else { + // create new universal auth configuration + + await addMutateAsync({ + organizationId: orgId, + identityId: identityAuthMethodData.identityId, + clientSecretTrustedIps, + accessTokenTTL: Number(accessTokenTTL), + accessTokenMaxTTL: Number(accessTokenMaxTTL), + accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), + accessTokenTrustedIps, + }); + } + + handlePopUpToggle("identityAuthMethod", false); + + createNotification({ + text: `Successfully ${identityAuthMethodData?.authMethod ? "updated" : "configured"} auth method`, + type: "success" + }); + + reset(); + } catch (err) { + console.error(err); + const error = err as any; + const text = error?.response?.data?.message + ?? `Failed to ${identityAuthMethodData?.authMethod ? "update" : "configure"} identity`; + + createNotification({ + text, + type: "error" + }); + } + } + + return ( +
+ ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + {clientSecretTrustedIpsFields.map(({ id }, index) => ( +
+ { + return ( + + { + if (subscription?.ipAllowlisting) { + field.onChange(e); + return; + } + + handlePopUpOpen("upgradePlan"); + }} + placeholder="123.456.789.0" + /> + + ); + }} + /> + { + if (subscription?.ipAllowlisting) { + removeClientSecretTrustedIp(index); + return; + } + + handlePopUpOpen("upgradePlan"); + }} + size="lg" + colorSchema="danger" + variant="plain" + ariaLabel="update" + className="p-3" + > + + +
+ ))} +
+ +
+ {accessTokenTrustedIpsFields.map(({ id }, index) => ( +
+ { + return ( + + { + if (subscription?.ipAllowlisting) { + field.onChange(e); + return; + } + + handlePopUpOpen("upgradePlan"); + }} + placeholder="123.456.789.0" + /> + + ); + }} + /> + { + if (subscription?.ipAllowlisting) { + removeAccessTokenTrustedIp(index); + return; + } + + handlePopUpOpen("upgradePlan"); + }} + size="lg" + colorSchema="danger" + variant="plain" + ariaLabel="update" + className="p-3" + > + + +
+ ))} +
+ +
+
+ + +
+ + ); +} \ No newline at end of file diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/index.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/index.tsx new file mode 100644 index 000000000..3aa8cee58 --- /dev/null +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/index.tsx @@ -0,0 +1 @@ +export { IdentitySection } from "./IdentitySection"; \ No newline at end of file diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/index.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/index.tsx new file mode 100644 index 000000000..3aa8cee58 --- /dev/null +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/index.tsx @@ -0,0 +1 @@ +export { IdentitySection } from "./IdentitySection"; \ No newline at end of file diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/index.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/index.tsx new file mode 100644 index 000000000..38c7a320e --- /dev/null +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/index.tsx @@ -0,0 +1 @@ +export { OrgIdentityTab } from "./OrgIdentityTab"; \ No newline at end of file diff --git a/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/AddMachineIdentityModal.tsx b/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/AddMachineIdentityModal.tsx deleted file mode 100644 index a896dae63..000000000 --- a/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/AddMachineIdentityModal.tsx +++ /dev/null @@ -1,596 +0,0 @@ -import { useEffect } from "react"; -import { Controller, useFieldArray, useForm } from "react-hook-form"; -import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { yupResolver } from "@hookform/resolvers/yup"; -import { motion } from "framer-motion"; -import * as yup from "yup"; - -import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; -import { - Button, - FormControl, - IconButton, - Input, - Modal, - ModalContent, - Select, - SelectItem, - Tab, - TabList, - TabPanel, - Tabs, - UpgradePlanModal} from "@app/components/v2"; -import { - useOrganization, - useSubscription -} from "@app/context"; -import { useToggle } from "@app/hooks"; -import { - useCreateMachineIdentity, - useGetRoles, - useUpdateMachineIdentity -} from "@app/hooks/api"; -import { MachineTrustedIp } from "@app/hooks/api/machineIdentities/types"; -import { UsePopUpState } from "@app/hooks/usePopUp"; - -enum TabSections { - General = "general", - Advanced = "advanced" -} - -const schema = yup.object({ - name: yup.string().required("MI name is required"), - accessTokenTTL: yup - .string() - .required("Access Token TTL is required"), - accessTokenMaxTTL: yup - .string() - .required("Access Max Token TTL is required"), - accessTokenNumUsesLimit: yup - .string() - .required("Access Token Max Number of Uses is required"), - role: yup.string(), - clientSecretTrustedIps: yup - .array( - yup.object({ - ipAddress: yup.string().max(50).required().label("IP Address") - }) - ) - .min(1) - .required() - .label("Client Secret Trusted IP"), - accessTokenTrustedIps: yup - .array( - yup.object({ - ipAddress: yup.string().max(50).required().label("IP Address") - }) - ) - .min(1) - .required() - .label("Access Token Trusted IP") -}).required(); - -export type FormData = yup.InferType; - -type Props = { - popUp: UsePopUpState<["machineIdentity", "upgradePlan"]>; - handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void; - handlePopUpToggle: (popUpName: keyof UsePopUpState<["machineIdentity", "upgradePlan"]>, state?: boolean) => void; -}; - -export const AddMachineIdentityModal = ({ - popUp, - handlePopUpOpen, - handlePopUpToggle -}: Props) => { - const { createNotification } = useNotificationContext(); - const [isServiceTokenJSONCopied, setIsServiceTokenJSONCopied] = useToggle(false); - - const { subscription } = useSubscription(); - const { currentOrg } = useOrganization(); - - const orgId = currentOrg?._id || ""; - - const { data: roles } = useGetRoles({ - orgId - }); - - const { mutateAsync: createMutateAsync } = useCreateMachineIdentity(); - const { mutateAsync: updateMutateAsync } = useUpdateMachineIdentity(); - - const { - control, - handleSubmit, - reset, - formState: { isSubmitting } - } = useForm({ - resolver: yupResolver(schema), - defaultValues: { - name: "", - accessTokenTTL: "7200", - accessTokenMaxTTL: "0", - accessTokenNumUsesLimit: "0", - clientSecretTrustedIps: [{ - ipAddress: "0.0.0.0/0" - }], - accessTokenTrustedIps: [{ - ipAddress: "0.0.0.0/0" - }], - } - }); - - useEffect(() => { - let timer: NodeJS.Timeout; - - if (isServiceTokenJSONCopied) { - timer = setTimeout(() => setIsServiceTokenJSONCopied.off(), 2000); - } - - return () => clearTimeout(timer); - }, [setIsServiceTokenJSONCopied]); - - useEffect(() => { - - const machineIdentity = popUp?.machineIdentity?.data as { - machineId: string; - name: string; - role: string; - customRole: { - name: string; - slug: string; - }; - clientSecretTrustedIps: MachineTrustedIp[]; - accessTokenTrustedIps: MachineTrustedIp[]; - accessTokenTTL: number; - accessTokenMaxTTL: number; - accessTokenNumUsesLimit: number; - }; - - if (!roles?.length) return; - - if (machineIdentity) { - reset({ - name: machineIdentity.name, - accessTokenNumUsesLimit: String(machineIdentity.accessTokenNumUsesLimit), - role: machineIdentity?.customRole?.slug ?? machineIdentity.role, - clientSecretTrustedIps: machineIdentity.clientSecretTrustedIps.map(({ - ipAddress, - prefix - }: MachineTrustedIp) => { - return ({ - ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}` - }); - }), - accessTokenTrustedIps: machineIdentity.accessTokenTrustedIps.map(({ - ipAddress, - prefix - }: MachineTrustedIp) => { - return ({ - ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}` - }); - }), - accessTokenTTL: String(machineIdentity.accessTokenTTL), - accessTokenMaxTTL: String(machineIdentity.accessTokenMaxTTL) - }); - } else { - reset({ - name: "", - accessTokenTTL: "7200", - accessTokenMaxTTL: "0", - accessTokenNumUsesLimit: "0", - role: roles[0].slug, - clientSecretTrustedIps: [{ - ipAddress: "0.0.0.0/0" - }], - accessTokenTrustedIps: [{ - ipAddress: "0.0.0.0/0" - }] - }); - } - }, [popUp?.machineIdentity?.data, roles]); - - const { - fields: clientSecretTrustedIpsFields, - append: appendClientSecretTrustedIp, - remove: removeClientSecretTrustedIp - } = useFieldArray({ control, name: "clientSecretTrustedIps" }); - const { - fields: accessTokenTrustedIpsFields, - append: appendAccessTokenTrustedIp, - remove: removeAccessTokenTrustedIp - } = useFieldArray({ control, name: "accessTokenTrustedIps" }); - - const onFormSubmit = async ({ - name, - accessTokenTTL, - accessTokenMaxTTL, - role, - clientSecretTrustedIps, - accessTokenTrustedIps, - accessTokenNumUsesLimit - }: FormData) => { - try { - - const machineIdentity = popUp?.machineIdentity?.data as { - machineId: string; - name: string; - role: string; - }; - - if (machineIdentity) { - // update - - await updateMutateAsync({ - machineId: machineIdentity.machineId, - name, - role: role || undefined, - clientSecretTrustedIps, - accessTokenTrustedIps, - accessTokenTTL: Number(accessTokenTTL), - accessTokenMaxTTL: Number(accessTokenMaxTTL), - accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit) - }); - - handlePopUpToggle("machineIdentity", false); - } else { - - await createMutateAsync({ - name, - role: role || undefined, - organizationId: orgId, - clientSecretTrustedIps, - accessTokenTrustedIps, - accessTokenTTL: Number(accessTokenTTL), - accessTokenMaxTTL: Number(accessTokenMaxTTL), - accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit) - }); - - handlePopUpToggle("machineIdentity", false); - } - - createNotification({ - text: `Successfully ${popUp?.machineIdentity?.data ? "updated" : "created"} machine identity`, - type: "success" - }); - - reset(); - } catch (err) { - console.error(err); - const error = err as any; - const text = error?.response?.data?.message - ?? `Failed to ${popUp?.machineIdentity?.data ? "updated" : "created"} machine identity`; - - createNotification({ - text, - type: "error" - }); - } - } - - return ( - { - handlePopUpToggle("machineIdentity", isOpen); - reset(); - }} - > - -
- - -
- General - Advanced -
-
- - - ( - - - - )} - /> - ( - - - - )} - /> - - - - -
- {/* ( - - - - )} - /> */} - {/* ( - - - - )} - /> */} - ( - - - - )} - /> - ( - - - - )} - /> - {clientSecretTrustedIpsFields.map(({ id }, index) => ( -
- { - return ( - - { - if (subscription?.ipAllowlisting) { - field.onChange(e); - return; - } - - handlePopUpOpen("upgradePlan"); - }} - placeholder="123.456.789.0" - /> - - ); - }} - /> - { - if (subscription?.ipAllowlisting) { - removeClientSecretTrustedIp(index); - return; - } - - handlePopUpOpen("upgradePlan"); - }} - size="lg" - colorSchema="danger" - variant="plain" - ariaLabel="update" - className="p-3" - > - - -
- ))} -
- -
- {accessTokenTrustedIpsFields.map(({ id }, index) => ( -
- { - return ( - - { - if (subscription?.ipAllowlisting) { - field.onChange(e); - return; - } - - handlePopUpOpen("upgradePlan"); - }} - placeholder="123.456.789.0" - /> - - ); - }} - /> - { - if (subscription?.ipAllowlisting) { - removeAccessTokenTrustedIp(index); - return; - } - - handlePopUpOpen("upgradePlan"); - }} - size="lg" - colorSchema="danger" - variant="plain" - ariaLabel="update" - className="p-3" - > - - -
- ))} -
- -
-
-
-
-
- - -
-
- handlePopUpToggle("upgradePlan", isOpen)} - text="You can use IP allowlisting if you switch to Infisical's Pro plan." - /> -
-
- ); -} \ No newline at end of file diff --git a/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/MachineIdentitySection.tsx b/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/MachineIdentitySection.tsx deleted file mode 100644 index 5e11513d7..000000000 --- a/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/MachineIdentitySection.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { faPlus } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; - -import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; -import { OrgPermissionCan } from "@app/components/permissions"; -import { - Button, - DeleteActionModal -} from "@app/components/v2"; -import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; -import { withPermission } from "@app/hoc"; -import { useDeleteMachineIdentity } from "@app/hooks/api"; -import { usePopUp } from "@app/hooks/usePopUp"; - -import { AddMachineIdentityModal } from "./AddMachineIdentityModal"; -import { CreateClientSecretModal } from "./CreateClientSecretModal"; -import { MachineIdentityTable } from "./MachineIdentityTable"; - -export const MachineIdentitySection = withPermission( - () => { - const { createNotification } = useNotificationContext(); - const { mutateAsync: deleteMutateAsync } = useDeleteMachineIdentity(); - const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ - "machineIdentity", - "deleteMachineIdentity", - "clientSecret", - "deleteClientSecret", - "upgradePlan" - ] as const); - - const onDeleteMachineIdentitySubmit = async (machineId: string) => { - try { - await deleteMutateAsync({ - machineId - }); - createNotification({ - text: "Successfully deleted machine identity", - type: "success" - }); - - handlePopUpClose("deleteMachineIdentity"); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to delete machine identity", - type: "error" - }); - } - } - - return ( -
-
-

- Machine Identities -

- - {(isAllowed) => ( - - )} - -
- - - - handlePopUpToggle("deleteMachineIdentity", isOpen)} - deleteKey="confirm" - onDeleteApproved={() => - onDeleteMachineIdentitySubmit( - (popUp?.deleteMachineIdentity?.data as { machineId: string })?.machineId - ) - } - /> -
- ); - }, - { action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.MachineIdentity } -); \ No newline at end of file diff --git a/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/MachineIdentityTable.tsx b/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/MachineIdentityTable.tsx deleted file mode 100644 index 783c27a4d..000000000 --- a/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/MachineIdentityTable.tsx +++ /dev/null @@ -1,284 +0,0 @@ -import { faKey,faPencil,faServer, faXmark } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; - -import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; -import { OrgPermissionCan } from "@app/components/permissions"; -import { - EmptyState, - IconButton, - Select, - SelectItem, - Table, - TableContainer, - TableSkeleton, - TBody, - Td, - Th, - THead, - Tooltip, - Tr} from "@app/components/v2"; -import { - OrgPermissionActions, - OrgPermissionSubjects, - useOrganization} from "@app/context"; -import { - useGetMachineMembershipOrgs, - useGetRoles, - useUpdateMachineIdentity, -} from "@app/hooks/api"; -import { MachineTrustedIp } from "@app/hooks/api/machineIdentities/types"; -import { UsePopUpState } from "@app/hooks/usePopUp"; - -type Props = { - handlePopUpOpen: ( - popUpName: keyof UsePopUpState<["deleteMachineIdentity", "machineIdentity", "clientSecret"]>, - data?: { - machineId?: string; - name?: string; - role?: string; - customRole?: { - name: string; - slug: string; - }; - clientSecretTrustedIps?: MachineTrustedIp[]; - accessTokenTrustedIps?: MachineTrustedIp[]; - accessTokenMaxTTL?: number; - accessTokenTTL?: number; - accessTokenNumUsesLimit?: number; - } - ) => void; - }; - -export const MachineIdentityTable = ({ - handlePopUpOpen -}: Props) => { - const { createNotification } = useNotificationContext(); - const { currentOrg } = useOrganization(); - const orgId = currentOrg?._id || ""; - - const { mutateAsync: updateMutateAsync } = useUpdateMachineIdentity(); - const { data, isLoading } = useGetMachineMembershipOrgs(currentOrg?._id || ""); - - const { data: roles } = useGetRoles({ - orgId - }); - - const handleChangeRole = async ({ - machineId, - role - }: { - machineId: string; - role: string; - }) => { - try { - - await updateMutateAsync({ - machineId, - role - }); - - createNotification({ - text: "Successfully updated machine identity role", - type: "success" - }); - } catch (err) { - console.error(err); - const error = err as any; - const text = error?.response?.data?.message ?? "Failed to update machine identity role" - - createNotification({ - text, - type: "error" - }); - } - } - - // const handleToggleStatus = async ({ - // serviceTokenDataId, - // isActive - // }: { - // serviceTokenDataId: string; - // isActive: boolean; - // }) => { - // try { - // await updateMutateAsync({ - // serviceTokenDataId, - // isActive - // }); - - // createNotification({ - // text: `Successfully ${isActive ? "enabled" : "disabled"} machine identity`, - // type: "success" - // }); - // } catch (err) { - // console.log(err); - // createNotification({ - // text: `Failed to ${isActive ? "enable" : "disable"} machine identity`, - // type: "error" - // }); - // } - // } - - return ( - - - - - - - {/* */} - - {/* */} - {/* */} - {/* */} - {/* */} - - - - {isLoading && } - {!isLoading && - data && - data.length > 0 && - data.map(({ - machineIdentity: { - _id, - name, - clientId, - // isActive, - clientSecretTrustedIps, - accessTokenTrustedIps, - // createdAt, - // expiresAt, - accessTokenMaxTTL, - accessTokenTTL, - accessTokenNumUsesLimit - }, - role, - customRole - }) => { - return ( - - - - - - - ); - })} - {!isLoading && data && data?.length === 0 && ( - - - - )} - -
NameClient IDStatusRoleTrusted IPsAccess Token TTLCreated AtValid Until -
{name}{clientId} - - {(isAllowed) => { - return ( - - ); - }} - - -
- - { - handlePopUpOpen("clientSecret", { - machineId: _id, - name - }); - }} - size="lg" - colorSchema="primary" - variant="plain" - ariaLabel="update" - // isDisabled={!isAllowed} - > - - - - - {(isAllowed) => ( - { - handlePopUpOpen("machineIdentity", { - machineId: _id, - name, - role, - customRole, - clientSecretTrustedIps, - accessTokenTrustedIps, - accessTokenTTL, - accessTokenMaxTTL, - accessTokenNumUsesLimit - }); - }} - size="lg" - colorSchema="primary" - variant="plain" - ariaLabel="update" - className="ml-4" - isDisabled={!isAllowed} - > - - - )} - - - {(isAllowed) => ( - { - handlePopUpOpen("deleteMachineIdentity", { - machineId: _id, - name - }); - }} - size="lg" - colorSchema="danger" - variant="plain" - ariaLabel="update" - className="ml-4" - isDisabled={!isAllowed} - > - - - )} - -
-
- -
-
- ); -} \ No newline at end of file diff --git a/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/index.tsx b/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/index.tsx deleted file mode 100644 index 06de47d26..000000000 --- a/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { MachineIdentitySection } from "./MachineIdentitySection"; \ No newline at end of file diff --git a/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/index.tsx b/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/index.tsx deleted file mode 100644 index 06de47d26..000000000 --- a/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { MachineIdentitySection } from "./MachineIdentitySection"; \ No newline at end of file diff --git a/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/index.tsx b/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/index.tsx deleted file mode 100644 index b66402617..000000000 --- a/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { OrgMachineIdentityTab } from "./OrgMachineIdentityTab"; \ No newline at end of file diff --git a/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleModifySection.tsx b/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleModifySection.tsx index 3fcb054d2..0b63986aa 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleModifySection.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleModifySection.tsx @@ -41,9 +41,9 @@ const SIMPLE_PERMISSION_OPTIONS = [ }, { title: "Machine identity management", - subtitle: "Create, view, update and remove machine identities from the organization", + subtitle: "Create, view, update and remove (machine) identities from the organization", icon: faServer, - formName: "machine-identity" + formName: "identity" }, { title: "Billing & usage", diff --git a/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleModifySection.utils.ts b/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleModifySection.utils.ts index 58702349d..18ffa174b 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleModifySection.utils.ts +++ b/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleModifySection.utils.ts @@ -32,7 +32,7 @@ export const formSchema = z.object({ "secret-scanning": generalPermissionSchema, sso: generalPermissionSchema, billing: generalPermissionSchema, - "machine-identity": generalPermissionSchema + "identity": generalPermissionSchema }) .optional() }); diff --git a/frontend/src/views/Org/MembersPage/components/index.tsx b/frontend/src/views/Org/MembersPage/components/index.tsx index 03acfd92c..9e13e717a 100644 --- a/frontend/src/views/Org/MembersPage/components/index.tsx +++ b/frontend/src/views/Org/MembersPage/components/index.tsx @@ -1,3 +1,3 @@ -export { OrgMachineIdentityTab } from "./OrgMachineIdentityTab"; +export { OrgIdentityTab } from "./OrgIdentityTab"; export { OrgMembersTab } from "./OrgMembersTab"; export { OrgRoleTabSection } from "./OrgRoleTabSection"; \ No newline at end of file diff --git a/frontend/src/views/Project/AuditLogsPage/components/LogsFilter.tsx b/frontend/src/views/Project/AuditLogsPage/components/LogsFilter.tsx index 29d935002..59fb9af36 100644 --- a/frontend/src/views/Project/AuditLogsPage/components/LogsFilter.tsx +++ b/frontend/src/views/Project/AuditLogsPage/components/LogsFilter.tsx @@ -50,11 +50,11 @@ export const LogsFilter = ({ control, reset }: Props) => { {actor.metadata.name} ); - case ActorType.MACHINE: + case ActorType.IDENTITY: return ( {actor.metadata.name} diff --git a/frontend/src/views/Project/AuditLogsPage/components/LogsTableRow.tsx b/frontend/src/views/Project/AuditLogsPage/components/LogsTableRow.tsx index b858869aa..153ebf33a 100644 --- a/frontend/src/views/Project/AuditLogsPage/components/LogsTableRow.tsx +++ b/frontend/src/views/Project/AuditLogsPage/components/LogsTableRow.tsx @@ -29,11 +29,11 @@ export const LogsTableRow = ({

Service token

); - case ActorType.MACHINE: + case ActorType.IDENTITY: return (

{`${actor.metadata.name}`}

-

Machine identity

+

Machine Identity

); default: @@ -167,22 +167,24 @@ export const LogsTableRow = ({

{`Name: ${event.metadata.name}`}

); - case EventType.CREATE_MACHINE_IDENTITY: + case EventType.CREATE_IDENTITY: return ( +

{`ID: ${event.metadata.identityId}`}

{`Name: ${event.metadata.name}`}

); - case EventType.UPDATE_MACHINE_IDENTITY: + case EventType.UPDATE_IDENTITY: return ( +

{`ID: ${event.metadata.identityId}`}

{`Name: ${event.metadata.name}`}

); - case EventType.DELETE_MACHINE_IDENTITY: + case EventType.DELETE_IDENTITY: return ( -

{`Name: ${event.metadata.name}`}

+

{`ID: ${event.metadata.identityId}`}

); case EventType.CREATE_ENVIRONMENT: diff --git a/frontend/src/views/Project/MembersPage/MembersPage.tsx b/frontend/src/views/Project/MembersPage/MembersPage.tsx index 3fa856c79..f4e88de4e 100644 --- a/frontend/src/views/Project/MembersPage/MembersPage.tsx +++ b/frontend/src/views/Project/MembersPage/MembersPage.tsx @@ -6,7 +6,7 @@ import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { withProjectPermission } from "@app/hoc"; import { - MachineIdentityTab, + IdentityTab, MemberListTab, ProjectRoleListTab, ServiceTokenTab @@ -15,7 +15,7 @@ import { enum TabSections { Member = "members", Roles = "roles", - MachineIdentities = "machine-identities", + Identities = "identities", ServiceTokens = "service-tokens" } @@ -30,7 +30,7 @@ export const MembersPage = withProjectPermission( People - +

Machine Identities

@@ -52,8 +52,8 @@ export const MembersPage = withProjectPermission( - - + + diff --git a/frontend/src/views/Project/MembersPage/components/MachineIdentityTab/MachineIdentityTab.tsx b/frontend/src/views/Project/MembersPage/components/IdentityTab/IdentityTab.tsx similarity index 69% rename from frontend/src/views/Project/MembersPage/components/MachineIdentityTab/MachineIdentityTab.tsx rename to frontend/src/views/Project/MembersPage/components/IdentityTab/IdentityTab.tsx index a21d1e43e..1f928daae 100644 --- a/frontend/src/views/Project/MembersPage/components/MachineIdentityTab/MachineIdentityTab.tsx +++ b/frontend/src/views/Project/MembersPage/components/IdentityTab/IdentityTab.tsx @@ -1,19 +1,19 @@ import { motion } from "framer-motion"; import { - MachineIdentitySection + IdentitySection, } from "./components"; -export const MachineIdentityTab = () => { +export const IdentityTab = () => { return ( - + ); } \ No newline at end of file diff --git a/frontend/src/views/Project/MembersPage/components/MachineIdentityTab/components/MachineIdentitySection/AddMachineIdentityModal.tsx b/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentitySection/IdentityModal.tsx similarity index 70% rename from frontend/src/views/Project/MembersPage/components/MachineIdentityTab/components/MachineIdentitySection/AddMachineIdentityModal.tsx rename to frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentitySection/IdentityModal.tsx index fdcc0e1c7..6cc8d9def 100644 --- a/frontend/src/views/Project/MembersPage/components/MachineIdentityTab/components/MachineIdentitySection/AddMachineIdentityModal.tsx +++ b/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentitySection/IdentityModal.tsx @@ -18,26 +18,26 @@ import { useWorkspace } from "@app/context"; import { - useAddMachineToWorkspace, - useGetMachineMembershipOrgs, + useAddIdentityToWorkspace, + useGetIdentityMembershipOrgs, useGetRoles, - useGetWorkspaceMachineMemberships + useGetWorkspaceIdentityMemberships } from "@app/hooks/api"; import { UsePopUpState } from "@app/hooks/usePopUp"; const schema = yup.object({ - machineId: yup.string().required("Machine identity id is required"), + identityId: yup.string().required("Identity id is required"), role: yup.string() }).required(); export type FormData = yup.InferType; type Props = { - popUp: UsePopUpState<["machineIdentity"]>; - handlePopUpToggle: (popUpName: keyof UsePopUpState<["machineIdentity"]>, state?: boolean) => void; + popUp: UsePopUpState<["identity"]>; + handlePopUpToggle: (popUpName: keyof UsePopUpState<["identity"]>, state?: boolean) => void; }; -export const AddMachineIdentityModal = ({ +export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => { @@ -48,28 +48,28 @@ export const AddMachineIdentityModal = ({ const orgId = currentOrg?._id || ""; const workspaceId = currentWorkspace?._id || ""; - - const { data: machineMembershipOrgs } = useGetMachineMembershipOrgs(orgId); - const { data: machineMemberships } = useGetWorkspaceMachineMemberships(workspaceId); + + const { data: identityMembershipOrgs } = useGetIdentityMembershipOrgs(orgId); + const { data: identityMemberships } = useGetWorkspaceIdentityMemberships(workspaceId); const { data: roles } = useGetRoles({ orgId, workspaceId }); - const { mutateAsync: addMachineToWorkspaceMutateAsync } = useAddMachineToWorkspace(); + const { mutateAsync: addIdentityToWorkspaceMutateAsync } = useAddIdentityToWorkspace(); - const filteredMachineMembershipOrgs = useMemo(() => { - const wsMachineIds = new Map(); + const filteredIdentityMembershipOrgs = useMemo(() => { + const wsIdentityIds = new Map(); - machineMemberships?.forEach((machineMembership) => { - wsMachineIds.set(machineMembership.machineIdentity._id, true); + identityMemberships?.forEach((identityMembership) => { + wsIdentityIds.set(identityMembership.identity._id, true); }); - return (machineMembershipOrgs || []).filter( - ({ machineIdentity: mi }) => !wsMachineIds.has(mi._id) + return (identityMembershipOrgs || []).filter( + ({ identity: i }) => !wsIdentityIds.has(i._id) ); - }, [machineMembershipOrgs, machineMemberships]); + }, [identityMembershipOrgs, identityMemberships]); const { control, @@ -81,29 +81,29 @@ export const AddMachineIdentityModal = ({ }); const onFormSubmit = async ({ - machineId, + identityId, role }: FormData) => { try { - - await addMachineToWorkspaceMutateAsync({ + + await addIdentityToWorkspaceMutateAsync({ workspaceId, - machineId, + identityId, role: role || undefined }); - + createNotification({ - text: "Successfully added machine identity to project", + text: "Successfully added identity to project", type: "success" }); reset(); - handlePopUpToggle("machineIdentity", false); + handlePopUpToggle("identity", false); } catch (err) { console.error(err); const error = err as any; const text = error?.response?.data?.message - ?? "Failed to add machine identity to project"; + ?? "Failed to add identity to project"; createNotification({ text, @@ -114,22 +114,22 @@ export const AddMachineIdentityModal = ({ return ( { - handlePopUpToggle("machineIdentity", isOpen); + handlePopUpToggle("identity", isOpen); reset(); }} > - - {filteredMachineMembershipOrgs.length ? ( + + {filteredIdentityMembershipOrgs.length ? (
( @@ -139,9 +139,9 @@ export const AddMachineIdentityModal = ({ onValueChange={(e) => onChange(e)} className="w-full" > - {filteredMachineMembershipOrgs.map(({ machineIdentity }) => ( - - {machineIdentity.name} + {filteredIdentityMembershipOrgs.map(({ identity }) => ( + + {identity.name} ))} @@ -182,7 +182,7 @@ export const AddMachineIdentityModal = ({ isLoading={isSubmitting} isDisabled={isSubmitting} > - {popUp?.machineIdentity?.data ? "Update" : "Create"} + {popUp?.identity?.data ? "Update" : "Create"} +
)} diff --git a/frontend/src/views/Project/MembersPage/components/MachineIdentityTab/components/MachineIdentitySection/MachineIdentitySection.tsx b/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentitySection/IdentitySection.tsx similarity index 61% rename from frontend/src/views/Project/MembersPage/components/MachineIdentityTab/components/MachineIdentitySection/MachineIdentitySection.tsx rename to frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentitySection/IdentitySection.tsx index 5083fbb94..6212d371a 100644 --- a/frontend/src/views/Project/MembersPage/components/MachineIdentityTab/components/MachineIdentitySection/MachineIdentitySection.tsx +++ b/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentitySection/IdentitySection.tsx @@ -12,47 +12,45 @@ import { ProjectPermissionSub, useWorkspace} from "@app/context"; import { withProjectPermission } from "@app/hoc"; -import { - useDeleteMachineFromWorkspace -} from "@app/hooks/api"; +import { useDeleteIdentityFromWorkspace } from "@app/hooks/api"; import { usePopUp } from "@app/hooks/usePopUp"; -import { AddMachineIdentityModal } from "./AddMachineIdentityModal"; -import { MachineIdentityTable } from "./MachineIdentityTable"; +import { IdentityModal } from "./IdentityModal"; +import { IdentityTable } from "./IdentityTable"; -export const MachineIdentitySection = withProjectPermission( +export const IdentitySection = withProjectPermission( () => { const { createNotification } = useNotificationContext(); const { currentWorkspace } = useWorkspace(); const workspaceId = currentWorkspace?._id ?? ""; - const { mutateAsync: deleteMutateAsync } = useDeleteMachineFromWorkspace(); + const { mutateAsync: deleteMutateAsync } = useDeleteIdentityFromWorkspace(); const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ - "machineIdentity", - "deleteMachineIdentity", + "identity", + "deleteIdentity", "upgradePlan" ] as const); - const onRemoveMachineIdentitySubmit = async (machineId: string) => { + const onRemoveIdentitySubmit = async (identityId: string) => { try { await deleteMutateAsync({ - machineId, - workspaceId + identityId, + workspaceId }); - + createNotification({ - text: "Successfully removed machine identity from project", + text: "Successfully removed identity from project", type: "success" }); - handlePopUpClose("deleteMachineIdentity"); + handlePopUpClose("deleteIdentity"); } catch (err) { console.error(err); const error = err as any; - const text = error?.response?.data?.message ?? "Failed to remove machine identity from project" + const text = error?.response?.data?.message ?? "Failed to remove identity from project" createNotification({ text, @@ -65,18 +63,18 @@ export const MachineIdentitySection = withProjectPermission(

- Machine Identities + Identities

{(isAllowed) => (
- - + + handlePopUpToggle("deleteMachineIdentity", isOpen)} + onChange={(isOpen) => handlePopUpToggle("deleteIdentity", isOpen)} deleteKey="confirm" onDeleteApproved={() => - onRemoveMachineIdentitySubmit( - (popUp?.deleteMachineIdentity?.data as { machineId: string })?.machineId + onRemoveIdentitySubmit( + (popUp?.deleteIdentity?.data as { identityId: string })?.identityId ) } />
); }, - { action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.MachineIdentity } + { action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Identity } ); \ No newline at end of file diff --git a/frontend/src/views/Project/MembersPage/components/MachineIdentityTab/components/MachineIdentitySection/MachineIdentityTable.tsx b/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentitySection/IdentityTable.tsx similarity index 83% rename from frontend/src/views/Project/MembersPage/components/MachineIdentityTab/components/MachineIdentitySection/MachineIdentityTable.tsx rename to frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentitySection/IdentityTable.tsx index fa1d76b24..5ce979e65 100644 --- a/frontend/src/views/Project/MembersPage/components/MachineIdentityTab/components/MachineIdentitySection/MachineIdentityTable.tsx +++ b/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentitySection/IdentityTable.tsx @@ -26,31 +26,21 @@ import { } from "@app/context"; import { useGetRoles, - useGetWorkspaceMachineMemberships, - useUpdateMachineWorkspaceRole -} from "@app/hooks/api"; -import { MachineTrustedIp} from "@app/hooks/api/machineIdentities/types"; + useGetWorkspaceIdentityMemberships, + useUpdateIdentityWorkspaceRole} from "@app/hooks/api"; import { UsePopUpState } from "@app/hooks/usePopUp"; type Props = { handlePopUpOpen: ( - popUpName: keyof UsePopUpState<["deleteMachineIdentity", "machineIdentity"]>, + popUpName: keyof UsePopUpState<["deleteIdentity", "identity"]>, data?: { - machineId?: string; + identityId?: string; name?: string; - role?: string; - customRole?: { - name: string; - slug: string; - }; - trustedIps?: MachineTrustedIp[]; - accessTokenTTL?: number; - isRefreshTokenRotationEnabled?: boolean; } ) => void; }; -export const MachineIdentityTable = ({ +export const IdentityTable = ({ handlePopUpOpen }: Props) => { const { createNotification } = useNotificationContext(); @@ -58,39 +48,38 @@ export const MachineIdentityTable = ({ const { currentWorkspace } = useWorkspace(); const orgId = currentOrg?._id || ""; const workspaceId = currentWorkspace?._id || ""; - - const { data, isLoading } = useGetWorkspaceMachineMemberships(currentWorkspace?._id || ""); - + const { data, isLoading } = useGetWorkspaceIdentityMemberships(currentWorkspace?._id || ""); + const { data: roles } = useGetRoles({ orgId, workspaceId }); - const { mutateAsync: updateMutateAsync } = useUpdateMachineWorkspaceRole(); + const { mutateAsync: updateMutateAsync } = useUpdateIdentityWorkspaceRole(); const handleChangeRole = async ({ - machineId, + identityId, role }: { - machineId: string; + identityId: string; role: string; }) => { try { await updateMutateAsync({ - machineId, + identityId, workspaceId, role }); createNotification({ - text: "Successfully updated machine identity role", + text: "Successfully updated identity role", type: "success" }); } catch (err) { console.error(err); const error = err as any; - const text = error?.response?.data?.message ?? "Failed to update machine identity role" + const text = error?.response?.data?.message ?? "Failed to update identity role" createNotification({ text, @@ -111,12 +100,12 @@ export const MachineIdentityTable = ({ - {isLoading && } + {isLoading && } {!isLoading && data && data.length > 0 && data.map(({ - machineIdentity: { + identity: { _id, name }, @@ -130,7 +119,7 @@ export const MachineIdentityTable = ({ {(isAllowed) => { return ( @@ -143,7 +132,7 @@ export const MachineIdentityTable = ({ dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800" onValueChange={(selectedRole) => handleChangeRole({ - machineId: _id, + identityId: _id, role: selectedRole }) } @@ -163,13 +152,13 @@ export const MachineIdentityTable = ({ {(isAllowed) => ( { - handlePopUpOpen("deleteMachineIdentity", { - machineId: _id, + handlePopUpOpen("deleteIdentity", { + identityId: _id, name }); }} @@ -191,7 +180,7 @@ export const MachineIdentityTable = ({ {!isLoading && data && data?.length === 0 && ( - + )} diff --git a/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentitySection/index.tsx b/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentitySection/index.tsx new file mode 100644 index 000000000..3aa8cee58 --- /dev/null +++ b/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentitySection/index.tsx @@ -0,0 +1 @@ +export { IdentitySection } from "./IdentitySection"; \ No newline at end of file diff --git a/frontend/src/views/Project/MembersPage/components/IdentityTab/components/index.tsx b/frontend/src/views/Project/MembersPage/components/IdentityTab/components/index.tsx new file mode 100644 index 000000000..3aa8cee58 --- /dev/null +++ b/frontend/src/views/Project/MembersPage/components/IdentityTab/components/index.tsx @@ -0,0 +1 @@ +export { IdentitySection } from "./IdentitySection"; \ No newline at end of file diff --git a/frontend/src/views/Project/MembersPage/components/IdentityTab/index.tsx b/frontend/src/views/Project/MembersPage/components/IdentityTab/index.tsx new file mode 100644 index 000000000..7b6d6dbc5 --- /dev/null +++ b/frontend/src/views/Project/MembersPage/components/IdentityTab/index.tsx @@ -0,0 +1 @@ +export { IdentityTab } from "./IdentityTab"; \ No newline at end of file diff --git a/frontend/src/views/Project/MembersPage/components/MachineIdentityTab/components/MachineIdentitySection/index.tsx b/frontend/src/views/Project/MembersPage/components/MachineIdentityTab/components/MachineIdentitySection/index.tsx deleted file mode 100644 index 06de47d26..000000000 --- a/frontend/src/views/Project/MembersPage/components/MachineIdentityTab/components/MachineIdentitySection/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { MachineIdentitySection } from "./MachineIdentitySection"; \ No newline at end of file diff --git a/frontend/src/views/Project/MembersPage/components/MachineIdentityTab/components/index.tsx b/frontend/src/views/Project/MembersPage/components/MachineIdentityTab/components/index.tsx deleted file mode 100644 index 06de47d26..000000000 --- a/frontend/src/views/Project/MembersPage/components/MachineIdentityTab/components/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { MachineIdentitySection } from "./MachineIdentitySection"; \ No newline at end of file diff --git a/frontend/src/views/Project/MembersPage/components/MachineIdentityTab/index.tsx b/frontend/src/views/Project/MembersPage/components/MachineIdentityTab/index.tsx deleted file mode 100644 index 568043da2..000000000 --- a/frontend/src/views/Project/MembersPage/components/MachineIdentityTab/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { MachineIdentityTab } from "./MachineIdentityTab"; \ No newline at end of file diff --git a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.tsx b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.tsx index 74eeec69b..a550d4e33 100644 --- a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.tsx +++ b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.tsx @@ -62,9 +62,9 @@ const SINGLE_PERMISSION_LIST = [ }, { title: "Machine identity management", - subtitle: "Add, view, update and remove machine identities from the project", + subtitle: "Add, view, update and remove (machine) identities from the project", icon: faServer, - formName: "machine-identity" + formName: "identity" }, { title: "Webhooks", diff --git a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.utils.ts b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.utils.ts index ef6a41c70..c6175bd58 100644 --- a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.utils.ts +++ b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.utils.ts @@ -33,7 +33,7 @@ export const formSchema = z.object({ .object({ secrets: z.record(multiEnvPermissionSchema).optional(), member: generalPermissionSchema, - "machine-identity": generalPermissionSchema, + "identity": generalPermissionSchema, role: generalPermissionSchema, integrations: generalPermissionSchema, webhooks: generalPermissionSchema, diff --git a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/SingleProjectPermission.tsx b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/SingleProjectPermission.tsx index 38fffb796..84a4c1c9a 100644 --- a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/SingleProjectPermission.tsx +++ b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/SingleProjectPermission.tsx @@ -23,7 +23,7 @@ type Props = { | "tags" | "audit-logs" | "ip-allowlist" - | "machine-identity" + | "identity" | ProjectPermissionSub.SecretApproval; isNonEditable?: boolean; setValue: UseFormSetValue; diff --git a/frontend/src/views/Project/MembersPage/components/index.tsx b/frontend/src/views/Project/MembersPage/components/index.tsx index f6281751f..a39af27f7 100644 --- a/frontend/src/views/Project/MembersPage/components/index.tsx +++ b/frontend/src/views/Project/MembersPage/components/index.tsx @@ -1,4 +1,4 @@ -export { MachineIdentityTab } from "./MachineIdentityTab"; +export { IdentityTab } from "./IdentityTab"; export { MemberListTab } from "./MemberListTab"; export { ProjectRoleListTab } from "./ProjectRoleListTab"; export { ServiceTokenTab } from "./ServiceTokenTab"; \ No newline at end of file