mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 18:27:36 +00:00
Move MI from refresh token to client id / client secrets approach
This commit is contained in:
@@ -25,7 +25,7 @@ declare module "jsonwebtoken" {
|
|||||||
userId: string;
|
userId: string;
|
||||||
refreshVersion?: number;
|
refreshVersion?: number;
|
||||||
}
|
}
|
||||||
export interface MachineRefreshTokenJwtPayload extends jwt.JwtPayload {
|
export interface MachineAccessTokenJwtPayload extends jwt.JwtPayload {
|
||||||
_id: string;
|
_id: string;
|
||||||
authTokenType: string;
|
authTokenType: string;
|
||||||
tokenVersion: number;
|
tokenVersion: number;
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import jwt from "jsonwebtoken";
|
import bcrypt from "bcrypt";
|
||||||
|
import crypto from "crypto";
|
||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import { Types } from "mongoose";
|
import { Types } from "mongoose";
|
||||||
import {
|
import {
|
||||||
|
IMachineIdentityClientSecretData,
|
||||||
IMachineIdentityTrustedIp,
|
IMachineIdentityTrustedIp,
|
||||||
MachineIdentity,
|
MachineIdentity,
|
||||||
|
MachineIdentityClientSecretData,
|
||||||
MachineMembership,
|
MachineMembership,
|
||||||
MachineMembershipOrg,
|
MachineMembershipOrg,
|
||||||
Organization,
|
Organization,
|
||||||
} from "../../../models";
|
} from "../../../models";
|
||||||
import {
|
import {
|
||||||
ActorType,
|
|
||||||
EventType,
|
EventType,
|
||||||
Role
|
Role
|
||||||
} from "../../models";
|
} from "../../models";
|
||||||
@@ -24,105 +26,291 @@ import {
|
|||||||
import { BadRequestError, ForbiddenRequestError, ResourceNotFoundError, UnauthorizedRequestError } from "../../../utils/errors";
|
import { BadRequestError, ForbiddenRequestError, ResourceNotFoundError, UnauthorizedRequestError } from "../../../utils/errors";
|
||||||
import { extractIPDetails, isValidIpOrCidr } from "../../../utils/ip";
|
import { extractIPDetails, isValidIpOrCidr } from "../../../utils/ip";
|
||||||
import { EEAuditLogService, EELicenseService } from "../../services";
|
import { EEAuditLogService, EELicenseService } from "../../services";
|
||||||
import { getAuthSecret } from "../../../config";
|
import { getAuthSecret, getSaltRounds } from "../../../config";
|
||||||
import { ADMIN, AuthTokenType, CUSTOM, MEMBER, NO_ACCESS } from "../../../variables";
|
import { ADMIN, AuthTokenType, CUSTOM, MEMBER, NO_ACCESS } from "../../../variables";
|
||||||
import {
|
import {
|
||||||
OrgPermissionActions,
|
OrgPermissionActions,
|
||||||
OrgPermissionSubjects
|
OrgPermissionSubjects
|
||||||
} from "../../services/RoleService";
|
} from "../../services/RoleService";
|
||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import { checkIPAgainstBlocklist } from "../../../utils/ip";
|
||||||
|
|
||||||
|
const packageClientSecretData = (clientSecretData: IMachineIdentityClientSecretData) => ({
|
||||||
|
_id: clientSecretData._id,
|
||||||
|
machineIdentity: clientSecretData.machineIdentity,
|
||||||
|
isActive: clientSecretData.isActive,
|
||||||
|
description: clientSecretData.description,
|
||||||
|
clientSecretPrefix: clientSecretData.clientSecretPrefix,
|
||||||
|
clientSecretUsageCount: clientSecretData.clientSecretUsageCount,
|
||||||
|
clientSecretUsageLimit: clientSecretData.clientSecretUsageLimit,
|
||||||
|
expiresAt: clientSecretData.expiresAt
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return machine identity access and refresh token as per refresh operation
|
* Return client secrets for machine with id [machineId]
|
||||||
* @param req
|
* @param req
|
||||||
* @param res
|
* @param res
|
||||||
*/
|
*/
|
||||||
export const refreshToken = async (req: Request, res: Response) => {
|
export const getMIClientSecrets = async (req: Request, res: Response) => {
|
||||||
|
const {
|
||||||
|
params: {
|
||||||
|
machineId
|
||||||
|
}
|
||||||
|
} = await validateRequest(reqValidator.GetClientSecretsV3, req);
|
||||||
|
|
||||||
|
const machineMembershipOrg = await MachineMembershipOrg.findOne({
|
||||||
|
machineIdentity: new Types.ObjectId(machineId)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!machineMembershipOrg) throw ResourceNotFoundError();
|
||||||
|
|
||||||
|
const { permission } = await getUserOrgPermissions(req.user._id, machineMembershipOrg.organization.toString());
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
OrgPermissionActions.Read,
|
||||||
|
OrgPermissionSubjects.MachineIdentity
|
||||||
|
);
|
||||||
|
|
||||||
|
const rolePermission = await getOrgRolePermissions(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 MachineIdentityClientSecretData
|
||||||
|
.find({
|
||||||
|
machineIdentity: machineMembershipOrg.machineIdentity,
|
||||||
|
isActive: true
|
||||||
|
})
|
||||||
|
.sort({ createdAt: -1 })
|
||||||
|
.limit(5);
|
||||||
|
|
||||||
|
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,
|
||||||
|
usageLimit
|
||||||
|
}
|
||||||
|
} = await validateRequest(reqValidator.CreateClientSecretV3, req);
|
||||||
|
|
||||||
|
const machineMembershipOrg = await MachineMembershipOrg.findOne({
|
||||||
|
machineIdentity: new Types.ObjectId(machineId)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!machineMembershipOrg) throw ResourceNotFoundError();
|
||||||
|
|
||||||
|
const { permission } = await getUserOrgPermissions(req.user._id, machineMembershipOrg.organization.toString());
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
OrgPermissionActions.Create,
|
||||||
|
OrgPermissionSubjects.MachineIdentity
|
||||||
|
);
|
||||||
|
|
||||||
|
const rolePermission = await getOrgRolePermissions(machineMembershipOrg.role, machineMembershipOrg.organization.toString());
|
||||||
|
const hasRequiredPrivileges = isAtLeastAsPrivilegedOrg(permission, rolePermission);
|
||||||
|
|
||||||
|
if (!hasRequiredPrivileges) throw ForbiddenRequestError({
|
||||||
|
message: "Failed to create client secret for more privileged MI"
|
||||||
|
});
|
||||||
|
|
||||||
|
let expiresAt;
|
||||||
|
if (ttl > 0) {
|
||||||
|
expiresAt = new Date(new Date().getTime() + ttl * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
const clientSecret = crypto.randomBytes(32).toString("hex");
|
||||||
|
const clientSecretHash = await bcrypt.hash(clientSecret, await getSaltRounds());
|
||||||
|
|
||||||
|
const machineIdentityClientSecretData = await new MachineIdentityClientSecretData({
|
||||||
|
machineIdentity: machineMembershipOrg.machineIdentity,
|
||||||
|
isActive: true,
|
||||||
|
description,
|
||||||
|
clientSecretPrefix: clientSecret.slice(0, 4),
|
||||||
|
clientSecretHash,
|
||||||
|
clientSecretUsageCount: 0,
|
||||||
|
clientSecretUsageLimit: usageLimit,
|
||||||
|
accessTokenVersion: 1,
|
||||||
|
expiresAt
|
||||||
|
}).save();
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
clientSecret,
|
||||||
|
clientSecretData: packageClientSecretData(machineIdentityClientSecretData)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete client secret with id [clientSecretId]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
export const deleteMIClientSecret = async (req: Request, res: Response) => {
|
||||||
|
const {
|
||||||
|
params: {
|
||||||
|
machineId,
|
||||||
|
clientSecretId
|
||||||
|
}
|
||||||
|
} = await validateRequest(reqValidator.DeleteClientSecretV3, req);
|
||||||
|
|
||||||
|
const machineMembershipOrg = await MachineMembershipOrg.findOne({
|
||||||
|
machineIdentity: new Types.ObjectId(machineId)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!machineMembershipOrg) throw ResourceNotFoundError();
|
||||||
|
|
||||||
|
const { permission } = await getUserOrgPermissions(req.user._id, machineMembershipOrg.organization.toString());
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
OrgPermissionActions.Delete,
|
||||||
|
OrgPermissionSubjects.MachineIdentity
|
||||||
|
);
|
||||||
|
|
||||||
|
const rolePermission = await getOrgRolePermissions(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 clientSecretData = await MachineIdentityClientSecretData.findOneAndDelete({
|
||||||
|
_id: clientSecretId,
|
||||||
|
machineIdentity: machineId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!clientSecretData) throw ResourceNotFoundError();
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
clientSecretData: packageClientSecretData(clientSecretData)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
const {
|
||||||
body: {
|
body: {
|
||||||
refreshToken
|
clientId,
|
||||||
|
clientSecret
|
||||||
}
|
}
|
||||||
} = await validateRequest(reqValidator.RefreshTokenV3, req);
|
} = await validateRequest(reqValidator.LoginMachineIdentityV3, req);
|
||||||
|
|
||||||
const decodedToken = <jwt.MachineRefreshTokenJwtPayload>(
|
const machineIdentity = await MachineIdentity.findOne({
|
||||||
jwt.verify(refreshToken, await getAuthSecret())
|
clientId,
|
||||||
);
|
|
||||||
|
|
||||||
if (decodedToken.authTokenType !== AuthTokenType.MACHINE_REFRESH_TOKEN) throw UnauthorizedRequestError();
|
|
||||||
|
|
||||||
let machineIdentity = await MachineIdentity.findOne({
|
|
||||||
_id: new Types.ObjectId(decodedToken._id),
|
|
||||||
isActive: true
|
isActive: true
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!machineIdentity) throw UnauthorizedRequestError();
|
if (!machineIdentity) throw UnauthorizedRequestError();
|
||||||
|
|
||||||
|
checkIPAgainstBlocklist({
|
||||||
|
ipAddress: req.realIP,
|
||||||
|
trustedIps: machineIdentity.clientSecretTrustedIps
|
||||||
|
});
|
||||||
|
|
||||||
if (decodedToken.tokenVersion !== machineIdentity.tokenVersion) {
|
const clientSecretData = await MachineIdentityClientSecretData.find({
|
||||||
// raise alarm
|
machineIdentity: machineIdentity._id,
|
||||||
throw UnauthorizedRequestError();
|
isActive: true
|
||||||
|
});
|
||||||
|
|
||||||
|
let validatedClientSecretDatum: IMachineIdentityClientSecretData | undefined;
|
||||||
|
|
||||||
|
for (const clientSecretDatum of clientSecretData) {
|
||||||
|
const isSecretValid = await bcrypt.compare(
|
||||||
|
clientSecret,
|
||||||
|
clientSecretDatum.clientSecretHash
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isSecretValid) {
|
||||||
|
validatedClientSecretDatum = clientSecretDatum;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const response: {
|
if (!validatedClientSecretDatum) throw UnauthorizedRequestError();
|
||||||
refreshToken?: string;
|
|
||||||
accessToken: string;
|
const {
|
||||||
expiresIn: number;
|
expiresAt,
|
||||||
tokenType: string;
|
clientSecretUsageCount,
|
||||||
} = {
|
clientSecretUsageLimit
|
||||||
refreshToken,
|
} = validatedClientSecretDatum;
|
||||||
accessToken: "",
|
|
||||||
expiresIn: 0,
|
if (expiresAt && new Date(expiresAt) < new Date()) {
|
||||||
tokenType: "Bearer"
|
// client secret expired
|
||||||
};
|
await MachineIdentityClientSecretData.findByIdAndUpdate(
|
||||||
|
validatedClientSecretDatum._id,
|
||||||
if (machineIdentity.isRefreshTokenRotationEnabled) {
|
|
||||||
machineIdentity = await MachineIdentity.findByIdAndUpdate(
|
|
||||||
machineIdentity._id,
|
|
||||||
{
|
{
|
||||||
$inc: {
|
isActive: false
|
||||||
tokenVersion: 1
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
new: true
|
new: true
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!machineIdentity) throw BadRequestError();
|
throw UnauthorizedRequestError();
|
||||||
|
}
|
||||||
response.refreshToken = createToken({
|
|
||||||
payload: {
|
if (clientSecretUsageLimit > 0 && clientSecretUsageCount === clientSecretUsageLimit) {
|
||||||
serviceTokenDataId: machineIdentity._id.toString(),
|
// number of times client secret can be used for
|
||||||
authTokenType: AuthTokenType.MACHINE_REFRESH_TOKEN,
|
// a login operation reached
|
||||||
tokenVersion: machineIdentity.tokenVersion
|
await MachineIdentityClientSecretData.findByIdAndUpdate(
|
||||||
|
validatedClientSecretDatum._id,
|
||||||
|
{
|
||||||
|
isActive: false
|
||||||
},
|
},
|
||||||
secret: await getAuthSecret()
|
{
|
||||||
});
|
new: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
throw UnauthorizedRequestError();
|
||||||
}
|
}
|
||||||
|
|
||||||
response.accessToken = createToken({
|
// increment usage count by 1
|
||||||
payload: {
|
await MachineIdentityClientSecretData.findByIdAndUpdate(
|
||||||
_id: machineIdentity._id.toString(),
|
validatedClientSecretDatum._id,
|
||||||
authTokenType: AuthTokenType.MACHINE_ACCESS_TOKEN,
|
|
||||||
tokenVersion: machineIdentity.tokenVersion
|
|
||||||
},
|
|
||||||
expiresIn: machineIdentity.accessTokenTTL,
|
|
||||||
secret: await getAuthSecret()
|
|
||||||
});
|
|
||||||
|
|
||||||
response.expiresIn = machineIdentity.accessTokenTTL;
|
|
||||||
|
|
||||||
await MachineIdentity.findByIdAndUpdate(
|
|
||||||
machineIdentity._id,
|
|
||||||
{
|
{
|
||||||
refreshTokenLastUsed: new Date(),
|
$inc: { clientSecretUsageCount: 1 }
|
||||||
$inc: { refreshTokenUsageCount: 1 }
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
new: true
|
new: true
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return res.status(200).send(response);
|
// token version
|
||||||
|
const accessToken = createToken({
|
||||||
|
payload: {
|
||||||
|
machineId: machineIdentity._id.toString(), // consider changing to clientId and making it more extensible
|
||||||
|
clientSecretDataId: validatedClientSecretDatum._id.toString(),
|
||||||
|
authTokenType: AuthTokenType.MACHINE_ACCESS_TOKEN,
|
||||||
|
tokenVersion: validatedClientSecretDatum.accessTokenVersion
|
||||||
|
},
|
||||||
|
expiresIn: machineIdentity.accessTokenTTL,
|
||||||
|
secret: await getAuthSecret()
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
accessToken,
|
||||||
|
expiresIn: machineIdentity.accessTokenTTL,
|
||||||
|
tokenType: "Bearer"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -137,10 +325,9 @@ export const createMachineIdentity = async (req: Request, res: Response) => {
|
|||||||
name,
|
name,
|
||||||
organizationId,
|
organizationId,
|
||||||
role,
|
role,
|
||||||
trustedIps,
|
clientSecretTrustedIps,
|
||||||
expiresIn,
|
accessTokenTrustedIps,
|
||||||
accessTokenTTL,
|
accessTokenTTL,
|
||||||
isRefreshTokenRotationEnabled
|
|
||||||
}
|
}
|
||||||
} = await validateRequest(reqValidator.CreateMachineIdentityV3, req);
|
} = await validateRequest(reqValidator.CreateMachineIdentityV3, req);
|
||||||
|
|
||||||
@@ -177,44 +364,44 @@ export const createMachineIdentity = async (req: Request, res: Response) => {
|
|||||||
const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId));
|
const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId));
|
||||||
|
|
||||||
// validate trusted ips
|
// validate trusted ips
|
||||||
const reformattedTrustedIps = trustedIps.map((trustedIp) => {
|
const reformattedClientSecretTrustedIps = clientSecretTrustedIps.map((clientSecretTrustedIp) => {
|
||||||
if (!plan.ipAllowlisting && trustedIp.ipAddress !== "0.0.0.0/0") return res.status(400).send({
|
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."
|
message: "Failed to add IP access range to service token due to plan restriction. Upgrade plan to add IP access range."
|
||||||
});
|
});
|
||||||
|
|
||||||
const isValidIPOrCidr = isValidIpOrCidr(trustedIp.ipAddress);
|
const isValidIPOrCidr = isValidIpOrCidr(clientSecretTrustedIp.ipAddress);
|
||||||
|
|
||||||
if (!isValidIPOrCidr) return res.status(400).send({
|
if (!isValidIPOrCidr) return res.status(400).send({
|
||||||
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
|
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
|
||||||
});
|
});
|
||||||
|
|
||||||
return extractIPDetails(trustedIp.ipAddress);
|
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);
|
||||||
});
|
});
|
||||||
|
|
||||||
let expiresAt;
|
|
||||||
if (expiresIn) {
|
|
||||||
expiresAt = new Date();
|
|
||||||
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
|
|
||||||
}
|
|
||||||
|
|
||||||
let user;
|
|
||||||
if (req.authData.actor.type === ActorType.USER) {
|
|
||||||
user = req.authData.authPayload._id;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isActive = true;
|
const isActive = true;
|
||||||
const machineIdentity = await new MachineIdentity({
|
const machineIdentity = await new MachineIdentity({
|
||||||
|
clientId: crypto.randomUUID(),
|
||||||
name,
|
name,
|
||||||
user,
|
|
||||||
organization: new Types.ObjectId(organizationId),
|
organization: new Types.ObjectId(organizationId),
|
||||||
refreshTokenUsageCount: 0,
|
|
||||||
accessTokenUsageCount: 0,
|
|
||||||
tokenVersion: 1,
|
|
||||||
trustedIps: reformattedTrustedIps,
|
|
||||||
isActive,
|
isActive,
|
||||||
expiresAt,
|
|
||||||
accessTokenTTL,
|
accessTokenTTL,
|
||||||
isRefreshTokenRotationEnabled
|
accessTokenUsageCount: 0,
|
||||||
|
clientSecretTrustedIps: reformattedClientSecretTrustedIps,
|
||||||
|
accessTokenTrustedIps: reformattedAccessTokenTrustedIps,
|
||||||
}).save();
|
}).save();
|
||||||
|
|
||||||
await new MachineMembershipOrg({
|
await new MachineMembershipOrg({
|
||||||
@@ -224,15 +411,6 @@ export const createMachineIdentity = async (req: Request, res: Response) => {
|
|||||||
customRole
|
customRole
|
||||||
}).save();
|
}).save();
|
||||||
|
|
||||||
const refreshToken = createToken({
|
|
||||||
payload: {
|
|
||||||
_id: machineIdentity._id.toString(),
|
|
||||||
authTokenType: AuthTokenType.MACHINE_REFRESH_TOKEN,
|
|
||||||
tokenVersion: machineIdentity.tokenVersion
|
|
||||||
},
|
|
||||||
secret: await getAuthSecret()
|
|
||||||
});
|
|
||||||
|
|
||||||
await EEAuditLogService.createAuditLog(
|
await EEAuditLogService.createAuditLog(
|
||||||
req.authData,
|
req.authData,
|
||||||
{
|
{
|
||||||
@@ -241,8 +419,8 @@ export const createMachineIdentity = async (req: Request, res: Response) => {
|
|||||||
name,
|
name,
|
||||||
isActive,
|
isActive,
|
||||||
role,
|
role,
|
||||||
trustedIps: reformattedTrustedIps as Array<IMachineIdentityTrustedIp>,
|
clientSecretTrustedIps: reformattedClientSecretTrustedIps as Array<IMachineIdentityTrustedIp>,
|
||||||
expiresAt
|
accessTokenTrustedIps: reformattedAccessTokenTrustedIps as Array<IMachineIdentityTrustedIp>
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -251,8 +429,7 @@ export const createMachineIdentity = async (req: Request, res: Response) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
machineIdentity,
|
machineIdentity
|
||||||
refreshToken
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,10 +445,9 @@ export const updateMachineIdentity = async (req: Request, res: Response) => {
|
|||||||
body: {
|
body: {
|
||||||
name,
|
name,
|
||||||
role,
|
role,
|
||||||
trustedIps,
|
clientSecretTrustedIps,
|
||||||
expiresIn,
|
accessTokenTrustedIps,
|
||||||
accessTokenTTL,
|
accessTokenTTL
|
||||||
isRefreshTokenRotationEnabled
|
|
||||||
}
|
}
|
||||||
} = await validateRequest(reqValidator.UpdateMachineIdentityV3, req);
|
} = await validateRequest(reqValidator.UpdateMachineIdentityV3, req);
|
||||||
|
|
||||||
@@ -312,38 +488,49 @@ export const updateMachineIdentity = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
const plan = await EELicenseService.getPlan(machineIdentity.organization);
|
const plan = await EELicenseService.getPlan(machineIdentity.organization);
|
||||||
|
|
||||||
// validate trusted ips
|
// validate client secret trusted ips
|
||||||
let reformattedTrustedIps;
|
let reformattedClientSecretTrustedIps;
|
||||||
if (trustedIps) {
|
if (clientSecretTrustedIps) {
|
||||||
reformattedTrustedIps = trustedIps.map((trustedIp) => {
|
reformattedClientSecretTrustedIps = clientSecretTrustedIps.map((clientSecretTrustedIp) => {
|
||||||
if (!plan.ipAllowlisting && trustedIp.ipAddress !== "0.0.0.0/0") return res.status(400).send({
|
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."
|
message: "Failed to update IP access range to service token due to plan restriction. Upgrade plan to update IP access range."
|
||||||
});
|
});
|
||||||
|
|
||||||
const isValidIPOrCidr = isValidIpOrCidr(trustedIp.ipAddress);
|
const isValidIPOrCidr = isValidIpOrCidr(clientSecretTrustedIp.ipAddress);
|
||||||
|
|
||||||
if (!isValidIPOrCidr) return res.status(400).send({
|
if (!isValidIPOrCidr) return res.status(400).send({
|
||||||
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
|
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
|
||||||
});
|
});
|
||||||
|
|
||||||
return extractIPDetails(trustedIp.ipAddress);
|
return extractIPDetails(clientSecretTrustedIp.ipAddress);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let expiresAt;
|
// validate access token trusted ips
|
||||||
if (expiresIn) {
|
let reformattedAccessTokenTrustedIps;
|
||||||
expiresAt = new Date();
|
if (accessTokenTrustedIps) {
|
||||||
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
|
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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
machineIdentity = await MachineIdentity.findByIdAndUpdate(
|
machineIdentity = await MachineIdentity.findByIdAndUpdate(
|
||||||
machineId,
|
machineId,
|
||||||
{
|
{
|
||||||
name,
|
name,
|
||||||
trustedIps: reformattedTrustedIps,
|
clientSecretTrustedIps: reformattedClientSecretTrustedIps,
|
||||||
expiresAt,
|
accessTokenTrustedIps: reformattedAccessTokenTrustedIps,
|
||||||
accessTokenTTL,
|
accessTokenTTL
|
||||||
isRefreshTokenRotationEnabled
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
new: true
|
new: true
|
||||||
@@ -381,8 +568,8 @@ export const updateMachineIdentity = async (req: Request, res: Response) => {
|
|||||||
metadata: {
|
metadata: {
|
||||||
name: machineIdentity.name,
|
name: machineIdentity.name,
|
||||||
role,
|
role,
|
||||||
trustedIps: reformattedTrustedIps as Array<IMachineIdentityTrustedIp>,
|
clientSecretTrustedIps: reformattedClientSecretTrustedIps as Array<IMachineIdentityTrustedIp>,
|
||||||
expiresAt
|
accessTokenTrustedIps: reformattedAccessTokenTrustedIps as Array<IMachineIdentityTrustedIp>
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -436,6 +623,10 @@ export const deleteMachineIdentity = async (req: Request, res: Response) => {
|
|||||||
machineIdentity: machineIdentity._id,
|
machineIdentity: machineIdentity._id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await MachineIdentityClientSecretData.deleteMany({
|
||||||
|
machineIdentity: machineIdentity._id
|
||||||
|
});
|
||||||
|
|
||||||
await EEAuditLogService.createAuditLog(
|
await EEAuditLogService.createAuditLog(
|
||||||
req.authData,
|
req.authData,
|
||||||
{
|
{
|
||||||
@@ -444,8 +635,8 @@ export const deleteMachineIdentity = async (req: Request, res: Response) => {
|
|||||||
name: machineIdentity.name,
|
name: machineIdentity.name,
|
||||||
isActive: machineIdentity.isActive,
|
isActive: machineIdentity.isActive,
|
||||||
role: machineMembershipOrg.role,
|
role: machineMembershipOrg.role,
|
||||||
trustedIps: machineIdentity.trustedIps as Array<IMachineIdentityTrustedIp>,
|
clientSecretTrustedIps: machineIdentity.clientSecretTrustedIps as Array<IMachineIdentityTrustedIp>,
|
||||||
expiresAt: machineIdentity.expiresAt
|
accessTokenTrustedIps: machineIdentity.accessTokenTrustedIps as Array<IMachineIdentityTrustedIp>,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -231,8 +231,8 @@ interface CreateMachineIdentityEvent {
|
|||||||
name: string;
|
name: string;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
role: string;
|
role: string;
|
||||||
trustedIps: Array<IMachineIdentityTrustedIp>;
|
clientSecretTrustedIps: Array<IMachineIdentityTrustedIp>;
|
||||||
expiresAt?: Date;
|
accessTokenTrustedIps: Array<IMachineIdentityTrustedIp>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,8 +241,8 @@ interface UpdateMachineIdentityEvent {
|
|||||||
metadata: {
|
metadata: {
|
||||||
name?: string;
|
name?: string;
|
||||||
role?: string;
|
role?: string;
|
||||||
trustedIps?: Array<IMachineIdentityTrustedIp>;
|
clientSecretTrustedIps?: Array<IMachineIdentityTrustedIp>;
|
||||||
expiresAt?: Date;
|
accessTokenTrustedIps?: Array<IMachineIdentityTrustedIp>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,8 +252,8 @@ interface DeleteMachineIdentityEvent {
|
|||||||
name: string;
|
name: string;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
role: string;
|
role: string;
|
||||||
expiresAt?: Date;
|
clientSecretTrustedIps: Array<IMachineIdentityTrustedIp>;
|
||||||
trustedIps: Array<IMachineIdentityTrustedIp>;
|
accessTokenTrustedIps: Array<IMachineIdentityTrustedIp>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,34 @@ import { requireAuth } from "../../../middleware";
|
|||||||
import { AuthMode } from "../../../variables";
|
import { AuthMode } from "../../../variables";
|
||||||
import { machineIdentityController } from "../../controllers/v3";
|
import { machineIdentityController } from "../../controllers/v3";
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
"/:machineId/client-secrets",
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: [AuthMode.JWT]
|
||||||
|
}),
|
||||||
|
machineIdentityController.getMIClientSecrets
|
||||||
|
);
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
"/me/token",
|
"/:machineId/client-secrets",
|
||||||
machineIdentityController.refreshToken
|
requireAuth({
|
||||||
|
acceptedAuthModes: [AuthMode.JWT]
|
||||||
|
}),
|
||||||
|
machineIdentityController.createMIClientSecret
|
||||||
|
);
|
||||||
|
|
||||||
|
router.delete(
|
||||||
|
"/:machineId/client-secrets/:clientSecretId",
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: [AuthMode.JWT]
|
||||||
|
}),
|
||||||
|
machineIdentityController.deleteMIClientSecret
|
||||||
|
);
|
||||||
|
|
||||||
|
// consider moving to /auth/app/login
|
||||||
|
router.post(
|
||||||
|
"/login",
|
||||||
|
machineIdentityController.loginMI
|
||||||
);
|
);
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ class EELicenseService {
|
|||||||
rbac: false,
|
rbac: false,
|
||||||
customRateLimits: false,
|
customRateLimits: false,
|
||||||
customAlerts: false,
|
customAlerts: false,
|
||||||
auditLogs: false,
|
auditLogs: true,
|
||||||
auditLogsRetentionDays: 0,
|
auditLogsRetentionDays: 0,
|
||||||
samlSSO: false,
|
samlSSO: false,
|
||||||
status: null,
|
status: null,
|
||||||
|
|||||||
@@ -327,7 +327,7 @@ export const getAuthDataProjectPermissions = async ({
|
|||||||
|
|
||||||
checkIPAgainstBlocklist({
|
checkIPAgainstBlocklist({
|
||||||
ipAddress: authData.ipAddress,
|
ipAddress: authData.ipAddress,
|
||||||
trustedIps: machineMembership.machineIdentity.trustedIps
|
trustedIps: machineMembership.machineIdentity.accessTokenTrustedIps
|
||||||
});
|
});
|
||||||
|
|
||||||
role = machineMembership.role;
|
role = machineMembership.role;
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export * from "./userAction";
|
|||||||
export * from "./workspace";
|
export * from "./workspace";
|
||||||
export * from "./serviceTokenData"; // TODO: deprecate
|
export * from "./serviceTokenData"; // TODO: deprecate
|
||||||
export * from "./machineIdentity";
|
export * from "./machineIdentity";
|
||||||
|
export * from "./machineIdentityClientSecretData";
|
||||||
export * from "./machineMembershipOrg";
|
export * from "./machineMembershipOrg";
|
||||||
export * from "./machineMembership";
|
export * from "./machineMembership";
|
||||||
export * from "./apiKeyData"; // TODO: deprecate
|
export * from "./apiKeyData"; // TODO: deprecate
|
||||||
|
|||||||
@@ -7,25 +7,27 @@ export interface IMachineIdentityTrustedIp {
|
|||||||
prefix: number;
|
prefix: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: rename to AppClient
|
||||||
|
|
||||||
export interface IMachineIdentity extends Document {
|
export interface IMachineIdentity extends Document {
|
||||||
_id: Types.ObjectId;
|
_id: Types.ObjectId;
|
||||||
|
clientId: string;
|
||||||
name: string;
|
name: string;
|
||||||
organization: Types.ObjectId;
|
organization: Types.ObjectId;
|
||||||
user: Types.ObjectId;
|
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
refreshTokenLastUsed?: Date;
|
|
||||||
accessTokenLastUsed?: Date;
|
|
||||||
refreshTokenUsageCount: number;
|
|
||||||
accessTokenUsageCount: number;
|
|
||||||
tokenVersion: number;
|
|
||||||
isRefreshTokenRotationEnabled: boolean;
|
|
||||||
expiresAt?: Date;
|
|
||||||
accessTokenTTL: number;
|
accessTokenTTL: number;
|
||||||
trustedIps: Array<IMachineIdentityTrustedIp>;
|
accessTokenLastUsed?: Date;
|
||||||
|
accessTokenUsageCount: number;
|
||||||
|
clientSecretTrustedIps: Array<IMachineIdentityTrustedIp>;
|
||||||
|
accessTokenTrustedIps: Array<IMachineIdentityTrustedIp>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const machineIdentitySchema = new Schema(
|
const machineIdentitySchema = new Schema(
|
||||||
{
|
{
|
||||||
|
clientId: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
name: {
|
name: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
@@ -35,55 +37,54 @@ const machineIdentitySchema = new Schema(
|
|||||||
ref: "Organization",
|
ref: "Organization",
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
user: {
|
|
||||||
type: Schema.Types.ObjectId,
|
|
||||||
ref: "User",
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
isActive: {
|
isActive: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
refreshTokenLastUsed: {
|
|
||||||
type: Date,
|
|
||||||
required: false
|
|
||||||
},
|
|
||||||
accessTokenLastUsed: {
|
|
||||||
type: Date,
|
|
||||||
required: false
|
|
||||||
},
|
|
||||||
refreshTokenUsageCount: {
|
|
||||||
type: Number,
|
|
||||||
default: 0,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
accessTokenUsageCount: {
|
|
||||||
type: Number,
|
|
||||||
default: 0,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
tokenVersion: {
|
|
||||||
type: Number,
|
|
||||||
default: 1,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
isRefreshTokenRotationEnabled: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
expiresAt: { // consider revising field name
|
|
||||||
type: Date,
|
|
||||||
required: false,
|
|
||||||
// expires: 0
|
|
||||||
},
|
|
||||||
accessTokenTTL: { // seconds
|
accessTokenTTL: { // seconds
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 7200,
|
default: 7200,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
trustedIps: {
|
accessTokenLastUsed: {
|
||||||
|
type: Date,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
accessTokenUsageCount: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
clientSecretTrustedIps: {
|
||||||
|
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
|
||||||
|
},
|
||||||
|
accessTokenTrustedIps: {
|
||||||
type: [
|
type: [
|
||||||
{
|
{
|
||||||
ipAddress: {
|
ipAddress: {
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { Document, Schema, Types, model } from "mongoose";
|
||||||
|
|
||||||
|
export interface IMachineIdentityClientSecretData extends Document {
|
||||||
|
_id: Types.ObjectId;
|
||||||
|
machineIdentity: Types.ObjectId;
|
||||||
|
isActive: boolean;
|
||||||
|
description: string;
|
||||||
|
clientSecretPrefix: string;
|
||||||
|
clientSecretHash: string;
|
||||||
|
clientSecretLastUsed?: Date;
|
||||||
|
clientSecretUsageCount: number;
|
||||||
|
clientSecretUsageLimit: number;
|
||||||
|
accessTokenVersion: number;
|
||||||
|
expiresAt?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
const machineIdentityClientSecretDataSchema = new Schema(
|
||||||
|
{
|
||||||
|
machineIdentity: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: "MachineIdentity",
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
isActive: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
clientSecretPrefix: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
clientSecretHash: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
clientSecretLastUsed: {
|
||||||
|
type: Date,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
clientSecretUsageCount: {
|
||||||
|
// number of times client secret has been used
|
||||||
|
// in login operation
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
clientSecretUsageLimit: {
|
||||||
|
// number of times client secret can be used for
|
||||||
|
// a login operation
|
||||||
|
type: Number,
|
||||||
|
default: 0, // default: used as many times as needed
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
accessTokenVersion: {
|
||||||
|
type: Number,
|
||||||
|
default: 1,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
expiresAt: {
|
||||||
|
type: Date,
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const MachineIdentityClientSecretData = model<IMachineIdentityClientSecretData>("MachineIdentityClientSecretData", machineIdentityClientSecretDataSchema);
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
import { Types } from "mongoose";
|
import { Types } from "mongoose";
|
||||||
import { MachineIdentity } from "../../../models";
|
import { MachineIdentity, MachineIdentityClientSecretData } 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";
|
||||||
@@ -12,45 +12,28 @@ interface ValidateMachineIdentityParams {
|
|||||||
export const validateMachineIdentity = async ({
|
export const validateMachineIdentity = async ({
|
||||||
authTokenValue
|
authTokenValue
|
||||||
}: ValidateMachineIdentityParams) => {
|
}: ValidateMachineIdentityParams) => {
|
||||||
const decodedToken = <jwt.MachineRefreshTokenJwtPayload>(
|
const decodedToken = <jwt.MachineAccessTokenJwtPayload>(
|
||||||
jwt.verify(authTokenValue, await getAuthSecret())
|
jwt.verify(authTokenValue, await getAuthSecret())
|
||||||
);
|
);
|
||||||
|
|
||||||
if (decodedToken.authTokenType !== AuthTokenType.MACHINE_ACCESS_TOKEN) throw UnauthorizedRequestError();
|
if (decodedToken.authTokenType !== AuthTokenType.MACHINE_ACCESS_TOKEN) throw UnauthorizedRequestError();
|
||||||
|
|
||||||
const machineIdentity = await MachineIdentity.findOne({
|
const machineIdentityClientSecretData = await MachineIdentityClientSecretData.findOne({
|
||||||
_id: new Types.ObjectId(decodedToken._id),
|
_id: new Types.ObjectId(decodedToken.clientSecretDataId),
|
||||||
isActive: true
|
isActive: true
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!machineIdentity) {
|
if (!machineIdentityClientSecretData) throw UnauthorizedRequestError();
|
||||||
throw UnauthorizedRequestError({
|
|
||||||
message: "Failed to authenticate"
|
if (decodedToken.tokenVersion !== machineIdentityClientSecretData.accessTokenVersion) {
|
||||||
});
|
|
||||||
} else if (machineIdentity?.expiresAt && new Date(machineIdentity.expiresAt) < new Date()) {
|
|
||||||
// case: service token expired
|
|
||||||
await MachineIdentity.findByIdAndUpdate(
|
|
||||||
machineIdentity._id,
|
|
||||||
{
|
|
||||||
isActive: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
new: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
throw UnauthorizedRequestError({
|
|
||||||
message: "Failed to authenticate",
|
|
||||||
});
|
|
||||||
} else if (decodedToken.tokenVersion !== machineIdentity.tokenVersion) {
|
|
||||||
// TODO: raise alarm
|
// TODO: raise alarm
|
||||||
throw UnauthorizedRequestError({
|
throw UnauthorizedRequestError({
|
||||||
message: "Failed to authenticate",
|
message: "Failed to authenticate",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await MachineIdentity.findByIdAndUpdate(
|
const machineIdentity = await MachineIdentity.findByIdAndUpdate(
|
||||||
machineIdentity._id,
|
machineIdentityClientSecretData.machineIdentity,
|
||||||
{
|
{
|
||||||
accessTokenLastUsed: new Date(),
|
accessTokenLastUsed: new Date(),
|
||||||
$inc: { accessTokenUsageCount: 1 }
|
$inc: { accessTokenUsageCount: 1 }
|
||||||
@@ -59,6 +42,10 @@ export const validateMachineIdentity = async ({
|
|||||||
new: true
|
new: true
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!machineIdentity) throw UnauthorizedRequestError({
|
||||||
|
message: "Failed to authenticate"
|
||||||
|
});
|
||||||
|
|
||||||
return machineIdentity;
|
return machineIdentity;
|
||||||
}
|
}
|
||||||
@@ -39,6 +39,6 @@ export const getAuthDataPayloadUserObj = (authData: AuthData) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (authData.authPayload instanceof MachineIdentity) {
|
if (authData.authPayload instanceof MachineIdentity) {
|
||||||
return { user: authData.authPayload.user };
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,34 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { NO_ACCESS } from "../variables";
|
import { NO_ACCESS } from "../variables";
|
||||||
|
|
||||||
export const RefreshTokenV3 = z.object({
|
export const GetClientSecretsV3 = z.object({
|
||||||
|
params: z.object({
|
||||||
|
machineId: z.string()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CreateClientSecretV3 = z.object({
|
||||||
|
params: z.object({
|
||||||
|
machineId: z.string()
|
||||||
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
refreshToken: z.string().trim()
|
description: z.string().trim().default(""),
|
||||||
|
usageLimit: z.number().min(0).default(0),
|
||||||
|
ttl: z.number().min(0).default(0),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const DeleteClientSecretV3 = z.object({
|
||||||
|
params: z.object({
|
||||||
|
machineId: z.string(),
|
||||||
|
clientSecretId: z.string()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
export const LoginMachineIdentityV3 = z.object({
|
||||||
|
body: z.object({
|
||||||
|
clientId: z.string().trim(),
|
||||||
|
clientSecret: z.string().trim()
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -12,16 +37,21 @@ export const CreateMachineIdentityV3 = z.object({
|
|||||||
name: z.string().trim(),
|
name: z.string().trim(),
|
||||||
organizationId: z.string().trim(),
|
organizationId: z.string().trim(),
|
||||||
role: z.string().trim().min(1).default(NO_ACCESS),
|
role: z.string().trim().min(1).default(NO_ACCESS),
|
||||||
trustedIps: z
|
clientSecretTrustedIps: z
|
||||||
.object({
|
.object({
|
||||||
ipAddress: z.string().trim(),
|
ipAddress: z.string().trim(),
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
.min(1)
|
.min(1)
|
||||||
.default([{ ipAddress: "0.0.0.0/0" }]),
|
.default([{ ipAddress: "0.0.0.0/0" }]),
|
||||||
expiresIn: z.number().optional(),
|
accessTokenTrustedIps: z
|
||||||
accessTokenTTL: z.number().int().min(1),
|
.object({
|
||||||
isRefreshTokenRotationEnabled: z.boolean().default(false)
|
ipAddress: z.string().trim(),
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.min(1)
|
||||||
|
.default([{ ipAddress: "0.0.0.0/0" }]),
|
||||||
|
accessTokenTTL: z.number().int().min(1)
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -32,16 +62,21 @@ export const UpdateMachineIdentityV3 = z.object({
|
|||||||
body: z.object({
|
body: z.object({
|
||||||
name: z.string().trim().optional(),
|
name: z.string().trim().optional(),
|
||||||
role: z.string().trim().min(1).optional(),
|
role: z.string().trim().min(1).optional(),
|
||||||
trustedIps: z
|
clientSecretTrustedIps: z
|
||||||
.object({
|
.object({
|
||||||
ipAddress: z.string().trim()
|
ipAddress: z.string().trim()
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
.min(1)
|
.min(1)
|
||||||
.optional(),
|
.optional(),
|
||||||
expiresIn: z.number().optional(),
|
accessTokenTrustedIps: z
|
||||||
accessTokenTTL: z.number().int().min(1).optional(),
|
.object({
|
||||||
isRefreshTokenRotationEnabled: z.boolean().optional()
|
ipAddress: z.string().trim(),
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.min(1)
|
||||||
|
.optional(),
|
||||||
|
accessTokenTTL: z.number().int().min(1).optional()
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
export {
|
export {
|
||||||
useCreateMachineIdentity,
|
useCreateMachineIdentity,
|
||||||
|
useCreateMachineIdentityClientSecret,
|
||||||
useDeleteMachineIdentity,
|
useDeleteMachineIdentity,
|
||||||
useUpdateMachineIdentity} from "./mutations";
|
useDeleteMachineIdentityClientSecret,
|
||||||
|
useUpdateMachineIdentity} from "./mutations";
|
||||||
|
export {
|
||||||
|
useGetMachineIdentityClientSecrets
|
||||||
|
} from "./queries";
|
||||||
@@ -3,12 +3,16 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
import { organizationKeys } from "../organization/queries";
|
import { organizationKeys } from "../organization/queries";
|
||||||
|
import { machineIdentityKeys } from "./queries";
|
||||||
import {
|
import {
|
||||||
|
CreateMachineIdentityClientSecretDTO,
|
||||||
|
CreateMachineIdentityClientSecretRes,
|
||||||
CreateMachineIdentityDTO,
|
CreateMachineIdentityDTO,
|
||||||
CreateMachineIdentityRes,
|
CreateMachineIdentityRes,
|
||||||
DeleteMachineIdentityDTO,
|
DeleteMachineIdentityDTO,
|
||||||
MachineIdentity,
|
MachineIdentity,
|
||||||
UpdateMachineIdentityDTO} from "./types";
|
UpdateMachineIdentityDTO,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
export const useCreateMachineIdentity = () => {
|
export const useCreateMachineIdentity = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@@ -17,12 +21,56 @@ export const useCreateMachineIdentity = () => {
|
|||||||
const { data } = await apiRequest.post("/api/v3/machines/", body);
|
const { data } = await apiRequest.post("/api/v3/machines/", body);
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
onSuccess: ({ machineIdentity }) => {
|
onSuccess: ({ machineIdentity }) => {
|
||||||
queryClient.invalidateQueries(organizationKeys.getOrgServiceMemberships(machineIdentity.organization));
|
queryClient.invalidateQueries(organizationKeys.getOrgServiceMemberships(machineIdentity.organization));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useCreateMachineIdentityClientSecret = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation<CreateMachineIdentityClientSecretRes, {}, CreateMachineIdentityClientSecretDTO>({
|
||||||
|
mutationFn: async ({
|
||||||
|
machineId,
|
||||||
|
description,
|
||||||
|
ttl,
|
||||||
|
usageLimit
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
const { data } = await apiRequest.post(`/api/v3/machines/${machineId}/client-secrets`, {
|
||||||
|
machineId,
|
||||||
|
description,
|
||||||
|
ttl,
|
||||||
|
usageLimit
|
||||||
|
});
|
||||||
|
|
||||||
|
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.delete(`/api/v3/machines/${machineId}/client-secrets/${clientSecretId}`);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { machineId }) => {
|
||||||
|
queryClient.invalidateQueries(machineIdentityKeys.getMachineIdentityClientSecrets(machineId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const useUpdateMachineIdentity = () => {
|
export const useUpdateMachineIdentity = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation<MachineIdentity, {}, UpdateMachineIdentityDTO>({
|
return useMutation<MachineIdentity, {}, UpdateMachineIdentityDTO>({
|
||||||
@@ -30,21 +78,17 @@ export const useUpdateMachineIdentity = () => {
|
|||||||
machineId,
|
machineId,
|
||||||
name,
|
name,
|
||||||
role,
|
role,
|
||||||
isActive,
|
clientSecretTrustedIps,
|
||||||
trustedIps,
|
accessTokenTrustedIps,
|
||||||
expiresIn,
|
accessTokenTTL
|
||||||
accessTokenTTL,
|
|
||||||
isRefreshTokenRotationEnabled
|
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
const { data: { machineIdentity } } = await apiRequest.patch(`/api/v3/machines/${machineId}`, {
|
const { data: { machineIdentity } } = await apiRequest.patch(`/api/v3/machines/${machineId}`, {
|
||||||
name,
|
name,
|
||||||
role,
|
role,
|
||||||
isActive,
|
clientSecretTrustedIps,
|
||||||
trustedIps,
|
accessTokenTrustedIps,
|
||||||
expiresIn,
|
|
||||||
accessTokenTTL,
|
accessTokenTTL,
|
||||||
isRefreshTokenRotationEnabled
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return machineIdentity;
|
return machineIdentity;
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
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 () => {
|
||||||
|
const { data: { clientSecretData } } = await apiRequest.get<{ clientSecretData: MachineIdentityClientSecret[] }>(
|
||||||
|
`/api/v3/machines/${machineId}/client-secrets`
|
||||||
|
);
|
||||||
|
|
||||||
|
return clientSecretData;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -9,21 +9,30 @@ export type MachineTrustedIp = {
|
|||||||
|
|
||||||
export type MachineIdentity = {
|
export type MachineIdentity = {
|
||||||
_id: string;
|
_id: string;
|
||||||
|
clientId: string;
|
||||||
name: string;
|
name: string;
|
||||||
organization: string;
|
organization: string;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
refreshTokenLastUsed?: string;
|
|
||||||
accessTokenLastUsed?: string;
|
|
||||||
refreshTokenUsageCount: number;
|
|
||||||
accessTokenUsageCount: number;
|
|
||||||
trustedIps: MachineTrustedIp[];
|
|
||||||
expiresAt?: string;
|
|
||||||
accessTokenTTL: number;
|
accessTokenTTL: number;
|
||||||
isRefreshTokenRotationEnabled: boolean;
|
accessTokenLastUsed?: string;
|
||||||
|
accessTokenUsageCount: number;
|
||||||
|
clientSecretTrustedIps: MachineTrustedIp[];
|
||||||
|
accessTokenTrustedIps: MachineTrustedIp[];
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type MachineIdentityClientSecret = {
|
||||||
|
_id: string;
|
||||||
|
machineIdentity: string;
|
||||||
|
isActive: boolean;
|
||||||
|
description: string;
|
||||||
|
clientSecretPrefix: string;
|
||||||
|
clientSecretUsageCount: number;
|
||||||
|
clientSecretUsageLimit: number;
|
||||||
|
expiresAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
export type MachineMembershipOrg = {
|
export type MachineMembershipOrg = {
|
||||||
_id: string;
|
_id: string;
|
||||||
machineIdentity: MachineIdentity;
|
machineIdentity: MachineIdentity;
|
||||||
@@ -48,30 +57,47 @@ export type CreateMachineIdentityDTO = {
|
|||||||
name: string;
|
name: string;
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
role?: string;
|
role?: string;
|
||||||
trustedIps: {
|
clientSecretTrustedIps: {
|
||||||
|
ipAddress: string;
|
||||||
|
}[];
|
||||||
|
accessTokenTrustedIps: {
|
||||||
ipAddress: string;
|
ipAddress: string;
|
||||||
}[];
|
}[];
|
||||||
expiresIn?: number;
|
|
||||||
accessTokenTTL: number;
|
accessTokenTTL: number;
|
||||||
isRefreshTokenRotationEnabled: boolean;
|
}
|
||||||
|
|
||||||
|
export type CreateMachineIdentityClientSecretDTO = {
|
||||||
|
machineId: string;
|
||||||
|
description?: string;
|
||||||
|
ttl?: number;
|
||||||
|
usageLimit?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CreateMachineIdentityClientSecretRes = {
|
||||||
|
clientSecret: string;
|
||||||
|
machineIdentity: string;
|
||||||
|
isActive: boolean;
|
||||||
|
description: string;
|
||||||
|
clientSecretUsageCount: number;
|
||||||
|
clientSecretUsageLimit: number;
|
||||||
|
expiresAt?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CreateMachineIdentityRes = {
|
export type CreateMachineIdentityRes = {
|
||||||
refreshToken: string;
|
|
||||||
machineIdentity: MachineIdentity;
|
machineIdentity: MachineIdentity;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UpdateMachineIdentityDTO = {
|
export type UpdateMachineIdentityDTO = {
|
||||||
machineId: string;
|
machineId: string;
|
||||||
isActive?: boolean;
|
|
||||||
name?: string;
|
name?: string;
|
||||||
role?: string;
|
role?: string;
|
||||||
trustedIps?: {
|
clientSecretTrustedIps?: {
|
||||||
|
ipAddress: string;
|
||||||
|
}[];
|
||||||
|
accessTokenTrustedIps?: {
|
||||||
ipAddress: string;
|
ipAddress: string;
|
||||||
}[];
|
}[];
|
||||||
expiresIn?: number;
|
|
||||||
accessTokenTTL?: number;
|
accessTokenTTL?: number;
|
||||||
isRefreshTokenRotationEnabled?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DeleteMachineIdentityDTO = {
|
export type DeleteMachineIdentityDTO = {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export const MembersPage = withPermission(
|
|||||||
<Tab value={TabSections.Member}>People</Tab>
|
<Tab value={TabSections.Member}>People</Tab>
|
||||||
<Tab value={TabSections.MachineIdentities}>
|
<Tab value={TabSections.MachineIdentities}>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<p>Machine Identities</p>
|
<p>App Clients</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">
|
||||||
Beta
|
Beta
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+310
-270
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect } from "react";
|
||||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||||
import { faCheck, faCopy,faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
|
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
@@ -16,7 +16,6 @@ import {
|
|||||||
ModalContent,
|
ModalContent,
|
||||||
Select,
|
Select,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
Switch,
|
|
||||||
Tab,
|
Tab,
|
||||||
TabList,
|
TabList,
|
||||||
TabPanel,
|
TabPanel,
|
||||||
@@ -40,18 +39,8 @@ enum TabSections {
|
|||||||
Advanced = "advanced"
|
Advanced = "advanced"
|
||||||
}
|
}
|
||||||
|
|
||||||
const expirations = [
|
|
||||||
{ label: "Never", value: "" },
|
|
||||||
{ label: "1 day", value: "86400" },
|
|
||||||
{ label: "7 days", value: "604800" },
|
|
||||||
{ label: "1 month", value: "2592000" },
|
|
||||||
{ label: "6 months", value: "15552000" },
|
|
||||||
{ label: "12 months", value: "31104000" }
|
|
||||||
];
|
|
||||||
|
|
||||||
const schema = yup.object({
|
const schema = yup.object({
|
||||||
name: yup.string().required("MI name is required"),
|
name: yup.string().required("MI name is required"),
|
||||||
expiresIn: yup.string(),
|
|
||||||
accessTokenTTL: yup
|
accessTokenTTL: yup
|
||||||
.string()
|
.string()
|
||||||
.test("is-positive-integer", "Access Token TTL must be a positive integer", (value) => {
|
.test("is-positive-integer", "Access Token TTL must be a positive integer", (value) => {
|
||||||
@@ -64,7 +53,7 @@ const schema = yup.object({
|
|||||||
})
|
})
|
||||||
.required("Access Token TTL is required"),
|
.required("Access Token TTL is required"),
|
||||||
role: yup.string(),
|
role: yup.string(),
|
||||||
trustedIps: yup
|
clientSecretTrustedIps: yup
|
||||||
.array(
|
.array(
|
||||||
yup.object({
|
yup.object({
|
||||||
ipAddress: yup.string().max(50).required().label("IP Address")
|
ipAddress: yup.string().max(50).required().label("IP Address")
|
||||||
@@ -72,8 +61,16 @@ const schema = yup.object({
|
|||||||
)
|
)
|
||||||
.min(1)
|
.min(1)
|
||||||
.required()
|
.required()
|
||||||
.label("Trusted IP"),
|
.label("Client Secret Trusted IP"),
|
||||||
isRefreshTokenRotationEnabled: yup.boolean().default(false)
|
accessTokenTrustedIps: yup
|
||||||
|
.array(
|
||||||
|
yup.object({
|
||||||
|
ipAddress: yup.string().max(50).required().label("IP Address")
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.min(1)
|
||||||
|
.required()
|
||||||
|
.label("Access Token Trusted IP")
|
||||||
}).required();
|
}).required();
|
||||||
|
|
||||||
export type FormData = yup.InferType<typeof schema>;
|
export type FormData = yup.InferType<typeof schema>;
|
||||||
@@ -90,7 +87,6 @@ export const AddMachineIdentityModal = ({
|
|||||||
handlePopUpToggle
|
handlePopUpToggle
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { createNotification } = useNotificationContext();
|
const { createNotification } = useNotificationContext();
|
||||||
const [newServiceTokenJSON, setNewServiceTokenJSON] = useState("");
|
|
||||||
const [isServiceTokenJSONCopied, setIsServiceTokenJSONCopied] = useToggle(false);
|
const [isServiceTokenJSONCopied, setIsServiceTokenJSONCopied] = useToggle(false);
|
||||||
|
|
||||||
const { subscription } = useSubscription();
|
const { subscription } = useSubscription();
|
||||||
@@ -115,9 +111,12 @@ export const AddMachineIdentityModal = ({
|
|||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: "",
|
name: "",
|
||||||
accessTokenTTL: "7200",
|
accessTokenTTL: "7200",
|
||||||
trustedIps: [{
|
clientSecretTrustedIps: [{
|
||||||
ipAddress: "0.0.0.0/0"
|
ipAddress: "0.0.0.0/0"
|
||||||
}]
|
}],
|
||||||
|
accessTokenTrustedIps: [{
|
||||||
|
ipAddress: "0.0.0.0/0"
|
||||||
|
}],
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -130,11 +129,6 @@ export const AddMachineIdentityModal = ({
|
|||||||
|
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [setIsServiceTokenJSONCopied]);
|
}, [setIsServiceTokenJSONCopied]);
|
||||||
|
|
||||||
const copyTokenToClipboard = () => {
|
|
||||||
navigator.clipboard.writeText(newServiceTokenJSON);
|
|
||||||
setIsServiceTokenJSONCopied.on();
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
||||||
@@ -146,9 +140,9 @@ export const AddMachineIdentityModal = ({
|
|||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
};
|
};
|
||||||
trustedIps: MachineTrustedIp[];
|
clientSecretTrustedIps: MachineTrustedIp[];
|
||||||
|
accessTokenTrustedIps: MachineTrustedIp[];
|
||||||
accessTokenTTL: number;
|
accessTokenTTL: number;
|
||||||
isRefreshTokenRotationEnabled: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!roles?.length) return;
|
if (!roles?.length) return;
|
||||||
@@ -156,9 +150,8 @@ export const AddMachineIdentityModal = ({
|
|||||||
if (machineIdentity) {
|
if (machineIdentity) {
|
||||||
reset({
|
reset({
|
||||||
name: machineIdentity.name,
|
name: machineIdentity.name,
|
||||||
expiresIn: "",
|
|
||||||
role: machineIdentity?.customRole?.slug ?? machineIdentity.role,
|
role: machineIdentity?.customRole?.slug ?? machineIdentity.role,
|
||||||
trustedIps: machineIdentity.trustedIps.map(({
|
clientSecretTrustedIps: machineIdentity.clientSecretTrustedIps.map(({
|
||||||
ipAddress,
|
ipAddress,
|
||||||
prefix
|
prefix
|
||||||
}: MachineTrustedIp) => {
|
}: MachineTrustedIp) => {
|
||||||
@@ -166,31 +159,48 @@ export const AddMachineIdentityModal = ({
|
|||||||
ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}`
|
ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}`
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
accessTokenTTL: String(machineIdentity.accessTokenTTL),
|
accessTokenTrustedIps: machineIdentity.accessTokenTrustedIps.map(({
|
||||||
isRefreshTokenRotationEnabled: machineIdentity.isRefreshTokenRotationEnabled
|
ipAddress,
|
||||||
|
prefix
|
||||||
|
}: MachineTrustedIp) => {
|
||||||
|
return ({
|
||||||
|
ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}`
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
accessTokenTTL: String(machineIdentity.accessTokenTTL)
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
reset({
|
reset({
|
||||||
name: "",
|
name: "",
|
||||||
expiresIn: "",
|
|
||||||
accessTokenTTL: "7200",
|
accessTokenTTL: "7200",
|
||||||
role: roles[0].slug,
|
role: roles[0].slug,
|
||||||
trustedIps: [{
|
clientSecretTrustedIps: [{
|
||||||
|
ipAddress: "0.0.0.0/0"
|
||||||
|
}],
|
||||||
|
accessTokenTrustedIps: [{
|
||||||
ipAddress: "0.0.0.0/0"
|
ipAddress: "0.0.0.0/0"
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [popUp?.machineIdentity?.data, roles]);
|
}, [popUp?.machineIdentity?.data, roles]);
|
||||||
|
|
||||||
const { fields: tokenTrustedIps, append: appendTrustedIp, remove: removeTrustedIp } = useFieldArray({ control, name: "trustedIps" });
|
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 ({
|
const onFormSubmit = async ({
|
||||||
name,
|
name,
|
||||||
expiresIn,
|
|
||||||
accessTokenTTL,
|
accessTokenTTL,
|
||||||
role,
|
role,
|
||||||
trustedIps,
|
clientSecretTrustedIps,
|
||||||
isRefreshTokenRotationEnabled
|
accessTokenTrustedIps
|
||||||
}: FormData) => {
|
}: FormData) => {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
@@ -207,26 +217,24 @@ export const AddMachineIdentityModal = ({
|
|||||||
machineId: machineIdentity.machineId,
|
machineId: machineIdentity.machineId,
|
||||||
name,
|
name,
|
||||||
role: role || undefined,
|
role: role || undefined,
|
||||||
trustedIps,
|
clientSecretTrustedIps,
|
||||||
expiresIn: (!expiresIn) ? undefined : Number(expiresIn),
|
accessTokenTrustedIps,
|
||||||
accessTokenTTL: Number(accessTokenTTL),
|
accessTokenTTL: Number(accessTokenTTL)
|
||||||
isRefreshTokenRotationEnabled
|
|
||||||
});
|
});
|
||||||
|
|
||||||
handlePopUpToggle("machineIdentity", false);
|
handlePopUpToggle("machineIdentity", false);
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
const { refreshToken } = await createMutateAsync({
|
await createMutateAsync({
|
||||||
name,
|
name,
|
||||||
role: role || undefined,
|
role: role || undefined,
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
trustedIps,
|
clientSecretTrustedIps,
|
||||||
expiresIn: (!expiresIn) ? undefined : Number(expiresIn),
|
accessTokenTrustedIps,
|
||||||
accessTokenTTL: Number(accessTokenTTL),
|
accessTokenTTL: Number(accessTokenTTL),
|
||||||
isRefreshTokenRotationEnabled
|
|
||||||
});
|
});
|
||||||
|
|
||||||
setNewServiceTokenJSON(refreshToken);
|
handlePopUpToggle("machineIdentity", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
createNotification({
|
createNotification({
|
||||||
@@ -247,248 +255,280 @@ export const AddMachineIdentityModal = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasServiceTokenJSON = Boolean(newServiceTokenJSON);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={popUp?.machineIdentity?.isOpen}
|
isOpen={popUp?.machineIdentity?.isOpen}
|
||||||
onOpenChange={(isOpen) => {
|
onOpenChange={(isOpen) => {
|
||||||
handlePopUpToggle("machineIdentity", isOpen);
|
handlePopUpToggle("machineIdentity", isOpen);
|
||||||
reset();
|
reset();
|
||||||
setNewServiceTokenJSON("");
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ModalContent title={`${popUp?.machineIdentity?.data ? "Update" : "Create"} Machine Identity`}>
|
<ModalContent title={`${popUp?.machineIdentity?.data ? "Update" : "Create"} App Client`}>
|
||||||
{!hasServiceTokenJSON ? (
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
<Tabs defaultValue={TabSections.General}>
|
||||||
<Tabs defaultValue={TabSections.General}>
|
<TabList>
|
||||||
<TabList>
|
<div className="flex flex-row border-b border-mineshaft-600 w-full">
|
||||||
<div className="flex flex-row border-b border-mineshaft-600 w-full">
|
<Tab value={TabSections.General}>General</Tab>
|
||||||
<Tab value={TabSections.General}>General</Tab>
|
<Tab value={TabSections.Advanced}>Advanced</Tab>
|
||||||
<Tab value={TabSections.Advanced}>Advanced</Tab>
|
</div>
|
||||||
</div>
|
</TabList>
|
||||||
</TabList>
|
<TabPanel value={TabSections.General}>
|
||||||
<TabPanel value={TabSections.General}>
|
<motion.div
|
||||||
<motion.div
|
key="panel-1"
|
||||||
key="panel-1"
|
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 }}
|
>
|
||||||
>
|
|
||||||
<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
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="expiresIn"
|
defaultValue=""
|
||||||
defaultValue=""
|
name="name"
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
render={({ field, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
label={`${popUp?.machineIdentity?.data ? "Update" : ""} Refresh Token Expires In`}
|
label="Name"
|
||||||
errorText={error?.message}
|
isError={Boolean(error)}
|
||||||
isError={Boolean(error)}
|
errorText={error?.message}
|
||||||
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>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{tokenTrustedIps.map(({ id }, index) => (
|
|
||||||
<div className="flex items-end space-x-2 mb-3" key={id}>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name={`trustedIps.${index}.ipAddress`}
|
|
||||||
defaultValue="0.0.0.0/0"
|
|
||||||
render={({ field, fieldState: { error } }) => {
|
|
||||||
return (
|
|
||||||
<FormControl
|
|
||||||
className="mb-0 flex-grow"
|
|
||||||
label={index === 0 ? "Trusted IP" : 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) {
|
|
||||||
removeTrustedIp(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) {
|
|
||||||
appendTrustedIp({
|
|
||||||
ipAddress: "0.0.0.0/0"
|
|
||||||
})
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
|
||||||
}}
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
|
||||||
size="xs"
|
|
||||||
>
|
>
|
||||||
Add IP Address
|
<Input
|
||||||
</Button>
|
{...field}
|
||||||
</div>
|
placeholder="Machine 1"
|
||||||
<Controller
|
/>
|
||||||
control={control}
|
</FormControl>
|
||||||
defaultValue="7200"
|
)}
|
||||||
name="accessTokenTTL"
|
/>
|
||||||
render={({ field, fieldState: { error } }) => (
|
<Controller
|
||||||
<FormControl
|
control={control}
|
||||||
label="Access Token TTL (seconds)"
|
name="role"
|
||||||
isError={Boolean(error)}
|
defaultValue=""
|
||||||
errorText={error?.message}
|
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"
|
||||||
>
|
>
|
||||||
<Input
|
{(roles || []).map(({ name, slug }) => (
|
||||||
{...field}
|
<SelectItem value={slug} key={`st-role-${slug}`}>
|
||||||
placeholder="7200"
|
{name}
|
||||||
/>
|
</SelectItem>
|
||||||
</FormControl>
|
))}
|
||||||
)}
|
</Select>
|
||||||
/>
|
</FormControl>
|
||||||
<div className="mt-8">
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</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="accessTokenTTL"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Access Token TTL (seconds)"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
placeholder="7200"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{clientSecretTrustedIpsFields.map(({ id }, index) => (
|
||||||
|
<div className="flex items-end space-x-2 mb-3" key={id}>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="isRefreshTokenRotationEnabled"
|
name={`clientSecretTrustedIps.${index}.ipAddress`}
|
||||||
render={({ field: { onChange, value } }) => (
|
defaultValue="0.0.0.0/0"
|
||||||
<Switch
|
render={({ field, fieldState: { error } }) => {
|
||||||
id="label-refresh-token-rotation"
|
return (
|
||||||
onCheckedChange={(isChecked) => onChange(isChecked)}
|
<FormControl
|
||||||
isChecked={value}
|
className="mb-0 flex-grow"
|
||||||
>
|
label={index === 0 ? "Client Secret Trusted IPs" : undefined}
|
||||||
Refresh Token Rotation
|
isError={Boolean(error)}
|
||||||
</Switch>
|
errorText={error?.message}
|
||||||
)}
|
>
|
||||||
|
<Input
|
||||||
|
value={field.value}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (subscription?.ipAllowlisting) {
|
||||||
|
field.onChange(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePopUpOpen("upgradePlan");
|
||||||
|
}}
|
||||||
|
placeholder="123.456.789.0"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<p className="mt-4 text-sm font-normal text-mineshaft-400">When enabled, as a result of exchanging a refresh token, a new refresh token will be issued and the existing token will be invalidated.</p>
|
<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>
|
||||||
|
))}
|
||||||
|
<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>
|
</div>
|
||||||
</TabPanel>
|
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||||
</Tabs>
|
<div className="flex items-end space-x-2 mb-3" key={id}>
|
||||||
<div className="flex items-center">
|
<Controller
|
||||||
<Button
|
control={control}
|
||||||
className="mr-4"
|
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
||||||
size="sm"
|
defaultValue="0.0.0.0/0"
|
||||||
type="submit"
|
render={({ field, fieldState: { error } }) => {
|
||||||
isLoading={isSubmitting}
|
return (
|
||||||
isDisabled={isSubmitting}
|
<FormControl
|
||||||
>
|
className="mb-0 flex-grow"
|
||||||
{popUp?.machineIdentity?.data ? "Update" : "Create"}
|
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
||||||
</Button>
|
isError={Boolean(error)}
|
||||||
<Button
|
errorText={error?.message}
|
||||||
colorSchema="secondary"
|
>
|
||||||
variant="plain"
|
<Input
|
||||||
onClick={() => handlePopUpToggle("machineIdentity", false)}
|
value={field.value}
|
||||||
>
|
onChange={(e) => {
|
||||||
Cancel
|
if (subscription?.ipAllowlisting) {
|
||||||
</Button>
|
field.onChange(e);
|
||||||
</div>
|
return;
|
||||||
</form>
|
}
|
||||||
) : (
|
|
||||||
<div className="mt-2 mb-3 mr-2 flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
handlePopUpOpen("upgradePlan");
|
||||||
<p className="mr-4 break-all">{newServiceTokenJSON}</p>
|
}}
|
||||||
<IconButton
|
placeholder="123.456.789.0"
|
||||||
ariaLabel="copy icon"
|
/>
|
||||||
colorSchema="secondary"
|
</FormControl>
|
||||||
className="group relative"
|
);
|
||||||
onClick={copyTokenToClipboard}
|
}}
|
||||||
|
/>
|
||||||
|
<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}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={isServiceTokenJSONCopied ? faCheck : faCopy} />
|
{popUp?.machineIdentity?.data ? "Update" : "Create"}
|
||||||
<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">
|
</Button>
|
||||||
Click to copy
|
<Button
|
||||||
</span>
|
colorSchema="secondary"
|
||||||
</IconButton>
|
variant="plain"
|
||||||
|
onClick={() => handlePopUpToggle("machineIdentity", false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</form>
|
||||||
<UpgradePlanModal
|
<UpgradePlanModal
|
||||||
isOpen={popUp?.upgradePlan?.isOpen}
|
isOpen={popUp?.upgradePlan?.isOpen}
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||||
|
|||||||
+284
@@ -0,0 +1,284 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { faCheck, faCopy, faKey,faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import * as yup from "yup";
|
||||||
|
|
||||||
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
EmptyState,
|
||||||
|
FormControl,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalContent
|
||||||
|
,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
TableSkeleton,
|
||||||
|
TBody,
|
||||||
|
Td,
|
||||||
|
Th,
|
||||||
|
THead,
|
||||||
|
Tr,
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useToggle } from "@app/hooks";
|
||||||
|
import {
|
||||||
|
useCreateMachineIdentityClientSecret,
|
||||||
|
useDeleteMachineIdentityClientSecret,
|
||||||
|
useGetMachineIdentityClientSecrets} from "@app/hooks/api";
|
||||||
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
const schema = yup.object({
|
||||||
|
description: yup.string(),
|
||||||
|
ttl: yup.string() // TODO: optional
|
||||||
|
});
|
||||||
|
|
||||||
|
export type FormData = yup.InferType<typeof schema>;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
popUp: UsePopUpState<["clientSecret"]>;
|
||||||
|
handlePopUpToggle: (popUpName: keyof UsePopUpState<["clientSecret"]>, state?: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CreateClientSecretModal = ({
|
||||||
|
popUp,
|
||||||
|
handlePopUpToggle
|
||||||
|
}: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { createNotification } = useNotificationContext();
|
||||||
|
const [token, setToken] = useState("");
|
||||||
|
const [isTokenCopied, setIsTokenCopied] = useToggle(false);
|
||||||
|
|
||||||
|
const popUpData = (popUp?.clientSecret?.data as {
|
||||||
|
machineId?: string;
|
||||||
|
name?: string;
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data, isLoading } = useGetMachineIdentityClientSecrets(popUpData?.machineId ?? "");
|
||||||
|
|
||||||
|
const { mutateAsync: createClientSecretMutateAsync } = useCreateMachineIdentityClientSecret();
|
||||||
|
const { mutateAsync: deleteClientSecretMutateAsync } = useDeleteMachineIdentityClientSecret();
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
formState: { isSubmitting }
|
||||||
|
} = useForm<FormData>({
|
||||||
|
resolver: yupResolver(schema),
|
||||||
|
defaultValues: {
|
||||||
|
description: "",
|
||||||
|
ttl: ""
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let timer: NodeJS.Timeout;
|
||||||
|
if (isTokenCopied) {
|
||||||
|
timer = setTimeout(() => setIsTokenCopied.off(), 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [isTokenCopied]);
|
||||||
|
|
||||||
|
const copyTokenToClipboard = () => {
|
||||||
|
navigator.clipboard.writeText(token);
|
||||||
|
setIsTokenCopied.on();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFormSubmit = async ({
|
||||||
|
description,
|
||||||
|
ttl
|
||||||
|
}: FormData) => {
|
||||||
|
try {
|
||||||
|
|
||||||
|
if (popUpData) {
|
||||||
|
|
||||||
|
const { clientSecret } = await createClientSecretMutateAsync({
|
||||||
|
machineId: popUpData.machineId,
|
||||||
|
description,
|
||||||
|
ttl: Number(ttl)
|
||||||
|
});
|
||||||
|
|
||||||
|
setToken(clientSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully created client secret",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to create client secret",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasToken = Boolean(token);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp?.clientSecret?.isOpen}
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
handlePopUpToggle("clientSecret", isOpen);
|
||||||
|
reset();
|
||||||
|
setToken("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalContent title={`Manage Client Secrets for ${popUpData?.name ?? ""}`}>
|
||||||
|
<h2 className="mb-4">New Client Secret</h2>
|
||||||
|
{hasToken ? (
|
||||||
|
<div>
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<p>We will only show this secret once</p>
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
type="submit"
|
||||||
|
onClick={() => {
|
||||||
|
reset();
|
||||||
|
setToken("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Got it
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<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">{token}</p>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="copy icon"
|
||||||
|
colorSchema="secondary"
|
||||||
|
className="group relative"
|
||||||
|
onClick={copyTokenToClipboard}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit(onFormSubmit)}
|
||||||
|
className="flex mb-8"
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
name="description"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Description (optional)"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
placeholder="Description"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
name="ttl"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="TTL (optional)"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
className="ml-4"
|
||||||
|
>
|
||||||
|
<div className="flex">
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
placeholder="7200"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
step="1"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
className="ml-4"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
<h2 className="mb-4">Client Secrets</h2>
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th>Description</Th>
|
||||||
|
<Th>Expires At</Th>
|
||||||
|
<Th>Client Secret</Th>
|
||||||
|
<Th className="w-5" />
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{isLoading && <TableSkeleton columns={4} innerKey="org-machine-identities-client-secrets" />}
|
||||||
|
{!isLoading &&
|
||||||
|
data &&
|
||||||
|
data.length > 0 &&
|
||||||
|
data.map(({
|
||||||
|
_id,
|
||||||
|
description,
|
||||||
|
machineIdentity,
|
||||||
|
expiresAt,
|
||||||
|
clientSecretPrefix
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Tr className="h-10" key={`mi-client-secret-${_id}`}>
|
||||||
|
<Td>{description === "" ? "-" : description}</Td>
|
||||||
|
<Td>{expiresAt ? format(new Date(expiresAt), "yyyy-MM-dd") : "-"}</Td>
|
||||||
|
<Td>{`${clientSecretPrefix}************`}</Td>
|
||||||
|
<Td className="flex">
|
||||||
|
<IconButton
|
||||||
|
onClick={async () => {
|
||||||
|
await deleteClientSecretMutateAsync({
|
||||||
|
machineId: machineIdentity,
|
||||||
|
clientSecretId: _id
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
size="lg"
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="plain"
|
||||||
|
ariaLabel="update"
|
||||||
|
className="ml-4"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
|
</IconButton>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{!isLoading && data && data?.length === 0 && (
|
||||||
|
<Tr>
|
||||||
|
<Td colSpan={4}>
|
||||||
|
<EmptyState title="No client secrets have been created for this app client yet" icon={faKey} />
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
)}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
+8
-2
@@ -13,6 +13,7 @@ import { useDeleteMachineIdentity } from "@app/hooks/api";
|
|||||||
import { usePopUp } from "@app/hooks/usePopUp";
|
import { usePopUp } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
import { AddMachineIdentityModal } from "./AddMachineIdentityModal";
|
import { AddMachineIdentityModal } from "./AddMachineIdentityModal";
|
||||||
|
import { CreateClientSecretModal } from "./CreateClientSecretModal";
|
||||||
import { MachineIdentityTable } from "./MachineIdentityTable";
|
import { MachineIdentityTable } from "./MachineIdentityTable";
|
||||||
|
|
||||||
export const MachineIdentitySection = withPermission(
|
export const MachineIdentitySection = withPermission(
|
||||||
@@ -22,6 +23,7 @@ export const MachineIdentitySection = withPermission(
|
|||||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||||
"machineIdentity",
|
"machineIdentity",
|
||||||
"deleteMachineIdentity",
|
"deleteMachineIdentity",
|
||||||
|
"clientSecret",
|
||||||
"upgradePlan"
|
"upgradePlan"
|
||||||
] as const);
|
] as const);
|
||||||
|
|
||||||
@@ -49,7 +51,7 @@ export const MachineIdentitySection = withPermission(
|
|||||||
<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 (MIs)
|
App Clients
|
||||||
</p>
|
</p>
|
||||||
<OrgPermissionCan
|
<OrgPermissionCan
|
||||||
I={OrgPermissionActions.Create}
|
I={OrgPermissionActions.Create}
|
||||||
@@ -63,7 +65,7 @@ export const MachineIdentitySection = withPermission(
|
|||||||
onClick={() => handlePopUpOpen("machineIdentity")}
|
onClick={() => handlePopUpOpen("machineIdentity")}
|
||||||
isDisabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
>
|
>
|
||||||
Create MI
|
Create client
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</OrgPermissionCan>
|
</OrgPermissionCan>
|
||||||
@@ -76,6 +78,10 @@ export const MachineIdentitySection = withPermission(
|
|||||||
handlePopUpOpen={handlePopUpOpen}
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
/>
|
/>
|
||||||
|
<CreateClientSecretModal
|
||||||
|
popUp={popUp}
|
||||||
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
|
/>
|
||||||
<DeleteActionModal
|
<DeleteActionModal
|
||||||
isOpen={popUp.deleteMachineIdentity.isOpen}
|
isOpen={popUp.deleteMachineIdentity.isOpen}
|
||||||
title={`Are you sure want to delete ${
|
title={`Are you sure want to delete ${
|
||||||
|
|||||||
+37
-16
@@ -1,6 +1,5 @@
|
|||||||
import { faPencil,faServer, faXmark } from "@fortawesome/free-solid-svg-icons";
|
import { faKey,faPencil,faServer, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { format } from "date-fns";
|
|
||||||
|
|
||||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
@@ -16,8 +15,8 @@ import {
|
|||||||
Td,
|
Td,
|
||||||
Th,
|
Th,
|
||||||
THead,
|
THead,
|
||||||
Tr
|
Tooltip,
|
||||||
} from "@app/components/v2";
|
Tr} from "@app/components/v2";
|
||||||
import {
|
import {
|
||||||
OrgPermissionActions,
|
OrgPermissionActions,
|
||||||
OrgPermissionSubjects,
|
OrgPermissionSubjects,
|
||||||
@@ -25,14 +24,14 @@ import {
|
|||||||
import {
|
import {
|
||||||
useGetMachineMembershipOrgs,
|
useGetMachineMembershipOrgs,
|
||||||
useGetRoles,
|
useGetRoles,
|
||||||
useUpdateMachineIdentity
|
useUpdateMachineIdentity,
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { MachineTrustedIp } from "@app/hooks/api/machineIdentities/types";
|
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<["deleteMachineIdentity", "machineIdentity", "clientSecret"]>,
|
||||||
data?: {
|
data?: {
|
||||||
machineId?: string;
|
machineId?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
@@ -41,9 +40,9 @@ type Props = {
|
|||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
};
|
};
|
||||||
trustedIps?: MachineTrustedIp[];
|
clientSecretTrustedIps?: MachineTrustedIp[];
|
||||||
|
accessTokenTrustedIps?: MachineTrustedIp[];
|
||||||
accessTokenTTL?: number;
|
accessTokenTTL?: number;
|
||||||
isRefreshTokenRotationEnabled?: boolean;
|
|
||||||
}
|
}
|
||||||
) => void;
|
) => void;
|
||||||
};
|
};
|
||||||
@@ -121,12 +120,13 @@ export const MachineIdentityTable = ({
|
|||||||
<THead>
|
<THead>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th>Name</Th>
|
<Th>Name</Th>
|
||||||
|
<Th>Client ID</Th>
|
||||||
{/* <Th>Status</Th> */}
|
{/* <Th>Status</Th> */}
|
||||||
<Th>Role</Th>
|
<Th>Role</Th>
|
||||||
{/* <Th>Trusted IPs</Th> */}
|
{/* <Th>Trusted IPs</Th> */}
|
||||||
{/* <Th>Access Token TTL</Th> */}
|
{/* <Th>Access Token TTL</Th> */}
|
||||||
{/* <Th>Created At</Th> */}
|
{/* <Th>Created At</Th> */}
|
||||||
<Th>Valid Until</Th>
|
{/* <Th>Valid Until</Th> */}
|
||||||
<Th className="w-5" />
|
<Th className="w-5" />
|
||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
@@ -139,12 +139,13 @@ export const MachineIdentityTable = ({
|
|||||||
machineIdentity: {
|
machineIdentity: {
|
||||||
_id,
|
_id,
|
||||||
name,
|
name,
|
||||||
|
clientId,
|
||||||
// isActive,
|
// isActive,
|
||||||
trustedIps,
|
clientSecretTrustedIps,
|
||||||
|
accessTokenTrustedIps,
|
||||||
// createdAt,
|
// createdAt,
|
||||||
expiresAt,
|
// expiresAt,
|
||||||
accessTokenTTL,
|
accessTokenTTL,
|
||||||
isRefreshTokenRotationEnabled
|
|
||||||
},
|
},
|
||||||
role,
|
role,
|
||||||
customRole
|
customRole
|
||||||
@@ -152,6 +153,7 @@ export const MachineIdentityTable = ({
|
|||||||
return (
|
return (
|
||||||
<Tr className="h-10" key={`st-v3-${_id}`}>
|
<Tr className="h-10" key={`st-v3-${_id}`}>
|
||||||
<Td>{name}</Td>
|
<Td>{name}</Td>
|
||||||
|
<Td>{clientId}</Td>
|
||||||
{/* <Td>
|
{/* <Td>
|
||||||
<OrgPermissionCan
|
<OrgPermissionCan
|
||||||
I={OrgPermissionActions.Edit}
|
I={OrgPermissionActions.Edit}
|
||||||
@@ -219,8 +221,26 @@ export const MachineIdentityTable = ({
|
|||||||
</Td> */}
|
</Td> */}
|
||||||
{/* <Td>{accessTokenTTL}</Td> */}
|
{/* <Td>{accessTokenTTL}</Td> */}
|
||||||
{/* <Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td> */}
|
{/* <Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td> */}
|
||||||
<Td>{expiresAt ? format(new Date(expiresAt), "yyyy-MM-dd") : "-"}</Td>
|
{/* <Td>{expiresAt ? format(new Date(expiresAt), "yyyy-MM-dd") : "-"}</Td> */}
|
||||||
|
|
||||||
<Td className="flex justify-end">
|
<Td className="flex justify-end">
|
||||||
|
<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
|
<OrgPermissionCan
|
||||||
I={OrgPermissionActions.Edit}
|
I={OrgPermissionActions.Edit}
|
||||||
a={OrgPermissionSubjects.MachineIdentity}
|
a={OrgPermissionSubjects.MachineIdentity}
|
||||||
@@ -233,15 +253,16 @@ export const MachineIdentityTable = ({
|
|||||||
name,
|
name,
|
||||||
role,
|
role,
|
||||||
customRole,
|
customRole,
|
||||||
trustedIps,
|
clientSecretTrustedIps,
|
||||||
|
accessTokenTrustedIps,
|
||||||
accessTokenTTL,
|
accessTokenTTL,
|
||||||
isRefreshTokenRotationEnabled
|
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
size="lg"
|
size="lg"
|
||||||
colorSchema="primary"
|
colorSchema="primary"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
ariaLabel="update"
|
ariaLabel="update"
|
||||||
|
className="ml-4"
|
||||||
isDisabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faPencil} />
|
<FontAwesomeIcon icon={faPencil} />
|
||||||
@@ -278,7 +299,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 MIs have been created in this organization" icon={faServer} />
|
<EmptyState title="No app clients have been created in this organization" icon={faServer} />
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export const MembersPage = withProjectPermission(
|
|||||||
<Tab value={TabSections.Member}>People</Tab>
|
<Tab value={TabSections.Member}>People</Tab>
|
||||||
<Tab value={TabSections.MachineIdentities}>
|
<Tab value={TabSections.MachineIdentities}>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<p>Machine Identities</p>
|
<p>App Clients</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">
|
||||||
Beta
|
Beta
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+2
-2
@@ -120,7 +120,7 @@ export const AddMachineIdentityModal = ({
|
|||||||
reset();
|
reset();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ModalContent title="Add Machine Identity to Project">
|
<ModalContent title="Add App Client to Project">
|
||||||
{filteredMachineMembershipOrgs.length ? (
|
{filteredMachineMembershipOrgs.length ? (
|
||||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
<Controller
|
<Controller
|
||||||
@@ -129,7 +129,7 @@ export const AddMachineIdentityModal = ({
|
|||||||
defaultValue={filteredMachineMembershipOrgs?.[0]?._id}
|
defaultValue={filteredMachineMembershipOrgs?.[0]?._id}
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
label="Machine Identity"
|
label="App Client"
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
>
|
>
|
||||||
|
|||||||
+2
-2
@@ -62,7 +62,7 @@ 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 (MIs)
|
App Clients
|
||||||
</p>
|
</p>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Create}
|
I={ProjectPermissionActions.Create}
|
||||||
@@ -76,7 +76,7 @@ export const MachineIdentitySection = withProjectPermission(
|
|||||||
onClick={() => handlePopUpOpen("machineIdentity")}
|
onClick={() => handlePopUpOpen("machineIdentity")}
|
||||||
isDisabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
>
|
>
|
||||||
Add MI
|
Add client
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
|
|||||||
+1
-1
@@ -188,7 +188,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 MIs have been added to this project" icon={faServer} />
|
<EmptyState title="No app clients have been added to this project" icon={faServer} />
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user