mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 08:27:53 +00:00
Added authz logic to MI
This commit is contained in:
@@ -393,6 +393,13 @@ export const getOrganizationMachineMemberships = async (req: Request, res: Respo
|
|||||||
const {
|
const {
|
||||||
params: { organizationId }
|
params: { organizationId }
|
||||||
} = await validateRequest(reqValidator.GetOrgServiceMembersV2, req);
|
} = await validateRequest(reqValidator.GetOrgServiceMembersV2, req);
|
||||||
|
|
||||||
|
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
OrgPermissionActions.Read,
|
||||||
|
OrgPermissionSubjects.MachineIdentity
|
||||||
|
);
|
||||||
|
|
||||||
const machineMemberships = await MachineMembershipOrg.find({
|
const machineMemberships = await MachineMembershipOrg.find({
|
||||||
organization: new Types.ObjectId(organizationId)
|
organization: new Types.ObjectId(organizationId)
|
||||||
|
|||||||
@@ -24,10 +24,12 @@ import * as reqValidator from "../../validation";
|
|||||||
import {
|
import {
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
ProjectPermissionSub,
|
ProjectPermissionSub,
|
||||||
getAuthDataProjectPermissions
|
getAuthDataProjectPermissions,
|
||||||
|
getRolePermissions,
|
||||||
|
isAtLeastAsPrivilegedWorkspace
|
||||||
} from "../../ee/services/ProjectRoleService";
|
} from "../../ee/services/ProjectRoleService";
|
||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
import { BadRequestError, ResourceNotFoundError } from "../../utils/errors";
|
import { BadRequestError, ForbiddenRequestError, ResourceNotFoundError } from "../../utils/errors";
|
||||||
import { ADMIN, MEMBER, VIEWER } from "../../variables";
|
import { ADMIN, MEMBER, VIEWER } from "../../variables";
|
||||||
|
|
||||||
interface V2PushSecret {
|
interface V2PushSecret {
|
||||||
@@ -523,7 +525,7 @@ export const addMachineToWorkspace = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionActions.Create,
|
ProjectPermissionActions.Create,
|
||||||
ProjectPermissionSub.ServiceTokens
|
ProjectPermissionSub.MachineIdentity
|
||||||
);
|
);
|
||||||
|
|
||||||
let machineMembership = await MachineMembership.findOne({
|
let machineMembership = await MachineMembership.findOne({
|
||||||
@@ -532,7 +534,7 @@ export const addMachineToWorkspace = async (req: Request, res: Response) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (machineMembership) throw BadRequestError({
|
if (machineMembership) throw BadRequestError({
|
||||||
message: "Machine identity already exists in workspace"
|
message: `Machine identity with id ${machineId} already exists in workspace with id ${workspaceId}`
|
||||||
});
|
});
|
||||||
|
|
||||||
const machineIdentity = await MachineIdentity.findById(machineId);
|
const machineIdentity = await MachineIdentity.findById(machineId);
|
||||||
@@ -545,6 +547,13 @@ export const addMachineToWorkspace = async (req: Request, res: Response) => {
|
|||||||
message: "Failed to add machine identity to workspace in another organization"
|
message: "Failed to add machine identity to workspace in another organization"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const rolePermission = await getRolePermissions(role, workspaceId);
|
||||||
|
const hasRequiredPrivileges = isAtLeastAsPrivilegedWorkspace(permission, rolePermission);
|
||||||
|
|
||||||
|
if (!hasRequiredPrivileges) throw ForbiddenRequestError({
|
||||||
|
message: "Failed to add a more privileged MI to workspace"
|
||||||
|
});
|
||||||
|
|
||||||
let customRole;
|
let customRole;
|
||||||
if (role) {
|
if (role) {
|
||||||
const isCustomRole = ![ADMIN, MEMBER, VIEWER].includes(role);
|
const isCustomRole = ![ADMIN, MEMBER, VIEWER].includes(role);
|
||||||
@@ -591,8 +600,8 @@ export const addMachineToWorkspace = async (req: Request, res: Response) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionActions.Create,
|
ProjectPermissionActions.Edit,
|
||||||
ProjectPermissionSub.ServiceTokens
|
ProjectPermissionSub.MachineIdentity
|
||||||
);
|
);
|
||||||
|
|
||||||
let machineMembership = await MachineMembership.findOne({
|
let machineMembership = await MachineMembership.findOne({
|
||||||
@@ -601,7 +610,7 @@ export const addMachineToWorkspace = async (req: Request, res: Response) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!machineMembership) throw BadRequestError({
|
if (!machineMembership) throw BadRequestError({
|
||||||
message: "Machine identity does not exist in workspace"
|
message: `Machine identity with id ${machineId} does not exist in workspace with id ${workspaceId}`
|
||||||
});
|
});
|
||||||
|
|
||||||
const machineIdentity = await MachineIdentity.findById(machineId);
|
const machineIdentity = await MachineIdentity.findById(machineId);
|
||||||
@@ -611,7 +620,14 @@ export const addMachineToWorkspace = async (req: Request, res: Response) => {
|
|||||||
if (!workspace) throw ResourceNotFoundError();
|
if (!workspace) throw ResourceNotFoundError();
|
||||||
|
|
||||||
if (!machineIdentity.organization.equals(workspace.organization)) throw BadRequestError({
|
if (!machineIdentity.organization.equals(workspace.organization)) throw BadRequestError({
|
||||||
message: "Failed to add machine identity to workspace in another organization"
|
message: "Failed to update machine identity in workspace in another organization"
|
||||||
|
});
|
||||||
|
|
||||||
|
const rolePermission = await getRolePermissions(role, workspaceId);
|
||||||
|
const hasRequiredPrivileges = isAtLeastAsPrivilegedWorkspace(permission, rolePermission);
|
||||||
|
|
||||||
|
if (!hasRequiredPrivileges) throw ForbiddenRequestError({
|
||||||
|
message: "Failed to update MI to a more privileged role"
|
||||||
});
|
});
|
||||||
|
|
||||||
let customRole;
|
let customRole;
|
||||||
@@ -665,7 +681,7 @@ export const addMachineToWorkspace = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionActions.Delete,
|
ProjectPermissionActions.Delete,
|
||||||
ProjectPermissionSub.ServiceTokens
|
ProjectPermissionSub.MachineIdentity
|
||||||
);
|
);
|
||||||
|
|
||||||
const machineMembership = await MachineMembership.findOneAndDelete({
|
const machineMembership = await MachineMembership.findOneAndDelete({
|
||||||
@@ -698,7 +714,7 @@ export const addMachineToWorkspace = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionActions.Read,
|
ProjectPermissionActions.Read,
|
||||||
ProjectPermissionSub.ServiceTokens
|
ProjectPermissionSub.MachineIdentity
|
||||||
);
|
);
|
||||||
|
|
||||||
const machineMemberships = await MachineMembership.find({
|
const machineMemberships = await MachineMembership.find({
|
||||||
|
|||||||
@@ -16,13 +16,21 @@ import {
|
|||||||
import { validateRequest } from "../../../helpers/validation";
|
import { validateRequest } from "../../../helpers/validation";
|
||||||
import * as reqValidator from "../../../validation/machineIdentity";
|
import * as reqValidator from "../../../validation/machineIdentity";
|
||||||
import { createToken } from "../../../helpers/auth";
|
import { createToken } from "../../../helpers/auth";
|
||||||
|
import {
|
||||||
|
getOrgRolePermissions,
|
||||||
import { BadRequestError, ResourceNotFoundError, UnauthorizedRequestError } from "../../../utils/errors";
|
getUserOrgPermissions,
|
||||||
|
isAtLeastAsPrivilegedOrg
|
||||||
|
} from "../../services/RoleService";
|
||||||
|
import { BadRequestError, ForbiddenRequestError, ResourceNotFoundError, UnauthorizedRequestError } from "../../../utils/errors";
|
||||||
import { extractIPDetails, isValidIpOrCidr } from "../../../utils/ip";
|
import { extractIPDetails, isValidIpOrCidr } from "../../../utils/ip";
|
||||||
import { EEAuditLogService, EELicenseService } from "../../services";
|
import { EEAuditLogService, EELicenseService } from "../../services";
|
||||||
import { getAuthSecret } from "../../../config";
|
import { getAuthSecret } from "../../../config";
|
||||||
import { ADMIN, AuthTokenType, CUSTOM, MEMBER } from "../../../variables";
|
import { ADMIN, AuthTokenType, CUSTOM, MEMBER } from "../../../variables";
|
||||||
|
import {
|
||||||
|
OrgPermissionActions,
|
||||||
|
OrgPermissionSubjects
|
||||||
|
} from "../../services/RoleService";
|
||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return machine identity access and refresh token as per refresh operation
|
* Return machine identity access and refresh token as per refresh operation
|
||||||
@@ -32,12 +40,12 @@ import { ADMIN, AuthTokenType, CUSTOM, MEMBER } from "../../../variables";
|
|||||||
export const refreshToken = async (req: Request, res: Response) => {
|
export const refreshToken = async (req: Request, res: Response) => {
|
||||||
const {
|
const {
|
||||||
body: {
|
body: {
|
||||||
refresh_token
|
refreshToken
|
||||||
}
|
}
|
||||||
} = await validateRequest(reqValidator.RefreshTokenV3, req);
|
} = await validateRequest(reqValidator.RefreshTokenV3, req);
|
||||||
|
|
||||||
const decodedToken = <jwt.ServiceRefreshTokenJwtPayload>(
|
const decodedToken = <jwt.ServiceRefreshTokenJwtPayload>(
|
||||||
jwt.verify(refresh_token, await getAuthSecret())
|
jwt.verify(refreshToken, await getAuthSecret())
|
||||||
);
|
);
|
||||||
|
|
||||||
if (decodedToken.authTokenType !== AuthTokenType.SERVICE_REFRESH_TOKEN) throw UnauthorizedRequestError();
|
if (decodedToken.authTokenType !== AuthTokenType.SERVICE_REFRESH_TOKEN) throw UnauthorizedRequestError();
|
||||||
@@ -55,15 +63,15 @@ import { ADMIN, AuthTokenType, CUSTOM, MEMBER } from "../../../variables";
|
|||||||
}
|
}
|
||||||
|
|
||||||
const response: {
|
const response: {
|
||||||
refresh_token?: string;
|
refreshToken?: string;
|
||||||
access_token: string;
|
accessToken: string;
|
||||||
expires_in: number;
|
expiresIn: number;
|
||||||
token_type: string;
|
tokenType: string;
|
||||||
} = {
|
} = {
|
||||||
refresh_token,
|
refreshToken,
|
||||||
access_token: "",
|
accessToken: "",
|
||||||
expires_in: 0,
|
expiresIn: 0,
|
||||||
token_type: "Bearer"
|
tokenType: "Bearer"
|
||||||
};
|
};
|
||||||
|
|
||||||
if (machineIdentity.isRefreshTokenRotationEnabled) {
|
if (machineIdentity.isRefreshTokenRotationEnabled) {
|
||||||
@@ -81,7 +89,7 @@ import { ADMIN, AuthTokenType, CUSTOM, MEMBER } from "../../../variables";
|
|||||||
|
|
||||||
if (!machineIdentity) throw BadRequestError();
|
if (!machineIdentity) throw BadRequestError();
|
||||||
|
|
||||||
response.refresh_token = createToken({
|
response.refreshToken = createToken({
|
||||||
payload: {
|
payload: {
|
||||||
serviceTokenDataId: machineIdentity._id.toString(),
|
serviceTokenDataId: machineIdentity._id.toString(),
|
||||||
authTokenType: AuthTokenType.SERVICE_REFRESH_TOKEN,
|
authTokenType: AuthTokenType.SERVICE_REFRESH_TOKEN,
|
||||||
@@ -91,9 +99,9 @@ import { ADMIN, AuthTokenType, CUSTOM, MEMBER } from "../../../variables";
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
response.access_token = createToken({
|
response.accessToken = createToken({
|
||||||
payload: {
|
payload: {
|
||||||
serviceTokenDataId: machineIdentity._id.toString(),
|
serviceTokenDataId: machineIdentity._id.toString(), // TODO: fix this
|
||||||
authTokenType: AuthTokenType.SERVICE_ACCESS_TOKEN,
|
authTokenType: AuthTokenType.SERVICE_ACCESS_TOKEN,
|
||||||
tokenVersion: machineIdentity.tokenVersion
|
tokenVersion: machineIdentity.tokenVersion
|
||||||
},
|
},
|
||||||
@@ -101,7 +109,7 @@ import { ADMIN, AuthTokenType, CUSTOM, MEMBER } from "../../../variables";
|
|||||||
secret: await getAuthSecret()
|
secret: await getAuthSecret()
|
||||||
});
|
});
|
||||||
|
|
||||||
response.expires_in = machineIdentity.accessTokenTTL;
|
response.expiresIn = machineIdentity.accessTokenTTL;
|
||||||
|
|
||||||
await MachineIdentity.findByIdAndUpdate(
|
await MachineIdentity.findByIdAndUpdate(
|
||||||
machineIdentity._id,
|
machineIdentity._id,
|
||||||
@@ -135,22 +143,23 @@ export const createMachineIdentity = async (req: Request, res: Response) => {
|
|||||||
isRefreshTokenRotationEnabled
|
isRefreshTokenRotationEnabled
|
||||||
}
|
}
|
||||||
} = await validateRequest(reqValidator.CreateMachineIdentityV3, req);
|
} = await validateRequest(reqValidator.CreateMachineIdentityV3, req);
|
||||||
|
|
||||||
// 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);
|
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
|
||||||
// if (!workspace) throw BadRequestError({ message: "Workspace not found" });
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
OrgPermissionActions.Create,
|
||||||
|
OrgPermissionSubjects.MachineIdentity
|
||||||
|
);
|
||||||
|
|
||||||
|
const rolePermission = await getOrgRolePermissions(role, organizationId);
|
||||||
|
const hasRequiredPrivileges = isAtLeastAsPrivilegedOrg(permission, rolePermission);
|
||||||
|
|
||||||
|
if (!hasRequiredPrivileges) throw ForbiddenRequestError({
|
||||||
|
message: "Failed to create a more privileged MI"
|
||||||
|
});
|
||||||
|
|
||||||
const organization = await Organization.findById(organizationId);
|
const organization = await Organization.findById(organizationId);
|
||||||
if (!organization) throw BadRequestError({ message: "Organization not found" });
|
if (!organization) throw BadRequestError({ message: `Organization with id ${organizationId} not found` });
|
||||||
|
|
||||||
const isCustomRole = ![ADMIN, MEMBER].includes(role);
|
const isCustomRole = ![ADMIN, MEMBER].includes(role);
|
||||||
|
|
||||||
@@ -217,7 +226,7 @@ export const createMachineIdentity = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
const refreshToken = createToken({
|
const refreshToken = createToken({
|
||||||
payload: {
|
payload: {
|
||||||
serviceTokenDataId: machineIdentity._id.toString(),
|
serviceTokenDataId: machineIdentity._id.toString(), // TODO: update
|
||||||
authTokenType: AuthTokenType.SERVICE_REFRESH_TOKEN,
|
authTokenType: AuthTokenType.SERVICE_REFRESH_TOKEN,
|
||||||
tokenVersion: machineIdentity.tokenVersion
|
tokenVersion: machineIdentity.tokenVersion
|
||||||
},
|
},
|
||||||
@@ -248,7 +257,7 @@ export const createMachineIdentity = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update service token V3 data with id [serviceTokenDataId]
|
* Update machine identity with id [machineId]
|
||||||
* @param req
|
* @param req
|
||||||
* @param res
|
* @param res
|
||||||
* @returns
|
* @returns
|
||||||
@@ -258,7 +267,6 @@ export const updateMachineIdentity = async (req: Request, res: Response) => {
|
|||||||
params: { machineId },
|
params: { machineId },
|
||||||
body: {
|
body: {
|
||||||
name,
|
name,
|
||||||
isActive,
|
|
||||||
role,
|
role,
|
||||||
trustedIps,
|
trustedIps,
|
||||||
expiresIn,
|
expiresIn,
|
||||||
@@ -269,21 +277,24 @@ export const updateMachineIdentity = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
let machineIdentity = await MachineIdentity.findById(machineId);
|
let machineIdentity = await MachineIdentity.findById(machineId);
|
||||||
if (!machineIdentity) throw ResourceNotFoundError({
|
if (!machineIdentity) throw ResourceNotFoundError({
|
||||||
message: "Service token not found"
|
message: `Machine identity with id ${machineId} not found`
|
||||||
});
|
});
|
||||||
|
|
||||||
// const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
// authData: req.authData,
|
|
||||||
// workspaceId: serviceTokenData.workspace
|
|
||||||
// });
|
|
||||||
|
|
||||||
// ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
// ProjectPermissionActions.Edit,
|
|
||||||
// ProjectPermissionSub.ServiceTokens
|
|
||||||
// );
|
|
||||||
|
|
||||||
// const workspace = await Workspace.findById(serviceTokenData.workspace);
|
const { permission } = await getUserOrgPermissions(req.user._id, machineIdentity.organization.toString());
|
||||||
// if (!workspace) throw BadRequestError({ message: "Workspace not found" });
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
OrgPermissionActions.Edit,
|
||||||
|
OrgPermissionSubjects.MachineIdentity
|
||||||
|
);
|
||||||
|
|
||||||
|
if (role) {
|
||||||
|
const rolePermission = await getOrgRolePermissions(role, machineIdentity.organization.toString());
|
||||||
|
const hasRequiredPrivileges = isAtLeastAsPrivilegedOrg(permission, rolePermission);
|
||||||
|
|
||||||
|
if (!hasRequiredPrivileges) throw ForbiddenRequestError({
|
||||||
|
message: "Failed to update MI to a more privileged role"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let customRole;
|
let customRole;
|
||||||
if (role) {
|
if (role) {
|
||||||
@@ -329,7 +340,6 @@ export const updateMachineIdentity = async (req: Request, res: Response) => {
|
|||||||
machineId,
|
machineId,
|
||||||
{
|
{
|
||||||
name,
|
name,
|
||||||
isActive,
|
|
||||||
trustedIps: reformattedTrustedIps,
|
trustedIps: reformattedTrustedIps,
|
||||||
expiresAt,
|
expiresAt,
|
||||||
accessTokenTTL,
|
accessTokenTTL,
|
||||||
@@ -341,7 +351,7 @@ export const updateMachineIdentity = async (req: Request, res: Response) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!machineIdentity) throw BadRequestError({
|
if (!machineIdentity) throw BadRequestError({
|
||||||
message: "Failed to update service token"
|
message: `Failed to update machine identity with id ${machineId}`
|
||||||
});
|
});
|
||||||
|
|
||||||
await MachineMembershipOrg.findOneAndUpdate(
|
await MachineMembershipOrg.findOneAndUpdate(
|
||||||
@@ -370,7 +380,6 @@ export const updateMachineIdentity = async (req: Request, res: Response) => {
|
|||||||
type: EventType.UPDATE_MACHINE_IDENTITY,
|
type: EventType.UPDATE_MACHINE_IDENTITY,
|
||||||
metadata: {
|
metadata: {
|
||||||
name: machineIdentity.name,
|
name: machineIdentity.name,
|
||||||
isActive,
|
|
||||||
role,
|
role,
|
||||||
trustedIps: reformattedTrustedIps as Array<IMachineIdentityTrustedIp>,
|
trustedIps: reformattedTrustedIps as Array<IMachineIdentityTrustedIp>,
|
||||||
expiresAt
|
expiresAt
|
||||||
@@ -387,7 +396,7 @@ export const updateMachineIdentity = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete service token data with id [serviceTokenDataId]
|
* Delete machine identity with id [machineId]
|
||||||
* @param req
|
* @param req
|
||||||
* @param res
|
* @param res
|
||||||
* @returns
|
* @returns
|
||||||
@@ -399,18 +408,15 @@ export const deleteMachineIdentity = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
let machineIdentity = await MachineIdentity.findById(machineId);
|
let machineIdentity = await MachineIdentity.findById(machineId);
|
||||||
if (!machineIdentity) throw ResourceNotFoundError({
|
if (!machineIdentity) throw ResourceNotFoundError({
|
||||||
message: "Service token not found"
|
message: `Machine identity with id ${machineId} not found`
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { permission } = await getUserOrgPermissions(req.user._id, machineIdentity.organization.toString());
|
||||||
|
|
||||||
// const { permission } = await getAuthDataProjectPermissions({
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
// authData: req.authData,
|
OrgPermissionActions.Delete,
|
||||||
// workspaceId: serviceTokenData.workspace
|
OrgPermissionSubjects.MachineIdentity
|
||||||
// });
|
);
|
||||||
|
|
||||||
// ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
// ProjectPermissionActions.Delete,
|
|
||||||
// ProjectPermissionSub.ServiceTokens
|
|
||||||
// );
|
|
||||||
|
|
||||||
machineIdentity = await MachineIdentity.findByIdAndDelete(machineId);
|
machineIdentity = await MachineIdentity.findByIdAndDelete(machineId);
|
||||||
|
|
||||||
|
|||||||
@@ -235,7 +235,6 @@ interface UpdateMachineIdentityEvent {
|
|||||||
type: EventType.UPDATE_MACHINE_IDENTITY;
|
type: EventType.UPDATE_MACHINE_IDENTITY;
|
||||||
metadata: {
|
metadata: {
|
||||||
name?: string;
|
name?: string;
|
||||||
isActive?: boolean;
|
|
||||||
role?: string;
|
role?: string;
|
||||||
trustedIps?: Array<IMachineIdentityTrustedIp>;
|
trustedIps?: Array<IMachineIdentityTrustedIp>;
|
||||||
expiresAt?: Date;
|
expiresAt?: Date;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { UnauthorizedRequestError } from "../../utils/errors";
|
|||||||
import { FieldCondition, FieldInstruction, JsInterpreter } from "@ucast/mongo2js";
|
import { FieldCondition, FieldInstruction, JsInterpreter } from "@ucast/mongo2js";
|
||||||
import picomatch from "picomatch";
|
import picomatch from "picomatch";
|
||||||
import { AuthData } from "../../interfaces/middleware";
|
import { AuthData } from "../../interfaces/middleware";
|
||||||
import { ActorType, IRole } from "../models";
|
import { ActorType, IRole, Role } from "../models";
|
||||||
import {
|
import {
|
||||||
IMachineIdentity,
|
IMachineIdentity,
|
||||||
MachineMembership,
|
MachineMembership,
|
||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
} from "../../models";
|
} from "../../models";
|
||||||
import { ADMIN, CUSTOM, MEMBER, VIEWER } from "../../variables";
|
import { ADMIN, CUSTOM, MEMBER, VIEWER } from "../../variables";
|
||||||
import { checkIPAgainstBlocklist } from "../../utils/ip";
|
import { checkIPAgainstBlocklist } from "../../utils/ip";
|
||||||
|
import { BadRequestError } from "../../utils/errors";
|
||||||
|
|
||||||
const $glob: FieldInstruction<string> = {
|
const $glob: FieldInstruction<string> = {
|
||||||
type: "field",
|
type: "field",
|
||||||
@@ -60,7 +61,8 @@ export enum ProjectPermissionSub {
|
|||||||
Secrets = "secrets",
|
Secrets = "secrets",
|
||||||
SecretRollback = "secret-rollback",
|
SecretRollback = "secret-rollback",
|
||||||
SecretApproval = "secret-approval",
|
SecretApproval = "secret-approval",
|
||||||
SecretRotation = "secret-rotation"
|
SecretRotation = "secret-rotation",
|
||||||
|
MachineIdentity = "machine-identity"
|
||||||
}
|
}
|
||||||
|
|
||||||
type SubjectFields = {
|
type SubjectFields = {
|
||||||
@@ -85,6 +87,7 @@ export type ProjectPermissionSet =
|
|||||||
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
|
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
|
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
|
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
|
||||||
|
| [ProjectPermissionActions, ProjectPermissionSub.MachineIdentity]
|
||||||
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace]
|
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace]
|
||||||
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace]
|
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace]
|
||||||
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
|
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
|
||||||
@@ -131,6 +134,11 @@ const buildAdminPermission = () => {
|
|||||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks);
|
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks);
|
||||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Webhooks);
|
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Webhooks);
|
||||||
|
|
||||||
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.MachineIdentity);
|
||||||
|
can(ProjectPermissionActions.Create, ProjectPermissionSub.MachineIdentity);
|
||||||
|
can(ProjectPermissionActions.Edit, ProjectPermissionSub.MachineIdentity);
|
||||||
|
can(ProjectPermissionActions.Delete, ProjectPermissionSub.MachineIdentity);
|
||||||
|
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens);
|
||||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens);
|
can(ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens);
|
||||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.ServiceTokens);
|
can(ProjectPermissionActions.Edit, ProjectPermissionSub.ServiceTokens);
|
||||||
@@ -196,6 +204,11 @@ const buildMemberPermission = () => {
|
|||||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks);
|
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks);
|
||||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Webhooks);
|
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Webhooks);
|
||||||
|
|
||||||
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.MachineIdentity);
|
||||||
|
can(ProjectPermissionActions.Create, ProjectPermissionSub.MachineIdentity);
|
||||||
|
can(ProjectPermissionActions.Edit, ProjectPermissionSub.MachineIdentity);
|
||||||
|
can(ProjectPermissionActions.Delete, ProjectPermissionSub.MachineIdentity);
|
||||||
|
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens);
|
||||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens);
|
can(ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens);
|
||||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.ServiceTokens);
|
can(ProjectPermissionActions.Edit, ProjectPermissionSub.ServiceTokens);
|
||||||
@@ -236,6 +249,7 @@ const buildViewerPermission = () => {
|
|||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
|
||||||
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.MachineIdentity);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments);
|
||||||
@@ -337,3 +351,59 @@ export const getAuthDataProjectPermissions = async ({
|
|||||||
throw UnauthorizedRequestError();
|
throw UnauthorizedRequestError();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getRolePermissions = async (role: string, workspaceId: string) => {
|
||||||
|
const isCustomRole = ![ADMIN, MEMBER, VIEWER].includes(role);
|
||||||
|
if (isCustomRole) {
|
||||||
|
const workspaceRole = await Role.findOne({
|
||||||
|
slug: role,
|
||||||
|
isOrgRole: false,
|
||||||
|
workspace: new Types.ObjectId(workspaceId)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!workspaceRole) throw BadRequestError({ message: "Role not found" });
|
||||||
|
|
||||||
|
return createMongoAbility<ProjectPermissionSet>(workspaceRole.permissions as RawRuleOf<MongoAbility<ProjectPermissionSet>>[], {
|
||||||
|
conditionsMatcher
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (role) {
|
||||||
|
case ADMIN:
|
||||||
|
return adminProjectPermissions;
|
||||||
|
case MEMBER:
|
||||||
|
return memberProjectPermissions;
|
||||||
|
case VIEWER:
|
||||||
|
return viewerProjectPermission;
|
||||||
|
default:
|
||||||
|
throw BadRequestError({ message: "Role not found" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts and formats permissions from a CASL Ability object or a raw permission set.
|
||||||
|
* @param ability
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const extractPermissions = (ability: MongoAbility<ProjectPermissionSet> | ProjectPermissionSet) => {
|
||||||
|
return ability.A.map((permission: any) => `${permission.action}_${permission.subject}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares two sets of permissions to determine if the first set is at least as privileged as the second set.
|
||||||
|
* The function checks if all permissions in the second set are contained within the first set and if the first set has equal or more permissions.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const isAtLeastAsPrivilegedWorkspace = (permissions1: MongoAbility<ProjectPermissionSet> | ProjectPermissionSet, permissions2: MongoAbility<ProjectPermissionSet> | ProjectPermissionSet) => {
|
||||||
|
|
||||||
|
const set1 = new Set(extractPermissions(permissions1));
|
||||||
|
const set2 = new Set(extractPermissions(permissions2));
|
||||||
|
|
||||||
|
for (const perm of set2) {
|
||||||
|
if (!set1.has(perm)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return set1.size >= set2.size;
|
||||||
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import { Types } from "mongoose";
|
||||||
import { AbilityBuilder, MongoAbility, RawRuleOf, createMongoAbility } from "@casl/ability";
|
import { AbilityBuilder, MongoAbility, RawRuleOf, createMongoAbility } from "@casl/ability";
|
||||||
import { MembershipOrg } from "../../models";
|
import { MembershipOrg } from "../../models";
|
||||||
import { IRole } from "../models";
|
import { IRole, Role } from "../models";
|
||||||
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
|
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
|
||||||
import { ACCEPTED } from "../../variables";
|
import { ACCEPTED, ADMIN, CUSTOM, MEMBER } from "../../variables";
|
||||||
import { conditionsMatcher } from "./ProjectRoleService";
|
import { conditionsMatcher } from "./ProjectRoleService";
|
||||||
|
|
||||||
export enum OrgPermissionActions {
|
export enum OrgPermissionActions {
|
||||||
@@ -21,7 +22,7 @@ export enum OrgPermissionSubjects {
|
|||||||
Sso = "sso",
|
Sso = "sso",
|
||||||
Billing = "billing",
|
Billing = "billing",
|
||||||
SecretScanning = "secret-scanning",
|
SecretScanning = "secret-scanning",
|
||||||
ServiceTokens = "service-tokens" // TODO: consider renaming
|
MachineIdentity = "machine-identity"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OrgPermissionSet =
|
export type OrgPermissionSet =
|
||||||
@@ -34,7 +35,7 @@ export type OrgPermissionSet =
|
|||||||
| [OrgPermissionActions, OrgPermissionSubjects.Sso]
|
| [OrgPermissionActions, OrgPermissionSubjects.Sso]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
|
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
|
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.ServiceTokens];
|
| [OrgPermissionActions, OrgPermissionSubjects.MachineIdentity];
|
||||||
|
|
||||||
const buildAdminPermission = () => {
|
const buildAdminPermission = () => {
|
||||||
const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
|
const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
|
||||||
@@ -77,10 +78,10 @@ const buildAdminPermission = () => {
|
|||||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing);
|
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing);
|
||||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Billing);
|
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Billing);
|
||||||
|
|
||||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.ServiceTokens);
|
can(OrgPermissionActions.Read, OrgPermissionSubjects.MachineIdentity);
|
||||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.ServiceTokens);
|
can(OrgPermissionActions.Create, OrgPermissionSubjects.MachineIdentity);
|
||||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.ServiceTokens);
|
can(OrgPermissionActions.Edit, OrgPermissionSubjects.MachineIdentity);
|
||||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.ServiceTokens);
|
can(OrgPermissionActions.Delete, OrgPermissionSubjects.MachineIdentity);
|
||||||
|
|
||||||
return build({ conditionsMatcher });
|
return build({ conditionsMatcher });
|
||||||
};
|
};
|
||||||
@@ -105,10 +106,10 @@ const buildMemberPermission = () => {
|
|||||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.SecretScanning);
|
can(OrgPermissionActions.Edit, OrgPermissionSubjects.SecretScanning);
|
||||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.SecretScanning);
|
can(OrgPermissionActions.Delete, OrgPermissionSubjects.SecretScanning);
|
||||||
|
|
||||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.ServiceTokens);
|
can(OrgPermissionActions.Read, OrgPermissionSubjects.MachineIdentity);
|
||||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.ServiceTokens);
|
can(OrgPermissionActions.Create, OrgPermissionSubjects.MachineIdentity);
|
||||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.ServiceTokens);
|
can(OrgPermissionActions.Edit, OrgPermissionSubjects.MachineIdentity);
|
||||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.ServiceTokens);
|
can(OrgPermissionActions.Delete, OrgPermissionSubjects.MachineIdentity);
|
||||||
|
|
||||||
return build({ conditionsMatcher });
|
return build({ conditionsMatcher });
|
||||||
};
|
};
|
||||||
@@ -132,11 +133,11 @@ export const getUserOrgPermissions = async (userId: string, orgId: string) => {
|
|||||||
throw UnauthorizedRequestError({ message: "User doesn't belong to organization" });
|
throw UnauthorizedRequestError({ message: "User doesn't belong to organization" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (membership.role === "admin") return { permission: adminPermissions, membership };
|
if (membership.role === ADMIN) return { permission: adminPermissions, membership };
|
||||||
|
|
||||||
if (membership.role === "member") return { permission: memberPermissions, membership };
|
if (membership.role === MEMBER) return { permission: memberPermissions, membership };
|
||||||
|
|
||||||
if (membership.role === "custom") {
|
if (membership.role === CUSTOM) {
|
||||||
const permission = createMongoAbility<OrgPermissionSet>(membership.customRole.permissions, {
|
const permission = createMongoAbility<OrgPermissionSet>(membership.customRole.permissions, {
|
||||||
conditionsMatcher
|
conditionsMatcher
|
||||||
});
|
});
|
||||||
@@ -144,4 +145,58 @@ export const getUserOrgPermissions = async (userId: string, orgId: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
throw BadRequestError({ message: "User role not found" });
|
throw BadRequestError({ message: "User role not found" });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getOrgRolePermissions = async (role: string, orgId: string) => {
|
||||||
|
const isCustomRole = ![ADMIN, MEMBER].includes(role);
|
||||||
|
if (isCustomRole) {
|
||||||
|
const orgRole = await Role.findOne({
|
||||||
|
slug: role,
|
||||||
|
isOrgRole: true,
|
||||||
|
organization: new Types.ObjectId(orgId)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!orgRole) throw BadRequestError({ message: "Org Role not found" });
|
||||||
|
|
||||||
|
return createMongoAbility<OrgPermissionSet>(orgRole.permissions as RawRuleOf<MongoAbility<OrgPermissionSet>>[], {
|
||||||
|
conditionsMatcher
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (role) {
|
||||||
|
case ADMIN:
|
||||||
|
return adminPermissions;
|
||||||
|
case MEMBER:
|
||||||
|
return memberPermissions;
|
||||||
|
default:
|
||||||
|
throw BadRequestError({ message: "User org role not found" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts and formats permissions from a CASL Ability object or a raw permission set.
|
||||||
|
* @param ability
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const extractPermissions = (ability: MongoAbility<OrgPermissionSet> | OrgPermissionSet) => {
|
||||||
|
return ability.A.map((permission: any) => `${permission.action}_${permission.subject}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares two sets of permissions to determine if the first set is at least as privileged as the second set.
|
||||||
|
* The function checks if all permissions in the second set are contained within the first set and if the first set has equal or more permissions.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const isAtLeastAsPrivilegedOrg = (permissions1: MongoAbility<OrgPermissionSet> | OrgPermissionSet, permissions2: MongoAbility<OrgPermissionSet> | OrgPermissionSet) => {
|
||||||
|
|
||||||
|
const set1 = new Set(extractPermissions(permissions1));
|
||||||
|
const set2 = new Set(extractPermissions(permissions2));
|
||||||
|
|
||||||
|
for (const perm of set2) {
|
||||||
|
if (!set1.has(perm)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return set1.size >= set2.size;
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ import { MEMBER } from "../variables";
|
|||||||
|
|
||||||
export const RefreshTokenV3 = z.object({
|
export const RefreshTokenV3 = z.object({
|
||||||
body: z.object({
|
body: z.object({
|
||||||
refresh_token: z.string().trim()
|
refreshToken: z.string().trim()
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -31,7 +31,6 @@ export const UpdateMachineIdentityV3 = z.object({
|
|||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
name: z.string().trim().optional(),
|
name: z.string().trim().optional(),
|
||||||
isActive: z.boolean().optional(),
|
|
||||||
role: z.string().trim().min(1).optional(),
|
role: z.string().trim().min(1).optional(),
|
||||||
trustedIps: z
|
trustedIps: z
|
||||||
.object({
|
.object({
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export enum OrgPermissionSubjects {
|
|||||||
Sso = "sso",
|
Sso = "sso",
|
||||||
Billing = "billing",
|
Billing = "billing",
|
||||||
SecretScanning = "secret-scanning",
|
SecretScanning = "secret-scanning",
|
||||||
ServiceTokens = "service-tokens"
|
MachineIdentity = "machine-identity"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OrgPermissionSet =
|
export type OrgPermissionSet =
|
||||||
@@ -29,6 +29,6 @@ export type OrgPermissionSet =
|
|||||||
| [OrgPermissionActions, OrgPermissionSubjects.Sso]
|
| [OrgPermissionActions, OrgPermissionSubjects.Sso]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
|
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
|
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.ServiceTokens];
|
| [OrgPermissionActions, OrgPermissionSubjects.MachineIdentity];
|
||||||
|
|
||||||
export type TOrgPermission = MongoAbility<OrgPermissionSet>;
|
export type TOrgPermission = MongoAbility<OrgPermissionSet>;
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ export enum ProjectPermissionSub {
|
|||||||
Secrets = "secrets",
|
Secrets = "secrets",
|
||||||
SecretRollback = "secret-rollback",
|
SecretRollback = "secret-rollback",
|
||||||
SecretApproval = "secret-approval",
|
SecretApproval = "secret-approval",
|
||||||
SecretRotation = "secret-rotation"
|
SecretRotation = "secret-rotation",
|
||||||
|
MachineIdentity = "machine-identity"
|
||||||
}
|
}
|
||||||
|
|
||||||
type SubjectFields = {
|
type SubjectFields = {
|
||||||
@@ -44,6 +45,7 @@ export type ProjectPermissionSet =
|
|||||||
| [ProjectPermissionActions, ProjectPermissionSub.Environments]
|
| [ProjectPermissionActions, ProjectPermissionSub.Environments]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.IpAllowList]
|
| [ProjectPermissionActions, ProjectPermissionSub.IpAllowList]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.Settings]
|
| [ProjectPermissionActions, ProjectPermissionSub.Settings]
|
||||||
|
| [ProjectPermissionActions, ProjectPermissionSub.MachineIdentity]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
|
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
|
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
|
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const MembersPage = withPermission(
|
|||||||
<TabList>
|
<TabList>
|
||||||
<Tab value={TabSections.Member}>People</Tab>
|
<Tab value={TabSections.Member}>People</Tab>
|
||||||
<Tab value={TabSections.MachineIdentities}>Machine Identities</Tab>
|
<Tab value={TabSections.MachineIdentities}>Machine Identities</Tab>
|
||||||
<Tab value={TabSections.Roles}>Roles</Tab>
|
<Tab value={TabSections.Roles}>Organization Roles</Tab>
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabPanel value={TabSections.Member}>
|
<TabPanel value={TabSections.Member}>
|
||||||
<OrgMembersTab />
|
<OrgMembersTab />
|
||||||
|
|||||||
+9
-9
@@ -32,7 +32,7 @@ import {
|
|||||||
useGetRoles,
|
useGetRoles,
|
||||||
useUpdateMachineIdentity
|
useUpdateMachineIdentity
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { ServiceTokenV3TrustedIp } from "@app/hooks/api/serviceTokens/types";
|
import { MachineTrustedIp } from "@app/hooks/api/machineIdentities/types";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
enum TabSections {
|
enum TabSections {
|
||||||
@@ -50,7 +50,7 @@ const expirations = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const schema = yup.object({
|
const schema = yup.object({
|
||||||
name: yup.string().required("ST V3 name is required"),
|
name: yup.string().required("MI name is required"),
|
||||||
expiresIn: yup.string(),
|
expiresIn: yup.string(),
|
||||||
accessTokenTTL: yup
|
accessTokenTTL: yup
|
||||||
.string()
|
.string()
|
||||||
@@ -63,7 +63,7 @@ const schema = yup.object({
|
|||||||
return !Number.isNaN(num) && num > 0 && String(num) === value;
|
return !Number.isNaN(num) && num > 0 && String(num) === value;
|
||||||
})
|
})
|
||||||
.required("Access Token TTL is required"),
|
.required("Access Token TTL is required"),
|
||||||
role: yup.string().required("ST V3 role is required"),
|
role: yup.string(),
|
||||||
trustedIps: yup
|
trustedIps: yup
|
||||||
.array(
|
.array(
|
||||||
yup.object({
|
yup.object({
|
||||||
@@ -146,7 +146,7 @@ export const AddMachineIdentityModal = ({
|
|||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
};
|
};
|
||||||
trustedIps: ServiceTokenV3TrustedIp[];
|
trustedIps: MachineTrustedIp[];
|
||||||
accessTokenTTL: number;
|
accessTokenTTL: number;
|
||||||
isRefreshTokenRotationEnabled: boolean;
|
isRefreshTokenRotationEnabled: boolean;
|
||||||
};
|
};
|
||||||
@@ -161,7 +161,7 @@ export const AddMachineIdentityModal = ({
|
|||||||
trustedIps: machineIdentity.trustedIps.map(({
|
trustedIps: machineIdentity.trustedIps.map(({
|
||||||
ipAddress,
|
ipAddress,
|
||||||
prefix
|
prefix
|
||||||
}: ServiceTokenV3TrustedIp) => {
|
}: MachineTrustedIp) => {
|
||||||
return ({
|
return ({
|
||||||
ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}`
|
ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}`
|
||||||
});
|
});
|
||||||
@@ -206,9 +206,9 @@ export const AddMachineIdentityModal = ({
|
|||||||
await updateMutateAsync({
|
await updateMutateAsync({
|
||||||
machineId: machineIdentity.machineId,
|
machineId: machineIdentity.machineId,
|
||||||
name,
|
name,
|
||||||
role,
|
role: role || undefined,
|
||||||
trustedIps,
|
trustedIps,
|
||||||
expiresIn: expiresIn === "" ? undefined : Number(expiresIn),
|
expiresIn: (!expiresIn) ? undefined : Number(expiresIn),
|
||||||
accessTokenTTL: Number(accessTokenTTL),
|
accessTokenTTL: Number(accessTokenTTL),
|
||||||
isRefreshTokenRotationEnabled
|
isRefreshTokenRotationEnabled
|
||||||
});
|
});
|
||||||
@@ -218,10 +218,10 @@ export const AddMachineIdentityModal = ({
|
|||||||
|
|
||||||
const { refreshToken } = await createMutateAsync({
|
const { refreshToken } = await createMutateAsync({
|
||||||
name,
|
name,
|
||||||
role,
|
role: role || undefined,
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
trustedIps,
|
trustedIps,
|
||||||
expiresIn: expiresIn === "" ? undefined : Number(expiresIn),
|
expiresIn: (!expiresIn) ? undefined : Number(expiresIn),
|
||||||
accessTokenTTL: Number(accessTokenTTL),
|
accessTokenTTL: Number(accessTokenTTL),
|
||||||
isRefreshTokenRotationEnabled
|
isRefreshTokenRotationEnabled
|
||||||
});
|
});
|
||||||
|
|||||||
+2
-2
@@ -53,7 +53,7 @@ export const MachineIdentitySection = withPermission(
|
|||||||
</p>
|
</p>
|
||||||
<OrgPermissionCan
|
<OrgPermissionCan
|
||||||
I={OrgPermissionActions.Create}
|
I={OrgPermissionActions.Create}
|
||||||
a={OrgPermissionSubjects.ServiceTokens}
|
a={OrgPermissionSubjects.MachineIdentity}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button
|
<Button
|
||||||
@@ -92,5 +92,5 @@ export const MachineIdentitySection = withPermission(
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
{ action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.ServiceTokens }
|
{ action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.MachineIdentity }
|
||||||
);
|
);
|
||||||
+7
-15
@@ -1,4 +1,3 @@
|
|||||||
import { useCallback } from "react";
|
|
||||||
import { faPencil,faServer, faXmark } from "@fortawesome/free-solid-svg-icons";
|
import { faPencil,faServer, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
@@ -28,7 +27,7 @@ import {
|
|||||||
useGetRoles,
|
useGetRoles,
|
||||||
useUpdateMachineIdentity
|
useUpdateMachineIdentity
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { ServiceTokenV3TrustedIp } from "@app/hooks/api/serviceTokens/types"
|
import { MachineTrustedIp } from "@app/hooks/api/machineIdentities/types";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -42,7 +41,7 @@ type Props = {
|
|||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
};
|
};
|
||||||
trustedIps?: ServiceTokenV3TrustedIp[];
|
trustedIps?: MachineTrustedIp[];
|
||||||
accessTokenTTL?: number;
|
accessTokenTTL?: number;
|
||||||
isRefreshTokenRotationEnabled?: boolean;
|
isRefreshTokenRotationEnabled?: boolean;
|
||||||
}
|
}
|
||||||
@@ -115,13 +114,6 @@ export const MachineIdentityTable = ({
|
|||||||
// });
|
// });
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
const findRoleFromId = useCallback(
|
|
||||||
(roleId: string) => {
|
|
||||||
return (roles || []).find(({ _id: id }) => id === roleId);
|
|
||||||
},
|
|
||||||
[roles]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableContainer>
|
<TableContainer>
|
||||||
@@ -163,7 +155,7 @@ export const MachineIdentityTable = ({
|
|||||||
{/* <Td>
|
{/* <Td>
|
||||||
<OrgPermissionCan
|
<OrgPermissionCan
|
||||||
I={OrgPermissionActions.Edit}
|
I={OrgPermissionActions.Edit}
|
||||||
a={OrgPermissionSubjects.ServiceTokens}
|
a={OrgPermissionSubjects.MachineIdentity}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Switch
|
<Switch
|
||||||
@@ -183,13 +175,13 @@ export const MachineIdentityTable = ({
|
|||||||
<Td>
|
<Td>
|
||||||
<OrgPermissionCan
|
<OrgPermissionCan
|
||||||
I={OrgPermissionActions.Edit}
|
I={OrgPermissionActions.Edit}
|
||||||
a={OrgPermissionSubjects.ServiceTokens}
|
a={OrgPermissionSubjects.MachineIdentity}
|
||||||
>
|
>
|
||||||
{(isAllowed) => {
|
{(isAllowed) => {
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
value={
|
value={
|
||||||
role === "custom" ? findRoleFromId(customRole)?.slug : role
|
role === "custom" ? customRole.slug : role
|
||||||
}
|
}
|
||||||
isDisabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
className="w-40 bg-mineshaft-600"
|
className="w-40 bg-mineshaft-600"
|
||||||
@@ -231,7 +223,7 @@ export const MachineIdentityTable = ({
|
|||||||
<Td className="flex justify-end">
|
<Td className="flex justify-end">
|
||||||
<OrgPermissionCan
|
<OrgPermissionCan
|
||||||
I={OrgPermissionActions.Edit}
|
I={OrgPermissionActions.Edit}
|
||||||
a={OrgPermissionSubjects.ServiceTokens}
|
a={OrgPermissionSubjects.MachineIdentity}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -258,7 +250,7 @@ export const MachineIdentityTable = ({
|
|||||||
</OrgPermissionCan>
|
</OrgPermissionCan>
|
||||||
<OrgPermissionCan
|
<OrgPermissionCan
|
||||||
I={OrgPermissionActions.Delete}
|
I={OrgPermissionActions.Delete}
|
||||||
a={OrgPermissionSubjects.ServiceTokens}
|
a={OrgPermissionSubjects.MachineIdentity}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|||||||
+8
-2
@@ -5,10 +5,10 @@ import {
|
|||||||
faContactCard,
|
faContactCard,
|
||||||
faMagnifyingGlass,
|
faMagnifyingGlass,
|
||||||
faMoneyBill,
|
faMoneyBill,
|
||||||
|
faServer,
|
||||||
faSignIn,
|
faSignIn,
|
||||||
faUserCog,
|
faUserCog,
|
||||||
faUsers
|
faUsers} from "@fortawesome/free-solid-svg-icons";
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
|
||||||
@@ -40,6 +40,12 @@ const SIMPLE_PERMISSION_OPTIONS = [
|
|||||||
icon: faUsers,
|
icon: faUsers,
|
||||||
formName: "member"
|
formName: "member"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Machine identity management",
|
||||||
|
subtitle: "Create, view, update and remove machine identities from the organization",
|
||||||
|
icon: faServer,
|
||||||
|
formName: "machine-identity"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "Billing & usage",
|
title: "Billing & usage",
|
||||||
subtitle: "Modify organization subscription plan",
|
subtitle: "Modify organization subscription plan",
|
||||||
|
|||||||
+2
-1
@@ -31,7 +31,8 @@ export const formSchema = z.object({
|
|||||||
"incident-contact": generalPermissionSchema,
|
"incident-contact": generalPermissionSchema,
|
||||||
"secret-scanning": generalPermissionSchema,
|
"secret-scanning": generalPermissionSchema,
|
||||||
sso: generalPermissionSchema,
|
sso: generalPermissionSchema,
|
||||||
billing: generalPermissionSchema
|
billing: generalPermissionSchema,
|
||||||
|
"machine-identity": generalPermissionSchema
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export const MembersPage = withProjectPermission(
|
|||||||
<Tab value={TabSections.Member}>People</Tab>
|
<Tab value={TabSections.Member}>People</Tab>
|
||||||
<Tab value={TabSections.MachineIdentities}>Machine Identities</Tab>
|
<Tab value={TabSections.MachineIdentities}>Machine Identities</Tab>
|
||||||
<Tab value={TabSections.ServiceTokens}>Service Tokens</Tab>
|
<Tab value={TabSections.ServiceTokens}>Service Tokens</Tab>
|
||||||
<Tab value={TabSections.Roles}>Roles</Tab>
|
<Tab value={TabSections.Roles}>Project Roles</Tab>
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabPanel value={TabSections.Member}>
|
<TabPanel value={TabSections.Member}>
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|||||||
+3
-3
@@ -26,8 +26,8 @@ import {
|
|||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
const schema = yup.object({
|
const schema = yup.object({
|
||||||
machineId: yup.string().required("ST V3 id is required"),
|
machineId: yup.string().required("Machine identity id is required"),
|
||||||
role: yup.string().required("ST V3 role is required")
|
role: yup.string().required("Machine identity role is required")
|
||||||
}).required();
|
}).required();
|
||||||
|
|
||||||
export type FormData = yup.InferType<typeof schema>;
|
export type FormData = yup.InferType<typeof schema>;
|
||||||
@@ -124,7 +124,7 @@ export const AddMachineIdentityModal = ({
|
|||||||
defaultValue={filteredMachineMembershipOrgs?.[0]?._id}
|
defaultValue={filteredMachineMembershipOrgs?.[0]?._id}
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
label="Service Account"
|
label="Machine Identity"
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
>
|
>
|
||||||
|
|||||||
+2
-2
@@ -66,7 +66,7 @@ export const MachineIdentitySection = withProjectPermission(
|
|||||||
</p>
|
</p>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Create}
|
I={ProjectPermissionActions.Create}
|
||||||
a={ProjectPermissionSub.ServiceTokens}
|
a={ProjectPermissionSub.MachineIdentity}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button
|
<Button
|
||||||
@@ -104,5 +104,5 @@ export const MachineIdentitySection = withProjectPermission(
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.ServiceTokens }
|
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.MachineIdentity }
|
||||||
);
|
);
|
||||||
+3
-3
@@ -135,7 +135,7 @@ export const MachineIdentityTable = ({
|
|||||||
<Td>
|
<Td>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Edit}
|
I={ProjectPermissionActions.Edit}
|
||||||
a={ProjectPermissionSub.ServiceTokens}
|
a={ProjectPermissionSub.MachineIdentity}
|
||||||
>
|
>
|
||||||
{(isAllowed) => {
|
{(isAllowed) => {
|
||||||
return (
|
return (
|
||||||
@@ -156,7 +156,7 @@ export const MachineIdentityTable = ({
|
|||||||
{(roles || [])
|
{(roles || [])
|
||||||
.map(({ slug, name: roleName }) => (
|
.map(({ slug, name: roleName }) => (
|
||||||
<SelectItem value={slug} key={`owner-option-${slug}`}>
|
<SelectItem value={slug} key={`owner-option-${slug}`}>
|
||||||
{roleName}
|
{roleName}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
@@ -168,7 +168,7 @@ export const MachineIdentityTable = ({
|
|||||||
<Td className="flex justify-end">
|
<Td className="flex justify-end">
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Delete}
|
I={ProjectPermissionActions.Delete}
|
||||||
a={ProjectPermissionSub.ServiceTokens}
|
a={ProjectPermissionSub.MachineIdentity}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|||||||
Reference in New Issue
Block a user