mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 17:40:44 +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";
|
||||
|
||||
@@ -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);
|
||||
@@ -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()
|
||||
})
|
||||
});
|
||||
Reference in New Issue
Block a user