Fix merge conflicts

This commit is contained in:
Tuan Dang
2024-05-19 22:19:49 -07:00
52 changed files with 2349 additions and 109 deletions
@@ -45,12 +45,19 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
`${TableName.IdentityAzureAuth}.identityId`
);
})
.leftJoin(TableName.IdentityKubernetesAuth, (qb) => {
qb.on(`${TableName.Identity}.authMethod`, db.raw("?", [IdentityAuthMethod.KUBERNETES_AUTH])).andOn(
`${TableName.Identity}.id`,
`${TableName.IdentityKubernetesAuth}.identityId`
);
})
.select(selectAllTableCols(TableName.IdentityAccessToken))
.select(
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityUniversalAuth).as("accessTokenTrustedIpsUa"),
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityGcpAuth).as("accessTokenTrustedIpsGcp"),
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityAwsAuth).as("accessTokenTrustedIpsAws"),
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityAwsAuth).as("accessTokenTrustedIpsAzure"),
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityAzureAuth).as("accessTokenTrustedIpsAzure"),
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityKubernetesAuth).as("accessTokenTrustedIpsK8s"),
db.ref("name").withSchema(TableName.Identity)
)
.first();
@@ -63,7 +70,8 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
doc.accessTokenTrustedIpsUa ||
doc.accessTokenTrustedIpsGcp ||
doc.accessTokenTrustedIpsAws ||
doc.accessTokenTrustedIpsAzure
doc.accessTokenTrustedIpsAzure ||
doc.accessTokenTrustedIpsK8s
};
} catch (error) {
throw new DatabaseError({ error, name: "IdAccessTokenFindOne" });
@@ -0,0 +1,10 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TIdentityKubernetesAuthDALFactory = ReturnType<typeof identityKubernetesAuthDALFactory>;
export const identityKubernetesAuthDALFactory = (db: TDbClient) => {
const kubernetesAuthOrm = ormify(db, TableName.IdentityKubernetesAuth);
return kubernetesAuthOrm;
};
@@ -0,0 +1,15 @@
/**
* Extracts the K8s service account name and namespace
* from the username in this format: system:serviceaccount:default:infisical-auth
*/
export const extractK8sUsername = (username: string) => {
const parts = username.split(":");
// Ensure that the username format is correct
if (parts.length === 4 && parts[0] === "system" && parts[1] === "serviceaccount") {
return {
namespace: parts[2],
name: parts[3]
};
}
throw new Error("Invalid username format");
};
@@ -0,0 +1,515 @@
import { ForbiddenError } from "@casl/ability";
import axios from "axios";
import https from "https";
import jwt from "jsonwebtoken";
import { IdentityAuthMethod, SecretKeyEncoding, TIdentityKubernetesAuthsUpdate } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { getConfig } from "@app/lib/config/env";
import {
decryptSymmetric,
encryptSymmetric,
generateAsymmetricKeyPair,
generateSymmetricKey,
infisicalSymmetricDecrypt,
infisicalSymmetricEncypt
} from "@app/lib/crypto/encryption";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
import { AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
import { TIdentityKubernetesAuthDALFactory } from "./identity-kubernetes-auth-dal";
import { extractK8sUsername } from "./identity-kubernetes-auth-fns";
import {
TAttachKubernetesAuthDTO,
TCreateTokenReviewResponse,
TGetKubernetesAuthDTO,
TLoginKubernetesAuthDTO,
TUpdateKubernetesAuthDTO
} from "./identity-kubernetes-auth-types";
type TIdentityKubernetesAuthServiceFactoryDep = {
identityKubernetesAuthDAL: Pick<
TIdentityKubernetesAuthDALFactory,
"create" | "findOne" | "transaction" | "updateById"
>;
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create">;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne" | "findById">;
identityDAL: Pick<TIdentityDALFactory, "updateById">;
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "transaction" | "create">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
};
export type TIdentityKubernetesAuthServiceFactory = ReturnType<typeof identityKubernetesAuthServiceFactory>;
export const identityKubernetesAuthServiceFactory = ({
identityKubernetesAuthDAL,
identityOrgMembershipDAL,
identityAccessTokenDAL,
identityDAL,
orgBotDAL,
permissionService,
licenseService
}: TIdentityKubernetesAuthServiceFactoryDep) => {
const login = async ({ identityId, jwt: serviceAccountJwt }: TLoginKubernetesAuthDTO) => {
const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId });
if (!identityKubernetesAuth) throw new UnauthorizedError();
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({
identityId: identityKubernetesAuth.identityId
});
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId });
if (!orgBot) throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" });
const key = infisicalSymmetricDecrypt({
ciphertext: orgBot.encryptedSymmetricKey,
iv: orgBot.symmetricKeyIV,
tag: orgBot.symmetricKeyTag,
keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding
});
const { encryptedCaCert, caCertIV, caCertTag, encryptedTokenReviewerJwt, tokenReviewerJwtIV, tokenReviewerJwtTag } =
identityKubernetesAuth;
let caCert = "";
if (encryptedCaCert && caCertIV && caCertTag) {
caCert = decryptSymmetric({
ciphertext: encryptedCaCert,
iv: caCertIV,
tag: caCertTag,
key
});
}
let tokenReviewerJwt = "";
if (encryptedTokenReviewerJwt && tokenReviewerJwtIV && tokenReviewerJwtTag) {
tokenReviewerJwt = decryptSymmetric({
ciphertext: encryptedTokenReviewerJwt,
iv: tokenReviewerJwtIV,
tag: tokenReviewerJwtTag,
key
});
}
const { data }: { data: TCreateTokenReviewResponse } = await axios.post(
`${identityKubernetesAuth.kubernetesHost}/apis/authentication.k8s.io/v1/tokenreviews`,
{
apiVersion: "authentication.k8s.io/v1",
kind: "TokenReview",
spec: {
token: serviceAccountJwt
}
},
{
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${tokenReviewerJwt}`
},
httpsAgent: new https.Agent({
ca: caCert,
rejectUnauthorized: !!caCert
})
}
);
if ("error" in data.status) throw new UnauthorizedError({ message: data.status.error });
// check the response to determine if the token is valid
if (!(data.status && data.status.authenticated)) throw new UnauthorizedError();
const { namespace: targetNamespace, name: targetName } = extractK8sUsername(data.status.user.username);
if (identityKubernetesAuth.allowedNamespaces) {
// validate if [targetNamespace] is in the list of allowed namespaces
const isNamespaceAllowed = identityKubernetesAuth.allowedNamespaces
.split(",")
.map((namespace) => namespace.trim())
.some((namespace) => namespace === targetNamespace);
if (!isNamespaceAllowed) throw new UnauthorizedError();
}
if (identityKubernetesAuth.allowedNames) {
// validate if [targetName] is in the list of allowed names
const isNameAllowed = identityKubernetesAuth.allowedNames
.split(",")
.map((name) => name.trim())
.some((name) => name === targetName);
if (!isNameAllowed) throw new UnauthorizedError();
}
if (identityKubernetesAuth.allowedAudience) {
// validate if [audience] is in the list of allowed audiences
const isAudienceAllowed = data.status.audiences.some(
(audience) => audience === identityKubernetesAuth.allowedAudience
);
if (!isAudienceAllowed) throw new UnauthorizedError();
}
const identityAccessToken = await identityKubernetesAuthDAL.transaction(async (tx) => {
const newToken = await identityAccessTokenDAL.create(
{
identityId: identityKubernetesAuth.identityId,
isAccessTokenRevoked: false,
accessTokenTTL: identityKubernetesAuth.accessTokenTTL,
accessTokenMaxTTL: identityKubernetesAuth.accessTokenMaxTTL,
accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityKubernetesAuth.accessTokenNumUsesLimit
},
tx
);
return newToken;
});
const appCfg = getConfig();
const accessToken = jwt.sign(
{
identityId: identityKubernetesAuth.identityId,
identityAccessTokenId: identityAccessToken.id,
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
} as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET,
{
expiresIn:
Number(identityAccessToken.accessTokenMaxTTL) === 0
? undefined
: Number(identityAccessToken.accessTokenMaxTTL)
}
);
return { accessToken, identityKubernetesAuth, identityAccessToken, identityMembershipOrg };
};
const attachKubernetesAuth = async ({
identityId,
kubernetesHost,
caCert,
tokenReviewerJwt,
allowedNamespaces,
allowedNames,
allowedAudience,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TAttachKubernetesAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
if (identityMembershipOrg.identity.authMethod)
throw new BadRequestError({
message: "Failed to add Kubernetes Auth to already configured identity"
});
if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) {
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
}
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity);
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => {
if (
!plan.ipAllowlisting &&
accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" &&
accessTokenTrustedIp.ipAddress !== "::/0"
)
throw new BadRequestError({
message:
"Failed to add IP access range to access token due to plan restriction. Upgrade plan to add IP access range."
});
if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress))
throw new BadRequestError({
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
});
return extractIPDetails(accessTokenTrustedIp.ipAddress);
});
const orgBot = await orgBotDAL.transaction(async (tx) => {
const doc = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId }, tx);
if (doc) return doc;
const { privateKey, publicKey } = generateAsymmetricKeyPair();
const key = generateSymmetricKey();
const {
ciphertext: encryptedPrivateKey,
iv: privateKeyIV,
tag: privateKeyTag,
encoding: privateKeyKeyEncoding,
algorithm: privateKeyAlgorithm
} = infisicalSymmetricEncypt(privateKey);
const {
ciphertext: encryptedSymmetricKey,
iv: symmetricKeyIV,
tag: symmetricKeyTag,
encoding: symmetricKeyKeyEncoding,
algorithm: symmetricKeyAlgorithm
} = infisicalSymmetricEncypt(key);
return orgBotDAL.create(
{
name: "Infisical org bot",
publicKey,
privateKeyIV,
encryptedPrivateKey,
symmetricKeyIV,
symmetricKeyTag,
encryptedSymmetricKey,
symmetricKeyAlgorithm,
orgId: identityMembershipOrg.orgId,
privateKeyTag,
privateKeyAlgorithm,
privateKeyKeyEncoding,
symmetricKeyKeyEncoding
},
tx
);
});
const key = infisicalSymmetricDecrypt({
ciphertext: orgBot.encryptedSymmetricKey,
iv: orgBot.symmetricKeyIV,
tag: orgBot.symmetricKeyTag,
keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding
});
const { ciphertext: encryptedCaCert, iv: caCertIV, tag: caCertTag } = encryptSymmetric(caCert, key);
const {
ciphertext: encryptedTokenReviewerJwt,
iv: tokenReviewerJwtIV,
tag: tokenReviewerJwtTag
} = encryptSymmetric(tokenReviewerJwt, key);
const identityKubernetesAuth = await identityKubernetesAuthDAL.transaction(async (tx) => {
const doc = await identityKubernetesAuthDAL.create(
{
identityId: identityMembershipOrg.identityId,
kubernetesHost,
encryptedCaCert,
caCertIV,
caCertTag,
encryptedTokenReviewerJwt,
tokenReviewerJwtIV,
tokenReviewerJwtTag,
allowedNamespaces,
allowedNames,
allowedAudience,
accessTokenMaxTTL,
accessTokenTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps)
},
tx
);
await identityDAL.updateById(
identityMembershipOrg.identityId,
{
authMethod: IdentityAuthMethod.KUBERNETES_AUTH
},
tx
);
return doc;
});
return { ...identityKubernetesAuth, caCert, tokenReviewerJwt, orgId: identityMembershipOrg.orgId };
};
const updateKubernetesAuth = async ({
identityId,
kubernetesHost,
caCert,
tokenReviewerJwt,
allowedNamespaces,
allowedNames,
allowedAudience,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TUpdateKubernetesAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.KUBERNETES_AUTH)
throw new BadRequestError({
message: "Failed to update Kubernetes Auth"
});
const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId });
if (
(accessTokenMaxTTL || identityKubernetesAuth.accessTokenMaxTTL) > 0 &&
(accessTokenTTL || identityKubernetesAuth.accessTokenMaxTTL) >
(accessTokenMaxTTL || identityKubernetesAuth.accessTokenMaxTTL)
) {
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
}
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => {
if (
!plan.ipAllowlisting &&
accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" &&
accessTokenTrustedIp.ipAddress !== "::/0"
)
throw new BadRequestError({
message:
"Failed to add IP access range to access token due to plan restriction. Upgrade plan to add IP access range."
});
if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress))
throw new BadRequestError({
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
});
return extractIPDetails(accessTokenTrustedIp.ipAddress);
});
const updateQuery: TIdentityKubernetesAuthsUpdate = {
kubernetesHost,
allowedNamespaces,
allowedNames,
allowedAudience,
accessTokenMaxTTL,
accessTokenTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps: reformattedAccessTokenTrustedIps
? JSON.stringify(reformattedAccessTokenTrustedIps)
: undefined
};
const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId });
if (!orgBot) throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" });
const key = infisicalSymmetricDecrypt({
ciphertext: orgBot.encryptedSymmetricKey,
iv: orgBot.symmetricKeyIV,
tag: orgBot.symmetricKeyTag,
keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding
});
if (caCert !== undefined) {
const { ciphertext: encryptedCACert, iv: caCertIV, tag: caCertTag } = encryptSymmetric(caCert, key);
updateQuery.encryptedCaCert = encryptedCACert;
updateQuery.caCertIV = caCertIV;
updateQuery.caCertTag = caCertTag;
}
if (tokenReviewerJwt !== undefined) {
const {
ciphertext: encryptedTokenReviewerJwt,
iv: tokenReviewerJwtIV,
tag: tokenReviewerJwtTag
} = encryptSymmetric(tokenReviewerJwt, key);
updateQuery.encryptedTokenReviewerJwt = encryptedTokenReviewerJwt;
updateQuery.tokenReviewerJwtIV = tokenReviewerJwtIV;
updateQuery.tokenReviewerJwtTag = tokenReviewerJwtTag;
}
const updatedKubernetesAuth = await identityKubernetesAuthDAL.updateById(identityKubernetesAuth.id, updateQuery);
return { ...updatedKubernetesAuth, orgId: identityMembershipOrg.orgId };
};
const getKubernetesAuth = async ({
identityId,
actorId,
actor,
actorAuthMethod,
actorOrgId
}: TGetKubernetesAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.KUBERNETES_AUTH)
throw new BadRequestError({
message: "The identity does not have Kubernetes Auth attached"
});
const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId });
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId });
if (!orgBot) throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" });
const key = infisicalSymmetricDecrypt({
ciphertext: orgBot.encryptedSymmetricKey,
iv: orgBot.symmetricKeyIV,
tag: orgBot.symmetricKeyTag,
keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding
});
const { encryptedCaCert, caCertIV, caCertTag, encryptedTokenReviewerJwt, tokenReviewerJwtIV, tokenReviewerJwtTag } =
identityKubernetesAuth;
let caCert = "";
if (encryptedCaCert && caCertIV && caCertTag) {
caCert = decryptSymmetric({
ciphertext: encryptedCaCert,
iv: caCertIV,
tag: caCertTag,
key
});
}
let tokenReviewerJwt = "";
if (encryptedTokenReviewerJwt && tokenReviewerJwtIV && tokenReviewerJwtTag) {
tokenReviewerJwt = decryptSymmetric({
ciphertext: encryptedTokenReviewerJwt,
iv: tokenReviewerJwtIV,
tag: tokenReviewerJwtTag,
key
});
}
return { ...identityKubernetesAuth, caCert, tokenReviewerJwt, orgId: identityMembershipOrg.orgId };
};
return {
login,
attachKubernetesAuth,
updateKubernetesAuth,
getKubernetesAuth
};
};
@@ -0,0 +1,61 @@
import { TProjectPermission } from "@app/lib/types";
export type TLoginKubernetesAuthDTO = {
identityId: string;
jwt: string;
};
export type TAttachKubernetesAuthDTO = {
identityId: string;
kubernetesHost: string;
caCert: string;
tokenReviewerJwt: string;
allowedNamespaces: string;
allowedNames: string;
allowedAudience: string;
accessTokenTTL: number;
accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number;
accessTokenTrustedIps: { ipAddress: string }[];
} & Omit<TProjectPermission, "projectId">;
export type TUpdateKubernetesAuthDTO = {
identityId: string;
kubernetesHost?: string;
caCert?: string;
tokenReviewerJwt?: string;
allowedNamespaces?: string;
allowedNames?: string;
allowedAudience?: string;
accessTokenTTL?: number;
accessTokenMaxTTL?: number;
accessTokenNumUsesLimit?: number;
accessTokenTrustedIps?: { ipAddress: string }[];
} & Omit<TProjectPermission, "projectId">;
export type TGetKubernetesAuthDTO = {
identityId: string;
} & Omit<TProjectPermission, "projectId">;
type TCreateTokenReviewSuccessResponse = {
authenticated: true;
user: {
username: string;
uid: string;
groups: string[];
};
audiences: string[];
};
type TCreateTokenReviewErrorResponse = {
error: string;
};
export type TCreateTokenReviewResponse = {
apiVersion: "authentication.k8s.io/v1";
kind: "TokenReview";
spec: {
token: string;
};
status: TCreateTokenReviewSuccessResponse | TCreateTokenReviewErrorResponse;
};
@@ -10,11 +10,16 @@ export type TIdentityProjectDALFactory = ReturnType<typeof identityProjectDALFac
export const identityProjectDALFactory = (db: TDbClient) => {
const identityProjectOrm = ormify(db, TableName.IdentityProjectMembership);
const findByProjectId = async (projectId: string, tx?: Knex) => {
const findByProjectId = async (projectId: string, filter: { identityId?: string } = {}, tx?: Knex) => {
try {
const docs = await (tx || db)(TableName.IdentityProjectMembership)
.where(`${TableName.IdentityProjectMembership}.projectId`, projectId)
.join(TableName.Identity, `${TableName.IdentityProjectMembership}.identityId`, `${TableName.Identity}.id`)
.where((qb) => {
if (filter.identityId) {
void qb.where("identityId", filter.identityId);
}
})
.join(
TableName.IdentityProjectMembershipRole,
`${TableName.IdentityProjectMembershipRole}.projectMembershipId`,
@@ -18,6 +18,7 @@ import { TIdentityProjectMembershipRoleDALFactory } from "./identity-project-mem
import {
TCreateProjectIdentityDTO,
TDeleteProjectIdentityDTO,
TGetProjectIdentityByIdentityIdDTO,
TListProjectIdentityDTO,
TUpdateProjectIdentityDTO
} from "./identity-project-types";
@@ -51,7 +52,7 @@ export const identityProjectServiceFactory = ({
actorOrgId,
actorAuthMethod,
projectId,
role
roles
}: TCreateProjectIdentityDTO) => {
const { permission } = await permissionService.getProjectPermission(
actor,
@@ -78,18 +79,33 @@ export const identityProjectServiceFactory = ({
message: `Failed to find identity with id ${identityId}`
});
const { permission: rolePermission, role: customRole } = await permissionService.getProjectPermissionByRole(
role,
project.id
for await (const { role: requestedRoleChange } of roles) {
const { permission: rolePermission } = await permissionService.getProjectPermissionByRole(
requestedRoleChange,
projectId
);
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasRequiredPriviledges) {
throw new ForbiddenRequestError({ message: "Failed to change to a more privileged role" });
}
}
// validate custom roles input
const customInputRoles = roles.filter(
({ role }) => !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole)
);
const hasCustomRole = Boolean(customInputRoles.length);
const customRoles = hasCustomRole
? await projectRoleDAL.find({
projectId,
$in: { slug: customInputRoles.map(({ role }) => role) }
})
: [];
if (customRoles.length !== customInputRoles.length) throw new BadRequestError({ message: "Custom role not found" });
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasPriviledge)
throw new ForbiddenRequestError({
message: "Failed to add identity to project with more privileged role"
});
const isCustomRole = Boolean(customRole);
const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug);
const projectIdentity = await identityProjectDAL.transaction(async (tx) => {
const identityProjectMembership = await identityProjectDAL.create(
{
@@ -98,16 +114,32 @@ export const identityProjectServiceFactory = ({
},
tx
);
const sanitizedProjectMembershipRoles = roles.map((inputRole) => {
const isCustomRole = Boolean(customRolesGroupBySlug?.[inputRole.role]?.[0]);
if (!inputRole.isTemporary) {
return {
projectMembershipId: identityProjectMembership.id,
role: isCustomRole ? ProjectMembershipRole.Custom : inputRole.role,
customRoleId: customRolesGroupBySlug[inputRole.role] ? customRolesGroupBySlug[inputRole.role][0].id : null
};
}
await identityProjectMembershipRoleDAL.create(
{
// check cron or relative here later for now its just relative
const relativeTimeInMs = ms(inputRole.temporaryRange);
return {
projectMembershipId: identityProjectMembership.id,
role: isCustomRole ? ProjectMembershipRole.Custom : role,
customRoleId: customRole?.id
},
tx
);
return identityProjectMembership;
role: isCustomRole ? ProjectMembershipRole.Custom : inputRole.role,
customRoleId: customRolesGroupBySlug[inputRole.role] ? customRolesGroupBySlug[inputRole.role][0].id : null,
isTemporary: true,
temporaryMode: ProjectUserMembershipTemporaryMode.Relative,
temporaryRange: inputRole.temporaryRange,
temporaryAccessStartTime: new Date(inputRole.temporaryAccessStartTime),
temporaryAccessEndTime: new Date(new Date(inputRole.temporaryAccessStartTime).getTime() + relativeTimeInMs)
};
});
const identityRoles = await identityProjectMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx);
return { ...identityProjectMembership, roles: identityRoles };
});
return projectIdentity;
};
@@ -251,10 +283,33 @@ export const identityProjectServiceFactory = ({
return identityMemberships;
};
const getProjectIdentityByIdentityId = async ({
projectId,
actor,
actorId,
actorAuthMethod,
actorOrgId,
identityId
}: TGetProjectIdentityByIdentityIdDTO) => {
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Identity);
const [identityMembership] = await identityProjectDAL.findByProjectId(projectId, { identityId });
if (!identityMembership) throw new BadRequestError({ message: `Membership not found for identity ${identityId}` });
return identityMembership;
};
return {
createProjectIdentity,
updateProjectIdentity,
deleteProjectIdentity,
listProjectIdentities
listProjectIdentities,
getProjectIdentityByIdentityId
};
};
@@ -4,7 +4,19 @@ import { ProjectUserMembershipTemporaryMode } from "../project-membership/projec
export type TCreateProjectIdentityDTO = {
identityId: string;
role: string;
roles: (
| {
role: string;
isTemporary?: false;
}
| {
role: string;
isTemporary: true;
temporaryMode: ProjectUserMembershipTemporaryMode.Relative;
temporaryRange: string;
temporaryAccessStartTime: string;
}
)[];
} & TProjectPermission;
export type TUpdateProjectIdentityDTO = {
@@ -29,3 +41,7 @@ export type TDeleteProjectIdentityDTO = {
} & TProjectPermission;
export type TListProjectIdentityDTO = TProjectPermission;
export type TGetProjectIdentityByIdentityIdDTO = {
identityId: string;
} & TProjectPermission;
@@ -9,11 +9,19 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
const projectMemberOrm = ormify(db, TableName.ProjectMembership);
// special query
const findAllProjectMembers = async (projectId: string) => {
const findAllProjectMembers = async (projectId: string, filter: { usernames?: string[]; username?: string } = {}) => {
try {
const docs = await db(TableName.ProjectMembership)
.where({ [`${TableName.ProjectMembership}.projectId` as "projectId"]: projectId })
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
.where((qb) => {
if (filter.usernames) {
void qb.whereIn("username", filter.usernames);
}
if (filter.username) {
void qb.where("username", filter.username);
}
})
.join<TUserEncryptionKeys>(
TableName.UserEncryptionKey,
`${TableName.UserEncryptionKey}.userId`,
@@ -34,6 +34,7 @@ import {
TAddUsersToWorkspaceNonE2EEDTO,
TDeleteProjectMembershipOldDTO,
TDeleteProjectMembershipsDTO,
TGetProjectMembershipByUsernameDTO,
TGetProjectMembershipDTO,
TUpdateProjectMembershipDTO
} from "./project-membership-types";
@@ -89,6 +90,28 @@ export const projectMembershipServiceFactory = ({
return projectMembershipDAL.findAllProjectMembers(projectId);
};
const getProjectMembershipByUsername = async ({
actorId,
actor,
actorOrgId,
actorAuthMethod,
projectId,
username
}: TGetProjectMembershipByUsernameDTO) => {
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
const [membership] = await projectMembershipDAL.findAllProjectMembers(projectId, { username });
if (!membership) throw new BadRequestError({ message: `Project membership not found for user ${username}` });
return membership;
};
const addUsersToProject = async ({
projectId,
actorId,
@@ -510,6 +533,7 @@ export const projectMembershipServiceFactory = ({
return {
getProjectMemberships,
getProjectMembershipByUsername,
updateProjectMembership,
addUsersToProjectNonE2EE,
deleteProjectMemberships,
@@ -9,6 +9,10 @@ export type TInviteUserToProjectDTO = {
emails: string[];
} & TProjectPermission;
export type TGetProjectMembershipByUsernameDTO = {
username: string;
} & TProjectPermission;
export type TUpdateProjectMembershipDTO = {
membershipId: string;
roles: (