Continue ST V3 roles

This commit is contained in:
Tuan Dang
2023-11-23 11:56:56 +07:00
parent c37ff79927
commit 597e1e1ca8
49 changed files with 1104 additions and 390 deletions

View File

@@ -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
});
}

View File

@@ -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
});
}

View File

@@ -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
});
}

View File

@@ -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
}
);

View File

@@ -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

View File

@@ -66,7 +66,7 @@ class EELicenseService {
secretVersioning: true,
pitRecovery: false,
ipAllowlisting: false,
rbac: false,
rbac: true,
customRateLimits: false,
customAlerts: false,
auditLogs: false,

View File

@@ -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:

View File

@@ -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" });
};
};

View File

@@ -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";

View 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);

View 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);

View File

@@ -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: [
{

View File

@@ -54,4 +54,12 @@ router.delete(
organizationsController.deleteOrganizationById
);
router.get(
"/:organizationId/service-memberships",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
organizationsController.getOrganizationServiceMemberships
);
export default router;

View File

@@ -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;

View File

@@ -34,12 +34,4 @@ router.post(
// --
router.get(
"/:workspaceId/service-token",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
workspacesController.getWorkspaceServiceTokenData
);
export default router;

View File

@@ -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() })
});

View File

@@ -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)
})
});

View File

@@ -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()
})
});

View File

@@ -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>;

View File

@@ -14,6 +14,7 @@ export {
useGetOrgPlansTable,
useGetOrgPlanTable,
useGetOrgPmtMethods,
useGetOrgServiceMemberships,
useGetOrgTaxIds,
useGetOrgTrialUrl,
useRenameOrg,

View File

@@ -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();

View File

@@ -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));
}
});
};

View File

@@ -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;
}

View File

@@ -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";

View File

@@ -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
});

View File

@@ -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>

View File

@@ -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>

View File

@@ -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
)

View File

@@ -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>
);
};

View File

@@ -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);

View File

@@ -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>
);
}

View File

@@ -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}>

View File

@@ -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 }
);

View File

@@ -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>
);

View File

@@ -0,0 +1 @@
export { ServiceTokenV3Section } from "./ServiceTokenV3Section";

View File

@@ -0,0 +1 @@
export { OrgServiceTokenTab } from "./OrgServiceTokenTab";

View File

@@ -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>

View File

@@ -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;

View File

@@ -64,8 +64,6 @@ export const ProjectRoleList = ({ onSelectRole }: Props) => {
}
};
// roles={roles as TRole<string>[]}
return (
<div className="w-full">
<div className="mb-4 flex">

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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 }
);

View File

@@ -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>
);
}

View File

@@ -0,0 +1 @@
export { ServiceTokenV3Section } from "./ServiceTokenV3Section";

View File

@@ -0,0 +1 @@
export { ServiceTokenV3Section } from "./ServiceTokenV3Section";

View File

@@ -0,0 +1 @@
export { ServiceAccountTab } from "./ServiceAccountTab";

View File

@@ -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>
);
}
}

View File

@@ -1,2 +1 @@
export { ServiceTokenSection } from "./ServiceTokenSection";
export { ServiceTokenV3Section } from "./ServiceTokenV3Section";
export { ServiceTokenSection } from "./ServiceTokenSection";