mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Continue ST V3 roles
This commit is contained in:
@@ -1,6 +1,11 @@
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import { Membership, MembershipOrg, Workspace } from "../../models";
|
||||
import {
|
||||
Membership,
|
||||
MembershipOrg,
|
||||
ServiceMembershipOrg,
|
||||
Workspace
|
||||
} from "../../models";
|
||||
import { Role } from "../../ee/models";
|
||||
import { deleteMembershipOrg } from "../../helpers/membershipOrg";
|
||||
import {
|
||||
@@ -377,3 +382,23 @@ export const deleteOrganizationById = async (req: Request, res: Response) => {
|
||||
organization
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return list of service memberships for organization with id [organizationId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getOrganizationServiceMemberships = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { organizationId }
|
||||
} = await validateRequest(reqValidator.GetOrgServiceMembersV2, req);
|
||||
|
||||
const serviceMemberships = await ServiceMembershipOrg.find({
|
||||
organization: new Types.ObjectId(organizationId)
|
||||
}).populate("service customRole");
|
||||
|
||||
return res.status(200).send({
|
||||
serviceMemberships
|
||||
});
|
||||
}
|
||||
@@ -1,6 +1,14 @@
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import { Key, Membership, ServiceTokenData, Workspace } from "../../models";
|
||||
import {
|
||||
Key,
|
||||
Membership,
|
||||
ServiceMembership,
|
||||
ServiceTokenData,
|
||||
ServiceTokenDataV3,
|
||||
Workspace
|
||||
} from "../../models";
|
||||
import { Role } from "../../ee/models";
|
||||
import {
|
||||
pullSecrets as pull,
|
||||
v2PushSecrets as push,
|
||||
@@ -19,6 +27,8 @@ import {
|
||||
getAuthDataProjectPermissions
|
||||
} from "../../ee/services/ProjectRoleService";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { BadRequestError, ResourceNotFoundError } from "../../utils/errors";
|
||||
import { ADMIN, MEMBER, VIEWER } from "../../variables";
|
||||
|
||||
interface V2PushSecret {
|
||||
type: string; // personal or shared
|
||||
@@ -491,3 +501,136 @@ export const toggleAutoCapitalization = async (req: Request, res: Response) => {
|
||||
workspace
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Add service account with id [serviceId] to workspace
|
||||
* with id [workspaceId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const addWorkspaceServiceMembership = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { workspaceId },
|
||||
body: {
|
||||
serviceId,
|
||||
role
|
||||
}
|
||||
} = await validateRequest(reqValidator.AddWorkspaceServiceMemberV2, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.ServiceTokens
|
||||
);
|
||||
|
||||
let serviceMembership = await ServiceMembership.findOne({
|
||||
service: new Types.ObjectId(serviceId),
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
if (serviceMembership) throw BadRequestError({
|
||||
message: "Service account already exists in workspace"
|
||||
});
|
||||
|
||||
const serviceTokenData = await ServiceTokenDataV3.findById(serviceId);
|
||||
if (!serviceTokenData) throw ResourceNotFoundError();
|
||||
|
||||
const workspace = await Workspace.findById(workspaceId);
|
||||
if (!workspace) throw ResourceNotFoundError();
|
||||
|
||||
if (!serviceTokenData.organization.equals(workspace.organization)) throw BadRequestError({
|
||||
message: "Failed to add service account to workspace in another organization"
|
||||
});
|
||||
|
||||
let customRole;
|
||||
if (role) {
|
||||
const isCustomRole = ![ADMIN, MEMBER, VIEWER].includes(role);
|
||||
if (isCustomRole) {
|
||||
customRole = await Role.findOne({
|
||||
slug: role,
|
||||
isOrgRole: false,
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
if (!customRole) throw BadRequestError({ message: "Role not found" });
|
||||
}
|
||||
}
|
||||
|
||||
serviceMembership = await new ServiceMembership({
|
||||
service: serviceTokenData._id,
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
role,
|
||||
customRole
|
||||
}).save();
|
||||
|
||||
return res.status(200).send({
|
||||
serviceMembership
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add service account with id [serviceId] to workspace
|
||||
* with id [workspaceId]
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const deleteWorkspaceServiceMembership = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { workspaceId, serviceId }
|
||||
} = await validateRequest(reqValidator.DeleteWorkspaceServiceMemberV2, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSub.ServiceTokens
|
||||
);
|
||||
|
||||
const serviceMembership = await ServiceMembership.findOneAndDelete({
|
||||
service: new Types.ObjectId(serviceId),
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
if (!serviceMembership) throw ResourceNotFoundError();
|
||||
|
||||
return res.status(200).send({
|
||||
serviceMembership
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return list of service memberships for workspace with id [workspaceId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getWorkspaceServiceMemberships = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { workspaceId }
|
||||
} = await validateRequest(reqValidator.GetWorkspaceServiceMembersV2, req);
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.ServiceTokens
|
||||
);
|
||||
|
||||
const serviceMemberships = await ServiceMembership.find({
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
}).populate("service customRole");
|
||||
|
||||
return res.status(200).send({
|
||||
serviceMemberships
|
||||
});
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import { Membership, Secret, ServiceTokenDataV3, User } from "../../models";
|
||||
import { Membership, Secret, User } from "../../models";
|
||||
import { SecretService } from "../../services";
|
||||
import { getAuthDataProjectPermissions } from "../../ee/services/ProjectRoleService";
|
||||
import { UnauthorizedRequestError } from "../../utils/errors";
|
||||
@@ -140,17 +140,3 @@ export const nameWorkspaceSecrets = async (req: Request, res: Response) => {
|
||||
message: "Successfully named workspace secrets"
|
||||
});
|
||||
};
|
||||
|
||||
export const getWorkspaceServiceTokenData = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { workspaceId }
|
||||
} = await validateRequest(reqValidator.GetWorkspaceServiceTokenDataV3, req);
|
||||
|
||||
const serviceTokenData = await ServiceTokenDataV3.find({
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
}).populate("customRole");
|
||||
|
||||
return res.status(200).send({
|
||||
serviceTokenData
|
||||
});
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
import jwt from "jsonwebtoken";
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import {
|
||||
IServiceTokenDataV3,
|
||||
IUser,
|
||||
import {
|
||||
ServiceTokenDataV3,
|
||||
ServiceTokenDataV3Key,
|
||||
Workspace
|
||||
ServiceTokenDataV3Key, // TODO: remove
|
||||
Organization,
|
||||
ServiceMembershipOrg
|
||||
} from "../../../models";
|
||||
import { IServiceTokenV3TrustedIp } from "../../../models/serviceTokenDataV3";
|
||||
import {
|
||||
@@ -17,44 +16,13 @@ import {
|
||||
import { validateRequest } from "../../../helpers/validation";
|
||||
import * as reqValidator from "../../../validation/serviceTokenDataV3";
|
||||
import { createToken } from "../../../helpers/auth";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
getAuthDataProjectPermissions
|
||||
} from "../../services/ProjectRoleService";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
|
||||
import { BadRequestError, ResourceNotFoundError, UnauthorizedRequestError } from "../../../utils/errors";
|
||||
import { extractIPDetails, isValidIpOrCidr } from "../../../utils/ip";
|
||||
import { EEAuditLogService, EELicenseService } from "../../services";
|
||||
import { getAuthSecret } from "../../../config";
|
||||
import { ADMIN, AuthTokenType, CUSTOM, MEMBER, VIEWER } from "../../../variables";
|
||||
|
||||
/**
|
||||
* Return project key for service token V3
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const getServiceTokenDataKey = async (req: Request, res: Response) => {
|
||||
const key = await ServiceTokenDataV3Key.findOne({
|
||||
serviceTokenData: (req.authData.authPayload as IServiceTokenDataV3)._id
|
||||
}).populate<{ sender: IUser }>("sender", "publicKey");
|
||||
|
||||
if (!key) throw ResourceNotFoundError({
|
||||
message: "Failed to find project key for service token"
|
||||
});
|
||||
|
||||
const { _id, workspace, encryptedKey, nonce, sender: { publicKey } } = key;
|
||||
|
||||
return res.status(200).send({
|
||||
key: {
|
||||
_id,
|
||||
workspace,
|
||||
encryptedKey,
|
||||
publicKey,
|
||||
nonce
|
||||
}
|
||||
});
|
||||
}
|
||||
import { ADMIN, AuthTokenType, CUSTOM, MEMBER } from "../../../variables";
|
||||
|
||||
/**
|
||||
* Return access and refresh token as per refresh operation
|
||||
@@ -159,44 +127,45 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: {
|
||||
name,
|
||||
workspaceId,
|
||||
publicKey,
|
||||
organizationId,
|
||||
role,
|
||||
trustedIps,
|
||||
expiresIn,
|
||||
accessTokenTTL,
|
||||
isRefreshTokenRotationEnabled,
|
||||
encryptedKey, // for ServiceTokenDataV3Key
|
||||
nonce, // for ServiceTokenDataV3Key
|
||||
isRefreshTokenRotationEnabled
|
||||
}
|
||||
} = await validateRequest(reqValidator.CreateServiceTokenV3, req);
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.ServiceTokens
|
||||
);
|
||||
// const { permission } = await getAuthDataProjectPermissions({
|
||||
// authData: req.authData,
|
||||
// workspaceId: new Types.ObjectId(workspaceId)
|
||||
// });
|
||||
|
||||
// ForbiddenError.from(permission).throwUnlessCan(
|
||||
// ProjectPermissionActions.Create,
|
||||
// ProjectPermissionSub.ServiceTokens
|
||||
// );
|
||||
|
||||
const workspace = await Workspace.findById(workspaceId);
|
||||
if (!workspace) throw BadRequestError({ message: "Workspace not found" });
|
||||
// const workspace = await Workspace.findById(workspaceId);
|
||||
// if (!workspace) throw BadRequestError({ message: "Workspace not found" });
|
||||
|
||||
const isCustomRole = ![ADMIN, MEMBER, VIEWER].includes(role);
|
||||
const organization = await Organization.findById(organizationId);
|
||||
if (!organization) throw BadRequestError({ message: "Organization not found" });
|
||||
|
||||
const isCustomRole = ![ADMIN, MEMBER].includes(role);
|
||||
|
||||
let customRole;
|
||||
if (isCustomRole) {
|
||||
customRole = await Role.findOne({
|
||||
slug: role,
|
||||
isOrgRole: false,
|
||||
workspace: workspace._id
|
||||
isOrgRole: true,
|
||||
organization: new Types.ObjectId(organizationId)
|
||||
});
|
||||
|
||||
if (!customRole) throw BadRequestError({ message: "Role not found" });
|
||||
}
|
||||
|
||||
const plan = await EELicenseService.getPlan(workspace.organization);
|
||||
const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId));
|
||||
|
||||
// validate trusted ips
|
||||
const reformattedTrustedIps = trustedIps.map((trustedIp) => {
|
||||
@@ -228,26 +197,22 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
|
||||
const serviceTokenData = await new ServiceTokenDataV3({
|
||||
name,
|
||||
user,
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
publicKey,
|
||||
organization: new Types.ObjectId(organizationId),
|
||||
refreshTokenUsageCount: 0,
|
||||
accessTokenUsageCount: 0,
|
||||
tokenVersion: 1,
|
||||
trustedIps: reformattedTrustedIps,
|
||||
role: isCustomRole ? CUSTOM : role,
|
||||
customRole,
|
||||
isActive,
|
||||
expiresAt,
|
||||
accessTokenTTL,
|
||||
isRefreshTokenRotationEnabled
|
||||
}).save();
|
||||
|
||||
await new ServiceTokenDataV3Key({
|
||||
encryptedKey,
|
||||
nonce,
|
||||
sender: req.user._id,
|
||||
serviceTokenData: serviceTokenData._id,
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
await new ServiceMembershipOrg({
|
||||
service: serviceTokenData._id,
|
||||
organization: serviceTokenData.organization,
|
||||
role: isCustomRole ? CUSTOM : role,
|
||||
customRole
|
||||
}).save();
|
||||
|
||||
const refreshToken = createToken({
|
||||
@@ -262,7 +227,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.CREATE_SERVICE_TOKEN_V3, // TODO: update
|
||||
type: EventType.CREATE_SERVICE_TOKEN_V3,
|
||||
metadata: {
|
||||
name,
|
||||
isActive,
|
||||
@@ -272,7 +237,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
organizationId: new Types.ObjectId(organizationId)
|
||||
}
|
||||
);
|
||||
|
||||
@@ -294,7 +259,7 @@ export const updateServiceTokenData = async (req: Request, res: Response) => {
|
||||
body: {
|
||||
name,
|
||||
isActive,
|
||||
role,
|
||||
role, // bring this somewhere else?
|
||||
trustedIps,
|
||||
expiresIn,
|
||||
accessTokenTTL,
|
||||
@@ -307,34 +272,34 @@ export const updateServiceTokenData = async (req: Request, res: Response) => {
|
||||
message: "Service token not found"
|
||||
});
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: serviceTokenData.workspace
|
||||
});
|
||||
// const { permission } = await getAuthDataProjectPermissions({
|
||||
// authData: req.authData,
|
||||
// workspaceId: serviceTokenData.workspace
|
||||
// });
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionSub.ServiceTokens
|
||||
);
|
||||
// ForbiddenError.from(permission).throwUnlessCan(
|
||||
// ProjectPermissionActions.Edit,
|
||||
// ProjectPermissionSub.ServiceTokens
|
||||
// );
|
||||
|
||||
const workspace = await Workspace.findById(serviceTokenData.workspace);
|
||||
if (!workspace) throw BadRequestError({ message: "Workspace not found" });
|
||||
// const workspace = await Workspace.findById(serviceTokenData.workspace);
|
||||
// if (!workspace) throw BadRequestError({ message: "Workspace not found" });
|
||||
|
||||
let customRole;
|
||||
if (role) {
|
||||
const isCustomRole = ![ADMIN, MEMBER, VIEWER].includes(role);
|
||||
const isCustomRole = ![ADMIN, MEMBER].includes(role);
|
||||
if (isCustomRole) {
|
||||
customRole = await Role.findOne({
|
||||
slug: role,
|
||||
isOrgRole: false,
|
||||
workspace: workspace._id
|
||||
isOrgRole: true,
|
||||
organization: serviceTokenData.organization
|
||||
});
|
||||
|
||||
if (!customRole) throw BadRequestError({ message: "Role not found" });
|
||||
}
|
||||
}
|
||||
|
||||
const plan = await EELicenseService.getPlan(workspace.organization);
|
||||
const plan = await EELicenseService.getPlan(serviceTokenData.organization);
|
||||
|
||||
// validate trusted ips
|
||||
let reformattedTrustedIps;
|
||||
@@ -365,15 +330,7 @@ export const updateServiceTokenData = async (req: Request, res: Response) => {
|
||||
{
|
||||
name,
|
||||
isActive,
|
||||
role: customRole ? CUSTOM : role,
|
||||
...(customRole ? {
|
||||
customRole
|
||||
} : {}),
|
||||
...(role && !customRole ? { // non-custom role
|
||||
$unset: {
|
||||
customRole: 1
|
||||
}
|
||||
} : {}),
|
||||
|
||||
trustedIps: reformattedTrustedIps,
|
||||
expiresAt,
|
||||
accessTokenTTL,
|
||||
@@ -387,6 +344,26 @@ export const updateServiceTokenData = async (req: Request, res: Response) => {
|
||||
if (!serviceTokenData) throw BadRequestError({
|
||||
message: "Failed to update service token"
|
||||
});
|
||||
|
||||
await ServiceMembershipOrg.findOneAndUpdate(
|
||||
{
|
||||
service: serviceTokenData._id
|
||||
},
|
||||
{
|
||||
role: customRole ? CUSTOM : role,
|
||||
...(customRole ? {
|
||||
customRole
|
||||
} : {}),
|
||||
...(role && !customRole ? { // non-custom role
|
||||
$unset: {
|
||||
customRole: 1
|
||||
}
|
||||
} : {})
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
@@ -401,7 +378,7 @@ export const updateServiceTokenData = async (req: Request, res: Response) => {
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: serviceTokenData.workspace
|
||||
organizationId: serviceTokenData.organization
|
||||
}
|
||||
);
|
||||
|
||||
@@ -426,21 +403,29 @@ export const deleteServiceTokenData = async (req: Request, res: Response) => {
|
||||
message: "Service token not found"
|
||||
});
|
||||
|
||||
const { permission } = await getAuthDataProjectPermissions({
|
||||
authData: req.authData,
|
||||
workspaceId: serviceTokenData.workspace
|
||||
});
|
||||
// const { permission } = await getAuthDataProjectPermissions({
|
||||
// authData: req.authData,
|
||||
// workspaceId: serviceTokenData.workspace
|
||||
// });
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSub.ServiceTokens
|
||||
);
|
||||
// ForbiddenError.from(permission).throwUnlessCan(
|
||||
// ProjectPermissionActions.Delete,
|
||||
// ProjectPermissionSub.ServiceTokens
|
||||
// );
|
||||
|
||||
serviceTokenData = await ServiceTokenDataV3.findByIdAndDelete(serviceTokenDataId);
|
||||
|
||||
if (!serviceTokenData) throw BadRequestError({
|
||||
message: "Failed to delete service token"
|
||||
});
|
||||
|
||||
const serviceMembershipOrg = await ServiceMembershipOrg.findOneAndDelete({
|
||||
service: serviceTokenData._id,
|
||||
});
|
||||
|
||||
if (!serviceMembershipOrg) throw BadRequestError({
|
||||
message: "Failed to delete service token"
|
||||
});
|
||||
|
||||
await ServiceTokenDataV3Key.findOneAndDelete({
|
||||
serviceTokenData: serviceTokenData._id
|
||||
@@ -453,13 +438,13 @@ export const deleteServiceTokenData = async (req: Request, res: Response) => {
|
||||
metadata: {
|
||||
name: serviceTokenData.name,
|
||||
isActive: serviceTokenData.isActive,
|
||||
role: serviceTokenData.role,
|
||||
role: serviceMembershipOrg.role,
|
||||
trustedIps: serviceTokenData.trustedIps as Array<IServiceTokenV3TrustedIp>,
|
||||
expiresAt: serviceTokenData.expiresAt
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId: serviceTokenData.workspace
|
||||
organizationId: serviceTokenData.organization
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -4,14 +4,6 @@ import { requireAuth } from "../../../middleware";
|
||||
import { AuthMode } from "../../../variables";
|
||||
import { serviceTokenDataController } from "../../controllers/v3";
|
||||
|
||||
router.get(
|
||||
"/me/key",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.SERVICE_ACCESS_TOKEN]
|
||||
}),
|
||||
serviceTokenDataController.getServiceTokenDataKey
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/me/token",
|
||||
serviceTokenDataController.refreshToken
|
||||
|
||||
@@ -66,7 +66,7 @@ class EELicenseService {
|
||||
secretVersioning: true,
|
||||
pitRecovery: false,
|
||||
ipAllowlisting: false,
|
||||
rbac: false,
|
||||
rbac: true,
|
||||
customRateLimits: false,
|
||||
customAlerts: false,
|
||||
auditLogs: false,
|
||||
|
||||
@@ -12,7 +12,12 @@ import { FieldCondition, FieldInstruction, JsInterpreter } from "@ucast/mongo2js
|
||||
import picomatch from "picomatch";
|
||||
import { AuthData } from "../../interfaces/middleware";
|
||||
import { ActorType, IRole } from "../models";
|
||||
import { Membership, ServiceTokenData, ServiceTokenDataV3 } from "../../models";
|
||||
import {
|
||||
IServiceTokenDataV3,
|
||||
Membership,
|
||||
ServiceMembership,
|
||||
ServiceTokenData
|
||||
} from "../../models";
|
||||
import { ADMIN, CUSTOM, MEMBER, VIEWER } from "../../variables";
|
||||
import { checkIPAgainstBlocklist } from "../../utils/ip";
|
||||
|
||||
@@ -265,10 +270,10 @@ export const getAuthDataProjectPermissions = async ({
|
||||
user: authData.authPayload._id,
|
||||
workspace: workspaceId
|
||||
})
|
||||
.populate<{
|
||||
customRole: IRole & { permissions: RawRuleOf<MongoAbility<ProjectPermissionSet>>[] };
|
||||
}>("customRole")
|
||||
.exec();
|
||||
.populate<{
|
||||
customRole: IRole & { permissions: RawRuleOf<MongoAbility<ProjectPermissionSet>>[] };
|
||||
}>("customRole")
|
||||
.exec();
|
||||
|
||||
if (!membership || (membership.role === "custom" && !membership.customRole)) {
|
||||
throw UnauthorizedRequestError();
|
||||
@@ -285,24 +290,27 @@ export const getAuthDataProjectPermissions = async ({
|
||||
break;
|
||||
}
|
||||
case ActorType.SERVICE_V3: {
|
||||
const serviceTokenData = await ServiceTokenDataV3
|
||||
.findById(authData.authPayload._id)
|
||||
.populate<{
|
||||
customRole: IRole & { permissions: RawRuleOf<MongoAbility<ProjectPermissionSet>>[] };
|
||||
}>("customRole")
|
||||
.exec();
|
||||
|
||||
if (!serviceTokenData || (serviceTokenData.role === "custom" && !serviceTokenData.customRole)) {
|
||||
const serviceMembership = await ServiceMembership.findOne({
|
||||
service: authData.authPayload._id,
|
||||
workspace: workspaceId
|
||||
})
|
||||
.populate<{
|
||||
customRole: IRole & { permissions: RawRuleOf<MongoAbility<ProjectPermissionSet>>[] };
|
||||
service: IServiceTokenDataV3
|
||||
}>("customRole service")
|
||||
.exec();
|
||||
|
||||
if (!serviceMembership || (serviceMembership.role === "custom" && !serviceMembership.customRole)) {
|
||||
throw UnauthorizedRequestError();
|
||||
}
|
||||
|
||||
checkIPAgainstBlocklist({
|
||||
ipAddress: authData.ipAddress,
|
||||
trustedIps: serviceTokenData.trustedIps
|
||||
trustedIps: serviceMembership.service.trustedIps
|
||||
});
|
||||
|
||||
role = serviceTokenData.role;
|
||||
customRole = serviceTokenData.customRole;
|
||||
role = serviceMembership.role;
|
||||
customRole = serviceMembership.customRole;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AbilityBuilder, MongoAbility, RawRuleOf, createMongoAbility } from "@casl/ability";
|
||||
import { MembershipOrg } from "../../models";
|
||||
import { IRole } from "../models/role";
|
||||
import { IRole } from "../models";
|
||||
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
|
||||
import { ACCEPTED } from "../../variables";
|
||||
import { conditionsMatcher } from "./ProjectRoleService";
|
||||
@@ -20,7 +20,8 @@ export enum OrgPermissionSubjects {
|
||||
IncidentAccount = "incident-contact",
|
||||
Sso = "sso",
|
||||
Billing = "billing",
|
||||
SecretScanning = "secret-scanning"
|
||||
SecretScanning = "secret-scanning",
|
||||
ServiceTokens = "service-tokens" // TODO: consider renaming
|
||||
}
|
||||
|
||||
export type OrgPermissionSet =
|
||||
@@ -32,7 +33,8 @@ export type OrgPermissionSet =
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.IncidentAccount]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Sso]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Billing];
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.ServiceTokens];
|
||||
|
||||
const buildAdminPermission = () => {
|
||||
const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
|
||||
@@ -75,6 +77,11 @@ const buildAdminPermission = () => {
|
||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing);
|
||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Billing);
|
||||
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.ServiceTokens);
|
||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.ServiceTokens);
|
||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.ServiceTokens);
|
||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.ServiceTokens);
|
||||
|
||||
return build({ conditionsMatcher });
|
||||
};
|
||||
|
||||
@@ -98,6 +105,11 @@ const buildMemberPermission = () => {
|
||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.SecretScanning);
|
||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.SecretScanning);
|
||||
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.ServiceTokens);
|
||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.ServiceTokens);
|
||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.ServiceTokens);
|
||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.ServiceTokens);
|
||||
|
||||
return build({ conditionsMatcher });
|
||||
};
|
||||
|
||||
@@ -105,6 +117,7 @@ export const memberPermissions = buildMemberPermission();
|
||||
|
||||
export const getUserOrgPermissions = async (userId: string, orgId: string) => {
|
||||
// TODO(akhilmhdh): speed this up by pulling from cache later
|
||||
|
||||
const membership = await MembershipOrg.findOne({
|
||||
user: userId,
|
||||
organization: orgId,
|
||||
@@ -131,4 +144,4 @@ export const getUserOrgPermissions = async (userId: string, orgId: string) => {
|
||||
}
|
||||
|
||||
throw BadRequestError({ message: "User role not found" });
|
||||
};
|
||||
};
|
||||
@@ -21,7 +21,9 @@ export * from "./userAction";
|
||||
export * from "./workspace";
|
||||
export * from "./serviceTokenData"; // TODO: deprecate
|
||||
export * from "./serviceTokenDataV3";
|
||||
export * from "./serviceTokenDataV3Key";
|
||||
export * from "./serviceMembershipOrg";
|
||||
export * from "./serviceMembership";
|
||||
export * from "./serviceTokenDataV3Key"; /// TODO: remove
|
||||
export * from "./apiKeyData"; // TODO: deprecate
|
||||
export * from "./apiKeyDataV2";
|
||||
export * from "./loginSRPDetail";
|
||||
|
||||
38
backend/src/models/serviceMembership.ts
Normal file
38
backend/src/models/serviceMembership.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Schema, Types, model } from "mongoose";
|
||||
import { ADMIN, CUSTOM, MEMBER, VIEWER } from "../variables";
|
||||
|
||||
export interface IServiceMembership {
|
||||
_id: Types.ObjectId;
|
||||
service: Types.ObjectId;
|
||||
workspace: Types.ObjectId;
|
||||
role: "admin" | "member" | "viewer" | "custom";
|
||||
customRole: Types.ObjectId;
|
||||
}
|
||||
|
||||
const serviceMembershipSchema = new Schema<IServiceMembership>(
|
||||
{
|
||||
service: { // TODO: consider renaming
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "ServiceTokenDataV3"
|
||||
},
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Workspace",
|
||||
required: true
|
||||
},
|
||||
role: {
|
||||
type: String,
|
||||
enum: [ADMIN, MEMBER, VIEWER, CUSTOM],
|
||||
required: true
|
||||
},
|
||||
customRole: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Role"
|
||||
}
|
||||
},
|
||||
{
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
export const ServiceMembership = model<IServiceMembership>("ServiceMembership", serviceMembershipSchema);
|
||||
37
backend/src/models/serviceMembershipOrg.ts
Normal file
37
backend/src/models/serviceMembershipOrg.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Schema, Types, model } from "mongoose";
|
||||
import { ADMIN, CUSTOM, MEMBER } from "../variables";
|
||||
|
||||
export interface IServiceMembershipOrg {
|
||||
_id: Types.ObjectId;
|
||||
service: Types.ObjectId;
|
||||
organization: Types.ObjectId;
|
||||
role: "admin" | "member" | "viewer" | "custom";
|
||||
customRole: Types.ObjectId;
|
||||
}
|
||||
|
||||
const serviceMembershipOrgSchema = new Schema<IServiceMembershipOrg>(
|
||||
{
|
||||
service: { // TODO: consider renaming
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "ServiceTokenDataV3"
|
||||
},
|
||||
organization: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Organization"
|
||||
},
|
||||
role: {
|
||||
type: String,
|
||||
enum: [ADMIN, MEMBER, CUSTOM],
|
||||
required: true
|
||||
},
|
||||
customRole: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Role"
|
||||
}
|
||||
},
|
||||
{
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
export const ServiceMembershipOrg = model<IServiceMembershipOrg>("ServiceMembershipOrg", serviceMembershipOrgSchema);
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Document, Schema, Types, model } from "mongoose";
|
||||
import { IPType } from "../ee/models";
|
||||
import { ADMIN, CUSTOM, MEMBER, VIEWER } from "../variables";
|
||||
|
||||
export interface IServiceTokenV3TrustedIp {
|
||||
ipAddress: string;
|
||||
@@ -11,9 +10,8 @@ export interface IServiceTokenV3TrustedIp {
|
||||
export interface IServiceTokenDataV3 extends Document {
|
||||
_id: Types.ObjectId;
|
||||
name: string;
|
||||
workspace: Types.ObjectId;
|
||||
organization: Types.ObjectId;
|
||||
user: Types.ObjectId;
|
||||
publicKey: string;
|
||||
isActive: boolean;
|
||||
refreshTokenLastUsed?: Date;
|
||||
accessTokenLastUsed?: Date;
|
||||
@@ -23,8 +21,6 @@ export interface IServiceTokenDataV3 extends Document {
|
||||
isRefreshTokenRotationEnabled: boolean;
|
||||
expiresAt?: Date;
|
||||
accessTokenTTL: number;
|
||||
role: "admin" | "member" | "viewer" | "custom";
|
||||
customRole: Types.ObjectId;
|
||||
trustedIps: Array<IServiceTokenV3TrustedIp>;
|
||||
}
|
||||
|
||||
@@ -34,9 +30,9 @@ const serviceTokenDataV3Schema = new Schema(
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
workspace: {
|
||||
organization: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Workspace",
|
||||
ref: "Organization",
|
||||
required: true
|
||||
},
|
||||
user: {
|
||||
@@ -44,10 +40,6 @@ const serviceTokenDataV3Schema = new Schema(
|
||||
ref: "User",
|
||||
required: true
|
||||
},
|
||||
publicKey: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
@@ -91,15 +83,6 @@ const serviceTokenDataV3Schema = new Schema(
|
||||
default: 7200,
|
||||
required: true
|
||||
},
|
||||
role: {
|
||||
type: String,
|
||||
enum: [ADMIN, MEMBER, VIEWER, CUSTOM],
|
||||
required: true
|
||||
},
|
||||
customRole: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Role"
|
||||
},
|
||||
trustedIps: {
|
||||
type: [
|
||||
{
|
||||
|
||||
@@ -54,4 +54,12 @@ router.delete(
|
||||
organizationsController.deleteOrganizationById
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/:organizationId/service-memberships",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT]
|
||||
}),
|
||||
organizationsController.getOrganizationServiceMemberships
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -93,4 +93,29 @@ router.patch(
|
||||
workspaceController.toggleAutoCapitalization
|
||||
);
|
||||
|
||||
// TODO: consider endpoint restructuring
|
||||
router.post(
|
||||
"/:workspaceId/service-memberships",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY]
|
||||
}),
|
||||
workspaceController.addWorkspaceServiceMembership
|
||||
);
|
||||
|
||||
router.delete(
|
||||
"/:workspaceId/service-memberships/:serviceId",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY]
|
||||
}),
|
||||
workspaceController.deleteWorkspaceServiceMembership
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/:workspaceId/service-memberships",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT]
|
||||
}),
|
||||
workspaceController.getWorkspaceServiceMemberships
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -34,12 +34,4 @@ router.post(
|
||||
|
||||
// --
|
||||
|
||||
router.get(
|
||||
"/:workspaceId/service-token",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT]
|
||||
}),
|
||||
workspacesController.getWorkspaceServiceTokenData
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -212,4 +212,12 @@ export const CreateOrgv2 = z.object({
|
||||
|
||||
export const DeleteOrgv2 = z.object({
|
||||
params: z.object({ organizationId: z.string().trim() })
|
||||
});
|
||||
|
||||
export const GetOrgServiceMembersV2 = z.object({
|
||||
params: z.object({ organizationId: z.string().trim() })
|
||||
});
|
||||
|
||||
export const GetOrgServiceTokenDataV3V2 = z.object({
|
||||
params: z.object({ organizationId: z.string().trim() })
|
||||
});
|
||||
@@ -10,8 +10,7 @@ export const RefreshTokenV3 = z.object({
|
||||
export const CreateServiceTokenV3 = z.object({
|
||||
body: z.object({
|
||||
name: z.string().trim(),
|
||||
workspaceId: z.string().trim(),
|
||||
publicKey: z.string().trim(),
|
||||
organizationId: z.string().trim(),
|
||||
role: z.string().trim().min(1).default(MEMBER),
|
||||
trustedIps: z // TODO: provide default
|
||||
.object({
|
||||
@@ -22,8 +21,6 @@ export const CreateServiceTokenV3 = z.object({
|
||||
.default([{ ipAddress: "0.0.0.0/0" }]),
|
||||
expiresIn: z.number().optional(),
|
||||
accessTokenTTL: z.number().int().min(1),
|
||||
encryptedKey: z.string().trim(),
|
||||
nonce: z.string().trim(),
|
||||
isRefreshTokenRotationEnabled: z.boolean().default(false)
|
||||
})
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import { AuthData } from "../interfaces/middleware";
|
||||
import { z } from "zod";
|
||||
import { EventType, UserAgentType } from "../ee/models";
|
||||
import { UnauthorizedRequestError } from "../utils/errors";
|
||||
import { MEMBER } from "../variables";
|
||||
|
||||
/**
|
||||
* Validate authenticated clients for workspace with id [workspaceId] based
|
||||
@@ -279,6 +280,29 @@ export const ToggleAutoCapitalizationV2 = z.object({
|
||||
})
|
||||
});
|
||||
|
||||
export const AddWorkspaceServiceMemberV2 = z.object({
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
serviceId: z.string().trim(), // TODO: consider renaming
|
||||
role: z.string().trim().min(1).default(MEMBER),
|
||||
})
|
||||
});
|
||||
|
||||
export const DeleteWorkspaceServiceMemberV2 = z.object({
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
serviceId: z.string().trim()
|
||||
})
|
||||
});
|
||||
|
||||
export const GetWorkspaceServiceMembersV2 = z.object({
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
}),
|
||||
});
|
||||
|
||||
export const GetWorkspaceBlinkIndexStatusV3 = z.object({
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
@@ -303,10 +327,4 @@ export const NameWorkspaceSecretsV3 = z.object({
|
||||
})
|
||||
.array()
|
||||
})
|
||||
});
|
||||
|
||||
export const GetWorkspaceServiceTokenDataV3 = z.object({
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
})
|
||||
});
|
||||
@@ -15,7 +15,8 @@ export enum OrgPermissionSubjects {
|
||||
IncidentAccount = "incident-contact",
|
||||
Sso = "sso",
|
||||
Billing = "billing",
|
||||
SecretScanning = "secret-scanning"
|
||||
SecretScanning = "secret-scanning",
|
||||
ServiceTokens = "service-tokens"
|
||||
}
|
||||
|
||||
export type OrgPermissionSet =
|
||||
@@ -27,6 +28,7 @@ export type OrgPermissionSet =
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.IncidentAccount]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Sso]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Billing];
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.ServiceTokens];
|
||||
|
||||
export type TOrgPermission = MongoAbility<OrgPermissionSet>;
|
||||
|
||||
@@ -14,6 +14,7 @@ export {
|
||||
useGetOrgPlansTable,
|
||||
useGetOrgPlanTable,
|
||||
useGetOrgPmtMethods,
|
||||
useGetOrgServiceMemberships,
|
||||
useGetOrgTaxIds,
|
||||
useGetOrgTrialUrl,
|
||||
useRenameOrg,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { ServiceMembershipOrg } from "../serviceTokens/types";
|
||||
import {
|
||||
BillingDetails,
|
||||
Invoice,
|
||||
@@ -15,7 +16,7 @@ import {
|
||||
TaxID
|
||||
} from "./types";
|
||||
|
||||
const organizationKeys = {
|
||||
export const organizationKeys = {
|
||||
getUserOrganizations: ["organization"] as const,
|
||||
getOrgPlanBillingInfo: (orgId: string) => [{ orgId }, "organization-plan-billing"] as const,
|
||||
getOrgPlanTable: (orgId: string) => [{ orgId }, "organization-plan-table"] as const,
|
||||
@@ -25,7 +26,8 @@ const organizationKeys = {
|
||||
getOrgPmtMethods: (orgId: string) => [{ orgId }, "organization-pmt-methods"] as const,
|
||||
getOrgTaxIds: (orgId: string) => [{ orgId }, "organization-tax-ids"] as const,
|
||||
getOrgInvoices: (orgId: string) => [{ orgId }, "organization-invoices"] as const,
|
||||
getOrgLicenses: (orgId: string) => [{ orgId }, "organization-licenses"] as const
|
||||
getOrgLicenses: (orgId: string) => [{ orgId }, "organization-licenses"] as const,
|
||||
getOrgServiceMemberships: (orgId: string) => [{ orgId }, "organization-service-memberships"] as const,
|
||||
};
|
||||
|
||||
export const fetchOrganizations = async () => {
|
||||
@@ -349,6 +351,22 @@ export const useGetOrgLicenses = (organizationId: string) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetOrgServiceMemberships = (organizationId: string) => {
|
||||
return useQuery({
|
||||
queryKey: organizationKeys.getOrgServiceMemberships(organizationId),
|
||||
queryFn: async () => {
|
||||
const {
|
||||
data: { serviceMemberships }
|
||||
} = await apiRequest.get<{ serviceMemberships: ServiceMembershipOrg[] }>(
|
||||
`/api/v2/organizations/${organizationId}/service-memberships`
|
||||
);
|
||||
|
||||
return serviceMemberships;
|
||||
},
|
||||
enabled: true
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteOrgById = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { workspaceKeys } from "../workspace/queries";
|
||||
import { organizationKeys } from "../organization/queries";
|
||||
import {
|
||||
CreateServiceTokenDataV3DTO,
|
||||
CreateServiceTokenDataV3Res,
|
||||
@@ -74,7 +74,7 @@ export const useCreateServiceTokenV3 = () => {
|
||||
return data;
|
||||
},
|
||||
onSuccess: ({ serviceTokenData }) => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getWorkspaceServiceTokenDataV3(serviceTokenData.workspace));
|
||||
queryClient.invalidateQueries(organizationKeys.getOrgServiceMemberships(serviceTokenData.organization));
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -104,8 +104,8 @@ export const useUpdateServiceTokenV3 = () => {
|
||||
|
||||
return serviceTokenData;
|
||||
},
|
||||
onSuccess: ({ workspace }) => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getWorkspaceServiceTokenDataV3(workspace));
|
||||
onSuccess: ({ organization }) => {
|
||||
queryClient.invalidateQueries(organizationKeys.getOrgServiceMemberships(organization));
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -119,8 +119,8 @@ export const useDeleteServiceTokenV3 = () => {
|
||||
const { data: { serviceTokenData } } = await apiRequest.delete(`/api/v3/service-token/${serviceTokenDataId}`);
|
||||
return serviceTokenData;
|
||||
},
|
||||
onSuccess: ({ workspace }) => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getWorkspaceServiceTokenDataV3(workspace));
|
||||
onSuccess: ({ organization }) => {
|
||||
queryClient.invalidateQueries(organizationKeys.getOrgServiceMemberships(organization));
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -1,3 +1,5 @@
|
||||
import { TRole } from "../roles/types";
|
||||
|
||||
export type ServiceTokenScope = {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
@@ -46,12 +48,7 @@ export type ServiceTokenV3TrustedIp = {
|
||||
export type ServiceTokenDataV3 = {
|
||||
_id: string;
|
||||
name: string;
|
||||
role: string;
|
||||
customRole?: {
|
||||
name: string;
|
||||
slug: string;
|
||||
};
|
||||
workspace: string;
|
||||
organization: string;
|
||||
isActive: boolean;
|
||||
refreshTokenLastUsed?: string;
|
||||
accessTokenLastUsed?: string;
|
||||
@@ -65,18 +62,35 @@ export type ServiceTokenDataV3 = {
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type ServiceMembershipOrg = {
|
||||
_id: string;
|
||||
service: ServiceTokenDataV3;
|
||||
organization: string;
|
||||
role: "admin" | "member" | "viewer" | "custom";
|
||||
customRole?: TRole;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export type ServiceMembership = {
|
||||
_id: string;
|
||||
service: ServiceTokenDataV3;
|
||||
organization: string;
|
||||
role: "admin" | "member" | "viewer" | "custom";
|
||||
customRole?: TRole;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export type CreateServiceTokenDataV3DTO = {
|
||||
name: string;
|
||||
organizationId: string;
|
||||
role?: string;
|
||||
workspaceId: string;
|
||||
publicKey: string;
|
||||
trustedIps: {
|
||||
ipAddress: string;
|
||||
}[];
|
||||
expiresIn?: number;
|
||||
accessTokenTTL: number;
|
||||
encryptedKey: string;
|
||||
nonce: string;
|
||||
isRefreshTokenRotationEnabled: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
export {
|
||||
useAddServiceToWorkspace,
|
||||
useAddUserToWorkspace,
|
||||
useCreateWorkspace,
|
||||
useCreateWsEnvironment,
|
||||
useDeleteServiceFromWorkspace,
|
||||
useDeleteUserFromWorkspace,
|
||||
useDeleteWorkspace,
|
||||
useDeleteWsEnvironment,
|
||||
@@ -12,11 +14,12 @@ export {
|
||||
useGetWorkspaceIndexStatus,
|
||||
useGetWorkspaceIntegrations,
|
||||
useGetWorkspaceSecrets,
|
||||
useGetWorkspaceServiceTokenDataV3,
|
||||
useGetWorkspaceServiceMemberships,
|
||||
useGetWorkspaceUsers,
|
||||
useNameWorkspaceSecrets,
|
||||
useRenameWorkspace,
|
||||
useReorderWsEnvironment,
|
||||
useToggleAutoCapitalization,
|
||||
useUpdateUserWorkspaceRole,
|
||||
useUpdateWsEnvironment} from "./queries";
|
||||
useUpdateWsEnvironment,
|
||||
} from "./queries";
|
||||
|
||||
@@ -5,7 +5,7 @@ import { apiRequest } from "@app/config/request";
|
||||
import { IntegrationAuth } from "../integrationAuth/types";
|
||||
import { TIntegration } from "../integrations/types";
|
||||
import { EncryptedSecret } from "../secrets/types";
|
||||
import { ServiceTokenDataV3 } from "../serviceTokens/types";
|
||||
import { ServiceMembership } from "../serviceTokens/types";
|
||||
import { TWorkspaceUser } from "../users/types";
|
||||
import {
|
||||
CreateEnvironmentDTO,
|
||||
@@ -32,7 +32,8 @@ export const workspaceKeys = {
|
||||
getWorkspaceAuditLogs: (workspaceId: string) => [{ workspaceId }] as const,
|
||||
getWorkspaceUsers: (workspaceId: string) => [{ workspaceId }] as const,
|
||||
getWorkspaceServiceTokenDataV3: (workspaceId: string) =>
|
||||
[{ workspaceId }, "workspace-service-token-data-v3"] as const
|
||||
[{ workspaceId }, "workspace-service-token-data-v3"] as const,
|
||||
getWorkspaceServiceMemberships: (workspaceId: string) => [{ workspaceId }, "organization-service-memberships"] as const
|
||||
};
|
||||
|
||||
const fetchWorkspaceById = async (workspaceId: string) => {
|
||||
@@ -356,17 +357,69 @@ export const useUpdateUserWorkspaceRole = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetWorkspaceServiceTokenDataV3 = (workspaceId: string) => {
|
||||
return useQuery({
|
||||
queryKey: workspaceKeys.getWorkspaceServiceTokenDataV3(workspaceId),
|
||||
queryFn: async () => {
|
||||
export const useAddServiceToWorkspace = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
serviceId,
|
||||
workspaceId,
|
||||
role
|
||||
}: {
|
||||
serviceId: string;
|
||||
workspaceId: string;
|
||||
role?: string;
|
||||
}) => {
|
||||
|
||||
const {
|
||||
data: { serviceTokenData }
|
||||
} = await apiRequest.get<{ serviceTokenData: ServiceTokenDataV3[] }>(
|
||||
`/api/v3/workspaces/${workspaceId}/service-token`
|
||||
data: { serviceMembership }
|
||||
} = await apiRequest.post(`/api/v2/workspace/${workspaceId}/service-memberships`, {
|
||||
serviceId,
|
||||
role
|
||||
});
|
||||
|
||||
return serviceMembership;
|
||||
},
|
||||
onSuccess: (_, { workspaceId }) => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getWorkspaceServiceMemberships(workspaceId));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteServiceFromWorkspace = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
serviceId,
|
||||
workspaceId,
|
||||
}: {
|
||||
serviceId: string;
|
||||
workspaceId: string;
|
||||
}) => {
|
||||
|
||||
const {
|
||||
data: { serviceMembership }
|
||||
} = await apiRequest.delete(`/api/v2/workspace/${workspaceId}/service-memberships/${serviceId}`);
|
||||
|
||||
return serviceMembership;
|
||||
},
|
||||
onSuccess: (_, { workspaceId }) => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getWorkspaceServiceMemberships(workspaceId));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetWorkspaceServiceMemberships = (workspaceId: string) => {
|
||||
return useQuery({
|
||||
queryKey: workspaceKeys.getWorkspaceServiceMemberships(workspaceId),
|
||||
queryFn: async () => {
|
||||
|
||||
const {
|
||||
data: { serviceMemberships }
|
||||
} = await apiRequest.get<{ serviceMemberships: ServiceMembership[] }>(
|
||||
`/api/v2/workspace/${workspaceId}/service-memberships`
|
||||
);
|
||||
|
||||
return serviceTokenData;
|
||||
return serviceMemberships;
|
||||
},
|
||||
enabled: true
|
||||
});
|
||||
|
||||
@@ -591,7 +591,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
isSelected={router.asPath === `/org/${currentOrg?._id}/members`}
|
||||
icon="system-outline-96-groups"
|
||||
>
|
||||
Members
|
||||
Access Control
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
@@ -1,41 +1,32 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||
import { withPermission } from "@app/hoc";
|
||||
import { useGetRoles } from "@app/hooks/api";
|
||||
import { TRole } from "@app/hooks/api/roles/types";
|
||||
|
||||
import { OrgMembersTable } from "./components/OrgMembersTable";
|
||||
import { OrgRoleTabSection } from "./components/OrgRoleTabSection";
|
||||
import { OrgServiceTokenTab } from "./components/OrgServiceTokenTab";
|
||||
|
||||
enum TabSections {
|
||||
Member = "members",
|
||||
Roles = "roles"
|
||||
Roles = "roles",
|
||||
ServiceTokens = "service-tokens"
|
||||
}
|
||||
|
||||
export const MembersPage = withPermission(
|
||||
() => {
|
||||
const { t } = useTranslation();
|
||||
const { currentOrg } = useOrganization();
|
||||
|
||||
const orgId = currentOrg?._id || "";
|
||||
|
||||
const { data: roles, isLoading: isRolesLoading } = useGetRoles({
|
||||
orgId
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mb-6 w-full py-6 px-6 max-w-7xl mx-auto">
|
||||
<p className="mr-4 mb-4 text-3xl font-semibold text-white">
|
||||
{t("section.members.org-members")}
|
||||
Access Control
|
||||
</p>
|
||||
<Tabs defaultValue={TabSections.Member}>
|
||||
<TabList>
|
||||
<Tab value={TabSections.Member}>Members</Tab>
|
||||
<Tab value={TabSections.ServiceTokens}>Service Accounts</Tab>
|
||||
<Tab value={TabSections.Roles}>Roles</Tab>
|
||||
</TabList>
|
||||
<TabPanel value={TabSections.Member}>
|
||||
@@ -46,17 +37,14 @@ export const MembersPage = withPermission(
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
>
|
||||
<OrgMembersTable
|
||||
roles={roles as TRole<undefined>[]}
|
||||
isRolesLoading={isRolesLoading}
|
||||
/>
|
||||
<OrgMembersTable />
|
||||
</motion.div>
|
||||
</TabPanel>
|
||||
<TabPanel value={TabSections.ServiceTokens}>
|
||||
<OrgServiceTokenTab />
|
||||
</TabPanel>
|
||||
<TabPanel value={TabSections.Roles}>
|
||||
<OrgRoleTabSection
|
||||
roles={roles as TRole<undefined>[]}
|
||||
isRolesLoading={isRolesLoading}
|
||||
/>
|
||||
<OrgRoleTabSection />
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
@@ -55,27 +55,21 @@ import {
|
||||
useAddUserToOrg,
|
||||
useDeleteOrgMembership,
|
||||
useGetOrgUsers,
|
||||
useGetRoles,
|
||||
useGetSSOConfig,
|
||||
useGetUserWorkspaceMemberships,
|
||||
useGetUserWsKey,
|
||||
useUpdateOrgUserRole,
|
||||
useUploadWsKey
|
||||
} from "@app/hooks/api";
|
||||
import { TRole } from "@app/hooks/api/roles/types";
|
||||
useUploadWsKey} from "@app/hooks/api";
|
||||
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
||||
|
||||
type Props = {
|
||||
roles?: TRole<undefined>[];
|
||||
isRolesLoading?: boolean;
|
||||
};
|
||||
|
||||
const addMemberFormSchema = yup.object({
|
||||
email: yup.string().email().required().label("Email").trim().lowercase()
|
||||
});
|
||||
|
||||
type TAddMemberForm = yup.InferType<typeof addMemberFormSchema>;
|
||||
|
||||
export const OrgMembersTable = ({ roles = [], isRolesLoading }: Props) => {
|
||||
export const OrgMembersTable = () => {
|
||||
const router = useRouter();
|
||||
const { createNotification } = useNotificationContext();
|
||||
|
||||
@@ -86,6 +80,10 @@ export const OrgMembersTable = ({ roles = [], isRolesLoading }: Props) => {
|
||||
const orgId = currentOrg?._id || "";
|
||||
const workspaceId = currentWorkspace?._id || "";
|
||||
|
||||
const { data: roles, isLoading: isRolesLoading } = useGetRoles({
|
||||
orgId
|
||||
});
|
||||
|
||||
const { data: ssoConfig, isLoading: isLoadingSSOConfig } = useGetSSOConfig(orgId);
|
||||
const [searchMemberFilter, setSearchMemberFilter] = useState("");
|
||||
const { data: serverDetails } = useFetchServerStatus();
|
||||
@@ -211,7 +209,7 @@ export const OrgMembersTable = ({ roles = [], isRolesLoading }: Props) => {
|
||||
|
||||
const findRoleFromId = useCallback(
|
||||
(roleId: string) => {
|
||||
return roles.find(({ _id: id }) => id === roleId);
|
||||
return (roles || []).find(({ _id: id }) => id === roleId);
|
||||
},
|
||||
[roles]
|
||||
);
|
||||
@@ -377,7 +375,7 @@ export const OrgMembersTable = ({ roles = [], isRolesLoading }: Props) => {
|
||||
onRoleChange(orgMembershipId, selectedRole)
|
||||
}
|
||||
>
|
||||
{roles
|
||||
{(roles || [])
|
||||
.filter(({ slug }) =>
|
||||
slug === "owner" ? isIamOwner || role === "owner" : true
|
||||
)
|
||||
|
||||
@@ -6,14 +6,8 @@ import { TRole } from "@app/hooks/api/roles/types";
|
||||
import { OrgRoleModifySection } from "./OrgRoleModifySection";
|
||||
import { OrgRoleTable } from "./OrgRoleTable";
|
||||
|
||||
type Props = {
|
||||
roles?: TRole<undefined>[];
|
||||
isRolesLoading?: boolean;
|
||||
};
|
||||
|
||||
export const OrgRoleTabSection = ({ roles = [], isRolesLoading }: Props) => {
|
||||
export const OrgRoleTabSection = () => {
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp(["editRole"] as const);
|
||||
|
||||
return popUp.editRole.isOpen ? (
|
||||
<motion.div
|
||||
key="role-modify"
|
||||
@@ -35,11 +29,7 @@ export const OrgRoleTabSection = ({ roles = [], isRolesLoading }: Props) => {
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: -30 }}
|
||||
>
|
||||
<OrgRoleTable
|
||||
roles={roles}
|
||||
isRolesLoading={isRolesLoading}
|
||||
onSelectRole={(role) => handlePopUpOpen("editRole", role)}
|
||||
/>
|
||||
<OrgRoleTable onSelectRole={(role) => handlePopUpOpen("editRole", role)} />
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -20,22 +20,26 @@ import {
|
||||
} from "@app/components/v2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useDeleteRole } from "@app/hooks/api";
|
||||
import { useDeleteRole, useGetRoles } from "@app/hooks/api";
|
||||
import { TRole } from "@app/hooks/api/roles/types";
|
||||
|
||||
type Props = {
|
||||
isRolesLoading?: boolean;
|
||||
roles?: TRole<undefined>[];
|
||||
onSelectRole: (role?: TRole<undefined>) => void;
|
||||
};
|
||||
|
||||
export const OrgRoleTable = ({ isRolesLoading, roles = [], onSelectRole }: Props) => {
|
||||
export const OrgRoleTable = ({
|
||||
onSelectRole
|
||||
}: Props) => {
|
||||
const [searchRoles, setSearchRoles] = useState("");
|
||||
const { currentOrg } = useOrganization();
|
||||
const orgId = currentOrg?._id || "";
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp(["deleteRole"] as const);
|
||||
|
||||
const { data: roles, isLoading: isRolesLoading } = useGetRoles({
|
||||
orgId
|
||||
});
|
||||
|
||||
const { mutateAsync: deleteRole } = useDeleteRole();
|
||||
|
||||
const handleRoleDelete = async () => {
|
||||
@@ -88,7 +92,7 @@ export const OrgRoleTable = ({ isRolesLoading, roles = [], onSelectRole }: Props
|
||||
</THead>
|
||||
<TBody>
|
||||
{isRolesLoading && <TableSkeleton columns={4} innerKey="org-roles" />}
|
||||
{roles?.map((role) => {
|
||||
{(roles as TRole<undefined>[])?.map((role) => {
|
||||
const { _id: id, name, slug } = role;
|
||||
const isNonMutatable = ["owner", "admin", "member"].includes(slug);
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { ServiceTokenV3Section } from "./components";
|
||||
|
||||
export const OrgServiceTokenTab = () => {
|
||||
return (
|
||||
<motion.div
|
||||
key="panel-service-token"
|
||||
transition={{ duration: 0.15 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
>
|
||||
<ServiceTokenV3Section />
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
@@ -4,15 +4,9 @@ import { faCheck, faCopy,faPlus, faXmark } from "@fortawesome/free-solid-svg-ico
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import { motion } from "framer-motion";
|
||||
import nacl from "tweetnacl";
|
||||
import { encodeBase64 } from "tweetnacl-util";
|
||||
import * as yup from "yup";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import {
|
||||
decryptAssymmetric,
|
||||
encryptAssymmetric
|
||||
} from "@app/components/utilities/cryptography/crypto";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
@@ -30,15 +24,14 @@ import {
|
||||
UpgradePlanModal} from "@app/components/v2";
|
||||
import {
|
||||
useOrganization,
|
||||
useSubscription,
|
||||
useWorkspace
|
||||
useSubscription
|
||||
} from "@app/context";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import {
|
||||
useCreateServiceTokenV3,
|
||||
useGetRoles,
|
||||
useGetUserWsKey,
|
||||
useUpdateServiceTokenV3} from "@app/hooks/api";
|
||||
useUpdateServiceTokenV3
|
||||
} from "@app/hooks/api";
|
||||
import { ServiceTokenV3TrustedIp } from "@app/hooks/api/serviceTokens/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
@@ -101,17 +94,13 @@ export const AddServiceTokenV3Modal = ({
|
||||
|
||||
const { subscription } = useSubscription();
|
||||
const { currentOrg } = useOrganization();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const orgId = currentOrg?._id || "";
|
||||
const workspaceId = currentWorkspace?._id || "";
|
||||
|
||||
const { data: roles } = useGetRoles({
|
||||
orgId,
|
||||
workspaceId
|
||||
orgId
|
||||
});
|
||||
|
||||
const { data: latestFileKey } = useGetUserWsKey(workspaceId);
|
||||
const { mutateAsync: createMutateAsync } = useCreateServiceTokenV3();
|
||||
const { mutateAsync: updateMutateAsync } = useUpdateServiceTokenV3();
|
||||
const { createNotification } = useNotificationContext();
|
||||
@@ -200,6 +189,7 @@ export const AddServiceTokenV3Modal = ({
|
||||
isRefreshTokenRotationEnabled
|
||||
}: FormData) => {
|
||||
try {
|
||||
|
||||
const serviceTokenData = popUp?.serviceTokenV3?.data as {
|
||||
serviceTokenDataId: string;
|
||||
name: string;
|
||||
@@ -221,63 +211,22 @@ export const AddServiceTokenV3Modal = ({
|
||||
|
||||
handlePopUpToggle("serviceTokenV3", false);
|
||||
} else {
|
||||
// create
|
||||
if (!workspaceId) return;
|
||||
if (!latestFileKey) return;
|
||||
|
||||
const pair = nacl.box.keyPair();
|
||||
const secretKeyUint8Array = pair.secretKey;
|
||||
const publicKeyUint8Array = pair.publicKey;
|
||||
const privateKey = encodeBase64(secretKeyUint8Array);
|
||||
const publicKey = encodeBase64(publicKeyUint8Array);
|
||||
|
||||
const key = decryptAssymmetric({
|
||||
ciphertext: latestFileKey.encryptedKey,
|
||||
nonce: latestFileKey.nonce,
|
||||
publicKey: latestFileKey.sender.publicKey,
|
||||
privateKey: localStorage.getItem("PRIVATE_KEY") as string
|
||||
});
|
||||
|
||||
const { ciphertext, nonce } = encryptAssymmetric({
|
||||
plaintext: key,
|
||||
publicKey,
|
||||
privateKey: localStorage.getItem("PRIVATE_KEY") as string
|
||||
});
|
||||
|
||||
const { refreshToken } = await createMutateAsync({
|
||||
name,
|
||||
role,
|
||||
workspaceId,
|
||||
publicKey,
|
||||
organizationId: orgId,
|
||||
trustedIps,
|
||||
expiresIn: expiresIn === "" ? undefined : Number(expiresIn),
|
||||
accessTokenTTL: Number(accessTokenTTL),
|
||||
encryptedKey: ciphertext,
|
||||
nonce,
|
||||
isRefreshTokenRotationEnabled
|
||||
});
|
||||
|
||||
const downloadData = {
|
||||
public_key: publicKey,
|
||||
private_key: privateKey,
|
||||
refresh_token: refreshToken
|
||||
};
|
||||
|
||||
const serviceTokenJSON = JSON.stringify(downloadData, null, 2);
|
||||
setNewServiceTokenJSON(serviceTokenJSON);
|
||||
|
||||
const blob = new Blob([serviceTokenJSON], { type: "application/json" });
|
||||
const href = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = href;
|
||||
link.download = `infisical_${name}.json`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
setNewServiceTokenJSON(refreshToken);
|
||||
}
|
||||
|
||||
createNotification({
|
||||
text: `Successfully ${popUp?.serviceTokenV3?.data ? "updated" : "created"} ST V3`,
|
||||
text: `Successfully ${popUp?.serviceTokenV3?.data ? "updated" : "created"} service account`,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
@@ -285,7 +234,7 @@ export const AddServiceTokenV3Modal = ({
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: `Failed to ${popUp?.serviceTokenV3?.data ? "updated" : "created"} ST V3`,
|
||||
text: `Failed to ${popUp?.serviceTokenV3?.data ? "updated" : "created"} service account`,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
@@ -302,7 +251,7 @@ export const AddServiceTokenV3Modal = ({
|
||||
setNewServiceTokenJSON("");
|
||||
}}
|
||||
>
|
||||
<ModalContent title={`${popUp?.serviceTokenV3?.data ? "Update" : "Create"} Service Token V3`}>
|
||||
<ModalContent title={`${popUp?.serviceTokenV3?.data ? "Update" : "Create"} Service Account`}>
|
||||
{!hasServiceTokenJSON ? (
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Tabs defaultValue={TabSections.General}>
|
||||
@@ -2,13 +2,13 @@ import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
DeleteActionModal
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { withProjectPermission } from "@app/hoc";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||
import { withPermission } from "@app/hoc";
|
||||
import {
|
||||
useDeleteServiceTokenV3
|
||||
} from "@app/hooks/api";
|
||||
@@ -17,7 +17,7 @@ import { usePopUp } from "@app/hooks/usePopUp";
|
||||
import { AddServiceTokenV3Modal } from "./AddServiceTokenV3Modal";
|
||||
import { ServiceTokenV3Table } from "./ServiceTokenV3Table";
|
||||
|
||||
export const ServiceTokenV3Section = withProjectPermission(
|
||||
export const ServiceTokenV3Section = withPermission(
|
||||
() => {
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { mutateAsync: deleteMutateAsync } = useDeleteServiceTokenV3();
|
||||
@@ -51,11 +51,11 @@ export const ServiceTokenV3Section = withProjectPermission(
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="flex justify-between mb-8">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">
|
||||
Service Tokens V3 (Beta)
|
||||
Service Accounts (Beta)
|
||||
</p>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.ServiceTokens}
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Create}
|
||||
a={OrgPermissionSubjects.ServiceTokens}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
@@ -65,10 +65,10 @@ export const ServiceTokenV3Section = withProjectPermission(
|
||||
onClick={() => handlePopUpOpen("serviceTokenV3")}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Create token
|
||||
Create account
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
<ServiceTokenV3Table
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
@@ -94,5 +94,5 @@ export const ServiceTokenV3Section = withProjectPermission(
|
||||
</div>
|
||||
);
|
||||
},
|
||||
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.ServiceTokens }
|
||||
{ action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.ServiceTokens }
|
||||
);
|
||||
@@ -3,7 +3,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { format } from "date-fns";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
EmptyState,
|
||||
IconButton,
|
||||
@@ -17,11 +17,13 @@ import {
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub , useWorkspace } from "@app/context";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
useOrganization} from "@app/context";
|
||||
import {
|
||||
useGetWorkspaceServiceTokenDataV3,
|
||||
useUpdateServiceTokenV3
|
||||
} from "@app/hooks/api";
|
||||
useGetOrgServiceMemberships,
|
||||
useUpdateServiceTokenV3} from "@app/hooks/api";
|
||||
import { ServiceTokenV3TrustedIp } from "@app/hooks/api/serviceTokens/types"
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
@@ -47,9 +49,9 @@ export const ServiceTokenV3Table = ({
|
||||
handlePopUpOpen
|
||||
}: Props) => {
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data, isLoading } = useGetWorkspaceServiceTokenDataV3(currentWorkspace?._id || "");
|
||||
const { currentOrg } = useOrganization();
|
||||
const { mutateAsync: updateMutateAsync } = useUpdateServiceTokenV3();
|
||||
const { data, isLoading } = useGetOrgServiceMemberships(currentOrg?._id || "");
|
||||
|
||||
const handleToggleServiceTokenDataStatus = async ({
|
||||
serviceTokenDataId,
|
||||
@@ -85,9 +87,9 @@ export const ServiceTokenV3Table = ({
|
||||
<Th>Name</Th>
|
||||
<Th>Status</Th>
|
||||
<Th>Role</Th>
|
||||
<Th>Trusted IPs</Th>
|
||||
<Th>Access Token TTL</Th>
|
||||
<Th>Created At</Th>
|
||||
{/* <Th>Trusted IPs</Th> */}
|
||||
{/* <Th>Access Token TTL</Th> */}
|
||||
{/* <Th>Created At</Th> */}
|
||||
<Th>Valid Until</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
@@ -98,24 +100,26 @@ export const ServiceTokenV3Table = ({
|
||||
data &&
|
||||
data.length > 0 &&
|
||||
data.map(({
|
||||
_id,
|
||||
name,
|
||||
isActive,
|
||||
service: {
|
||||
_id,
|
||||
name,
|
||||
isActive,
|
||||
trustedIps,
|
||||
// createdAt,
|
||||
expiresAt,
|
||||
accessTokenTTL,
|
||||
isRefreshTokenRotationEnabled
|
||||
},
|
||||
role,
|
||||
customRole,
|
||||
trustedIps,
|
||||
createdAt,
|
||||
expiresAt,
|
||||
accessTokenTTL,
|
||||
isRefreshTokenRotationEnabled
|
||||
customRole
|
||||
}) => {
|
||||
return (
|
||||
<Tr className="h-10" key={`st-v3-${_id}`}>
|
||||
<Td>{name}</Td>
|
||||
<Td>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.ServiceTokens}
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Edit}
|
||||
a={OrgPermissionSubjects.ServiceTokens}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Switch
|
||||
@@ -130,10 +134,10 @@ export const ServiceTokenV3Table = ({
|
||||
<p className="w-12 mr-4">{isActive ? "Active" : "Inactive"}</p>
|
||||
</Switch>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</OrgPermissionCan>
|
||||
</Td>
|
||||
<Td>{customRole?.slug ?? role}</Td>
|
||||
<Td>
|
||||
{/* <Td>
|
||||
{trustedIps.map(({
|
||||
_id: trustedIpId,
|
||||
ipAddress,
|
||||
@@ -145,14 +149,14 @@ export const ServiceTokenV3Table = ({
|
||||
</p>
|
||||
);
|
||||
})}
|
||||
</Td>
|
||||
<Td>{accessTokenTTL}</Td>
|
||||
<Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td>
|
||||
</Td> */}
|
||||
{/* <Td>{accessTokenTTL}</Td> */}
|
||||
{/* <Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td> */}
|
||||
<Td>{expiresAt ? format(new Date(expiresAt), "yyyy-MM-dd") : "-"}</Td>
|
||||
<Td className="flex justify-end">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.ServiceTokens}
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Edit}
|
||||
a={OrgPermissionSubjects.ServiceTokens}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
@@ -176,10 +180,10 @@ export const ServiceTokenV3Table = ({
|
||||
<FontAwesomeIcon icon={faPencil} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.ServiceTokens}
|
||||
</OrgPermissionCan>
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Delete}
|
||||
a={OrgPermissionSubjects.ServiceTokens}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
@@ -199,7 +203,7 @@ export const ServiceTokenV3Table = ({
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</OrgPermissionCan>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
export { ServiceTokenV3Section } from "./ServiceTokenV3Section";
|
||||
@@ -0,0 +1 @@
|
||||
export { OrgServiceTokenTab } from "./OrgServiceTokenTab";
|
||||
@@ -7,11 +7,13 @@ import { withProjectPermission } from "@app/hoc";
|
||||
|
||||
import { MemberListTab } from "./components/MemberListTab";
|
||||
import { ProjectRoleListTab } from "./components/ProjectRoleListTab";
|
||||
import { ServiceAccountTab } from "./components/ServiceAccountTab";
|
||||
import { ServiceTokenTab } from "./components/ServiceTokenTab";
|
||||
|
||||
enum TabSections {
|
||||
Member = "members",
|
||||
Roles = "roles",
|
||||
ServiceAccounts = "service-accounts",
|
||||
ServiceTokens = "service-tokens"
|
||||
}
|
||||
|
||||
@@ -26,6 +28,7 @@ export const MembersPage = withProjectPermission(
|
||||
<Tabs defaultValue={TabSections.Member}>
|
||||
<TabList>
|
||||
<Tab value={TabSections.Member}>Members</Tab>
|
||||
<Tab value={TabSections.ServiceAccounts}>Service Accounts</Tab>
|
||||
<Tab value={TabSections.ServiceTokens}>Service Tokens</Tab>
|
||||
<Tab value={TabSections.Roles}>Roles</Tab>
|
||||
</TabList>
|
||||
@@ -40,6 +43,9 @@ export const MembersPage = withProjectPermission(
|
||||
<MemberListTab />
|
||||
</motion.div>
|
||||
</TabPanel>
|
||||
<TabPanel value={TabSections.ServiceAccounts}>
|
||||
<ServiceAccountTab />
|
||||
</TabPanel>
|
||||
<TabPanel value={TabSections.ServiceTokens}>
|
||||
<ServiceTokenTab />
|
||||
</TabPanel>
|
||||
|
||||
@@ -78,7 +78,7 @@ export const MemberListTab = () => {
|
||||
const { data: wsKey } = useGetUserWsKey(workspaceId);
|
||||
const { data: members, isLoading: isMembersLoading } = useGetWorkspaceUsers(workspaceId);
|
||||
const { data: orgUsers } = useGetOrgUsers(orgId);
|
||||
|
||||
|
||||
const [searchMemberFilter, setSearchMemberFilter] = useState("");
|
||||
|
||||
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||
@@ -205,7 +205,7 @@ export const MemberListTab = () => {
|
||||
({ status, user: u }) => status === "accepted" && !wsUserEmails.has(u.email)
|
||||
);
|
||||
}, [orgUsers, members]);
|
||||
|
||||
|
||||
const onGrantAccess = async (grantedUserId: string, publicKey: string) => {
|
||||
try {
|
||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
||||
|
||||
@@ -64,8 +64,6 @@ export const ProjectRoleList = ({ onSelectRole }: Props) => {
|
||||
}
|
||||
};
|
||||
|
||||
// roles={roles as TRole<string>[]}
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="mb-4 flex">
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import {
|
||||
ServiceTokenV3Section
|
||||
} from "./components";
|
||||
|
||||
export const ServiceAccountTab = () => {
|
||||
return (
|
||||
<motion.div
|
||||
key="panel-service-token"
|
||||
transition={{ duration: 0.15 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
>
|
||||
<ServiceTokenV3Section />
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
import { useMemo } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import Link from "next/link";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem,
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
useOrganization,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { useAddServiceToWorkspace ,
|
||||
useGetOrgServiceMemberships,
|
||||
useGetWorkspaceServiceMemberships
|
||||
} from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const schema = yup.object({
|
||||
serviceTokenDataId: yup.string()
|
||||
}).required();
|
||||
|
||||
export type FormData = yup.InferType<typeof schema>;
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["serviceTokenV3"]>;
|
||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["serviceTokenV3"]>, state?: boolean) => void;
|
||||
};
|
||||
|
||||
export const AddServiceTokenV3Modal = ({
|
||||
popUp,
|
||||
handlePopUpToggle
|
||||
}: Props) => {
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { currentOrg } = useOrganization();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const orgId = currentOrg?._id || "";
|
||||
const workspaceId = currentWorkspace?._id || "";
|
||||
|
||||
const { data: orgServices } = useGetOrgServiceMemberships(orgId);
|
||||
const { data: services } = useGetWorkspaceServiceMemberships(workspaceId);
|
||||
const addServiceToWorkspace = useAddServiceToWorkspace();
|
||||
|
||||
const filteredOrgServices = useMemo(() => {
|
||||
const wsServiceIds = new Map();
|
||||
|
||||
services?.forEach((service) => {
|
||||
wsServiceIds.set(service.service._id, true);
|
||||
});
|
||||
|
||||
return (orgServices || []).filter(
|
||||
({ service: s }) => !wsServiceIds.has(s._id)
|
||||
);
|
||||
}, [orgServices, services]);
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<FormData>({
|
||||
resolver: yupResolver(schema)
|
||||
});
|
||||
|
||||
const onFormSubmit = async ({
|
||||
serviceTokenDataId
|
||||
}: FormData) => {
|
||||
|
||||
try {
|
||||
await addServiceToWorkspace.mutateAsync({
|
||||
workspaceId,
|
||||
serviceId: serviceTokenDataId as string,
|
||||
role: "member"
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: `Successfully ${popUp?.serviceTokenV3?.data ? "updated" : "created"} ST V3`,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
reset();
|
||||
handlePopUpToggle("serviceTokenV3", false);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: `Failed to ${popUp?.serviceTokenV3?.data ? "updated" : "created"} ST V3`,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.serviceTokenV3?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("serviceTokenV3", isOpen);
|
||||
reset();
|
||||
}}
|
||||
>
|
||||
<ModalContent title="Add a service account to the project">
|
||||
{filteredOrgServices.length ? (
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="serviceTokenDataId"
|
||||
defaultValue={filteredOrgServices?.[0]?._id}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Service Account"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mt-4"
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{filteredOrgServices.map(({ service }) => (
|
||||
<SelectItem value={service._id} key={`org-service-${service._id}`}>
|
||||
{service.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
{popUp?.serviceTokenV3?.data ? "Update" : "Create"}
|
||||
</Button>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div>All the service accounts in your organization are already added.</div>
|
||||
<Link href={`/org/${currentWorkspace?.organization}/members`}>
|
||||
<Button variant="outline_bg">Add service accounts to organization</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
DeleteActionModal
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
useWorkspace} from "@app/context";
|
||||
import { withProjectPermission } from "@app/hoc";
|
||||
import {
|
||||
useDeleteServiceFromWorkspace
|
||||
} from "@app/hooks/api";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { AddServiceTokenV3Modal } from "./AddServiceTokenV3Modal";
|
||||
import { ServiceTokenV3Table } from "./ServiceTokenV3Table";
|
||||
|
||||
export const ServiceTokenV3Section = withProjectPermission(
|
||||
() => {
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const workspaceId = currentWorkspace?._id ?? "";
|
||||
|
||||
const { mutateAsync: deleteMutateAsync } = useDeleteServiceFromWorkspace();
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"serviceTokenV3",
|
||||
"deleteServiceTokenV3",
|
||||
"upgradePlan"
|
||||
] as const);
|
||||
|
||||
const onRemoveServiceTokenDataSubmit = async (serviceTokenDataId: string) => {
|
||||
try {
|
||||
|
||||
await deleteMutateAsync({
|
||||
serviceId: serviceTokenDataId,
|
||||
workspaceId
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully removed service account from project",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
handlePopUpClose("deleteServiceTokenV3");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to delete service account from project",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="flex justify-between mb-8">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">
|
||||
Service Accounts (Beta)
|
||||
</p>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.ServiceTokens}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("serviceTokenV3")}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Add service account
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<ServiceTokenV3Table
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
/>
|
||||
<AddServiceTokenV3Modal
|
||||
popUp={popUp}
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteServiceTokenV3.isOpen}
|
||||
title={`Are you sure want to remove ${
|
||||
(popUp?.deleteServiceTokenV3?.data as { name: string })?.name || ""
|
||||
} from the project?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteServiceTokenV3", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={() =>
|
||||
onRemoveServiceTokenDataSubmit(
|
||||
(popUp?.deleteServiceTokenV3?.data as { serviceTokenDataId: string })?.serviceTokenDataId
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.ServiceTokens }
|
||||
);
|
||||
@@ -0,0 +1,116 @@
|
||||
import { faKey, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { format } from "date-fns";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub , useWorkspace } from "@app/context";
|
||||
import { useGetWorkspaceServiceMemberships } from "@app/hooks/api";
|
||||
import { ServiceTokenV3TrustedIp } from "@app/hooks/api/serviceTokens/types"
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
type Props = {
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<["deleteServiceTokenV3", "serviceTokenV3"]>,
|
||||
data?: {
|
||||
serviceTokenDataId?: string;
|
||||
name?: string;
|
||||
role?: string;
|
||||
customRole?: {
|
||||
name: string;
|
||||
slug: string;
|
||||
};
|
||||
trustedIps?: ServiceTokenV3TrustedIp[];
|
||||
accessTokenTTL?: number;
|
||||
isRefreshTokenRotationEnabled?: boolean;
|
||||
}
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const ServiceTokenV3Table = ({
|
||||
handlePopUpOpen
|
||||
}: Props) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data, isLoading } = useGetWorkspaceServiceMemberships(currentWorkspace?._id || "");
|
||||
|
||||
return (
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Name</Th>
|
||||
<Th>Role</Th>
|
||||
<Th>Added on</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isLoading && <TableSkeleton columns={7} innerKey="service-tokens" />}
|
||||
{!isLoading &&
|
||||
data &&
|
||||
data.length > 0 &&
|
||||
data.map(({
|
||||
service: {
|
||||
_id,
|
||||
name
|
||||
},
|
||||
role,
|
||||
customRole,
|
||||
createdAt
|
||||
}) => {
|
||||
return (
|
||||
<Tr className="h-10" key={`st-v3-${_id}`}>
|
||||
<Td>{name}</Td>
|
||||
<Td>{customRole?.slug ?? role}</Td>
|
||||
<Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td>
|
||||
<Td className="flex justify-end">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.ServiceTokens}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
handlePopUpOpen("deleteServiceTokenV3", {
|
||||
serviceTokenDataId: _id,
|
||||
name
|
||||
});
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="ml-4"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
{!isLoading && data && data?.length === 0 && (
|
||||
<Tr>
|
||||
<Td colSpan={7}>
|
||||
<EmptyState title="No service token v3 on file" icon={faKey} />
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { ServiceTokenV3Section } from "./ServiceTokenV3Section";
|
||||
@@ -0,0 +1 @@
|
||||
export { ServiceTokenV3Section } from "./ServiceTokenV3Section";
|
||||
@@ -0,0 +1 @@
|
||||
export { ServiceAccountTab } from "./ServiceAccountTab";
|
||||
@@ -2,7 +2,6 @@ import { motion } from "framer-motion";
|
||||
|
||||
import {
|
||||
ServiceTokenSection,
|
||||
ServiceTokenV3Section
|
||||
} from "./components";
|
||||
|
||||
export const ServiceTokenTab = () => {
|
||||
@@ -14,8 +13,7 @@ export const ServiceTokenTab = () => {
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
>
|
||||
<ServiceTokenV3Section />
|
||||
<ServiceTokenSection />
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1 @@
|
||||
export { ServiceTokenSection } from "./ServiceTokenSection";
|
||||
export { ServiceTokenV3Section } from "./ServiceTokenV3Section";
|
||||
export { ServiceTokenSection } from "./ServiceTokenSection";
|
||||
Reference in New Issue
Block a user