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
@@ -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
+1 -1
View File
@@ -66,7 +66,7 @@ class EELicenseService {
secretVersioning: true,
pitRecovery: false,
ipAllowlisting: false,
rbac: false,
rbac: true,
customRateLimits: false,
customAlerts: false,
auditLogs: false,
+24 -16
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:
+17 -4
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" });
};
};