Restructure MIs to more generic Identity

This commit is contained in:
Tuan Dang
2023-12-09 22:18:38 +07:00
parent 89d0c0e3c3
commit 50ef23e8a0
102 changed files with 3652 additions and 2981 deletions
+789 -6
View File
@@ -1,21 +1,54 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import { Types } from "mongoose";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import crypto from "crypto";
import bcrypt from "bcrypt";
import * as bigintConversion from "bigint-conversion"; import * as bigintConversion from "bigint-conversion";
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const jsrp = require("jsrp"); 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 { clearTokens, createToken, issueAuthTokens } from "../../helpers/auth";
import { checkUserDevice } from "../../helpers/user"; import { checkUserDevice } from "../../helpers/user";
import { AuthTokenType } from "../../variables"; import { AuthTokenType } from "../../variables";
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors"; import {
BadRequestError,
ForbiddenRequestError,
ResourceNotFoundError,
UnauthorizedRequestError
} from "../../utils/errors";
import { import {
getAuthSecret, getAuthSecret,
getHttpsEnabled, getHttpsEnabled,
getJwtAuthLifetime getJwtAuthLifetime,
getSaltRounds
} from "../../config"; } from "../../config";
import { ActorType } from "../../ee/models"; import { ActorType, EventType, IRole } from "../../ee/models";
import { validateRequest } from "../../helpers/validation"; import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/auth"; 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" { declare module "jsonwebtoken" {
export interface AuthnJwtPayload extends jwt.JwtPayload { export interface AuthnJwtPayload extends jwt.JwtPayload {
@@ -25,10 +58,10 @@ declare module "jsonwebtoken" {
userId: string; userId: string;
refreshVersion?: number; refreshVersion?: number;
} }
export interface MachineAccessTokenJwtPayload extends jwt.JwtPayload { export interface IdentityAccessTokenJwtPayload extends jwt.JwtPayload {
_id: string; _id: string;
clientSecretId: string; clientSecretId: string;
machineAccessTokenId: string; identityAccessTokenId: string;
authTokenType: string; authTokenType: string;
} }
} }
@@ -268,3 +301,753 @@ export const getNewToken = async (req: Request, res: Response) => {
export const handleAuthProviderCallback = (req: Request, res: Response) => { export const handleAuthProviderCallback = (req: Request, res: Response) => {
res.redirect(`/login/provider/success?token=${encodeURIComponent(req.providerAuthToken)}`); 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.IdentityAccessTokenJwtPayload>(
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<IIdentityTrustedIp>,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps: reformattedAccessTokenTrustedIps as Array<IIdentityTrustedIp>
}
}
);
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<IIdentityTrustedIp>,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps: reformattedAccessTokenTrustedIps as Array<IIdentityTrustedIp>
}
}
);
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)
})
}
@@ -1,7 +1,7 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { import {
MachineMembershipOrg, IdentityMembershipOrg,
Membership, Membership,
MembershipOrg, MembershipOrg,
Workspace 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 req
* @param res * @param res
* @returns * @returns
*/ */
export const getOrganizationMachineMemberships = async (req: Request, res: Response) => { export const getOrganizationIdentityMemberships = async (req: Request, res: Response) => {
const { const {
params: { organizationId } params: { organizationId }
} = await validateRequest(reqValidator.GetOrgServiceMembersV2, req); } = await validateRequest(reqValidator.GetOrgIdentityMembershipsV2, req);
const { permission } = await getAuthDataOrgPermissions({ const { permission } = await getAuthDataOrgPermissions({
authData: req.authData, authData: req.authData,
@@ -435,14 +435,14 @@ export const getOrganizationMachineMemberships = async (req: Request, res: Respo
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read, OrgPermissionActions.Read,
OrgPermissionSubjects.MachineIdentity OrgPermissionSubjects.Identity
); );
const machineMemberships = await MachineMembershipOrg.find({ const identityMemberships = await IdentityMembershipOrg.find({
organization: new Types.ObjectId(organizationId) organization: new Types.ObjectId(organizationId)
}).populate("machineIdentity customRole"); }).populate("identity customRole");
return res.status(200).send({ return res.status(200).send({
machineMemberships identityMemberships
}); });
} }
@@ -1,10 +1,10 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { import {
IMachineIdentity, IIdentity,
IdentityMembership,
IdentityMembershipOrg,
Key, Key,
MachineIdentity,
MachineMembership,
Membership, Membership,
ServiceTokenData, ServiceTokenData,
Workspace 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] * with id [workspaceId]
* @param req * @param req
* @param res * @param res
*/ */
export const addMachineToWorkspace = async (req: Request, res: Response) => { export const addIdentityToWorkspace = async (req: Request, res: Response) => {
const { const {
params: { workspaceId, machineId }, params: { workspaceId, identityId },
body: { body: {
role role
} }
} = await validateRequest(reqValidator.AddMachineToWorkspaceV2, req); } = await validateRequest(reqValidator.AddIdentityToWorkspaceV2, req);
const { permission } = await getAuthDataProjectPermissions({ const { permission } = await getAuthDataProjectPermissions({
authData: req.authData, authData: req.authData,
@@ -526,35 +526,40 @@ export const addMachineToWorkspace = async (req: Request, res: Response) => {
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create, ProjectPermissionActions.Create,
ProjectPermissionSub.MachineIdentity ProjectPermissionSub.Identity
); );
let machineMembership = await MachineMembership.findOne({ let identityMembership = await IdentityMembership.findOne({
machineIdentity: new Types.ObjectId(machineId), identity: new Types.ObjectId(identityId),
workspace: new Types.ObjectId(workspaceId) workspace: new Types.ObjectId(workspaceId)
}); });
if (machineMembership) throw BadRequestError({ if (identityMembership) throw BadRequestError({
message: `Machine identity with id ${machineId} already exists in project with id ${workspaceId}` 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); const workspace = await Workspace.findById(workspaceId);
if (!workspace) throw ResourceNotFoundError(); 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({ if (!identityMembershipOrg.organization.equals(workspace.organization)) throw BadRequestError({
message: "Failed to add machine identity to project in another organization" message: "Failed to add identity to project in another organization"
}); });
const rolePermission = await getWorkspaceRolePermissions(role, workspaceId); const rolePermission = await getWorkspaceRolePermissions(role, workspaceId);
const isAsPrivilegedAsIntendedRole = isAtLeastAsPrivilegedWorkspace(permission, rolePermission); const isAsPrivilegedAsIntendedRole = isAtLeastAsPrivilegedWorkspace(permission, rolePermission);
if (!isAsPrivilegedAsIntendedRole) throw ForbiddenRequestError({ 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; let customRole;
@@ -571,31 +576,31 @@ export const addMachineToWorkspace = async (req: Request, res: Response) => {
} }
} }
machineMembership = await new MachineMembership({ identityMembership = await new IdentityMembership({
machineIdentity: machineIdentity._id, identity: identityMembershipOrg.identity,
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
role: customRole ? CUSTOM : role, role: customRole ? CUSTOM : role,
customRole customRole
}).save(); }).save();
return res.status(200).send({ 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] * with id [workspaceId] to [role]
* @param req * @param req
* @param res * @param res
*/ */
export const updateMachineWorkspaceRole = async (req: Request, res: Response) => { export const updateIdentityWorkspaceRole = async (req: Request, res: Response) => {
const { const {
params: { workspaceId, machineId }, params: { workspaceId, identityId },
body: { body: {
role role
} }
} = await validateRequest(reqValidator.UpdateMachineWorkspaceRoleV2, req); } = await validateRequest(reqValidator.UpdateIdentityWorkspaceRoleV2, req);
const { permission } = await getAuthDataProjectPermissions({ const { permission } = await getAuthDataProjectPermissions({
authData: req.authData, authData: req.authData,
@@ -604,37 +609,37 @@ export const addMachineToWorkspace = async (req: Request, res: Response) => {
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit, ProjectPermissionActions.Edit,
ProjectPermissionSub.MachineIdentity ProjectPermissionSub.Identity
); );
let machineMembership = await MachineMembership let identityMembership = await IdentityMembership
.findOne({ .findOne({
machineIdentity: new Types.ObjectId(machineId), identity: new Types.ObjectId(identityId),
workspace: new Types.ObjectId(workspaceId) workspace: new Types.ObjectId(workspaceId)
}) })
.populate<{ .populate<{
machineIdentity: IMachineIdentity, identity: IIdentity,
customRole: IRole customRole: IRole
}>("machineIdentity customRole"); }>("identity customRole");
if (!machineMembership) throw BadRequestError({ if (!identityMembership) throw BadRequestError({
message: `Machine identity with id ${machineId} does not exist in project with id ${workspaceId}` message: `Identity with id ${identityId} does not exist in project with id ${workspaceId}`
}); });
const machineIdentityRolePermission = await getWorkspaceRolePermissions( const identityRolePermission = await getWorkspaceRolePermissions(
machineMembership?.customRole?.slug ?? machineMembership.role, identityMembership?.customRole?.slug ?? identityMembership.role,
machineMembership.workspace.toString() identityMembership.workspace.toString()
); );
const isAsPrivilegedAsMachine = isAtLeastAsPrivilegedWorkspace(permission, machineIdentityRolePermission); const isAsPrivilegedAsIdentity = isAtLeastAsPrivilegedWorkspace(permission, identityRolePermission);
if (!isAsPrivilegedAsMachine) throw ForbiddenRequestError({ if (!isAsPrivilegedAsIdentity) throw ForbiddenRequestError({
message: "Failed to update role of more privileged MI" message: "Failed to update role of more privileged identity"
}); });
const rolePermission = await getWorkspaceRolePermissions(role, workspaceId); const rolePermission = await getWorkspaceRolePermissions(role, workspaceId);
const isAsPrivilegedAsIntendedRole = isAtLeastAsPrivilegedWorkspace(permission, rolePermission); const isAsPrivilegedAsIntendedRole = isAtLeastAsPrivilegedWorkspace(permission, rolePermission);
if (!isAsPrivilegedAsIntendedRole) throw ForbiddenRequestError({ 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; 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), workspace: new Types.ObjectId(workspaceId),
}, },
{ {
@@ -666,20 +671,20 @@ export const addMachineToWorkspace = async (req: Request, res: Response) => {
); );
return res.status(200).send({ 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] * with id [workspaceId]
* @param req * @param req
* @param res * @param res
*/ */
export const deleteMachineFromWorkspace = async (req: Request, res: Response) => { export const deleteIdentityFromWorkspace = async (req: Request, res: Response) => {
const { const {
params: { workspaceId, machineId } params: { workspaceId, identityId }
} = await validateRequest(reqValidator.DeleteMachineFromWorkspaceV2, req); } = await validateRequest(reqValidator.DeleteIdentityFromWorkspaceV2, req);
const { permission } = await getAuthDataProjectPermissions({ const { permission } = await getAuthDataProjectPermissions({
authData: req.authData, authData: req.authData,
@@ -688,49 +693,49 @@ export const addMachineToWorkspace = async (req: Request, res: Response) => {
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete, ProjectPermissionActions.Delete,
ProjectPermissionSub.MachineIdentity ProjectPermissionSub.Identity
); );
const machineMembership = await MachineMembership const identityMembership = await IdentityMembership
.findOne({ .findOne({
machineIdentity: new Types.ObjectId(machineId), identity: new Types.ObjectId(identityId),
workspace: new Types.ObjectId(workspaceId) workspace: new Types.ObjectId(workspaceId)
}) })
.populate<{ .populate<{
machineIdentity: IMachineIdentity, identity: IIdentity,
customRole: IRole customRole: IRole
}>("machineIdentity customRole"); }>("identity customRole");
if (!machineMembership) throw ResourceNotFoundError({ if (!identityMembership) throw ResourceNotFoundError({
message: `Machine with id ${machineId} does not exist in project with id ${workspaceId}` message: `Identity with id ${identityId} does not exist in project with id ${workspaceId}`
}); });
const machineIdentityRolePermission = await getWorkspaceRolePermissions( const identityRolePermission = await getWorkspaceRolePermissions(
machineMembership?.customRole?.slug ?? machineMembership.role, identityMembership?.customRole?.slug ?? identityMembership.role,
machineMembership.workspace.toString() identityMembership.workspace.toString()
); );
const isAsPrivilegedAsMachine = isAtLeastAsPrivilegedWorkspace(permission, machineIdentityRolePermission); const isAsPrivilegedAsIdentity = isAtLeastAsPrivilegedWorkspace(permission, identityRolePermission);
if (!isAsPrivilegedAsMachine) throw ForbiddenRequestError({ if (!isAsPrivilegedAsIdentity) throw ForbiddenRequestError({
message: "Failed to remove more privileged MI from project" message: "Failed to remove more privileged identity from project"
}); });
await MachineMembership.findByIdAndDelete(machineMembership._id); await IdentityMembership.findByIdAndDelete(identityMembership._id);
return res.status(200).send({ 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 req
* @param res * @param res
* @returns * @returns
*/ */
export const getWorkspaceMachineMemberships = async (req: Request, res: Response) => { export const getWorkspaceIdentityMemberships = async (req: Request, res: Response) => {
const { const {
params: { workspaceId } params: { workspaceId }
} = await validateRequest(reqValidator.GetWorkspaceMachineMembersV2, req); } = await validateRequest(reqValidator.GetWorkspaceIdentityMembersV2, req);
const { permission } = await getAuthDataProjectPermissions({ const { permission } = await getAuthDataProjectPermissions({
authData: req.authData, authData: req.authData,
@@ -739,14 +744,14 @@ export const addMachineToWorkspace = async (req: Request, res: Response) => {
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionActions.Read,
ProjectPermissionSub.MachineIdentity ProjectPermissionSub.Identity
); );
const machineMemberships = await MachineMembership.find({ const identityMemberships = await IdentityMembership.find({
workspace: new Types.ObjectId(workspaceId) workspace: new Types.ObjectId(workspaceId)
}).populate("machineIdentity customRole"); }).populate("identity customRole");
return res.status(200).send({ return res.status(200).send({
machineMemberships identityMemberships
}); });
} }
@@ -94,7 +94,7 @@ const checkSecretsPermission = async ({
}); });
return { authVerifier: () => true }; return { authVerifier: () => true };
} }
case ActorType.MACHINE: { case ActorType.IDENTITY: {
const { permission } = await getAuthDataProjectPermissions({ const { permission } = await getAuthDataProjectPermissions({
authData, authData,
workspaceId: new Types.ObjectId(workspaceId) workspaceId: new Types.ObjectId(workspaceId)
@@ -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
});
}
+2 -2
View File
@@ -1,4 +1,4 @@
import * as machineIdentitiesController from "./machineIdentitiesController"; import * as identitiesController from "./identitiesController";
import * as secretController from "./secretController"; import * as secretController from "./secretController";
import * as secretSnapshotController from "./secretSnapshotController"; import * as secretSnapshotController from "./secretSnapshotController";
import * as organizationsController from "./organizationsController"; import * as organizationsController from "./organizationsController";
@@ -14,7 +14,7 @@ import * as secretRotationProviderController from "./secretRotationProviderContr
import * as secretRotationController from "./secretRotationController"; import * as secretRotationController from "./secretRotationController";
export { export {
machineIdentitiesController, identitiesController,
secretController, secretController,
secretSnapshotController, secretSnapshotController,
organizationsController, organizationsController,
@@ -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.MachineAccessTokenJwtPayload>(
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<IMachineIdentityTrustedIp>,
accessTokenTrustedIps: reformattedAccessTokenTrustedIps as Array<IMachineIdentityTrustedIp>
}
},
{
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<IMachineIdentityTrustedIp>,
accessTokenTrustedIps: reformattedAccessTokenTrustedIps as Array<IMachineIdentityTrustedIp>
}
},
{
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<IMachineIdentityTrustedIp>,
accessTokenTrustedIps: machineIdentity.accessTokenTrustedIps as Array<IMachineIdentityTrustedIp>,
}
},
{
organizationId: machineIdentity.organization
}
);
return res.status(200).send({
machineIdentity
});
}
@@ -2,8 +2,8 @@ import { Request, Response } from "express";
import { PipelineStage, Types } from "mongoose"; import { PipelineStage, Types } from "mongoose";
import { import {
Folder, Folder,
MachineIdentity, Identity,
MachineMembership, IdentityMembership,
Membership, Membership,
Secret, Secret,
ServiceTokenData, ServiceTokenData,
@@ -18,7 +18,7 @@ import {
FolderVersion, FolderVersion,
IPType, IPType,
ISecretVersion, ISecretVersion,
MachineActor, IdentityActor,
SecretSnapshot, SecretSnapshot,
SecretVersion, SecretVersion,
ServiceActor, ServiceActor,
@@ -679,8 +679,8 @@ export const getWorkspaceAuditLogs = async (req: Request, res: Response) => {
case ActorType.SERVICE: case ActorType.SERVICE:
actorMetadataQuery = "actor.metadata.serviceId"; actorMetadataQuery = "actor.metadata.serviceId";
break; break;
case ActorType.MACHINE: case ActorType.IDENTITY:
actorMetadataQuery = "actor.metadata.machineId"; actorMetadataQuery = "actor.metadata.identityId";
break; 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) workspace: new Types.ObjectId(workspaceId)
}); });
const machineActors: MachineActor[] = ( const identityActors: IdentityActor[] = (
await MachineIdentity.find({ await Identity.find({
_id: { _id: {
$in: machineIds $in: identityIds
} }
}) })
).map((machineIdentity) => ({ ).map((identity) => ({
type: ActorType.MACHINE, type: ActorType.IDENTITY,
metadata: { metadata: {
machineId: machineIdentity._id.toString(), identityId: identity._id.toString(),
name: machineIdentity.name name: identity.name
} }
})); }));
const actors = [...userActors, ...serviceActors, ...machineActors]; const actors = [...userActors, ...serviceActors, ...identityActors];
return res.status(200).send({ return res.status(200).send({
actors actors
+1 -1
View File
@@ -10,7 +10,7 @@ export interface IAuditLog {
event: Event; event: Event;
userAgent: string; userAgent: string;
userAgentType: UserAgentType; userAgentType: UserAgentType;
expiresAt: Date; expiresAt?: Date;
} }
const auditLogSchema = new Schema<IAuditLog>( const auditLogSchema = new Schema<IAuditLog>(
+13 -10
View File
@@ -1,7 +1,7 @@
export enum ActorType { // would extend to AWS, Azure, ... export enum ActorType { // would extend to AWS, Azure, ...
USER = "user", // userIdentity USER = "user", // userIdentity
SERVICE = "service", SERVICE = "service",
MACHINE = "machine" // machineIdentity IDENTITY = "identity"
} }
export enum UserAgentType { export enum UserAgentType {
@@ -31,13 +31,16 @@ export enum EventType {
DELETE_TRUSTED_IP = "delete-trusted-ip", DELETE_TRUSTED_IP = "delete-trusted-ip",
CREATE_SERVICE_TOKEN = "create-service-token", // v2 CREATE_SERVICE_TOKEN = "create-service-token", // v2
DELETE_SERVICE_TOKEN = "delete-service-token", // v2 DELETE_SERVICE_TOKEN = "delete-service-token", // v2
CREATE_MACHINE_IDENTITY = "create-machine-identity", CREATE_IDENTITY = "create-identity",
UPDATE_MACHINE_IDENTITY = "update-machine-identity", UPDATE_IDENTITY = "update-identity",
DELETE_MACHINE_IDENTITY = "delete-machine-identity", DELETE_IDENTITY = "delete-identity",
LOGIN_MACHINE_IDENTITY = "login-machine-identity", LOGIN_IDENTITY_UNIVERSAL_AUTH = "login-identity-universal-auth",
CREATE_MACHINE_IDENTITY_CLIENT_SECRET = "create-machine-identity-secret", ADD_IDENTITY_UNIVERSAL_AUTH = "add-identity-universal-auth",
REVOKE_MACHINE_IDENTITY_CLIENT_SECRET = "revoke-machine-identity-secret", UPDATE_IDENTITY_UNIVERSAL_AUTH = "update-identity-universal-auth",
GET_MACHINE_IDENTITY_CLIENT_SECRETS = "get-machine-identity-secrets", 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", CREATE_ENVIRONMENT = "create-environment",
UPDATE_ENVIRONMENT = "update-environment", UPDATE_ENVIRONMENT = "update-environment",
DELETE_ENVIRONMENT = "delete-environment", DELETE_ENVIRONMENT = "delete-environment",
+71 -48
View File
@@ -1,5 +1,5 @@
import { ActorType, EventType } from "./enums"; import { ActorType, EventType } from "./enums";
import { IMachineIdentityTrustedIp } from "../../../models/machineIdentity"; import { IIdentityTrustedIp } from "../../../models";
interface UserActorMetadata { interface UserActorMetadata {
userId: string; userId: string;
@@ -11,8 +11,8 @@ interface ServiceActorMetadata {
name: string; name: string;
} }
interface MachineActorMetadata { interface IdentityActorMetadata {
machineId: string; identityId: string;
name: string; name: string;
} }
@@ -26,16 +26,12 @@ export interface ServiceActor {
metadata: ServiceActorMetadata; metadata: ServiceActorMetadata;
} }
export interface MachineActor { export interface IdentityActor {
type: ActorType.MACHINE; type: ActorType.IDENTITY;
metadata: MachineActorMetadata; metadata: IdentityActorMetadata;
} }
// export interface MachineActor { export type Actor = UserActor | ServiceActor | IdentityActor;
// type: ActorType.Machine;
// }
export type Actor = UserActor | ServiceActor | MachineActor;
interface GetSecretsEvent { interface GetSecretsEvent {
type: EventType.GET_SECRETS; type: EventType.GET_SECRETS;
@@ -225,66 +221,90 @@ interface DeleteServiceTokenEvent {
}; };
} }
interface CreateMachineIdentityEvent { interface CreateIdentityEvent { // note: currently not logging org-role
type: EventType.CREATE_MACHINE_IDENTITY; type: EventType.CREATE_IDENTITY;
metadata: { metadata: {
identityId: string;
name: string; name: string;
role: string;
clientSecretTrustedIps: Array<IMachineIdentityTrustedIp>;
accessTokenTrustedIps: Array<IMachineIdentityTrustedIp>;
}; };
} }
interface UpdateMachineIdentityEvent { interface UpdateIdentityEvent {
type: EventType.UPDATE_MACHINE_IDENTITY; type: EventType.UPDATE_IDENTITY;
metadata: { metadata: {
identityId: string;
name?: string; name?: string;
role?: string;
clientSecretTrustedIps?: Array<IMachineIdentityTrustedIp>;
accessTokenTrustedIps?: Array<IMachineIdentityTrustedIp>;
}; };
} }
interface DeleteMachineIdentityEvent { interface DeleteIdentityEvent {
type: EventType.DELETE_MACHINE_IDENTITY; type: EventType.DELETE_IDENTITY;
metadata: { metadata: {
name: string; identityId: string;
role: string;
clientSecretTrustedIps: Array<IMachineIdentityTrustedIp>;
accessTokenTrustedIps: Array<IMachineIdentityTrustedIp>;
}; };
} }
interface LoginMachineIdentityEvent { interface LoginIdentityUniversalAuthEvent {
type: EventType.LOGIN_MACHINE_IDENTITY ; type: EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH ;
metadata: { metadata: {
machineId: string; identityId: string;
machineAccessTokenId: string;
clientSecretId: string; clientSecretId: string;
identityAccessTokenId: string; identityAccessTokenId: string;
}; };
} }
interface CreateMachineIdentitySecretEvent { interface AddIdentityUniversalAuthEvent {
type: EventType.CREATE_MACHINE_IDENTITY_CLIENT_SECRET ; type: EventType.ADD_IDENTITY_UNIVERSAL_AUTH;
metadata: { metadata: {
machineId: string; identityId: string;
clientSecretTrustedIps: Array<IIdentityTrustedIp>;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: Array<IIdentityTrustedIp>;
};
}
interface UpdateIdentityUniversalAuthEvent {
type: EventType.UPDATE_IDENTITY_UNIVERSAL_AUTH;
metadata: {
identityId: string;
clientSecretTrustedIps?: Array<IIdentityTrustedIp>;
accessTokenTTL?: number;
accessTokenMaxTTL?: number;
accessTokenNumUsesLimit?: number;
accessTokenTrustedIps?: Array<IIdentityTrustedIp>;
};
}
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; clientSecretId: string;
}; };
} }
interface DeleteMachineIdentitySecretEvent { interface GetIdentityUniversalAuthClientSecretsEvent {
type: EventType.REVOKE_MACHINE_IDENTITY_CLIENT_SECRET ; type: EventType.GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS;
metadata: { metadata: {
machineId: string; identityId: string;
clientSecretId: string;
}; };
} }
interface GetMachineIdentitySecretsEvent {
type: EventType.GET_MACHINE_IDENTITY_CLIENT_SECRETS ; interface RevokeIdentityUniversalAuthClientSecretEvent {
type: EventType.REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET ;
metadata: { metadata: {
machineId: string; identityId: string;
clientSecretId: string;
}; };
} }
@@ -530,13 +550,16 @@ export type Event =
| DeleteTrustedIPEvent | DeleteTrustedIPEvent
| CreateServiceTokenEvent | CreateServiceTokenEvent
| DeleteServiceTokenEvent | DeleteServiceTokenEvent
| CreateMachineIdentityEvent | CreateIdentityEvent
| UpdateMachineIdentityEvent | UpdateIdentityEvent
| DeleteMachineIdentityEvent | DeleteIdentityEvent
| CreateMachineIdentitySecretEvent | LoginIdentityUniversalAuthEvent
| DeleteMachineIdentitySecretEvent | AddIdentityUniversalAuthEvent
| LoginMachineIdentityEvent | UpdateIdentityUniversalAuthEvent
| GetMachineIdentitySecretsEvent | GetIdentityUniversalAuthEvent
| CreateIdentityUniversalAuthClientSecretEvent
| GetIdentityUniversalAuthClientSecretsEvent
| RevokeIdentityUniversalAuthClientSecretEvent
| CreateEnvironmentEvent | CreateEnvironmentEvent
| UpdateEnvironmentEvent | UpdateEnvironmentEvent
| DeleteEnvironmentEvent | DeleteEnvironmentEvent
+31
View File
@@ -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;
+2 -2
View File
@@ -1,4 +1,4 @@
import machineIdentities from "./machineIdentities"; import identities from "./identities";
import secret from "./secret"; import secret from "./secret";
import secretSnapshot from "./secretSnapshot"; import secretSnapshot from "./secretSnapshot";
import organizations from "./organizations"; import organizations from "./organizations";
@@ -14,7 +14,7 @@ import secretRotationProvider from "./secretRotationProvider";
import secretRotation from "./secretRotation"; import secretRotation from "./secretRotation";
export { export {
machineIdentities, identities,
secret, secret,
secretSnapshot, secretSnapshot,
organizations, organizations,
@@ -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;
+22 -12
View File
@@ -3,7 +3,6 @@ import { AuditLog, Event } from "../models";
import { AuthData } from "../../interfaces/middleware"; import { AuthData } from "../../interfaces/middleware";
import EELicenseService from "./EELicenseService"; import EELicenseService from "./EELicenseService";
import { Workspace } from "../../models"; import { Workspace } from "../../models";
import { OrganizationNotFoundError } from "../../utils/errors";
interface EventScope { interface EventScope {
workspaceId?: Types.ObjectId; workspaceId?: Types.ObjectId;
@@ -14,31 +13,42 @@ type ValidEventScope =
| Required<Pick<EventScope, "workspaceId">> | Required<Pick<EventScope, "workspaceId">>
| Required<Pick<EventScope, "organizationId">> | Required<Pick<EventScope, "organizationId">>
| Required<EventScope> | Required<EventScope>
| Record<string, never>;
export default class EEAuditLogService { 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 MS_IN_DAY = 24 * 60 * 60 * 1000;
const organizationId = ("organizationId" in eventScope) let organizationId;
? eventScope.organizationId if ("organizationId" in eventScope) {
: (await Workspace.findById(eventScope.workspaceId).select("organization").lean())?.organization; organizationId = eventScope.organizationId;
}
if (!organizationId) throw OrganizationNotFoundError({ let workspaceId;
message: "createAuditLog: Failed to create audit log due to missing organizationId" if ("workspaceId" in eventScope) {
}); workspaceId = eventScope.workspaceId;
const ttl = (await EELicenseService.getPlan(organizationId)).auditLogsRetentionDays * MS_IN_DAY; 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({ const auditLog = await new AuditLog({
actor: authData.actor, actor: authData.actor,
organization: organizationId, organization: organizationId,
workspace: ("workspaceId" in eventScope) ? eventScope.workspaceId : undefined, workspace: workspaceId,
ipAddress: authData.ipAddress, ipAddress: authData.ipAddress,
event, event,
userAgent: authData.userAgent, userAgent: authData.userAgent,
userAgentType: authData.userAgentType, userAgentType: authData.userAgentType,
expiresAt: new Date(Date.now() + ttl) expiresAt
}); });
if (shouldSave) { if (shouldSave) {
+21 -27
View File
@@ -13,13 +13,12 @@ import picomatch from "picomatch";
import { AuthData } from "../../interfaces/middleware"; import { AuthData } from "../../interfaces/middleware";
import { ActorType, IRole, Role } from "../models"; import { ActorType, IRole, Role } from "../models";
import { import {
IMachineIdentity, IIdentity,
MachineMembership, IdentityMembership,
Membership, Membership,
ServiceTokenData ServiceTokenData
} from "../../models"; } from "../../models";
import { ADMIN, CUSTOM, MEMBER, NO_ACCESS, VIEWER } from "../../variables"; import { ADMIN, CUSTOM, MEMBER, NO_ACCESS, VIEWER } from "../../variables";
import { checkIPAgainstBlocklist } from "../../utils/ip";
import { BadRequestError } from "../../utils/errors"; import { BadRequestError } from "../../utils/errors";
const $glob: FieldInstruction<string> = { const $glob: FieldInstruction<string> = {
@@ -62,7 +61,7 @@ export enum ProjectPermissionSub {
SecretRollback = "secret-rollback", SecretRollback = "secret-rollback",
SecretApproval = "secret-approval", SecretApproval = "secret-approval",
SecretRotation = "secret-rotation", SecretRotation = "secret-rotation",
MachineIdentity = "machine-identity" Identity = "identity"
} }
type SubjectFields = { type SubjectFields = {
@@ -87,7 +86,7 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens] | [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval] | [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation] | [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
| [ProjectPermissionActions, ProjectPermissionSub.MachineIdentity] | [ProjectPermissionActions, ProjectPermissionSub.Identity]
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace] | [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace] | [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace]
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback] | [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
@@ -134,10 +133,10 @@ const buildAdminPermission = () => {
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks); can(ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Webhooks); can(ProjectPermissionActions.Delete, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Read, ProjectPermissionSub.MachineIdentity); can(ProjectPermissionActions.Read, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Create, ProjectPermissionSub.MachineIdentity); can(ProjectPermissionActions.Create, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.MachineIdentity); can(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.MachineIdentity); can(ProjectPermissionActions.Delete, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens); can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens); can(ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens);
@@ -204,10 +203,10 @@ const buildMemberPermission = () => {
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks); can(ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Webhooks); can(ProjectPermissionActions.Delete, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Read, ProjectPermissionSub.MachineIdentity); can(ProjectPermissionActions.Read, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Create, ProjectPermissionSub.MachineIdentity); can(ProjectPermissionActions.Create, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.MachineIdentity); can(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.MachineIdentity); can(ProjectPermissionActions.Delete, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens); can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens); can(ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens);
@@ -249,7 +248,7 @@ const buildViewerPermission = () => {
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role); can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks); can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Read, ProjectPermissionSub.MachineIdentity); can(ProjectPermissionActions.Read, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens); can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings); can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments); can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments);
@@ -310,28 +309,23 @@ export const getAuthDataProjectPermissions = async ({
role = "viewer"; role = "viewer";
break; break;
} }
case ActorType.MACHINE: { case ActorType.IDENTITY: {
const machineMembership = await MachineMembership.findOne({ const identityMembership = await IdentityMembership.findOne({
machineIdentity: authData.authPayload._id, identity: authData.authPayload._id,
workspace: workspaceId workspace: workspaceId
}) })
.populate<{ .populate<{
customRole: IRole & { permissions: RawRuleOf<MongoAbility<ProjectPermissionSet>>[] }; customRole: IRole & { permissions: RawRuleOf<MongoAbility<ProjectPermissionSet>>[] };
machineIdentity: IMachineIdentity identity: IIdentity
}>("customRole machineIdentity") }>("customRole identity")
.exec(); .exec();
if (!machineMembership || (machineMembership.role === "custom" && !machineMembership.customRole)) { if (!identityMembership || (identityMembership.role === "custom" && !identityMembership.customRole)) {
throw UnauthorizedRequestError(); throw UnauthorizedRequestError();
} }
checkIPAgainstBlocklist({
ipAddress: authData.ipAddress,
trustedIps: machineMembership.machineIdentity.accessTokenTrustedIps
});
role = machineMembership.role; role = identityMembership.role;
customRole = machineMembership.customRole; customRole = identityMembership.customRole;
break; break;
} }
+20 -26
View File
@@ -1,13 +1,12 @@
import { Types } from "mongoose"; import { Types } from "mongoose";
import { AbilityBuilder, MongoAbility, RawRuleOf, createMongoAbility } from "@casl/ability"; import { AbilityBuilder, MongoAbility, RawRuleOf, createMongoAbility } from "@casl/ability";
import { import {
IMachineIdentity, IIdentity,
MachineMembershipOrg, IdentityMembershipOrg,
MembershipOrg MembershipOrg
} from "../../models"; } from "../../models";
import { ActorType, IRole, Role } from "../models"; import { ActorType, IRole, Role } from "../models";
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors"; import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
import { checkIPAgainstBlocklist } from "../../utils/ip";
import { ACCEPTED, ADMIN, CUSTOM, MEMBER, NO_ACCESS} from "../../variables"; import { ACCEPTED, ADMIN, CUSTOM, MEMBER, NO_ACCESS} from "../../variables";
import { conditionsMatcher } from "./ProjectRoleService"; import { conditionsMatcher } from "./ProjectRoleService";
import { AuthData } from "../../interfaces/middleware"; import { AuthData } from "../../interfaces/middleware";
@@ -28,7 +27,7 @@ export enum OrgPermissionSubjects {
Sso = "sso", Sso = "sso",
Billing = "billing", Billing = "billing",
SecretScanning = "secret-scanning", SecretScanning = "secret-scanning",
MachineIdentity = "machine-identity" Identity = "identity"
} }
export type OrgPermissionSet = export type OrgPermissionSet =
@@ -41,7 +40,7 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.Sso] | [OrgPermissionActions, OrgPermissionSubjects.Sso]
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning] | [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
| [OrgPermissionActions, OrgPermissionSubjects.Billing] | [OrgPermissionActions, OrgPermissionSubjects.Billing]
| [OrgPermissionActions, OrgPermissionSubjects.MachineIdentity]; | [OrgPermissionActions, OrgPermissionSubjects.Identity];
const buildAdminPermission = () => { const buildAdminPermission = () => {
const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility); const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
@@ -84,10 +83,10 @@ const buildAdminPermission = () => {
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing); can(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Billing); can(OrgPermissionActions.Delete, OrgPermissionSubjects.Billing);
can(OrgPermissionActions.Read, OrgPermissionSubjects.MachineIdentity); can(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
can(OrgPermissionActions.Create, OrgPermissionSubjects.MachineIdentity); can(OrgPermissionActions.Create, OrgPermissionSubjects.Identity);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.MachineIdentity); can(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.MachineIdentity); can(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity);
return build({ conditionsMatcher }); return build({ conditionsMatcher });
}; };
@@ -112,10 +111,10 @@ const buildMemberPermission = () => {
can(OrgPermissionActions.Edit, OrgPermissionSubjects.SecretScanning); can(OrgPermissionActions.Edit, OrgPermissionSubjects.SecretScanning);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.SecretScanning); can(OrgPermissionActions.Delete, OrgPermissionSubjects.SecretScanning);
can(OrgPermissionActions.Read, OrgPermissionSubjects.MachineIdentity); can(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
can(OrgPermissionActions.Create, OrgPermissionSubjects.MachineIdentity); can(OrgPermissionActions.Create, OrgPermissionSubjects.Identity);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.MachineIdentity); can(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.MachineIdentity); can(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity);
return build({ conditionsMatcher }); 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" message: "Failed to access organization-level resources with service token"
}); });
} }
case ActorType.MACHINE: { case ActorType.IDENTITY: {
const machineMembershipOrg = await MachineMembershipOrg.findOne({ const identityMembershipOrg = await IdentityMembershipOrg.findOne({
machineIdentity: authData.authPayload._id, identity: authData.authPayload._id,
organization: organizationId organization: organizationId
}) })
.populate<{ .populate<{
customRole: IRole & { permissions: RawRuleOf<MongoAbility<OrgPermissionSet>>[] }; customRole: IRole & { permissions: RawRuleOf<MongoAbility<OrgPermissionSet>>[] };
machineIdentity: IMachineIdentity identity: IIdentity
}>("customRole machineIdentity") }>("customRole identity")
.exec(); .exec();
if (!machineMembershipOrg || (machineMembershipOrg.role === "custom" && !machineMembershipOrg.customRole)) { if (!identityMembershipOrg || (identityMembershipOrg.role === "custom" && !identityMembershipOrg.customRole)) {
throw UnauthorizedRequestError(); throw UnauthorizedRequestError();
} }
checkIPAgainstBlocklist({
ipAddress: authData.ipAddress,
trustedIps: machineMembershipOrg.machineIdentity.accessTokenTrustedIps
});
role = machineMembershipOrg.role; role = identityMembershipOrg.role;
customRole = machineMembershipOrg.customRole; customRole = identityMembershipOrg.customRole;
break; break;
} }
default: default:
+6 -6
View File
@@ -4,13 +4,13 @@ import {
BotKey, BotKey,
BotOrg, BotOrg,
Folder, Folder,
Identity,
IdentityMembership,
IdentityMembershipOrg,
IncidentContactOrg, IncidentContactOrg,
Integration, Integration,
IntegrationAuth, IntegrationAuth,
Key, Key,
MachineIdentity,
MachineMembership,
MachineMembershipOrg,
Membership, Membership,
MembershipOrg, MembershipOrg,
Organization, Organization,
@@ -125,11 +125,11 @@ export const deleteOrganization = async ({
organization: organization._id organization: organization._id
}); });
await MachineIdentity.deleteMany({ await Identity.deleteMany({
organization: organization._id organization: organization._id
}); });
await MachineMembershipOrg.deleteMany({ await IdentityMembershipOrg.deleteMany({
organization: organization._id organization: organization._id
}); });
@@ -277,7 +277,7 @@ export const deleteOrganization = async ({
} }
}); });
await MachineMembership.deleteMany({ await IdentityMembership.deleteMany({
workspace: { workspace: {
$in: workspaceIds $in: workspaceIds
} }
+2 -2
View File
@@ -3,10 +3,10 @@ import {
Bot, Bot,
BotKey, BotKey,
Folder, Folder,
IdentityMembership,
Integration, Integration,
IntegrationAuth, IntegrationAuth,
Key, Key,
MachineMembership,
Membership, Membership,
Secret, Secret,
SecretBlindIndexData, SecretBlindIndexData,
@@ -178,7 +178,7 @@ export const deleteWorkspace = async ({
workspace: workspace._id workspace: workspace._id
}); });
await MachineMembership.deleteMany({ await IdentityMembership.deleteMany({
workspace: workspace._id workspace: workspace._id
}); });
+3 -3
View File
@@ -25,7 +25,7 @@ import {
secretSnapshot as eeSecretSnapshotRouter, secretSnapshot as eeSecretSnapshotRouter,
users as eeUsersRouter, users as eeUsersRouter,
workspace as eeWorkspaceRouter, workspace as eeWorkspaceRouter,
machineIdentities as v1MachineIdentitiesRouter, identities as v1IdentitiesRouter,
roles as v1RoleRouter, roles as v1RoleRouter,
secretApprovalPolicy as v1SecretApprovalPolicyRouter, secretApprovalPolicy as v1SecretApprovalPolicyRouter,
secretApprovalRequest as v1SecretApprovalRequestRouter, secretApprovalRequest as v1SecretApprovalRequestRouter,
@@ -198,7 +198,7 @@ const main = async () => {
} }
// (EE) routes // (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", eeSecretRouter);
app.use("/api/v1/secret-snapshot", eeSecretSnapshotRouter); app.use("/api/v1/secret-snapshot", eeSecretSnapshotRouter);
app.use("/api/v1/users", eeUsersRouter); app.use("/api/v1/users", eeUsersRouter);
@@ -212,7 +212,7 @@ const main = async () => {
// v1 routes // v1 routes
app.use("/api/v1/signup", v1SignupRouter); 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/admin", v1AdminRouter);
app.use("/api/v1/bot", v1BotRouter); app.use("/api/v1/bot", v1BotRouter);
app.use("/api/v1/user", v1UserRouter); app.use("/api/v1/user", v1UserRouter);
+6 -6
View File
@@ -1,6 +1,6 @@
import { Types } from "mongoose"; import { Types } from "mongoose";
import { IMachineIdentity, IServiceTokenData, IUser } from "../../models"; import { IIdentity, IServiceTokenData, IUser } from "../../models";
import { MachineActor, ServiceActor, UserActor, UserAgentType } from "../../ee/models"; import { IdentityActor, ServiceActor, UserActor, UserAgentType } from "../../ee/models";
interface BaseAuthData { interface BaseAuthData {
ipAddress: string; ipAddress: string;
@@ -14,9 +14,9 @@ export interface UserAuthData extends BaseAuthData {
authPayload: IUser; authPayload: IUser;
} }
export interface MachineIdentityAuthData extends BaseAuthData { export interface IdentityAuthData extends BaseAuthData {
actor: MachineActor; actor: IdentityActor;
authPayload: IMachineIdentity; authPayload: IIdentity;
} }
export interface ServiceTokenAuthData extends BaseAuthData { export interface ServiceTokenAuthData extends BaseAuthData {
@@ -24,4 +24,4 @@ export interface ServiceTokenAuthData extends BaseAuthData {
authPayload: IServiceTokenData; authPayload: IServiceTokenData;
} }
export type AuthData = UserAuthData | MachineIdentityAuthData | ServiceTokenAuthData; export type AuthData = UserAuthData | IdentityAuthData | ServiceTokenAuthData;
+1 -1
View File
@@ -50,7 +50,7 @@ const requireAuth = ({
case AuthMode.SERVICE_TOKEN: case AuthMode.SERVICE_TOKEN:
req.serviceTokenData = authData.authPayload; req.serviceTokenData = authData.authPayload;
break; break;
case AuthMode.MACHINE_ACCESS_TOKEN: case AuthMode.IDENTITY_ACCESS_TOKEN:
req.serviceTokenData = authData.authPayload; req.serviceTokenData = authData.authPayload;
break; break;
case AuthMode.API_KEY: case AuthMode.API_KEY:
+38
View File
@@ -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<IIdentity>("Identity", identitySchema);
+38 -7
View File
@@ -1,15 +1,18 @@
import { Document, Schema, Types, model } from "mongoose"; import { Document, Schema, Types, model } from "mongoose";
import { IIdentityTrustedIp } from "./identity";
import { IPType } from "../ee/models/trustedIp";
export interface IIdentityAccessToken extends Document { export interface IIdentityAccessToken extends Document {
_id: Types.ObjectId; _id: Types.ObjectId;
machineIdentity?: Types.ObjectId; identity: Types.ObjectId;
machineIdentityClientSecret?: Types.ObjectId; identityUniversalAuthClientSecret?: Types.ObjectId;
accessTokenLastUsedAt?: Date; accessTokenLastUsedAt?: Date;
accessTokenLastRenewedAt?: Date; accessTokenLastRenewedAt?: Date;
accessTokenNumUses: number; accessTokenNumUses: number;
accessTokenNumUsesLimit: number; accessTokenNumUsesLimit: number;
accessTokenTTL: number; accessTokenTTL: number;
accessTokenMaxTTL: number; accessTokenMaxTTL: number;
accessTokenTrustedIps: Array<IIdentityTrustedIp>;
isAccessTokenRevoked: boolean; isAccessTokenRevoked: boolean;
updatedAt: Date; updatedAt: Date;
createdAt: Date; createdAt: Date;
@@ -17,14 +20,14 @@ export interface IIdentityAccessToken extends Document {
const identityAccessTokenSchema = new Schema( const identityAccessTokenSchema = new Schema(
{ {
machineIdentity: { identity: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: "MachineIdentity", ref: "Identity",
required: false required: false
}, },
machineIdentityClientSecret: { identityUniversalAuthClientSecret: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: "MachineIdentityClientSecret", ref: "IdentityUniversalAuthClientSecret",
required: false required: false
}, },
accessTokenLastUsedAt: { accessTokenLastUsedAt: {
@@ -59,6 +62,34 @@ const identityAccessTokenSchema = new Schema(
default: 7200, default: 7200,
required: true 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: { isAccessTokenRevoked: {
type: Boolean, type: Boolean,
default: false, default: false,
@@ -70,4 +101,4 @@ const identityAccessTokenSchema = new Schema(
} }
); );
export const IdentityAccessToken = model<IIdentityAccessToken>("IdentityAccessToken", identityAccessTokenSchema); export const IdentityAccessToken = model<IIdentityAccessToken>("IdentityAccessToken", identityAccessTokenSchema);
@@ -1,19 +1,19 @@
import { Schema, Types, model } from "mongoose"; import { Schema, Types, model } from "mongoose";
import { ADMIN, CUSTOM, MEMBER, NO_ACCESS, VIEWER } from "../variables"; import { ADMIN, CUSTOM, MEMBER, NO_ACCESS, VIEWER } from "../variables";
export interface IMachineMembership { export interface IIdentityMembership {
_id: Types.ObjectId; _id: Types.ObjectId;
machineIdentity: Types.ObjectId; identity: Types.ObjectId;
workspace: Types.ObjectId; workspace: Types.ObjectId;
role: "admin" | "member" | "viewer" | "no-access" | "custom"; role: "admin" | "member" | "viewer" | "no-access" | "custom";
customRole: Types.ObjectId; customRole: Types.ObjectId;
} }
const machineMembershipSchema = new Schema<IMachineMembership>( const identityMembershipSchema = new Schema<IIdentityMembership>(
{ {
machineIdentity: { identity: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: "MachineIdentity" ref: "Identity"
}, },
workspace: { workspace: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
@@ -36,4 +36,4 @@ const machineMembershipSchema = new Schema<IMachineMembership>(
} }
); );
export const MachineMembership = model<IMachineMembership>("MachineMembership", machineMembershipSchema); export const IdentityMembership = model<IIdentityMembership>("IdentityMembership", identityMembershipSchema);
@@ -1,19 +1,19 @@
import { Schema, Types, model } from "mongoose"; import { Schema, Types, model } from "mongoose";
import { ADMIN, CUSTOM, MEMBER, NO_ACCESS} from "../variables"; import { ADMIN, CUSTOM, MEMBER, NO_ACCESS} from "../variables";
export interface IMachineMembershipOrg { export interface IIdentityMembershipOrg {
_id: Types.ObjectId; _id: Types.ObjectId;
machineIdentity: Types.ObjectId; identity: Types.ObjectId;
organization: Types.ObjectId; organization: Types.ObjectId;
role: "admin" | "member" | "no-access" | "custom"; role: "admin" | "member" | "no-access" | "custom";
customRole: Types.ObjectId; customRole: Types.ObjectId;
} }
const machineMembershipOrgSchema = new Schema<IMachineMembershipOrg>( const identityMembershipOrgSchema = new Schema<IIdentityMembershipOrg>(
{ {
machineIdentity: { identity: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: "MachineIdentity" ref: "Identity"
}, },
organization: { organization: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
@@ -34,4 +34,4 @@ const machineMembershipOrgSchema = new Schema<IMachineMembershipOrg>(
} }
); );
export const MachineMembershipOrg = model<IMachineMembershipOrg>("MachineMembershipOrg", machineMembershipOrgSchema); export const IdentityMembershipOrg = model<IIdentityMembershipOrg>("IdentityMembershipOrg", identityMembershipOrgSchema);
@@ -1,57 +1,29 @@
import { Document, Schema, Types, model } from "mongoose"; import { Document, Schema, Types, model } from "mongoose";
import { IPType } from "../ee/models"; import { IPType } from "../ee/models";
import { IIdentityTrustedIp } from "./identity";
export interface IMachineIdentityTrustedIp { export interface IIdentityUniversalAuth extends Document {
ipAddress: string;
type: IPType;
prefix: number;
}
export interface IMachineIdentity extends Document {
_id: Types.ObjectId; _id: Types.ObjectId;
identity: Types.ObjectId;
clientId: string; clientId: string;
name: string; clientSecretTrustedIps: Array<IIdentityTrustedIp>;
organization: Types.ObjectId;
accessTokenTTL: number; accessTokenTTL: number;
accessTokenMaxTTL: number; accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number; accessTokenNumUsesLimit: number;
clientSecretTrustedIps: Array<IMachineIdentityTrustedIp>; accessTokenTrustedIps: Array<IIdentityTrustedIp>;
accessTokenTrustedIps: Array<IMachineIdentityTrustedIp>;
} }
const machineIdentitySchema = new Schema( const identityUniversalAuthSchema = new Schema(
{ {
identity: {
type: Schema.Types.ObjectId,
ref: "Identity",
required: true
},
clientId: { clientId: {
type: String, type: String,
required: true 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: { clientSecretTrustedIps: {
type: [ type: [
{ {
@@ -80,6 +52,24 @@ const machineIdentitySchema = new Schema(
}], }],
required: true 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: { accessTokenTrustedIps: {
type: [ type: [
{ {
@@ -114,6 +104,4 @@ const machineIdentitySchema = new Schema(
} }
); );
machineIdentitySchema.index({ clientId: 1 }) export const IdentityUniversalAuth = model<IIdentityUniversalAuth>("IdentityUniversalAuth", identityUniversalAuthSchema);
export const MachineIdentity = model<IMachineIdentity>("MachineIdentity", machineIdentitySchema);
@@ -1,8 +1,9 @@
import { Document, Schema, Types, model } from "mongoose"; import { Document, Schema, Types, model } from "mongoose";
export interface IMachineIdentityClientSecret extends Document { export interface IIdentityUniversalAuthClientSecret extends Document {
_id: Types.ObjectId; _id: Types.ObjectId;
machineIdentity: Types.ObjectId; identity: Types.ObjectId;
identityUniversalAuth : Types.ObjectId;
description: string; description: string;
clientSecretPrefix: string; clientSecretPrefix: string;
clientSecretHash: string; clientSecretHash: string;
@@ -15,11 +16,16 @@ export interface IMachineIdentityClientSecret extends Document {
isClientSecretRevoked: boolean; isClientSecretRevoked: boolean;
} }
const machineIdentityClientSecretSchema = new Schema( const identityUniversalAuthClientSecretSchema = new Schema(
{ {
machineIdentity: { identity: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: "MachineIdentity", ref: "Identity",
required: true
},
identityUniversalAuth: {
type: Schema.Types.ObjectId,
ref: "IdentityUniversalAuth",
required: true required: true
}, },
description: { description: {
@@ -68,8 +74,8 @@ const machineIdentityClientSecretSchema = new Schema(
} }
); );
machineIdentityClientSecretSchema.index( identityUniversalAuthClientSecretSchema.index(
{ machineIdentity: 1, isClientSecretRevoked: 1 } { identityUniversalAuth: 1, isClientSecretRevoked: 1 }
) );
export const MachineIdentityClientSecret = model<IMachineIdentityClientSecret>("MachineIdentityClientSecret", machineIdentityClientSecretSchema); export const IdentityUniversalAuthClientSecret = model<IIdentityUniversalAuthClientSecret>("IdentityUniversalAuthClientSecret", identityUniversalAuthClientSecretSchema);
+8 -4
View File
@@ -20,11 +20,15 @@ export * from "./user";
export * from "./userAction"; export * from "./userAction";
export * from "./workspace"; export * from "./workspace";
export * from "./serviceTokenData"; // TODO: deprecate 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 "./identityAccessToken";
export * from "./machineMembershipOrg";
export * from "./machineMembership";
export * from "./apiKeyData"; // TODO: deprecate export * from "./apiKeyData"; // TODO: deprecate
export * from "./apiKeyDataV2"; export * from "./apiKeyDataV2";
export * from "./loginSRPDetail"; export * from "./loginSRPDetail";
+60
View File
@@ -48,4 +48,64 @@ router.delete(
authController.revokeAllSessions 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; export default router;
+2 -2
View File
@@ -55,11 +55,11 @@ router.delete(
); );
router.get( router.get(
"/:organizationId/machine-memberships", "/:organizationId/identity-memberships",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT] acceptedAuthModes: [AuthMode.JWT]
}), }),
organizationsController.getOrganizationMachineMemberships organizationsController.getOrganizationIdentityMemberships
); );
export default router; export default router;
+3 -3
View File
@@ -6,7 +6,7 @@ import {
import { AuthMode } from "../../variables"; import { AuthMode } from "../../variables";
import { serviceTokenDataController } from "../../controllers/v2"; import { serviceTokenDataController } from "../../controllers/v2";
router.get( // TODO: deprecate (moving to machine identity) router.get( // TODO: deprecate (moving to identity)
"/", "/",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.SERVICE_TOKEN] acceptedAuthModes: [AuthMode.SERVICE_TOKEN]
@@ -14,7 +14,7 @@ router.get( // TODO: deprecate (moving to machine identity)
serviceTokenDataController.getServiceTokenData serviceTokenDataController.getServiceTokenData
); );
router.post( // TODO: deprecate (moving to machine identity) router.post( // TODO: deprecate (moving to identity)
"/", "/",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT] acceptedAuthModes: [AuthMode.JWT]
@@ -22,7 +22,7 @@ router.post( // TODO: deprecate (moving to machine identity)
serviceTokenDataController.createServiceTokenData serviceTokenDataController.createServiceTokenData
); );
router.delete( // TODO: deprecate (moving to machine identity) router.delete( // TODO: deprecate (moving to identity)
"/:serviceTokenDataId", "/:serviceTokenDataId",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT] acceptedAuthModes: [AuthMode.JWT]
+9 -8
View File
@@ -94,35 +94,36 @@ router.patch(
); );
router.post( router.post(
"/:workspaceId/machine-memberships/:machineId", "/:workspaceId/identity-memberships/:identityId",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY]
}), }),
workspaceController.addMachineToWorkspace workspaceController.addIdentityToWorkspace
); );
router.patch( router.patch(
"/:workspaceId/machine-memberships/:machineId", "/:workspaceId/identity-memberships/:identityId",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY]
}), }),
workspaceController.updateMachineWorkspaceRole workspaceController.updateIdentityWorkspaceRole
); );
router.delete( router.delete(
"/:workspaceId/machine-memberships/:machineId", "/:workspaceId/identity-memberships/:identityId",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY] acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY]
}), }),
workspaceController.deleteMachineFromWorkspace workspaceController.deleteIdentityFromWorkspace
); );
router.get( router.get(
"/:workspaceId/machine-memberships", "/:workspaceId/identity-memberships",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT] acceptedAuthModes: [AuthMode.JWT]
}), }),
workspaceController.getWorkspaceMachineMemberships workspaceController.getWorkspaceIdentityMemberships
); );
export default router; export default router;
+10 -10
View File
@@ -7,7 +7,7 @@ import { AuthMode } from "../../variables";
router.get( router.get(
"/raw", "/raw",
requireAuth({ 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 secretsController.getSecretsRaw
); );
@@ -15,7 +15,7 @@ router.get(
router.get( router.get(
"/raw/:secretName", "/raw/:secretName",
requireAuth({ 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({ requireBlindIndicesEnabled({
locationWorkspaceId: "query" locationWorkspaceId: "query"
@@ -29,7 +29,7 @@ router.get(
router.post( router.post(
"/raw/:secretName", "/raw/:secretName",
requireAuth({ 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({ requireBlindIndicesEnabled({
locationWorkspaceId: "body" locationWorkspaceId: "body"
@@ -43,7 +43,7 @@ router.post(
router.patch( router.patch(
"/raw/:secretName", "/raw/:secretName",
requireAuth({ 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({ requireBlindIndicesEnabled({
locationWorkspaceId: "body" locationWorkspaceId: "body"
@@ -57,7 +57,7 @@ router.patch(
router.delete( router.delete(
"/raw/:secretName", "/raw/:secretName",
requireAuth({ 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({ requireBlindIndicesEnabled({
locationWorkspaceId: "body" locationWorkspaceId: "body"
@@ -71,7 +71,7 @@ router.delete(
router.get( router.get(
"/", "/",
requireAuth({ 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({ requireBlindIndicesEnabled({
locationWorkspaceId: "query" locationWorkspaceId: "query"
@@ -116,7 +116,7 @@ router.delete(
router.post( router.post(
"/:secretName", "/:secretName",
requireAuth({ 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({ requireBlindIndicesEnabled({
locationWorkspaceId: "body" locationWorkspaceId: "body"
@@ -127,7 +127,7 @@ router.post(
router.get( router.get(
"/:secretName", "/:secretName",
requireAuth({ 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({ requireBlindIndicesEnabled({
locationWorkspaceId: "query" locationWorkspaceId: "query"
@@ -138,7 +138,7 @@ router.get(
router.patch( router.patch(
"/:secretName", "/:secretName",
requireAuth({ 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({ requireBlindIndicesEnabled({
locationWorkspaceId: "body" locationWorkspaceId: "body"
@@ -149,7 +149,7 @@ router.patch(
router.delete( router.delete(
"/:secretName", "/:secretName",
requireAuth({ 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({ requireBlindIndicesEnabled({
locationWorkspaceId: "body" locationWorkspaceId: "body"
@@ -1,33 +1,33 @@
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { import { IIdentity, IdentityAccessToken } from "../../../models";
IMachineIdentity,
IdentityAccessToken,
} from "../../../models";
import { getAuthSecret } from "../../../config"; import { getAuthSecret } from "../../../config";
import { AuthTokenType } from "../../../variables"; import { AuthTokenType } from "../../../variables";
import { UnauthorizedRequestError } from "../../errors"; import { UnauthorizedRequestError } from "../../errors";
import { checkIPAgainstBlocklist } from "../../../utils/ip";
interface ValidateMachineIdentityParams { interface ValidateIdentityParams {
authTokenValue: string; authTokenValue: string;
ipAddress: string;
} }
export const validateMachineIdentity = async ({ export const validateIdentity = async ({
authTokenValue authTokenValue,
}: ValidateMachineIdentityParams) => { ipAddress
const decodedToken = <jwt.MachineAccessTokenJwtPayload>( }: ValidateIdentityParams) => {
const decodedToken = <jwt.IdentityAccessTokenJwtPayload>(
jwt.verify(authTokenValue, await getAuthSecret()) 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({ .findOne({
_id: decodedToken.identityAccessTokenId, _id: decodedToken.identityAccessTokenId,
isAccessTokenRevoked: false isAccessTokenRevoked: false
}) })
.populate<{ machineIdentity: IMachineIdentity }>("machineIdentity"); .populate<{ identity: IIdentity }>("identity");
if (!machineIdentityAccessToken || !machineIdentityAccessToken?.machineIdentity) throw UnauthorizedRequestError(); if (!identityAccessToken || !identityAccessToken?.identity) throw UnauthorizedRequestError();
const { const {
accessTokenNumUsesLimit, accessTokenNumUsesLimit,
@@ -36,7 +36,12 @@ export const validateMachineIdentity = async ({
accessTokenLastRenewedAt, accessTokenLastRenewedAt,
accessTokenMaxTTL, accessTokenMaxTTL,
createdAt: accessTokenCreatedAt createdAt: accessTokenCreatedAt
} = machineIdentityAccessToken; } = identityAccessToken;
checkIPAgainstBlocklist({
ipAddress,
trustedIps: identityAccessToken.accessTokenTrustedIps
});
// ttl check // ttl check
if (accessTokenTTL > 0) { if (accessTokenTTL > 0) {
@@ -48,7 +53,7 @@ export const validateMachineIdentity = async ({
const expirationDate = new Date(accessTokenRenewed.getTime() + ttlInMilliseconds); const expirationDate = new Date(accessTokenRenewed.getTime() + ttlInMilliseconds);
if (currentDate > expirationDate) throw UnauthorizedRequestError({ 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 { } else {
// access token has never been renewed // access token has never been renewed
@@ -57,7 +62,7 @@ export const validateMachineIdentity = async ({
const expirationDate = new Date(accessTokenCreated.getTime() + ttlInMilliseconds); const expirationDate = new Date(accessTokenCreated.getTime() + ttlInMilliseconds);
if (currentDate > expirationDate) throw UnauthorizedRequestError({ 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); const expirationDate = new Date(accessTokenCreated.getTime() + ttlInMilliseconds);
if (currentDate > expirationDate) throw UnauthorizedRequestError({ 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( await IdentityAccessToken.findByIdAndUpdate(
machineIdentityAccessToken._id, identityAccessToken._id,
{ {
accessTokenLastUsedAt: new Date(), accessTokenLastUsedAt: new Date(),
$inc: { accessTokenNumUses: 1 } $inc: { accessTokenNumUses: 1 }
@@ -95,5 +100,5 @@ export const validateMachineIdentity = async ({
} }
); );
return machineIdentityAccessToken.machineIdentity; return identityAccessToken.identity;
} }
@@ -2,4 +2,4 @@ export * from "./apiKey";
export * from "./apiKeyV2"; export * from "./apiKeyV2";
export * from "./jwt"; export * from "./jwt";
export * from "./serviceTokenV2"; export * from "./serviceTokenV2";
export * from "./machineIdentity"; export * from "./identity";
@@ -1,6 +1,6 @@
import { AuthData } from "../../../interfaces/middleware"; import { AuthData } from "../../../interfaces/middleware";
import { import {
MachineIdentity, Identity,
ServiceTokenData, ServiceTokenData,
User User
} from "../../../models"; } from "../../../models";
@@ -19,7 +19,7 @@ import {
return { serviceTokenDataId: authData.authPayload._id }; return { serviceTokenDataId: authData.authPayload._id };
} }
if (authData.authPayload instanceof MachineIdentity) { if (authData.authPayload instanceof Identity) {
return { serviceTokenDataId: authData.authPayload._id }; return { serviceTokenDataId: authData.authPayload._id };
} }
}; };
@@ -38,7 +38,7 @@ export const getAuthDataPayloadUserObj = (authData: AuthData) => {
return { user: authData.authPayload.user }; return { user: authData.authPayload.user };
} }
if (authData.authPayload instanceof MachineIdentity) { if (authData.authPayload instanceof Identity) {
return {}; return {};
} }
} }
+12 -11
View File
@@ -7,8 +7,8 @@ import { UnauthorizedRequestError } from "../../errors";
import { import {
validateAPIKey, validateAPIKey,
validateAPIKeyV2, validateAPIKeyV2,
validateIdentity,
validateJWT, validateJWT,
validateMachineIdentity,
validateServiceTokenV2 validateServiceTokenV2
} from "../authModeValidators"; } from "../authModeValidators";
import { getUserAgentType } from "../../posthog"; import { getUserAgentType } from "../../posthog";
@@ -36,7 +36,7 @@ interface GetAuthDataParams {
* - SERVICE_TOKEN * - SERVICE_TOKEN
* - API_KEY * - API_KEY
* - JWT * - JWT
* - MACHINE_ACCESS_TOKEN (from machine identity) * - IDENTITY_ACCESS_TOKEN (from identity)
* - API_KEY_V2 * - API_KEY_V2
* @param {Object} params * @param {Object} params
* @param {Object.<string, (string|string[]|undefined)>} params.headers - The HTTP request headers, usually from Express's `req.headers`. * @param {Object.<string, (string|string[]|undefined)>} 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 }; return { authMode: AuthMode.JWT, authTokenValue };
case AuthTokenType.API_KEY: case AuthTokenType.API_KEY:
return { authMode: AuthMode.API_KEY_V2, authTokenValue }; return { authMode: AuthMode.API_KEY_V2, authTokenValue };
case AuthTokenType.MACHINE_ACCESS_TOKEN: case AuthTokenType.IDENTITY_ACCESS_TOKEN:
return { authMode: AuthMode.MACHINE_ACCESS_TOKEN, authTokenValue }; return { authMode: AuthMode.IDENTITY_ACCESS_TOKEN, authTokenValue };
default: default:
throw UnauthorizedRequestError({ throw UnauthorizedRequestError({
message: "Failed to authenticate unknown authentication method" message: "Failed to authenticate unknown authentication method"
@@ -115,20 +115,21 @@ export const getAuthData = async ({
userAgentType userAgentType
} }
} }
case AuthMode.MACHINE_ACCESS_TOKEN: { case AuthMode.IDENTITY_ACCESS_TOKEN: {
const machineIdentity = await validateMachineIdentity({ const identity = await validateIdentity({
authTokenValue authTokenValue,
ipAddress
}); });
return { return {
actor: { actor: {
type: ActorType.MACHINE, type: ActorType.IDENTITY,
metadata: { metadata: {
machineId: machineIdentity._id.toString(), identityId: identity._id.toString(),
name: machineIdentity.name name: identity.name
} }
}, },
authPayload: machineIdentity, authPayload: identity,
ipAddress, ipAddress,
userAgent, userAgent,
userAgentType userAgentType
+93
View File
@@ -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({ export const VerifyMfaTokenV2 = z.object({
body: z.object({ body: z.object({
mfaToken: z.string().trim() mfaToken: z.string().trim()
+26
View File
@@ -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()
}),
});
+1 -1
View File
@@ -8,5 +8,5 @@ export * from "./membershipOrg";
export * from "./organization"; export * from "./organization";
export * from "./secrets"; export * from "./secrets";
export * from "./serviceTokenData"; export * from "./serviceTokenData";
export * from "./machineIdentity"; export * from "./identities";
export * from "./apiKeyDataV3"; export * from "./apiKeyDataV3";
+2 -2
View File
@@ -58,9 +58,9 @@ const validateClientForIntegrationAuth = async ({
throw UnauthorizedRequestError({ throw UnauthorizedRequestError({
message: "Failed service token authorization for integration authorization" message: "Failed service token authorization for integration authorization"
}); });
case ActorType.MACHINE: case ActorType.IDENTITY:
throw UnauthorizedRequestError({ throw UnauthorizedRequestError({
message: "Failed machine authorization for integration authorization" message: "Failed identity authorization for integration authorization"
}); });
} }
}; };
-97
View File
@@ -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()
}),
});
+6 -2
View File
@@ -46,9 +46,9 @@ export const validateClientForOrganization = async ({
throw UnauthorizedRequestError({ throw UnauthorizedRequestError({
message: "Failed service token authorization for organization" message: "Failed service token authorization for organization"
}); });
case ActorType.MACHINE: case ActorType.IDENTITY:
throw UnauthorizedRequestError({ 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({ export const GetOrgServiceMembersV2 = z.object({
params: z.object({ organizationId: z.string().trim() }) params: z.object({ organizationId: z.string().trim() })
});
export const GetOrgIdentityMembershipsV2 = z.object({
params: z.object({ organizationId: z.string().trim() })
}); });
+9 -9
View File
@@ -60,9 +60,9 @@ export const validateClientForWorkspace = async ({
requiredPermissions requiredPermissions
}); });
return { membership, workspace }; return { membership, workspace };
case ActorType.MACHINE: case ActorType.IDENTITY:
throw UnauthorizedRequestError({ 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({ params: z.object({
workspaceId: z.string().trim(), workspaceId: z.string().trim(),
machineId: z.string().trim() identityId: z.string().trim()
}), }),
body: z.object({ body: z.object({
role: z.string().trim().min(1).default(NO_ACCESS), role: z.string().trim().min(1).default(NO_ACCESS),
}) })
}); });
export const UpdateMachineWorkspaceRoleV2 = z.object({ export const UpdateIdentityWorkspaceRoleV2 = z.object({
params: z.object({ params: z.object({
workspaceId: z.string().trim(), workspaceId: z.string().trim(),
machineId: z.string().trim() identityId: z.string().trim()
}), }),
body: z.object({ body: z.object({
role: z.string().trim().min(1).default(NO_ACCESS), role: z.string().trim().min(1).default(NO_ACCESS),
}) })
}); });
export const DeleteMachineFromWorkspaceV2 = z.object({ export const DeleteIdentityFromWorkspaceV2 = z.object({
params: z.object({ params: z.object({
workspaceId: z.string().trim(), 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({ params: z.object({
workspaceId: z.string().trim() workspaceId: z.string().trim()
}), }),
+2 -3
View File
@@ -7,14 +7,13 @@ export enum AuthTokenType {
MFA_TOKEN = "mfaToken", // TODO: remove in favor of claim MFA_TOKEN = "mfaToken", // TODO: remove in favor of claim
PROVIDER_TOKEN = "providerToken", // TODO: remove in favor of claim PROVIDER_TOKEN = "providerToken", // TODO: remove in favor of claim
API_KEY = "apiKey", API_KEY = "apiKey",
MACHINE_ACCESS_TOKEN = "machineAccessToken", IDENTITY_ACCESS_TOKEN = "identityAccessToken",
MACHINE_REFRESH_TOKEN = "machineRefreshToken"
} }
export enum AuthMode { export enum AuthMode {
JWT = "jwt", JWT = "jwt",
SERVICE_TOKEN = "serviceToken", SERVICE_TOKEN = "serviceToken",
MACHINE_ACCESS_TOKEN = "machineAccessToken", IDENTITY_ACCESS_TOKEN = "identityAccessToken",
API_KEY = "apiKey", API_KEY = "apiKey",
API_KEY_V2 = "apiKeyV2" API_KEY_V2 = "apiKeyV2"
} }
@@ -16,7 +16,7 @@ export enum OrgPermissionSubjects {
Sso = "sso", Sso = "sso",
Billing = "billing", Billing = "billing",
SecretScanning = "secret-scanning", SecretScanning = "secret-scanning",
MachineIdentity = "machine-identity" Identity = "identity"
} }
export type OrgPermissionSet = export type OrgPermissionSet =
@@ -29,6 +29,6 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.Sso] | [OrgPermissionActions, OrgPermissionSubjects.Sso]
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning] | [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
| [OrgPermissionActions, OrgPermissionSubjects.Billing] | [OrgPermissionActions, OrgPermissionSubjects.Billing]
| [OrgPermissionActions, OrgPermissionSubjects.MachineIdentity]; | [OrgPermissionActions, OrgPermissionSubjects.Identity];
export type TOrgPermission = MongoAbility<OrgPermissionSet>; export type TOrgPermission = MongoAbility<OrgPermissionSet>;
@@ -23,7 +23,7 @@ export enum ProjectPermissionSub {
SecretRollback = "secret-rollback", SecretRollback = "secret-rollback",
SecretApproval = "secret-approval", SecretApproval = "secret-approval",
SecretRotation = "secret-rotation", SecretRotation = "secret-rotation",
MachineIdentity = "machine-identity" Identity = "identity"
} }
type SubjectFields = { type SubjectFields = {
@@ -45,7 +45,7 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions, ProjectPermissionSub.Environments] | [ProjectPermissionActions, ProjectPermissionSub.Environments]
| [ProjectPermissionActions, ProjectPermissionSub.IpAllowList] | [ProjectPermissionActions, ProjectPermissionSub.IpAllowList]
| [ProjectPermissionActions, ProjectPermissionSub.Settings] | [ProjectPermissionActions, ProjectPermissionSub.Settings]
| [ProjectPermissionActions, ProjectPermissionSub.MachineIdentity] | [ProjectPermissionActions, ProjectPermissionSub.Identity]
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens] | [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval] | [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation] | [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
+11 -3
View File
@@ -16,9 +16,17 @@ export const eventToNameMap: { [K in EventType]: string } = {
[EventType.DELETE_TRUSTED_IP]: "Delete trusted IP", [EventType.DELETE_TRUSTED_IP]: "Delete trusted IP",
[EventType.CREATE_SERVICE_TOKEN]: "Create service token", [EventType.CREATE_SERVICE_TOKEN]: "Create service token",
[EventType.DELETE_SERVICE_TOKEN]: "Delete service token", [EventType.DELETE_SERVICE_TOKEN]: "Delete service token",
[EventType.CREATE_MACHINE_IDENTITY]: "Create machine identity", [EventType.CREATE_IDENTITY]: "Create identity",
[EventType.UPDATE_MACHINE_IDENTITY]: "Update machine identity", [EventType.UPDATE_IDENTITY]: "Update identity",
[EventType.DELETE_MACHINE_IDENTITY]: "Delete machine 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.CREATE_ENVIRONMENT]: "Create environment",
[EventType.UPDATE_ENVIRONMENT]: "Update environment", [EventType.UPDATE_ENVIRONMENT]: "Update environment",
[EventType.DELETE_ENVIRONMENT]: "Delete environment", [EventType.DELETE_ENVIRONMENT]: "Delete environment",
+11 -4
View File
@@ -1,7 +1,7 @@
export enum ActorType { export enum ActorType {
USER = "user", USER = "user",
SERVICE = "service", SERVICE = "service",
MACHINE = "machine" IDENTITY = "identity"
} }
export enum UserAgentType { export enum UserAgentType {
@@ -27,9 +27,16 @@ export enum EventType {
DELETE_TRUSTED_IP = "delete-trusted-ip", DELETE_TRUSTED_IP = "delete-trusted-ip",
CREATE_SERVICE_TOKEN = "create-service-token", // v2 CREATE_SERVICE_TOKEN = "create-service-token", // v2
DELETE_SERVICE_TOKEN = "delete-service-token", // v2 DELETE_SERVICE_TOKEN = "delete-service-token", // v2
CREATE_MACHINE_IDENTITY = "create-machine-identity", CREATE_IDENTITY = "create-identity",
UPDATE_MACHINE_IDENTITY = "update-machine-identity", UPDATE_IDENTITY = "update-identity",
DELETE_MACHINE_IDENTITY = "delete-machine-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", CREATE_ENVIRONMENT = "create-environment",
UPDATE_ENVIRONMENT = "update-environment", UPDATE_ENVIRONMENT = "update-environment",
DELETE_ENVIRONMENT = "delete-environment", DELETE_ENVIRONMENT = "delete-environment",
+98 -33
View File
@@ -1,3 +1,4 @@
import { IdentityTrustedIp } from "../identities/types";
import { ActorType, EventType, UserAgentType } from "./enums"; import { ActorType, EventType, UserAgentType } from "./enums";
interface UserActorMetadata { interface UserActorMetadata {
@@ -10,8 +11,8 @@ interface ServiceActorMetadata {
name: string; name: string;
} }
interface MachineActorMetadata { interface IdentityActorMetadata {
machineId: string; identityId: string;
name: string; name: string;
} }
@@ -25,12 +26,12 @@ export interface ServiceActor {
metadata: ServiceActorMetadata; metadata: ServiceActorMetadata;
} }
export interface MachineActor { export interface IdentityActor {
type: ActorType.MACHINE; type: ActorType.IDENTITY;
metadata: MachineActorMetadata; metadata: IdentityActorMetadata;
} }
export type Actor = UserActor | ServiceActor | MachineActor; export type Actor = UserActor | ServiceActor | IdentityActor;
interface GetSecretsEvent { interface GetSecretsEvent {
type: EventType.GET_SECRETS; type: EventType.GET_SECRETS;
@@ -193,34 +194,91 @@ interface DeleteServiceTokenEvent {
}; };
} }
interface CreateMachineIdentityEvent { interface CreateIdentityEvent { // note: currently not logging org-role
type: EventType.CREATE_MACHINE_IDENTITY; type: EventType.CREATE_IDENTITY;
metadata: { metadata: {
name: string; identityId: string;
isActive: boolean; name: string;
role: string; };
expiresAt?: Date;
}
} }
interface UpdateMachineIdentityEvent { interface UpdateIdentityEvent {
type: EventType.UPDATE_MACHINE_IDENTITY; type: EventType.UPDATE_IDENTITY;
metadata: { metadata: {
name?: string; identityId: string;
isActive?: boolean; name?: string;
role?: string; };
expiresAt?: Date;
}
} }
interface DeleteMachineIdentityEvent { interface DeleteIdentityEvent {
type: EventType.DELETE_MACHINE_IDENTITY; type: EventType.DELETE_IDENTITY;
metadata: { metadata: {
name: string; identityId: string;
isActive: boolean; };
role?: string; }
expiresAt?: Date;
} 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<IdentityTrustedIp>;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: Array<IdentityTrustedIp>;
};
}
interface UpdateIdentityUniversalAuthEvent {
type: EventType.UPDATE_IDENTITY_UNIVERSAL_AUTH;
metadata: {
identityId: string;
clientSecretTrustedIps?: Array<IdentityTrustedIp>;
accessTokenTTL?: number;
accessTokenMaxTTL?: number;
accessTokenNumUsesLimit?: number;
accessTokenTrustedIps?: Array<IdentityTrustedIp>;
};
}
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 { interface CreateEnvironmentEvent {
@@ -419,9 +477,16 @@ export type Event =
| DeleteTrustedIPEvent | DeleteTrustedIPEvent
| CreateServiceTokenEvent | CreateServiceTokenEvent
| DeleteServiceTokenEvent | DeleteServiceTokenEvent
| CreateMachineIdentityEvent | CreateIdentityEvent
| UpdateMachineIdentityEvent | UpdateIdentityEvent
| DeleteMachineIdentityEvent | DeleteIdentityEvent
| LoginIdentityUniversalAuthEvent
| AddIdentityUniversalAuthEvent
| UpdateIdentityUniversalAuthEvent
| GetIdentityUniversalAuthEvent
| CreateIdentityUniversalAuthClientSecretEvent
| GetIdentityUniversalAuthClientSecretsEvent
| RevokeIdentityUniversalAuthClientSecretEvent
| CreateEnvironmentEvent | CreateEnvironmentEvent
| UpdateEnvironmentEvent | UpdateEnvironmentEvent
| DeleteEnvironmentEvent | DeleteEnvironmentEvent
@@ -0,0 +1,5 @@
import { IdentityAuthMethod } from "./enums";
export const identityAuthToNameMap: { [I in IdentityAuthMethod]: string } = {
[IdentityAuthMethod.UNIVERSAL_AUTH]: "Universal Auth"
};
@@ -0,0 +1,3 @@
export enum IdentityAuthMethod {
UNIVERSAL_AUTH = "universal-auth"
}
@@ -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";
@@ -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<Identity, {}, CreateIdentityDTO>({
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<Identity, {}, UpdateIdentityDTO>({
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<Identity, {}, DeleteIdentityDTO>({
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<IdentityUniversalAuth, {}, AddIdentityUniversalAuthDTO>({
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<IdentityUniversalAuth, {}, UpdateIdentityUniversalAuthDTO>({
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<CreateIdentityUniversalAuthClientSecretRes, {}, CreateIdentityUniversalAuthClientSecretDTO>({
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<Identity, {}, DeleteIdentityUniversalAuthClientSecretDTO>({
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));
}
});
};
@@ -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;
}
});
}
@@ -1,29 +1,108 @@
import { TRole } from "../roles/types"; import { TRole } from "../roles/types";
import { IdentityAuthMethod } from "./enums";
export type MachineTrustedIp = { export type IdentityTrustedIp = {
_id: string; _id: string;
ipAddress: string; ipAddress: string;
type: "ipv4" | "ipv6"; type: "ipv4" | "ipv6";
prefix?: number; prefix?: number;
} }
export type MachineIdentity = { export type Identity = {
_id: string; _id: string;
clientId: string;
name: string; name: string;
organization: string; authMethod?: IdentityAuthMethod;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
clientSecretTrustedIps: MachineTrustedIp[];
accessTokenTrustedIps: MachineTrustedIp[];
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
}; };
export type MachineIdentityClientSecret = { export type IdentityMembershipOrg = {
_id: string; _id: string;
machineIdentity: string; identity: Identity;
organization: string;
role: "admin" | "member" | "viewer" | "no-access" | "custom";
customRole?: TRole<string>;
createdAt: string;
updatedAt: string;
}
export type IdentityMembership = {
_id: string;
identity: Identity;
organization: string;
role: "admin" | "member" | "viewer" | "no-access" | "custom";
customRole?: TRole<string>;
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; description: string;
clientSecretPrefix: string; clientSecretPrefix: string;
clientSecretNumUses: number; clientSecretNumUses: number;
@@ -31,80 +110,14 @@ export type MachineIdentityClientSecret = {
clientSecretTTL: number; clientSecretTTL: number;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
isClientSecretRevoked: boolean;
} }
export type MachineMembershipOrg = { export type CreateIdentityUniversalAuthClientSecretRes = {
_id: string;
machineIdentity: MachineIdentity;
organization: string;
role: "admin" | "member" | "viewer" | "no-access" | "custom";
customRole?: TRole<string>;
createdAt: string;
updatedAt: string;
}
export type MachineMembership = {
_id: string;
machineIdentity: MachineIdentity;
organization: string;
role: "admin" | "member" | "viewer" | "no-access" | "custom";
customRole?: TRole<string>;
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 = {
clientSecret: string; clientSecret: string;
machineIdentity: string; clientSecretData: ClientSecretData;
isActive: boolean;
description: string;
clientSecretNumUses: number;
clientSecretNumUsesLimit: number;
expiresAt?: Date;
} }
export type CreateMachineIdentityRes = { export type DeleteIdentityUniversalAuthClientSecretDTO = {
machineIdentity: MachineIdentity; identityId: string;
} clientSecretId: string;
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;
} }
+1 -1
View File
@@ -3,11 +3,11 @@ export * from "./apiKeys";
export * from "./auditLogs"; export * from "./auditLogs";
export * from "./auth"; export * from "./auth";
export * from "./bots"; export * from "./bots";
export * from "./identities";
export * from "./incidentContacts"; export * from "./incidentContacts";
export * from "./integrationAuth"; export * from "./integrationAuth";
export * from "./integrations"; export * from "./integrations";
export * from "./keys"; export * from "./keys";
export * from "./machineIdentities";
export * from "./organization"; export * from "./organization";
export * from "./roles"; export * from "./roles";
export * from "./secretApproval"; export * from "./secretApproval";
@@ -1,9 +0,0 @@
export {
useCreateMachineIdentity,
useCreateMachineIdentityClientSecret,
useDeleteMachineIdentity,
useDeleteMachineIdentityClientSecret,
useUpdateMachineIdentity} from "./mutations";
export {
useGetMachineIdentityClientSecrets
} from "./queries";
@@ -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<CreateMachineIdentityRes, {}, CreateMachineIdentityDTO>({
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<CreateMachineIdentityClientSecretRes, {}, CreateMachineIdentityClientSecretDTO>({
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<MachineIdentity, {}, UpdateMachineIdentityDTO>({
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<MachineIdentity, {}, DeleteMachineIdentityDTO>({
mutationFn: async ({
machineId
}) => {
const { data: { machineIdentity } } = await apiRequest.delete(`/api/v1/machine-identities/${machineId}`);
return machineIdentity;
},
onSuccess: ({ organization }) => {
queryClient.invalidateQueries(organizationKeys.getOrgServiceMemberships(organization));
}
});
};
@@ -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;
}
});
}
+3 -2
View File
@@ -6,7 +6,7 @@ export {
useDeleteOrgById, useDeleteOrgById,
useDeleteOrgPmtMethod, useDeleteOrgPmtMethod,
useDeleteOrgTaxId, useDeleteOrgTaxId,
useGetMachineMembershipOrgs, useGetIdentityMembershipOrgs,
useGetOrganizations, useGetOrganizations,
useGetOrgBillingDetails, useGetOrgBillingDetails,
useGetOrgInvoices, useGetOrgInvoices,
@@ -18,4 +18,5 @@ export {
useGetOrgTaxIds, useGetOrgTaxIds,
useGetOrgTrialUrl, useGetOrgTrialUrl,
useRenameOrg, useRenameOrg,
useUpdateOrgBillingDetails} from "./queries"; useUpdateOrgBillingDetails
} from "./queries";
@@ -2,7 +2,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { MachineMembershipOrg } from "../machineIdentities/types"; import { IdentityMembershipOrg } from "../identities/types";
import { import {
BillingDetails, BillingDetails,
Invoice, Invoice,
@@ -27,7 +27,7 @@ export const organizationKeys = {
getOrgTaxIds: (orgId: string) => [{ orgId }, "organization-tax-ids"] as const, getOrgTaxIds: (orgId: string) => [{ orgId }, "organization-tax-ids"] as const,
getOrgInvoices: (orgId: string) => [{ orgId }, "organization-invoices"] as const, getOrgInvoices: (orgId: string) => [{ orgId }, "organization-invoices"] as const,
getOrgLicenses: (orgId: string) => [{ orgId }, "organization-licenses"] 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 () => { 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({ return useQuery({
queryKey: organizationKeys.getOrgServiceMemberships(organizationId), queryKey: organizationKeys.getOrgIdentityMemberships(organizationId),
queryFn: async () => { queryFn: async () => {
const { const {
data: { machineMemberships } data: { identityMemberships }
} = await apiRequest.get<{ machineMemberships: MachineMembershipOrg[] }>( } = await apiRequest.get<{ identityMemberships: IdentityMembershipOrg[] }>(
`/api/v2/organizations/${organizationId}/machine-memberships` `/api/v2/organizations/${organizationId}/identity-memberships`
); );
return machineMemberships; return identityMemberships;
}, },
enabled: true enabled: true
}); });
+4 -4
View File
@@ -1,9 +1,9 @@
export { export {
useAddMachineToWorkspace, useAddIdentityToWorkspace,
useAddUserToWorkspace, useAddUserToWorkspace,
useCreateWorkspace, useCreateWorkspace,
useCreateWsEnvironment, useCreateWsEnvironment,
useDeleteMachineFromWorkspace, useDeleteIdentityFromWorkspace,
useDeleteUserFromWorkspace, useDeleteUserFromWorkspace,
useDeleteWorkspace, useDeleteWorkspace,
useDeleteWsEnvironment, useDeleteWsEnvironment,
@@ -11,15 +11,15 @@ export {
useGetUserWorkspaces, useGetUserWorkspaces,
useGetWorkspaceAuthorizations, useGetWorkspaceAuthorizations,
useGetWorkspaceById, useGetWorkspaceById,
useGetWorkspaceIdentityMemberships,
useGetWorkspaceIndexStatus, useGetWorkspaceIndexStatus,
useGetWorkspaceIntegrations, useGetWorkspaceIntegrations,
useGetWorkspaceMachineMemberships,
useGetWorkspaceSecrets, useGetWorkspaceSecrets,
useGetWorkspaceUsers, useGetWorkspaceUsers,
useNameWorkspaceSecrets, useNameWorkspaceSecrets,
useRenameWorkspace, useRenameWorkspace,
useReorderWsEnvironment, useReorderWsEnvironment,
useToggleAutoCapitalization, useToggleAutoCapitalization,
useUpdateMachineWorkspaceRole, useUpdateIdentityWorkspaceRole,
useUpdateUserWorkspaceRole, useUpdateUserWorkspaceRole,
useUpdateWsEnvironment} from "./queries"; useUpdateWsEnvironment} from "./queries";
+31 -36
View File
@@ -2,9 +2,9 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { IdentityMembership } from "../identities/types";
import { IntegrationAuth } from "../integrationAuth/types"; import { IntegrationAuth } from "../integrationAuth/types";
import { TIntegration } from "../integrations/types"; import { TIntegration } from "../integrations/types";
import { MachineMembership } from "../machineIdentities/types";
import { EncryptedSecret } from "../secrets/types"; import { EncryptedSecret } from "../secrets/types";
import { TWorkspaceUser } from "../users/types"; import { TWorkspaceUser } from "../users/types";
import { import {
@@ -31,7 +31,7 @@ export const workspaceKeys = {
getAllUserWorkspace: ["workspaces"] as const, getAllUserWorkspace: ["workspaces"] as const,
getWorkspaceAuditLogs: (workspaceId: string) => [{ workspaceId }] as const, getWorkspaceAuditLogs: (workspaceId: string) => [{ workspaceId }] as const,
getWorkspaceUsers: (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) => { const fetchWorkspaceById = async (workspaceId: string) => {
@@ -356,97 +356,92 @@ export const useUpdateUserWorkspaceRole = () => {
}); });
}; };
export const useAddMachineToWorkspace = () => { export const useAddIdentityToWorkspace = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: async ({ mutationFn: async ({
machineId, identityId,
workspaceId, workspaceId,
role role
}: { }: {
machineId: string; identityId: string;
workspaceId: string; workspaceId: string;
role?: string; role?: string;
}) => { }) => {
const { const {
data: { serviceMembership } data: { identityMembership }
} = await apiRequest.post(`/api/v2/workspace/${workspaceId}/machine-memberships/${machineId}`, { } = await apiRequest.post(`/api/v2/workspace/${workspaceId}/identity-memberships/${identityId}`, {
role role
}); });
return serviceMembership; return identityMembership;
}, },
onSuccess: (_, { workspaceId }) => { onSuccess: (_, { workspaceId }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceMachineMemberships(workspaceId)); queryClient.invalidateQueries(workspaceKeys.getWorkspaceIdentityMemberships(workspaceId));
} }
}); });
}; };
export const useUpdateMachineWorkspaceRole = () => { export const useUpdateIdentityWorkspaceRole = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: async ({ mutationFn: async ({
machineId, identityId,
workspaceId, workspaceId,
role role
}: { }: {
machineId: string; identityId: string;
workspaceId: string; workspaceId: string;
role?: string; role?: string;
}) => { }) => {
const { const {
data: { serviceMembership } data: { identityMembership }
} = await apiRequest.patch(`/api/v2/workspace/${workspaceId}/machine-memberships/${machineId}`, { } = await apiRequest.patch(`/api/v2/workspace/${workspaceId}/identity-memberships/${identityId}`, {
role role
}); });
return serviceMembership; return identityMembership;
}, },
onSuccess: (_, { workspaceId }) => { onSuccess: (_, { workspaceId }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceMachineMemberships(workspaceId)); queryClient.invalidateQueries(workspaceKeys.getWorkspaceIdentityMemberships(workspaceId));
} }
}); });
}; };
export const useDeleteMachineFromWorkspace = () => { export const useDeleteIdentityFromWorkspace = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: async ({ mutationFn: async ({
machineId, identityId,
workspaceId, workspaceId,
}: { }: {
machineId: string; identityId: string;
workspaceId: string; workspaceId: string;
}) => { }) => {
const { const {
data: { serviceMembership } data: { identityMembership }
} = await apiRequest.delete(`/api/v2/workspace/${workspaceId}/machine-memberships/${machineId}`); } = await apiRequest.delete(`/api/v2/workspace/${workspaceId}/identity-memberships/${identityId}`);
return identityMembership;
return serviceMembership;
}, },
onSuccess: (_, { workspaceId }) => { onSuccess: (_, { workspaceId }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceMachineMemberships(workspaceId)); queryClient.invalidateQueries(workspaceKeys.getWorkspaceIdentityMemberships(workspaceId));
} }
}); });
}; };
export const useGetWorkspaceIdentityMemberships = (workspaceId: string) => {
export const useGetWorkspaceMachineMemberships = (workspaceId: string) => {
return useQuery({ return useQuery({
queryKey: workspaceKeys.getWorkspaceMachineMemberships(workspaceId), queryKey: workspaceKeys.getWorkspaceIdentityMemberships(workspaceId),
queryFn: async () => { queryFn: async () => {
const { const {
data: { machineMemberships } data: { identityMemberships }
} = await apiRequest.get<{ machineMemberships: MachineMembership[] }>( } = await apiRequest.get<{ identityMemberships: IdentityMembership[] }>(
`/api/v2/workspace/${workspaceId}/machine-memberships` `/api/v2/workspace/${workspaceId}/identity-memberships`
); );
return identityMemberships;
return machineMemberships;
}, },
enabled: true enabled: true
}); });
}; };
@@ -4,14 +4,15 @@ import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { withPermission } from "@app/hoc"; import { withPermission } from "@app/hoc";
import { import {
OrgMachineIdentityTab, OrgIdentityTab,
OrgMembersTab, OrgMembersTab,
OrgRoleTabSection} from "./components"; OrgRoleTabSection
} from "./components";
enum TabSections { enum TabSections {
Member = "members", Member = "members",
Roles = "roles", Roles = "roles",
MachineIdentities = "machine-identities" Identities = "identities"
} }
export const MembersPage = withPermission( export const MembersPage = withPermission(
@@ -25,7 +26,7 @@ export const MembersPage = withPermission(
<Tabs defaultValue={TabSections.Member}> <Tabs defaultValue={TabSections.Member}>
<TabList> <TabList>
<Tab value={TabSections.Member}>People</Tab> <Tab value={TabSections.Member}>People</Tab>
<Tab value={TabSections.MachineIdentities}> <Tab value={TabSections.Identities}>
<div className="flex items-center"> <div className="flex items-center">
<p>Machine Identities</p> <p>Machine Identities</p>
<div className="ml-2 rounded-md text-yellow text-sm inline-block bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] opacity-80 hover:opacity-100 cursor-default"> <div className="ml-2 rounded-md text-yellow text-sm inline-block bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] opacity-80 hover:opacity-100 cursor-default">
@@ -38,8 +39,8 @@ export const MembersPage = withPermission(
<TabPanel value={TabSections.Member}> <TabPanel value={TabSections.Member}>
<OrgMembersTab /> <OrgMembersTab />
</TabPanel> </TabPanel>
<TabPanel value={TabSections.MachineIdentities}> <TabPanel value={TabSections.Identities}>
<OrgMachineIdentityTab /> <OrgIdentityTab />
</TabPanel> </TabPanel>
<TabPanel value={TabSections.Roles}> <TabPanel value={TabSections.Roles}>
<OrgRoleTabSection /> <OrgRoleTabSection />
@@ -1,8 +1,10 @@
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { MachineIdentitySection } from "./components"; import {
IdentitySection
} from "./components";
export const OrgMachineIdentityTab = () => { export const OrgIdentityTab = () => {
return ( return (
<motion.div <motion.div
key="panel-service-token" key="panel-service-token"
@@ -11,7 +13,7 @@ export const OrgMachineIdentityTab = () => {
animate={{ opacity: 1, translateX: 0 }} animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }} exit={{ opacity: 0, translateX: 30 }}
> >
<MachineIdentitySection /> <IdentitySection />
</motion.div> </motion.div>
); );
} }
@@ -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<typeof schema>;
export const IdentityAuthMethodModal = ({
popUp,
handlePopUpOpen,
handlePopUpToggle
}: Props) => {
const {
control,
// watch,
} = useForm<FormData>({
resolver: yupResolver(schema)
});
const identityAuthMethodData = popUp?.identityAuthMethod?.data as {
identityId: string;
name: string;
authMethod?: IdentityAuthMethod;
}
// const authMethod = watch("authMethod");
const renderIdentityAuthForm = () => {
return (
<IdentityUniversalAuthForm
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
identityAuthMethodData={identityAuthMethodData}
/>
);
}
return (
<Modal
isOpen={popUp?.identityAuthMethod?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("identityAuthMethod", isOpen);
}}
>
<ModalContent title={`${identityAuthMethodData?.authMethod ? "Update" : "Configure"} Identity Auth Method for ${identityAuthMethodData?.name ?? ""}`}>
<Controller
control={control}
name="authMethod"
defaultValue={IdentityAuthMethod.UNIVERSAL_AUTH}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Auth Method"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{identityAuthMethods.map(({ label, value }) => (
<SelectItem value={String(value || "")} key={label}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
{renderIdentityAuthForm()}
<UpgradePlanModal
isOpen={popUp?.upgradePlan?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can use IP allowlisting if you switch to Infisical's Pro plan."
/>
</ModalContent>
</Modal>
);
}
@@ -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<typeof schema>;
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<FormData>({
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 (
<Modal
isOpen={popUp?.identity?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("identity", isOpen);
reset();
}}
>
<ModalContent title={`${popUp?.identity?.data ? "Update" : "Create"} Identity`}>
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
defaultValue=""
name="name"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Name"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
placeholder="Machine 1"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="role"
defaultValue=""
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label={`${popUp?.identity?.data ? "Update" : ""} Role`}
errorText={error?.message}
isError={Boolean(error)}
className="mt-4"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{(roles || []).map(({ name, slug }) => (
<SelectItem value={slug} key={`st-role-${slug}`}>
{name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{popUp?.identity?.data ? "Update" : "Create"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identity", false)}
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
);
}
@@ -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 (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex justify-between mb-8">
<p className="text-xl font-semibold text-mineshaft-100">
Identities
</p>
<OrgPermissionCan
I={OrgPermissionActions.Create}
a={OrgPermissionSubjects.Identity}
>
{(isAllowed) => (
<Button
colorSchema="secondary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("identity")}
isDisabled={!isAllowed}
>
Create identity
</Button>
)}
</OrgPermissionCan>
</div>
<IdentityTable handlePopUpOpen={handlePopUpOpen} />
<IdentityModal
popUp={popUp}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
<IdentityAuthMethodModal
popUp={popUp}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
<IdentityUniversalAuthClientSecretModal
popUp={popUp}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
<DeleteActionModal
isOpen={popUp.deleteIdentity.isOpen}
title={`Are you sure want to delete ${
(popUp?.deleteIdentity?.data as { name: string })?.name || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteIdentity", isOpen)}
deleteKey="confirm"
onDeleteApproved={() =>
onDeleteIdentitySubmit(
(popUp?.deleteIdentity?.data as { identityId: string })?.identityId
)
}
/>
</div>
);
},
{ action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.Identity }
);
@@ -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 (
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Name</Th>
<Th>Role</Th>
<Th>Auth Method</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={4} innerKey="org-identities" />}
{!isLoading &&
data &&
data.length > 0 &&
data.map(({
identity: {
_id,
name,
authMethod
},
role,
customRole
}) => {
return (
<Tr className="h-10" key={`identity-${_id}`}>
<Td>{name}</Td>
<Td>
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Identity}
>
{(isAllowed) => {
return (
<Select
value={
role === "custom" ? (customRole?.slug as string) : role
}
isDisabled={!isAllowed}
className="w-40 bg-mineshaft-600"
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
onValueChange={(selectedRole) =>
handleChangeRole({
identityId: _id,
role: selectedRole
})
}
>
{(roles || [])
.map(({ slug, name: roleName }) => (
<SelectItem value={slug} key={`owner-option-${slug}`}>
{roleName}
</SelectItem>
))}
</Select>
);
}}
</OrgPermissionCan>
</Td>
<Td>{authMethod ? identityAuthToNameMap[authMethod] : "Not configured"}</Td>
<Td>
<div className="flex justify-end items-center">
{authMethod === IdentityAuthMethod.UNIVERSAL_AUTH && (
<Tooltip content="Manage client ID/secrets">
<IconButton
onClick={async () => {
handlePopUpOpen("universalAuthClientSecret", {
identityId: _id,
name
});
}}
size="lg"
colorSchema="primary"
variant="plain"
ariaLabel="update"
// isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faKey} />
</IconButton>
</Tooltip>
)}
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Identity}
>
{(isAllowed) => (
<Tooltip content="Manage auth method">
<IconButton
onClick={async () => {
handlePopUpOpen("identityAuthMethod", {
identityId: _id,
name,
authMethod
});
}}
size="lg"
colorSchema="primary"
variant="plain"
ariaLabel="update"
className="ml-4"
isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faLock} />
</IconButton>
</Tooltip>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Identity}
>
{(isAllowed) => (
<IconButton
onClick={async () => {
handlePopUpOpen("identity", {
identityId: _id,
name,
role,
customRole
});
}}
size="lg"
colorSchema="primary"
variant="plain"
ariaLabel="update"
className="ml-4"
isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faPencil} />
</IconButton>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.Identity}
>
{(isAllowed) => (
<IconButton
onClick={() => {
handlePopUpOpen("deleteIdentity", {
identityId: _id,
name
});
}}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
className="ml-4"
isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
)}
</OrgPermissionCan>
</div>
</Td>
</Tr>
);
})}
{!isLoading && data && data?.length === 0 && (
<Tr>
<Td colSpan={4}>
<EmptyState title="No identities have been created in this organization" icon={faServer} />
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
);
}
@@ -28,9 +28,10 @@ import {
} from "@app/components/v2"; } from "@app/components/v2";
import { useToggle } from "@app/hooks"; import { useToggle } from "@app/hooks";
import { import {
useCreateMachineIdentityClientSecret, useCreateIdentityUniversalAuthClientSecret,
useDeleteMachineIdentityClientSecret, useDeleteIdentityUniversalAuthClientSecret,
useGetMachineIdentityClientSecrets} from "@app/hooks/api"; useGetIdentityUniversalAuth,
useGetIdentityUniversalAuthClientSecrets} from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = yup.object({ const schema = yup.object({
@@ -42,18 +43,18 @@ const schema = yup.object({
export type FormData = yup.InferType<typeof schema>; export type FormData = yup.InferType<typeof schema>;
type Props = { type Props = {
popUp: UsePopUpState<["clientSecret", "deleteClientSecret"]>; popUp: UsePopUpState<["universalAuthClientSecret", "deleteUniversalAuthClientSecret"]>;
handlePopUpOpen: ( handlePopUpOpen: (
popUpName: keyof UsePopUpState<["deleteClientSecret"]>, popUpName: keyof UsePopUpState<["deleteUniversalAuthClientSecret"]>,
data?: { data?: {
clientSecretPrefix: string; clientSecretPrefix: string;
clientSecretId: string; clientSecretId: string;
} }
) => void; ) => 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, popUp,
handlePopUpOpen, handlePopUpOpen,
handlePopUpToggle handlePopUpToggle
@@ -63,15 +64,16 @@ export const CreateClientSecretModal = ({
const [token, setToken] = useState(""); const [token, setToken] = useState("");
const [isTokenCopied, setIsTokenCopied] = useToggle(false); const [isTokenCopied, setIsTokenCopied] = useToggle(false);
const popUpData = (popUp?.clientSecret?.data as { const popUpData = (popUp?.universalAuthClientSecret?.data as {
machineId?: string; identityId?: string;
name?: string; name?: string;
}); });
const { data, isLoading } = useGetMachineIdentityClientSecrets(popUpData?.machineId ?? ""); const { data, isLoading } = useGetIdentityUniversalAuthClientSecrets(popUpData?.identityId ?? "");
const { data: identityUniversalAuth } = useGetIdentityUniversalAuth(popUpData?.identityId ?? "");
const { mutateAsync: createClientSecretMutateAsync } = useCreateMachineIdentityClientSecret();
const { mutateAsync: deleteClientSecretMutateAsync } = useDeleteMachineIdentityClientSecret(); const { mutateAsync: createClientSecretMutateAsync } = useCreateIdentityUniversalAuthClientSecret();
const { mutateAsync: deleteClientSecretMutateAsync } = useDeleteIdentityUniversalAuthClientSecret();
const { const {
control, control,
@@ -108,10 +110,10 @@ export const CreateClientSecretModal = ({
}: FormData) => { }: FormData) => {
try { try {
if (!popUpData?.machineId) return; if (!popUpData?.identityId) return;
const { clientSecret } = await createClientSecretMutateAsync({ const { clientSecret } = await createClientSecretMutateAsync({
machineId: popUpData.machineId, identityId: popUpData.identityId,
description, description,
ttl: Number(ttl), ttl: Number(ttl),
numUsesLimit: Number(numUsesLimit) numUsesLimit: Number(numUsesLimit)
@@ -141,19 +143,19 @@ export const CreateClientSecretModal = ({
}) => { }) => {
try { try {
if (!popUpData?.machineId) return; if (!popUpData?.identityId) return;
await deleteClientSecretMutateAsync({ await deleteClientSecretMutateAsync({
machineId: popUpData.machineId, identityId: popUpData.identityId,
clientSecretId clientSecretId
}); });
if (token.startsWith(clientSecretPrefix)) { if (token.startsWith(clientSecretPrefix)) {
reset(); reset();
setToken(""); setToken("");
} }
handlePopUpToggle("deleteClientSecret", false); handlePopUpToggle("deleteUniversalAuthClientSecret", false);
createNotification({ createNotification({
text: "Successfully deleted client secret", text: "Successfully deleted client secret",
@@ -173,14 +175,29 @@ export const CreateClientSecretModal = ({
return ( return (
<Modal <Modal
isOpen={popUp?.clientSecret?.isOpen} isOpen={popUp?.universalAuthClientSecret?.isOpen}
onOpenChange={(isOpen) => { onOpenChange={(isOpen) => {
handlePopUpToggle("clientSecret", isOpen); handlePopUpToggle("universalAuthClientSecret", isOpen);
reset(); reset();
setToken(""); setToken("");
}} }}
> >
<ModalContent title={`Manage Client Secrets for ${popUpData?.name ?? ""}`}> <ModalContent title={`Manage Client ID/Secrets for ${popUpData?.name ?? ""}`}>
<h2 className="mb-4">Client ID</h2>
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 break-all">{identityUniversalAuth?.clientId ?? ""}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => navigator.clipboard.writeText(identityUniversalAuth?.clientId ?? "")}
>
<FontAwesomeIcon icon={isTokenCopied ? faCheck : faCopy} />
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
{t("common.click-to-copy")}
</span>
</IconButton>
</div>
<h2 className="mb-4">New Client Secret</h2> <h2 className="mb-4">New Client Secret</h2>
{hasToken ? ( {hasToken ? (
<div> <div>
@@ -306,7 +323,7 @@ export const CreateClientSecretModal = ({
</Tr> </Tr>
</THead> </THead>
<TBody> <TBody>
{isLoading && <TableSkeleton columns={5} innerKey="org-machine-identities-client-secrets" />} {isLoading && <TableSkeleton columns={5} innerKey="org-identities-client-secrets" />}
{!isLoading && {!isLoading &&
data && data &&
data.length > 0 && data.length > 0 &&
@@ -333,7 +350,7 @@ export const CreateClientSecretModal = ({
<Td> <Td>
<IconButton <IconButton
onClick={() => { onClick={() => {
handlePopUpOpen("deleteClientSecret", { handlePopUpOpen("deleteUniversalAuthClientSecret", {
clientSecretPrefix, clientSecretPrefix,
clientSecretId: _id clientSecretId: _id
}); });
@@ -352,7 +369,7 @@ export const CreateClientSecretModal = ({
{!isLoading && data && data?.length === 0 && ( {!isLoading && data && data?.length === 0 && (
<Tr> <Tr>
<Td colSpan={5}> <Td colSpan={5}>
<EmptyState title="No client secrets have been created for this machine identity yet" icon={faKey} /> <EmptyState title="No client secrets have been created for this identity yet" icon={faKey} />
</Td> </Td>
</Tr> </Tr>
)} )}
@@ -360,14 +377,14 @@ export const CreateClientSecretModal = ({
</Table> </Table>
</TableContainer> </TableContainer>
<DeleteActionModal <DeleteActionModal
isOpen={popUp.deleteClientSecret.isOpen} isOpen={popUp.deleteUniversalAuthClientSecret.isOpen}
title={`Are you sure want to delete the client secret ${ title={`Are you sure want to delete the client secret ${
(popUp?.deleteClientSecret?.data as { clientSecretPrefix: string })?.clientSecretPrefix || "" (popUp?.deleteUniversalAuthClientSecret?.data as { clientSecretPrefix: string })?.clientSecretPrefix || ""
}************?`} }************?`}
onChange={(isOpen) => handlePopUpToggle("deleteClientSecret", isOpen)} onChange={(isOpen) => handlePopUpToggle("deleteUniversalAuthClientSecret", isOpen)}
deleteKey="confirm" deleteKey="confirm"
onDeleteApproved={() => { onDeleteApproved={() => {
const deleteClientSecretData = (popUp?.deleteClientSecret?.data as { const deleteClientSecretData = (popUp?.deleteUniversalAuthClientSecret?.data as {
clientSecretId: string; clientSecretId: string;
clientSecretPrefix: string; clientSecretPrefix: string;
}); });
@@ -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<typeof schema>;
type Props = {
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["identityAuthMethod"]>, state?: boolean) => void;
identityAuthMethodData: {
identityId: string;
name: string;
authMethod?: IdentityAuthMethod;
}
}
export const 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<FormData>({
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 (
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
defaultValue="7200"
name="accessTokenTTL"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token TTL (seconds)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
placeholder="7200"
type="number"
min="0"
step="1"
/>
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="7200"
name="accessTokenMaxTTL"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token Max TTL (seconds)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
placeholder="7200"
type="number"
min="0"
step="1"
/>
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="0"
name="accessTokenNumUsesLimit"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token Max Number of Uses"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
placeholder="0"
type="number"
min="0"
step="1"
/>
</FormControl>
)}
/>
{clientSecretTrustedIpsFields.map(({ id }, index) => (
<div className="flex items-end space-x-2 mb-3" key={id}>
<Controller
control={control}
name={`clientSecretTrustedIps.${index}.ipAddress`}
defaultValue="0.0.0.0/0"
render={({ field, fieldState: { error } }) => {
return (
<FormControl
className="mb-0 flex-grow"
label={index === 0 ? "Client Secret Trusted IPs" : undefined}
isError={Boolean(error)}
errorText={error?.message}
>
<Input
value={field.value}
onChange={(e) => {
if (subscription?.ipAllowlisting) {
field.onChange(e);
return;
}
handlePopUpOpen("upgradePlan");
}}
placeholder="123.456.789.0"
/>
</FormControl>
);
}}
/>
<IconButton
onClick={() => {
if (subscription?.ipAllowlisting) {
removeClientSecretTrustedIp(index);
return;
}
handlePopUpOpen("upgradePlan");
}}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
className="p-3"
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</div>
))}
<div className="my-4 ml-1">
<Button
variant="outline_bg"
onClick={() => {
if (subscription?.ipAllowlisting) {
appendClientSecretTrustedIp({
ipAddress: "0.0.0.0/0"
})
return;
}
handlePopUpOpen("upgradePlan");
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
>
Add IP Address
</Button>
</div>
{accessTokenTrustedIpsFields.map(({ id }, index) => (
<div className="flex items-end space-x-2 mb-3" key={id}>
<Controller
control={control}
name={`accessTokenTrustedIps.${index}.ipAddress`}
defaultValue="0.0.0.0/0"
render={({ field, fieldState: { error } }) => {
return (
<FormControl
className="mb-0 flex-grow"
label={index === 0 ? "Access Token Trusted IPs" : undefined}
isError={Boolean(error)}
errorText={error?.message}
>
<Input
value={field.value}
onChange={(e) => {
if (subscription?.ipAllowlisting) {
field.onChange(e);
return;
}
handlePopUpOpen("upgradePlan");
}}
placeholder="123.456.789.0"
/>
</FormControl>
);
}}
/>
<IconButton
onClick={() => {
if (subscription?.ipAllowlisting) {
removeAccessTokenTrustedIp(index);
return;
}
handlePopUpOpen("upgradePlan");
}}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
className="p-3"
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</div>
))}
<div className="my-4 ml-1">
<Button
variant="outline_bg"
onClick={() => {
if (subscription?.ipAllowlisting) {
appendAccessTokenTrustedIp({
ipAddress: "0.0.0.0/0"
})
return;
}
handlePopUpOpen("upgradePlan");
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
>
Add IP Address
</Button>
</div>
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{identityAuthMethodData?.authMethod ? "Update" : "Configure"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button>
</div>
</form>
);
}
@@ -0,0 +1 @@
export { IdentitySection } from "./IdentitySection";
@@ -0,0 +1 @@
export { IdentitySection } from "./IdentitySection";
@@ -0,0 +1 @@
export { OrgIdentityTab } from "./OrgIdentityTab";
@@ -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<typeof schema>;
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<FormData>({
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 (
<Modal
isOpen={popUp?.machineIdentity?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("machineIdentity", isOpen);
reset();
}}
>
<ModalContent title={`${popUp?.machineIdentity?.data ? "Update" : "Create"} Machine Identity`}>
<form onSubmit={handleSubmit(onFormSubmit)}>
<Tabs defaultValue={TabSections.General}>
<TabList>
<div className="flex flex-row border-b border-mineshaft-600 w-full">
<Tab value={TabSections.General}>General</Tab>
<Tab value={TabSections.Advanced}>Advanced</Tab>
</div>
</TabList>
<TabPanel value={TabSections.General}>
<motion.div
key="panel-1"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<Controller
control={control}
defaultValue=""
name="name"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Name"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
placeholder="Machine 1"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="role"
defaultValue=""
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label={`${popUp?.machineIdentity?.data ? "Update" : ""} Role`}
errorText={error?.message}
isError={Boolean(error)}
className="mt-4"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{(roles || []).map(({ name, slug }) => (
<SelectItem value={slug} key={`st-role-${slug}`}>
{name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
</motion.div>
</TabPanel>
<TabPanel value={TabSections.Advanced}>
<div>
{/* <Controller
control={control}
name="expiresIn"
defaultValue=""
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label={`${popUp?.machineIdentity?.data ? "Update" : ""} Refresh Token Expires In`}
errorText={error?.message}
isError={Boolean(error)}
className="mt-4"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{expirations.map(({ label, value }) => (
<SelectItem value={String(value || "")} key={`api-key-expiration-${label}`}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/> */}
{/* <Controller
control={control}
defaultValue="7200"
name="accessTokenMaxTTL"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token Max TTL (seconds)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
placeholder="7200"
type="number"
min="0"
step="1"
/>
</FormControl>
)}
/> */}
<Controller
control={control}
defaultValue="7200"
name="accessTokenTTL"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token TTL (seconds)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
placeholder="7200"
type="number"
min="0"
step="1"
/>
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="0"
name="accessTokenNumUsesLimit"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token Max Number of Uses"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
placeholder="0"
type="number"
min="0"
step="1"
/>
</FormControl>
)}
/>
{clientSecretTrustedIpsFields.map(({ id }, index) => (
<div className="flex items-end space-x-2 mb-3" key={id}>
<Controller
control={control}
name={`clientSecretTrustedIps.${index}.ipAddress`}
defaultValue="0.0.0.0/0"
render={({ field, fieldState: { error } }) => {
return (
<FormControl
className="mb-0 flex-grow"
label={index === 0 ? "Client Secret Trusted IPs" : undefined}
isError={Boolean(error)}
errorText={error?.message}
>
<Input
value={field.value}
onChange={(e) => {
if (subscription?.ipAllowlisting) {
field.onChange(e);
return;
}
handlePopUpOpen("upgradePlan");
}}
placeholder="123.456.789.0"
/>
</FormControl>
);
}}
/>
<IconButton
onClick={() => {
if (subscription?.ipAllowlisting) {
removeClientSecretTrustedIp(index);
return;
}
handlePopUpOpen("upgradePlan");
}}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
className="p-3"
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</div>
))}
<div className="my-4 ml-1">
<Button
variant="outline_bg"
onClick={() => {
if (subscription?.ipAllowlisting) {
appendClientSecretTrustedIp({
ipAddress: "0.0.0.0/0"
})
return;
}
handlePopUpOpen("upgradePlan");
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
>
Add IP Address
</Button>
</div>
{accessTokenTrustedIpsFields.map(({ id }, index) => (
<div className="flex items-end space-x-2 mb-3" key={id}>
<Controller
control={control}
name={`accessTokenTrustedIps.${index}.ipAddress`}
defaultValue="0.0.0.0/0"
render={({ field, fieldState: { error } }) => {
return (
<FormControl
className="mb-0 flex-grow"
label={index === 0 ? "Access Token Trusted IPs" : undefined}
isError={Boolean(error)}
errorText={error?.message}
>
<Input
value={field.value}
onChange={(e) => {
if (subscription?.ipAllowlisting) {
field.onChange(e);
return;
}
handlePopUpOpen("upgradePlan");
}}
placeholder="123.456.789.0"
/>
</FormControl>
);
}}
/>
<IconButton
onClick={() => {
if (subscription?.ipAllowlisting) {
removeAccessTokenTrustedIp(index);
return;
}
handlePopUpOpen("upgradePlan");
}}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
className="p-3"
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</div>
))}
<div className="my-4 ml-1">
<Button
variant="outline_bg"
onClick={() => {
if (subscription?.ipAllowlisting) {
appendAccessTokenTrustedIp({
ipAddress: "0.0.0.0/0"
})
return;
}
handlePopUpOpen("upgradePlan");
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
>
Add IP Address
</Button>
</div>
</div>
</TabPanel>
</Tabs>
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{popUp?.machineIdentity?.data ? "Update" : "Create"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("machineIdentity", false)}
>
Cancel
</Button>
</div>
</form>
<UpgradePlanModal
isOpen={popUp?.upgradePlan?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can use IP allowlisting if you switch to Infisical's Pro plan."
/>
</ModalContent>
</Modal>
);
}
@@ -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 (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex justify-between mb-8">
<p className="text-xl font-semibold text-mineshaft-100">
Machine Identities
</p>
<OrgPermissionCan
I={OrgPermissionActions.Create}
a={OrgPermissionSubjects.MachineIdentity}
>
{(isAllowed) => (
<Button
colorSchema="secondary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("machineIdentity")}
isDisabled={!isAllowed}
>
Create identity
</Button>
)}
</OrgPermissionCan>
</div>
<MachineIdentityTable
handlePopUpOpen={handlePopUpOpen}
/>
<AddMachineIdentityModal
popUp={popUp}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
<CreateClientSecretModal
popUp={popUp}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
<DeleteActionModal
isOpen={popUp.deleteMachineIdentity.isOpen}
title={`Are you sure want to delete ${
(popUp?.deleteMachineIdentity?.data as { name: string })?.name || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteMachineIdentity", isOpen)}
deleteKey="confirm"
onDeleteApproved={() =>
onDeleteMachineIdentitySubmit(
(popUp?.deleteMachineIdentity?.data as { machineId: string })?.machineId
)
}
/>
</div>
);
},
{ action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.MachineIdentity }
);
@@ -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 (
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Name</Th>
<Th>Client ID</Th>
{/* <Th>Status</Th> */}
<Th>Role</Th>
{/* <Th>Trusted IPs</Th> */}
{/* <Th>Access Token TTL</Th> */}
{/* <Th>Created At</Th> */}
{/* <Th>Valid Until</Th> */}
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={7} innerKey="org-machine-identities" />}
{!isLoading &&
data &&
data.length > 0 &&
data.map(({
machineIdentity: {
_id,
name,
clientId,
// isActive,
clientSecretTrustedIps,
accessTokenTrustedIps,
// createdAt,
// expiresAt,
accessTokenMaxTTL,
accessTokenTTL,
accessTokenNumUsesLimit
},
role,
customRole
}) => {
return (
<Tr className="h-10" key={`machine-identity-${_id}`}>
<Td>{name}</Td>
<Td>{clientId}</Td>
<Td>
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.MachineIdentity}
>
{(isAllowed) => {
return (
<Select
value={
role === "custom" ? (customRole?.slug as string) : role
}
isDisabled={!isAllowed}
className="w-40 bg-mineshaft-600"
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
onValueChange={(selectedRole) =>
handleChangeRole({
machineId: _id,
role: selectedRole
})
}
>
{(roles || [])
.map(({ slug, name: roleName }) => (
<SelectItem value={slug} key={`owner-option-${slug}`}>
{roleName}
</SelectItem>
))}
</Select>
);
}}
</OrgPermissionCan>
</Td>
<Td>
<div className="flex justify-end items-center">
<Tooltip content="Manage client secrets">
<IconButton
onClick={async () => {
handlePopUpOpen("clientSecret", {
machineId: _id,
name
});
}}
size="lg"
colorSchema="primary"
variant="plain"
ariaLabel="update"
// isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faKey} />
</IconButton>
</Tooltip>
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.MachineIdentity}
>
{(isAllowed) => (
<IconButton
onClick={async () => {
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}
>
<FontAwesomeIcon icon={faPencil} />
</IconButton>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.MachineIdentity}
>
{(isAllowed) => (
<IconButton
onClick={() => {
handlePopUpOpen("deleteMachineIdentity", {
machineId: _id,
name
});
}}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
className="ml-4"
isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
)}
</OrgPermissionCan>
</div>
</Td>
</Tr>
);
})}
{!isLoading && data && data?.length === 0 && (
<Tr>
<Td colSpan={7}>
<EmptyState title="No machine identities have been created in this organization" icon={faServer} />
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
);
}
@@ -1 +0,0 @@
export { MachineIdentitySection } from "./MachineIdentitySection";
@@ -1 +0,0 @@
export { MachineIdentitySection } from "./MachineIdentitySection";
@@ -1 +0,0 @@
export { OrgMachineIdentityTab } from "./OrgMachineIdentityTab";
@@ -41,9 +41,9 @@ const SIMPLE_PERMISSION_OPTIONS = [
}, },
{ {
title: "Machine identity management", 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, icon: faServer,
formName: "machine-identity" formName: "identity"
}, },
{ {
title: "Billing & usage", title: "Billing & usage",
@@ -32,7 +32,7 @@ export const formSchema = z.object({
"secret-scanning": generalPermissionSchema, "secret-scanning": generalPermissionSchema,
sso: generalPermissionSchema, sso: generalPermissionSchema,
billing: generalPermissionSchema, billing: generalPermissionSchema,
"machine-identity": generalPermissionSchema "identity": generalPermissionSchema
}) })
.optional() .optional()
}); });
@@ -1,3 +1,3 @@
export { OrgMachineIdentityTab } from "./OrgMachineIdentityTab"; export { OrgIdentityTab } from "./OrgIdentityTab";
export { OrgMembersTab } from "./OrgMembersTab"; export { OrgMembersTab } from "./OrgMembersTab";
export { OrgRoleTabSection } from "./OrgRoleTabSection"; export { OrgRoleTabSection } from "./OrgRoleTabSection";
@@ -50,11 +50,11 @@ export const LogsFilter = ({ control, reset }: Props) => {
{actor.metadata.name} {actor.metadata.name}
</SelectItem> </SelectItem>
); );
case ActorType.MACHINE: case ActorType.IDENTITY:
return ( return (
<SelectItem <SelectItem
value={`${actor.type}-${actor.metadata.machineId}`} value={`${actor.type}-${actor.metadata.identityId}`}
key={`machine-filter-${actor.metadata.machineId}`} key={`identity-filter-${actor.metadata.identityId}`}
> >
{actor.metadata.name} {actor.metadata.name}
</SelectItem> </SelectItem>
@@ -29,11 +29,11 @@ export const LogsTableRow = ({
<p>Service token</p> <p>Service token</p>
</Td> </Td>
); );
case ActorType.MACHINE: case ActorType.IDENTITY:
return ( return (
<Td> <Td>
<p>{`${actor.metadata.name}`}</p> <p>{`${actor.metadata.name}`}</p>
<p>Machine identity</p> <p>Machine Identity</p>
</Td> </Td>
); );
default: default:
@@ -167,22 +167,24 @@ export const LogsTableRow = ({
<p>{`Name: ${event.metadata.name}`}</p> <p>{`Name: ${event.metadata.name}`}</p>
</Td> </Td>
); );
case EventType.CREATE_MACHINE_IDENTITY: case EventType.CREATE_IDENTITY:
return ( return (
<Td> <Td>
<p>{`ID: ${event.metadata.identityId}`}</p>
<p>{`Name: ${event.metadata.name}`}</p> <p>{`Name: ${event.metadata.name}`}</p>
</Td> </Td>
); );
case EventType.UPDATE_MACHINE_IDENTITY: case EventType.UPDATE_IDENTITY:
return ( return (
<Td> <Td>
<p>{`ID: ${event.metadata.identityId}`}</p>
<p>{`Name: ${event.metadata.name}`}</p> <p>{`Name: ${event.metadata.name}`}</p>
</Td> </Td>
); );
case EventType.DELETE_MACHINE_IDENTITY: case EventType.DELETE_IDENTITY:
return ( return (
<Td> <Td>
<p>{`Name: ${event.metadata.name}`}</p> <p>{`ID: ${event.metadata.identityId}`}</p>
</Td> </Td>
); );
case EventType.CREATE_ENVIRONMENT: case EventType.CREATE_ENVIRONMENT:
@@ -6,7 +6,7 @@ import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { withProjectPermission } from "@app/hoc"; import { withProjectPermission } from "@app/hoc";
import { import {
MachineIdentityTab, IdentityTab,
MemberListTab, MemberListTab,
ProjectRoleListTab, ProjectRoleListTab,
ServiceTokenTab ServiceTokenTab
@@ -15,7 +15,7 @@ import {
enum TabSections { enum TabSections {
Member = "members", Member = "members",
Roles = "roles", Roles = "roles",
MachineIdentities = "machine-identities", Identities = "identities",
ServiceTokens = "service-tokens" ServiceTokens = "service-tokens"
} }
@@ -30,7 +30,7 @@ export const MembersPage = withProjectPermission(
<Tabs defaultValue={TabSections.Member}> <Tabs defaultValue={TabSections.Member}>
<TabList> <TabList>
<Tab value={TabSections.Member}>People</Tab> <Tab value={TabSections.Member}>People</Tab>
<Tab value={TabSections.MachineIdentities}> <Tab value={TabSections.Identities}>
<div className="flex items-center"> <div className="flex items-center">
<p>Machine Identities</p> <p>Machine Identities</p>
<div className="ml-2 rounded-md text-yellow text-sm inline-block bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] opacity-80 hover:opacity-100 cursor-default"> <div className="ml-2 rounded-md text-yellow text-sm inline-block bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] opacity-80 hover:opacity-100 cursor-default">
@@ -52,8 +52,8 @@ export const MembersPage = withProjectPermission(
<MemberListTab /> <MemberListTab />
</motion.div> </motion.div>
</TabPanel> </TabPanel>
<TabPanel value={TabSections.MachineIdentities}> <TabPanel value={TabSections.Identities}>
<MachineIdentityTab /> <IdentityTab />
</TabPanel> </TabPanel>
<TabPanel value={TabSections.ServiceTokens}> <TabPanel value={TabSections.ServiceTokens}>
<ServiceTokenTab /> <ServiceTokenTab />
@@ -1,19 +1,19 @@
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { import {
MachineIdentitySection IdentitySection,
} from "./components"; } from "./components";
export const MachineIdentityTab = () => { export const IdentityTab = () => {
return ( return (
<motion.div <motion.div
key="panel-machine-identity" key="panel-identity"
transition={{ duration: 0.15 }} transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }} initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }} animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }} exit={{ opacity: 0, translateX: 30 }}
> >
<MachineIdentitySection /> <IdentitySection />
</motion.div> </motion.div>
); );
} }
@@ -18,26 +18,26 @@ import {
useWorkspace useWorkspace
} from "@app/context"; } from "@app/context";
import { import {
useAddMachineToWorkspace, useAddIdentityToWorkspace,
useGetMachineMembershipOrgs, useGetIdentityMembershipOrgs,
useGetRoles, useGetRoles,
useGetWorkspaceMachineMemberships useGetWorkspaceIdentityMemberships
} from "@app/hooks/api"; } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = yup.object({ const schema = yup.object({
machineId: yup.string().required("Machine identity id is required"), identityId: yup.string().required("Identity id is required"),
role: yup.string() role: yup.string()
}).required(); }).required();
export type FormData = yup.InferType<typeof schema>; export type FormData = yup.InferType<typeof schema>;
type Props = { type Props = {
popUp: UsePopUpState<["machineIdentity"]>; popUp: UsePopUpState<["identity"]>;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["machineIdentity"]>, state?: boolean) => void; handlePopUpToggle: (popUpName: keyof UsePopUpState<["identity"]>, state?: boolean) => void;
}; };
export const AddMachineIdentityModal = ({ export const IdentityModal = ({
popUp, popUp,
handlePopUpToggle handlePopUpToggle
}: Props) => { }: Props) => {
@@ -48,28 +48,28 @@ export const AddMachineIdentityModal = ({
const orgId = currentOrg?._id || ""; const orgId = currentOrg?._id || "";
const workspaceId = currentWorkspace?._id || ""; const workspaceId = currentWorkspace?._id || "";
const { data: machineMembershipOrgs } = useGetMachineMembershipOrgs(orgId); const { data: identityMembershipOrgs } = useGetIdentityMembershipOrgs(orgId);
const { data: machineMemberships } = useGetWorkspaceMachineMemberships(workspaceId); const { data: identityMemberships } = useGetWorkspaceIdentityMemberships(workspaceId);
const { data: roles } = useGetRoles({ const { data: roles } = useGetRoles({
orgId, orgId,
workspaceId workspaceId
}); });
const { mutateAsync: addMachineToWorkspaceMutateAsync } = useAddMachineToWorkspace(); const { mutateAsync: addIdentityToWorkspaceMutateAsync } = useAddIdentityToWorkspace();
const filteredMachineMembershipOrgs = useMemo(() => { const filteredIdentityMembershipOrgs = useMemo(() => {
const wsMachineIds = new Map(); const wsIdentityIds = new Map();
machineMemberships?.forEach((machineMembership) => { identityMemberships?.forEach((identityMembership) => {
wsMachineIds.set(machineMembership.machineIdentity._id, true); wsIdentityIds.set(identityMembership.identity._id, true);
}); });
return (machineMembershipOrgs || []).filter( return (identityMembershipOrgs || []).filter(
({ machineIdentity: mi }) => !wsMachineIds.has(mi._id) ({ identity: i }) => !wsIdentityIds.has(i._id)
); );
}, [machineMembershipOrgs, machineMemberships]); }, [identityMembershipOrgs, identityMemberships]);
const { const {
control, control,
@@ -81,29 +81,29 @@ export const AddMachineIdentityModal = ({
}); });
const onFormSubmit = async ({ const onFormSubmit = async ({
machineId, identityId,
role role
}: FormData) => { }: FormData) => {
try { try {
await addMachineToWorkspaceMutateAsync({ await addIdentityToWorkspaceMutateAsync({
workspaceId, workspaceId,
machineId, identityId,
role: role || undefined role: role || undefined
}); });
createNotification({ createNotification({
text: "Successfully added machine identity to project", text: "Successfully added identity to project",
type: "success" type: "success"
}); });
reset(); reset();
handlePopUpToggle("machineIdentity", false); handlePopUpToggle("identity", false);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
const error = err as any; const error = err as any;
const text = error?.response?.data?.message const text = error?.response?.data?.message
?? "Failed to add machine identity to project"; ?? "Failed to add identity to project";
createNotification({ createNotification({
text, text,
@@ -114,22 +114,22 @@ export const AddMachineIdentityModal = ({
return ( return (
<Modal <Modal
isOpen={popUp?.machineIdentity?.isOpen} isOpen={popUp?.identity?.isOpen}
onOpenChange={(isOpen) => { onOpenChange={(isOpen) => {
handlePopUpToggle("machineIdentity", isOpen); handlePopUpToggle("identity", isOpen);
reset(); reset();
}} }}
> >
<ModalContent title="Add Machine Identity to Project"> <ModalContent title="Add Identity to Project">
{filteredMachineMembershipOrgs.length ? ( {filteredIdentityMembershipOrgs.length ? (
<form onSubmit={handleSubmit(onFormSubmit)}> <form onSubmit={handleSubmit(onFormSubmit)}>
<Controller <Controller
control={control} control={control}
name="machineId" name="identityId"
defaultValue={filteredMachineMembershipOrgs?.[0]?._id} defaultValue={filteredIdentityMembershipOrgs?.[0]?._id}
render={({ field: { onChange, ...field }, fieldState: { error } }) => ( render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl <FormControl
label="Machine Identity" label="Identity"
errorText={error?.message} errorText={error?.message}
isError={Boolean(error)} isError={Boolean(error)}
> >
@@ -139,9 +139,9 @@ export const AddMachineIdentityModal = ({
onValueChange={(e) => onChange(e)} onValueChange={(e) => onChange(e)}
className="w-full" className="w-full"
> >
{filteredMachineMembershipOrgs.map(({ machineIdentity }) => ( {filteredIdentityMembershipOrgs.map(({ identity }) => (
<SelectItem value={machineIdentity._id} key={`org-service-${machineIdentity._id}`}> <SelectItem value={identity._id} key={`org-identity-${identity._id}`}>
{machineIdentity.name} {identity.name}
</SelectItem> </SelectItem>
))} ))}
</Select> </Select>
@@ -182,7 +182,7 @@ export const AddMachineIdentityModal = ({
isLoading={isSubmitting} isLoading={isSubmitting}
isDisabled={isSubmitting} isDisabled={isSubmitting}
> >
{popUp?.machineIdentity?.data ? "Update" : "Create"} {popUp?.identity?.data ? "Update" : "Create"}
</Button> </Button>
<Button colorSchema="secondary" variant="plain"> <Button colorSchema="secondary" variant="plain">
Cancel Cancel
@@ -191,9 +191,9 @@ export const AddMachineIdentityModal = ({
</form> </form>
) : ( ) : (
<div className="flex flex-col space-y-4"> <div className="flex flex-col space-y-4">
<div>All the machine identities in your organization are already added.</div> <div>All identities in your organization are already added.</div>
<Link href={`/org/${currentWorkspace?.organization}/members`}> <Link href={`/org/${currentWorkspace?.organization}/members`}>
<Button variant="outline_bg">Create a new/another machine identities</Button> <Button variant="outline_bg">Create a new/another identities</Button>
</Link> </Link>
</div> </div>
)} )}
@@ -12,47 +12,45 @@ import {
ProjectPermissionSub, ProjectPermissionSub,
useWorkspace} from "@app/context"; useWorkspace} from "@app/context";
import { withProjectPermission } from "@app/hoc"; import { withProjectPermission } from "@app/hoc";
import { import { useDeleteIdentityFromWorkspace } from "@app/hooks/api";
useDeleteMachineFromWorkspace
} from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp"; import { usePopUp } from "@app/hooks/usePopUp";
import { AddMachineIdentityModal } from "./AddMachineIdentityModal"; import { IdentityModal } from "./IdentityModal";
import { MachineIdentityTable } from "./MachineIdentityTable"; import { IdentityTable } from "./IdentityTable";
export const MachineIdentitySection = withProjectPermission( export const IdentitySection = withProjectPermission(
() => { () => {
const { createNotification } = useNotificationContext(); const { createNotification } = useNotificationContext();
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const workspaceId = currentWorkspace?._id ?? ""; const workspaceId = currentWorkspace?._id ?? "";
const { mutateAsync: deleteMutateAsync } = useDeleteMachineFromWorkspace(); const { mutateAsync: deleteMutateAsync } = useDeleteIdentityFromWorkspace();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"machineIdentity", "identity",
"deleteMachineIdentity", "deleteIdentity",
"upgradePlan" "upgradePlan"
] as const); ] as const);
const onRemoveMachineIdentitySubmit = async (machineId: string) => { const onRemoveIdentitySubmit = async (identityId: string) => {
try { try {
await deleteMutateAsync({ await deleteMutateAsync({
machineId, identityId,
workspaceId workspaceId
}); });
createNotification({ createNotification({
text: "Successfully removed machine identity from project", text: "Successfully removed identity from project",
type: "success" type: "success"
}); });
handlePopUpClose("deleteMachineIdentity"); handlePopUpClose("deleteIdentity");
} catch (err) { } catch (err) {
console.error(err); console.error(err);
const error = err as any; 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({ createNotification({
text, text,
@@ -65,18 +63,18 @@ export const MachineIdentitySection = withProjectPermission(
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex justify-between mb-8"> <div className="flex justify-between mb-8">
<p className="text-xl font-semibold text-mineshaft-100"> <p className="text-xl font-semibold text-mineshaft-100">
Machine Identities Identities
</p> </p>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Create} I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.MachineIdentity} a={ProjectPermissionSub.Identity}
> >
{(isAllowed) => ( {(isAllowed) => (
<Button <Button
colorSchema="secondary" colorSchema="secondary"
type="submit" type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />} leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("machineIdentity")} onClick={() => handlePopUpOpen("identity")}
isDisabled={!isAllowed} isDisabled={!isAllowed}
> >
Add identity Add identity
@@ -84,28 +82,28 @@ export const MachineIdentitySection = withProjectPermission(
)} )}
</ProjectPermissionCan> </ProjectPermissionCan>
</div> </div>
<MachineIdentityTable <IdentityTable
handlePopUpOpen={handlePopUpOpen} handlePopUpOpen={handlePopUpOpen}
/> />
<AddMachineIdentityModal <IdentityModal
popUp={popUp} popUp={popUp}
handlePopUpToggle={handlePopUpToggle} handlePopUpToggle={handlePopUpToggle}
/> />
<DeleteActionModal <DeleteActionModal
isOpen={popUp.deleteMachineIdentity.isOpen} isOpen={popUp.deleteIdentity.isOpen}
title={`Are you sure want to remove ${ title={`Are you sure want to remove ${
(popUp?.deleteMachineIdentity?.data as { name: string })?.name || "" (popUp?.deleteIdentity?.data as { name: string })?.name || ""
} from the project?`} } from the project?`}
onChange={(isOpen) => handlePopUpToggle("deleteMachineIdentity", isOpen)} onChange={(isOpen) => handlePopUpToggle("deleteIdentity", isOpen)}
deleteKey="confirm" deleteKey="confirm"
onDeleteApproved={() => onDeleteApproved={() =>
onRemoveMachineIdentitySubmit( onRemoveIdentitySubmit(
(popUp?.deleteMachineIdentity?.data as { machineId: string })?.machineId (popUp?.deleteIdentity?.data as { identityId: string })?.identityId
) )
} }
/> />
</div> </div>
); );
}, },
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.MachineIdentity } { action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Identity }
); );
@@ -26,31 +26,21 @@ import {
} from "@app/context"; } from "@app/context";
import { import {
useGetRoles, useGetRoles,
useGetWorkspaceMachineMemberships, useGetWorkspaceIdentityMemberships,
useUpdateMachineWorkspaceRole useUpdateIdentityWorkspaceRole} from "@app/hooks/api";
} from "@app/hooks/api";
import { MachineTrustedIp} from "@app/hooks/api/machineIdentities/types";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = { type Props = {
handlePopUpOpen: ( handlePopUpOpen: (
popUpName: keyof UsePopUpState<["deleteMachineIdentity", "machineIdentity"]>, popUpName: keyof UsePopUpState<["deleteIdentity", "identity"]>,
data?: { data?: {
machineId?: string; identityId?: string;
name?: string; name?: string;
role?: string;
customRole?: {
name: string;
slug: string;
};
trustedIps?: MachineTrustedIp[];
accessTokenTTL?: number;
isRefreshTokenRotationEnabled?: boolean;
} }
) => void; ) => void;
}; };
export const MachineIdentityTable = ({ export const IdentityTable = ({
handlePopUpOpen handlePopUpOpen
}: Props) => { }: Props) => {
const { createNotification } = useNotificationContext(); const { createNotification } = useNotificationContext();
@@ -58,39 +48,38 @@ export const MachineIdentityTable = ({
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const orgId = currentOrg?._id || ""; const orgId = currentOrg?._id || "";
const workspaceId = currentWorkspace?._id || ""; const workspaceId = currentWorkspace?._id || "";
const { data, isLoading } = useGetWorkspaceIdentityMemberships(currentWorkspace?._id || "");
const { data, isLoading } = useGetWorkspaceMachineMemberships(currentWorkspace?._id || "");
const { data: roles } = useGetRoles({ const { data: roles } = useGetRoles({
orgId, orgId,
workspaceId workspaceId
}); });
const { mutateAsync: updateMutateAsync } = useUpdateMachineWorkspaceRole(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityWorkspaceRole();
const handleChangeRole = async ({ const handleChangeRole = async ({
machineId, identityId,
role role
}: { }: {
machineId: string; identityId: string;
role: string; role: string;
}) => { }) => {
try { try {
await updateMutateAsync({ await updateMutateAsync({
machineId, identityId,
workspaceId, workspaceId,
role role
}); });
createNotification({ createNotification({
text: "Successfully updated machine identity role", text: "Successfully updated identity role",
type: "success" type: "success"
}); });
} catch (err) { } catch (err) {
console.error(err); console.error(err);
const error = err as any; 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({ createNotification({
text, text,
@@ -111,12 +100,12 @@ export const MachineIdentityTable = ({
</Tr> </Tr>
</THead> </THead>
<TBody> <TBody>
{isLoading && <TableSkeleton columns={7} innerKey="project-machine-identities" />} {isLoading && <TableSkeleton columns={7} innerKey="project-identities" />}
{!isLoading && {!isLoading &&
data && data &&
data.length > 0 && data.length > 0 &&
data.map(({ data.map(({
machineIdentity: { identity: {
_id, _id,
name name
}, },
@@ -130,7 +119,7 @@ export const MachineIdentityTable = ({
<Td> <Td>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Edit} I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.MachineIdentity} a={ProjectPermissionSub.Identity}
> >
{(isAllowed) => { {(isAllowed) => {
return ( return (
@@ -143,7 +132,7 @@ export const MachineIdentityTable = ({
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800" dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
onValueChange={(selectedRole) => onValueChange={(selectedRole) =>
handleChangeRole({ handleChangeRole({
machineId: _id, identityId: _id,
role: selectedRole role: selectedRole
}) })
} }
@@ -163,13 +152,13 @@ export const MachineIdentityTable = ({
<Td className="flex justify-end"> <Td className="flex justify-end">
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Delete} I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.MachineIdentity} a={ProjectPermissionSub.Identity}
> >
{(isAllowed) => ( {(isAllowed) => (
<IconButton <IconButton
onClick={() => { onClick={() => {
handlePopUpOpen("deleteMachineIdentity", { handlePopUpOpen("deleteIdentity", {
machineId: _id, identityId: _id,
name name
}); });
}} }}
@@ -191,7 +180,7 @@ export const MachineIdentityTable = ({
{!isLoading && data && data?.length === 0 && ( {!isLoading && data && data?.length === 0 && (
<Tr> <Tr>
<Td colSpan={7}> <Td colSpan={7}>
<EmptyState title="No machine identities have been added to this project" icon={faServer} /> <EmptyState title="No identities have been added to this project" icon={faServer} />
</Td> </Td>
</Tr> </Tr>
)} )}
@@ -0,0 +1 @@
export { IdentitySection } from "./IdentitySection";
@@ -0,0 +1 @@
export { IdentitySection } from "./IdentitySection";
@@ -0,0 +1 @@
export { IdentityTab } from "./IdentityTab";
@@ -1 +0,0 @@
export { MachineIdentitySection } from "./MachineIdentitySection";
@@ -1 +0,0 @@
export { MachineIdentitySection } from "./MachineIdentitySection";
@@ -1 +0,0 @@
export { MachineIdentityTab } from "./MachineIdentityTab";
@@ -62,9 +62,9 @@ const SINGLE_PERMISSION_LIST = [
}, },
{ {
title: "Machine identity management", 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, icon: faServer,
formName: "machine-identity" formName: "identity"
}, },
{ {
title: "Webhooks", title: "Webhooks",
@@ -33,7 +33,7 @@ export const formSchema = z.object({
.object({ .object({
secrets: z.record(multiEnvPermissionSchema).optional(), secrets: z.record(multiEnvPermissionSchema).optional(),
member: generalPermissionSchema, member: generalPermissionSchema,
"machine-identity": generalPermissionSchema, "identity": generalPermissionSchema,
role: generalPermissionSchema, role: generalPermissionSchema,
integrations: generalPermissionSchema, integrations: generalPermissionSchema,
webhooks: generalPermissionSchema, webhooks: generalPermissionSchema,

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