Add org-scoped auth to org-level endpoints, add migration file for org enableAuth field

This commit is contained in:
Tuan Dang
2024-02-04 14:44:08 -08:00
parent c8633bf546
commit f138973ac7
47 changed files with 18779 additions and 15932 deletions
@@ -0,0 +1,157 @@
import crypto from "node:crypto";
import bcrypt from "bcrypt";
import { TAuthTokens, TAuthTokenSessions } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { UnauthorizedError } from "@app/lib/errors";
import { AuthModeJwtTokenPayload } from "../auth/auth-type";
import { TUserDALFactory } from "../user/user-dal";
import { TTokenDALFactory } from "./auth-token-dal";
import { TCreateTokenForUserDTO, TIssueAuthTokenDTO, TokenType, TValidateTokenForUserDTO } from "./auth-token-types";
type TAuthTokenServiceFactoryDep = {
tokenDAL: TTokenDALFactory;
userDAL: Pick<TUserDALFactory, "findById">;
};
export type TAuthTokenServiceFactory = ReturnType<typeof tokenServiceFactory>;
export const getTokenConfig = (tokenType: TokenType) => {
// generate random token based on specified token use-case
// type [type]
switch (tokenType) {
case TokenType.TOKEN_EMAIL_CONFIRMATION: {
// generate random 6-digit code
const token = String(crypto.randomInt(10 ** 5, 10 ** 6 - 1));
const expiresAt = new Date(new Date().getTime() + 86400000);
return { token, expiresAt };
}
case TokenType.TOKEN_EMAIL_MFA: {
// generate random 6-digit code
const token = String(crypto.randomInt(10 ** 5, 10 ** 6 - 1));
const triesLeft = 5;
const expiresAt = new Date(new Date().getTime() + 300000);
return { token, triesLeft, expiresAt };
}
case TokenType.TOKEN_EMAIL_ORG_INVITATION: {
// generate random hex
const token = crypto.randomBytes(16).toString("hex");
const expiresAt = new Date(new Date().getTime() + 259200000);
return { token, expiresAt };
}
case TokenType.TOKEN_EMAIL_PASSWORD_RESET: {
// generate random hex
const token = crypto.randomBytes(16).toString("hex");
const expiresAt = new Date(new Date().getTime() + 86400000);
return { token, expiresAt };
}
default: {
const token = crypto.randomBytes(16).toString("hex");
const expiresAt = new Date();
return { token, expiresAt };
}
}
};
export const tokenServiceFactory = ({ tokenDAL, userDAL }: TAuthTokenServiceFactoryDep) => {
const createTokenForUser = async ({ type, userId, orgId }: TCreateTokenForUserDTO) => {
const { token, ...tkCfg } = getTokenConfig(type);
const appCfg = getConfig();
const tokenHash = await bcrypt.hash(token, appCfg.SALT_ROUNDS);
await tokenDAL.transaction(async (tx) => {
await tokenDAL.delete({ userId, type, orgId: orgId || null }, tx);
const newToken = await tokenDAL.create(
{
tokenHash,
expiresAt: tkCfg.expiresAt,
type,
userId,
orgId,
triesLeft: tkCfg?.triesLeft
},
tx
);
return newToken;
});
return token;
};
const validateTokenForUser = async ({
type,
userId,
code,
orgId
}: TValidateTokenForUserDTO): Promise<TAuthTokens | undefined> => {
const token = await tokenDAL.findOne({ type, userId, orgId: orgId || null });
// validate token
if (!token) throw new Error("Failed to find token");
if (token?.expiresAt && new Date(token.expiresAt) < new Date()) {
await tokenDAL.delete({ type, userId, orgId });
throw new Error("Token expired. Please try again");
}
const isValidToken = await bcrypt.compare(code, token.tokenHash);
if (!isValidToken) {
if (token?.triesLeft) {
if (token.triesLeft === 1) {
await tokenDAL.deleteTokenForUser({ type, userId, orgId: orgId || null });
} else {
await tokenDAL.decrementTriesField({ type, userId, orgId: orgId || null });
}
}
throw new Error("Invalid token");
}
const deletedToken = await tokenDAL.delete({ type, userId, orgId: orgId || null });
return deletedToken?.[0];
};
const getUserTokenSession = async ({
userId,
ip,
userAgent
}: TIssueAuthTokenDTO): Promise<TAuthTokenSessions | undefined> => {
let session = await tokenDAL.findOneTokenSession({ userId, ip, userAgent });
if (!session) {
session = await tokenDAL.insertTokenSession(userId, ip, userAgent);
}
return session;
};
const clearTokenSessionById = async (userId: string, sessionId: string): Promise<TAuthTokenSessions | undefined> =>
tokenDAL.incrementTokenSessionVersion(userId, sessionId);
const getUserTokenSessionById = async (id: string, userId: string) => tokenDAL.findOneTokenSession({ id, userId });
const getTokenSessionByUser = async (userId: string) => tokenDAL.findTokenSessions({ userId });
const revokeAllMySessions = async (userId: string) => tokenDAL.deleteTokenSession({ userId });
// to parse jwt identity in inject identity plugin
const fnValidateJwtIdentity = async (token: AuthModeJwtTokenPayload) => {
const session = await tokenDAL.findOneTokenSession({
id: token.tokenVersionId,
userId: token.userId
});
if (!session) throw new UnauthorizedError({ name: "Session not found" });
if (token.accessVersion !== session.accessVersion) throw new UnauthorizedError({ name: "Stale session" });
const user = await userDAL.findById(session.userId);
if (!user || !user.isAccepted) throw new UnauthorizedError({ name: "Token user not found" });
return { user, tokenVersionId: token.tokenVersionId, orgId: token.organizationId };
};
return {
createTokenForUser,
validateTokenForUser,
getUserTokenSession,
clearTokenSessionById,
getTokenSessionByUser,
revokeAllMySessions,
fnValidateJwtIdentity,
getUserTokenSessionById
};
};
+45
View File
@@ -0,0 +1,45 @@
import jwt from "jsonwebtoken";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { AuthModeProviderJwtTokenPayload, AuthModeProviderSignUpTokenPayload, AuthTokenType } from "./auth-type";
export const validateProviderAuthToken = (providerToken: string, email: string) => {
if (!providerToken) throw new UnauthorizedError();
const appCfg = getConfig();
const decodedToken = jwt.verify(providerToken, appCfg.AUTH_SECRET) as AuthModeProviderJwtTokenPayload;
if (decodedToken.authTokenType !== AuthTokenType.PROVIDER_TOKEN) throw new UnauthorizedError();
if (decodedToken.email !== email) throw new Error("Invalid auth credentials");
if (decodedToken.organizationId) {
return { orgId: decodedToken.organizationId };
}
return {};
};
export const validateSignUpAuthorization = (token: string, userId: string, validate = true) => {
const appCfg = getConfig();
const [AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE] = <[string, string]>token?.split(" ", 2) ?? [null, null];
if (AUTH_TOKEN_TYPE === null) {
throw new BadRequestError({ message: "Missing Authorization Header in the request header." });
}
if (AUTH_TOKEN_TYPE.toLowerCase() !== "bearer") {
throw new BadRequestError({
message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.`
});
}
if (AUTH_TOKEN_VALUE === null) {
throw new BadRequestError({
message: "Missing Authorization Body in the request header"
});
}
const decodedToken = jwt.verify(AUTH_TOKEN_VALUE, appCfg.AUTH_SECRET) as AuthModeProviderSignUpTokenPayload;
if (!validate) return decodedToken;
if (decodedToken.authTokenType !== AuthTokenType.SIGNUP_TOKEN) throw new UnauthorizedError();
if (decodedToken.userId !== userId) throw new UnauthorizedError();
};
@@ -0,0 +1,322 @@
import jwt from "jsonwebtoken";
import { TUsers, UserDeviceSchema } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
import { BadRequestError } from "@app/lib/errors";
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
import { TokenType } from "../auth-token/auth-token-types";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TUserDALFactory } from "../user/user-dal";
import { validateProviderAuthToken } from "./auth-fns";
import {
TLoginClientProofDTO,
TLoginGenServerPublicKeyDTO,
TOauthLoginDTO,
TVerifyMfaTokenDTO
} from "./auth-login-type";
import { AuthMethod, AuthTokenType } from "./auth-type";
type TAuthLoginServiceFactoryDep = {
userDAL: TUserDALFactory;
tokenService: TAuthTokenServiceFactory;
smtpService: TSmtpService;
};
export type TAuthLoginFactory = ReturnType<typeof authLoginServiceFactory>;
export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }: TAuthLoginServiceFactoryDep) => {
/*
* Private
* Not exported. This is to update user device list
* If new device is found. Will be saved and a mail will be send
*/
const updateUserDeviceSession = async (user: TUsers, ip: string, userAgent: string) => {
const devices = await UserDeviceSchema.parseAsync(user.devices || []);
const isDeviceSeen = devices.some((device) => device.ip === ip && device.userAgent === userAgent);
if (!isDeviceSeen) {
const newDeviceList = devices.concat([{ ip, userAgent }]);
await userDAL.updateById(user.id, { devices: JSON.stringify(newDeviceList) });
await smtpService.sendMail({
template: SmtpTemplates.NewDeviceJoin,
subjectLine: "Successful login from new device",
recipients: [user.email],
substitutions: {
email: user.email,
timestamp: new Date().toString(),
ip,
userAgent
}
});
}
};
/*
* Private
* Send mfa code via email
* */
const sendUserMfaCode = async ({
userId,
email
}: {
userId: string;
email: string;
}) => {
const code = await tokenService.createTokenForUser({
type: TokenType.TOKEN_EMAIL_MFA,
userId
});
await smtpService.sendMail({
template: SmtpTemplates.EmailMfa,
subjectLine: "Infisical MFA code",
recipients: [email],
substitutions: {
code
}
});
};
/*
* Check user device and send mail if new device
* generate the auth and refresh token. fn shared by mfa verification and login verification with mfa disabled
*/
const generateUserTokens = async ({
user,
ip,
userAgent,
organizationId
}: {
user: TUsers;
ip: string;
userAgent: string;
organizationId?: string;
}) => {
const cfg = getConfig();
await updateUserDeviceSession(user, ip, userAgent);
const tokenSession = await tokenService.getUserTokenSession({
userAgent,
ip,
userId: user.id
});
if (!tokenSession) throw new Error("Failed to create token");
const accessToken = jwt.sign(
{
authTokenType: AuthTokenType.ACCESS_TOKEN,
userId: user.id,
tokenVersionId: tokenSession.id,
accessVersion: tokenSession.accessVersion,
organizationId
},
cfg.AUTH_SECRET,
{ expiresIn: cfg.JWT_AUTH_LIFETIME }
);
const refreshToken = jwt.sign(
{
authTokenType: AuthTokenType.REFRESH_TOKEN,
userId: user.id,
tokenVersionId: tokenSession.id,
refreshVersion: tokenSession.refreshVersion,
organizationId
},
cfg.AUTH_SECRET,
{ expiresIn: cfg.JWT_REFRESH_LIFETIME }
);
return { access: accessToken, refresh: refreshToken };
};
/*
* Step 1 of login. To get server public key in exchange of client public key
*/
const loginGenServerPublicKey = async ({
email,
providerAuthToken,
clientPublicKey
}: TLoginGenServerPublicKeyDTO) => {
const userEnc = await userDAL.findUserEncKeyByEmail(email);
if (!userEnc || (userEnc && !userEnc.isAccepted)) {
throw new Error("Failed to find user");
}
if (!userEnc.authMethods?.includes(AuthMethod.EMAIL)) {
validateProviderAuthToken(providerAuthToken as string, email);
}
const serverSrpKey = await generateSrpServerKey(userEnc.salt, userEnc.verifier);
const userEncKeys = await userDAL.updateUserEncryptionByUserId(userEnc.userId, {
clientPublicKey,
serverPrivateKey: serverSrpKey.privateKey
});
if (!userEncKeys) throw new Error("Failed to update encryption key");
return { salt: userEncKeys.salt, serverPublicKey: serverSrpKey.pubKey };
};
/*
* Step 2 of login. Pass the client proof and with multi factor setup handle the required steps
*/
const loginExchangeClientProof = async ({
email,
clientProof,
providerAuthToken,
ip,
userAgent
}: TLoginClientProofDTO) => {
const userEnc = await userDAL.findUserEncKeyByEmail(email);
if (!userEnc) throw new Error("Failed to find user");
const cfg = getConfig();
let organizationId;
if (!userEnc.authMethods?.includes(AuthMethod.EMAIL)) {
const { orgId } = validateProviderAuthToken(providerAuthToken as string, email);
organizationId = orgId;
}
if (!userEnc.serverPrivateKey || !userEnc.clientPublicKey) throw new Error("Failed to authenticate. Try again?");
const isValidClientProof = await srpCheckClientProof(
userEnc.salt,
userEnc.verifier,
userEnc.serverPrivateKey,
userEnc.clientPublicKey,
clientProof
);
if (!isValidClientProof) throw new Error("Failed to authenticate. Try again?");
await userDAL.updateUserEncryptionByUserId(userEnc.userId, {
serverPrivateKey: null,
clientPublicKey: null
});
// send multi factor auth token if they it enabled
if (userEnc.isMfaEnabled) {
const mfaToken = jwt.sign({
authTokenType: AuthTokenType.MFA_TOKEN,
userId: userEnc.userId,
organizationId
}, cfg.AUTH_SECRET, {
expiresIn: cfg.JWT_MFA_LIFETIME
});
await sendUserMfaCode({
userId: userEnc.userId,
email: userEnc.email
});
return { isMfaEnabled: true, token: mfaToken } as const;
}
const token = await generateUserTokens({
user: {
...userEnc,
id: userEnc.userId
},
ip,
userAgent,
organizationId
});
return { token, isMfaEnabled: false, user: userEnc } as const;
};
/*
* Multi factor authentication re-send code, Get user id from token
* saved in frontend
*/
const resendMfaToken = async (userId: string) => {
const user = await userDAL.findById(userId);
if (!user) return;
await sendUserMfaCode({
userId: user.id,
email: user.email,
});
};
/*
* Multi factor authentication verification of code
* Third step of login in which user completes with mfa
* */
const verifyMfaToken = async ({ userId, mfaToken, ip, userAgent, orgId }: TVerifyMfaTokenDTO) => {
await tokenService.validateTokenForUser({
type: TokenType.TOKEN_EMAIL_MFA,
userId,
code: mfaToken
});
const userEnc = await userDAL.findUserEncKeyByUserId(userId);
if (!userEnc) throw new Error("Failed to authenticate user");
const token = await generateUserTokens({
user: {
...userEnc,
id: userEnc.userId
},
ip,
userAgent,
organizationId: orgId
});
return { token, user: userEnc };
};
/*
* OAuth2 login for google,github, and other oauth2 provider
* */
const oauth2Login = async ({
email,
firstName,
lastName,
authMethod,
callbackPort,
isSignupAllowed
}: TOauthLoginDTO) => {
let user = await userDAL.findUserByEmail(email);
const appCfg = getConfig();
const isOauthSignUpDisabled = !isSignupAllowed && !user;
if (isOauthSignUpDisabled) throw new BadRequestError({ message: "User signup disabled", name: "Oauth 2 login" });
if (!user) {
user = await userDAL.create({ email, firstName, lastName, authMethods: [authMethod] });
}
const isLinkingRequired = !user?.authMethods?.includes(authMethod);
const isUserCompleted = user.isAccepted;
const providerAuthToken = jwt.sign(
{
authTokenType: AuthTokenType.PROVIDER_TOKEN,
userId: user.id,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
authMethod,
isUserCompleted,
isLinkingRequired,
...(callbackPort
? {
callbackPort
}
: {})
},
appCfg.AUTH_SECRET,
{
expiresIn: appCfg.JWT_PROVIDER_AUTH_LIFETIME
}
);
return { isUserCompleted, providerAuthToken };
};
/*
* logout user by incrementing the version by 1 meaning any old session will become invalid
* as there number is behind
* */
const logout = async (userId: string, sessionId: string) => {
await tokenService.clearTokenSessionById(userId, sessionId);
};
return {
loginGenServerPublicKey,
loginExchangeClientProof,
logout,
oauth2Login,
resendMfaToken,
verifyMfaToken,
generateUserTokens
};
};
@@ -0,0 +1,32 @@
import { AuthMethod } from "./auth-type";
export type TLoginGenServerPublicKeyDTO = {
email: string;
clientPublicKey: string;
providerAuthToken?: string;
};
export type TLoginClientProofDTO = {
email: string;
clientProof: string;
providerAuthToken?: string;
ip: string;
userAgent: string;
};
export type TVerifyMfaTokenDTO = {
userId: string;
mfaToken: string;
ip: string;
userAgent: string;
orgId?: string;
};
export type TOauthLoginDTO = {
email: string;
firstName: string;
lastName?: string;
authMethod: AuthMethod;
callbackPort?: string;
isSignupAllowed?: boolean;
};
+68
View File
@@ -0,0 +1,68 @@
export enum AuthMethod {
EMAIL = "email",
GOOGLE = "google",
GITHUB = "github",
GITLAB = "gitlab",
OKTA_SAML = "okta-saml",
AZURE_SAML = "azure-saml",
JUMPCLOUD_SAML = "jumpcloud-saml"
}
export enum AuthTokenType {
ACCESS_TOKEN = "accessToken",
REFRESH_TOKEN = "refreshToken",
SIGNUP_TOKEN = "signupToken", // TODO: remove in favor of claim
MFA_TOKEN = "mfaToken", // TODO: remove in favor of claim
PROVIDER_TOKEN = "providerToken", // TODO: remove in favor of claim
API_KEY = "apiKey",
SERVICE_ACCESS_TOKEN = "serviceAccessToken",
SERVICE_REFRESH_TOKEN = "serviceRefreshToken",
IDENTITY_ACCESS_TOKEN = "identityAccessToken"
}
export enum AuthMode {
JWT = "jwt",
SERVICE_TOKEN = "serviceToken",
API_KEY = "apiKey",
IDENTITY_ACCESS_TOKEN = "identityAccessToken"
}
export enum ActorType { // would extend to AWS, Azure, ...
USER = "user", // userIdentity
SERVICE = "service",
IDENTITY = "identity",
Machine = "machine"
}
export type AuthModeJwtTokenPayload = {
authTokenType: AuthTokenType.ACCESS_TOKEN;
userId: string;
tokenVersionId: string;
accessVersion: number;
organizationId?: string;
};
export type AuthModeMfaJwtTokenPayload = {
authTokenType: AuthTokenType.MFA_TOKEN;
userId: string;
organizationId?: string;
};
export type AuthModeRefreshJwtTokenPayload = {
authTokenType: AuthTokenType.REFRESH_TOKEN;
userId: string;
tokenVersionId: string;
refreshVersion: number;
organizationId?: string;
};
export type AuthModeProviderJwtTokenPayload = {
authTokenType: AuthTokenType.PROVIDER_TOKEN;
email: string;
organizationId?: string;
};
export type AuthModeProviderSignUpTokenPayload = {
authTokenType: AuthTokenType.SIGNUP_TOKEN;
userId: string;
};
@@ -0,0 +1,435 @@
import crypto from "node:crypto";
import { ForbiddenError } from "@casl/ability";
import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import { IdentityAuthMethod } 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 { isAtLeastAsPrivileged } from "@app/lib/casl";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
import { checkIPAgainstBlocklist, extractIPDetails, isValidIpOrCidr, TIp } from "@app/lib/ip";
import { ActorType, 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 { TIdentityUaClientSecretDALFactory } from "./identity-ua-client-secret-dal";
import { TIdentityUaDALFactory } from "./identity-ua-dal";
import {
TAttachUaDTO,
TCreateUaClientSecretDTO,
TGetUaClientSecretsDTO,
TGetUaDTO,
TRevokeUaClientSecretDTO,
TUpdateUaDTO
} from "./identity-ua-types";
type TIdentityUaServiceFactoryDep = {
identityUaDAL: TIdentityUaDALFactory;
identityUaClientSecretDAL: TIdentityUaClientSecretDALFactory;
identityAccessTokenDAL: TIdentityAccessTokenDALFactory;
identityOrgMembershipDAL: TIdentityOrgDALFactory;
identityDAL: Pick<TIdentityDALFactory, "updateById">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
};
export type TIdentityUaServiceFactory = ReturnType<typeof identityUaServiceFactory>;
export const identityUaServiceFactory = ({
identityUaDAL,
identityUaClientSecretDAL,
identityAccessTokenDAL,
identityOrgMembershipDAL,
identityDAL,
permissionService,
licenseService
}: TIdentityUaServiceFactoryDep) => {
const login = async (clientId: string, clientSecret: string, ip: string) => {
const identityUa = await identityUaDAL.findOne({ clientId });
if (!identityUa) throw new UnauthorizedError();
checkIPAgainstBlocklist({
ipAddress: ip,
trustedIps: identityUa.clientSecretTrustedIps as TIp[]
});
const clientSecrtInfo = await identityUaClientSecretDAL.find({
identityUAId: identityUa.id,
isClientSecretRevoked: false
});
const validClientSecretInfo = clientSecrtInfo.find(({ clientSecretHash }) =>
bcrypt.compareSync(clientSecret, clientSecretHash)
);
if (!validClientSecretInfo) throw new UnauthorizedError();
const { clientSecretTTL, clientSecretNumUses, clientSecretNumUsesLimit } = validClientSecretInfo;
if (clientSecretTTL > 0) {
const clientSecretCreated = new Date(validClientSecretInfo.createdAt);
const ttlInMilliseconds = clientSecretTTL * 1000;
const currentDate = new Date();
const expirationTime = new Date(clientSecretCreated.getTime() + ttlInMilliseconds);
if (currentDate > expirationTime) {
await identityUaClientSecretDAL.updateById(validClientSecretInfo.id, {
isClientSecretRevoked: true
});
throw new UnauthorizedError({
message: "Failed to authenticate identity credentials due to expired client secret"
});
}
}
if (clientSecretNumUsesLimit > 0 && clientSecretNumUses === clientSecretNumUsesLimit) {
// number of times client secret can be used for
// a login operation reached
await identityUaClientSecretDAL.updateById(validClientSecretInfo.id, {
isClientSecretRevoked: true
});
throw new UnauthorizedError({
message: "Failed to authenticate identity credentials due to client secret number of uses limit reached"
});
}
const identityAccessToken = await identityUaDAL.transaction(async (tx) => {
const uaClientSecretDoc = await identityUaClientSecretDAL.incrementUsage(validClientSecretInfo.id, tx);
const newToken = await identityAccessTokenDAL.create(
{
identityId: identityUa.identityId,
isAccessTokenRevoked: false,
identityUAClientSecretId: uaClientSecretDoc.id,
accessTokenTTL: identityUa.accessTokenTTL,
accessTokenMaxTTL: identityUa.accessTokenMaxTTL,
accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityUa.accessTokenNumUsesLimit
},
tx
);
return newToken;
});
const appCfg = getConfig();
const accessToken = jwt.sign(
{
identityId: identityUa.identityId,
clientSecretId: validClientSecretInfo.id,
identityAccessTokenId: identityAccessToken.id,
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
} as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET,
{
expiresIn: identityAccessToken.accessTokenMaxTTL === 0 ? undefined : identityAccessToken.accessTokenMaxTTL
}
);
return { accessToken, identityUa, validClientSecretInfo, identityAccessToken };
};
const attachUa = async ({
accessTokenMaxTTL,
identityId,
accessTokenNumUsesLimit,
accessTokenTTL,
accessTokenTrustedIps,
clientSecretTrustedIps,
actorId,
actor,
actorOrgScope
}: TAttachUaDTO) => {
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 universal 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, actorOrgScope);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity);
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
const reformattedClientSecretTrustedIps = clientSecretTrustedIps.map((clientSecretTrustedIp) => {
if (
!plan.ipAllowlisting &&
clientSecretTrustedIp.ipAddress !== "0.0.0.0/0" &&
clientSecretTrustedIp.ipAddress !== "::/0"
)
throw new BadRequestError({
message:
"Failed to add IP access range to service token due to plan restriction. Upgrade plan to add IP access range."
});
if (!isValidIpOrCidr(clientSecretTrustedIp.ipAddress))
throw new BadRequestError({
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
});
return extractIPDetails(clientSecretTrustedIp.ipAddress);
});
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 service 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 identityUa = await identityUaDAL.transaction(async (tx) => {
const doc = await identityUaDAL.create(
{
identityId: identityMembershipOrg.identityId,
clientId: crypto.randomUUID(),
clientSecretTrustedIps: JSON.stringify(reformattedClientSecretTrustedIps),
accessTokenMaxTTL,
accessTokenTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps)
},
tx
);
await identityDAL.updateById(
identityMembershipOrg.identityId,
{
authMethod: IdentityAuthMethod.Univeral
},
tx
);
return doc;
});
return { ...identityUa, orgId: identityMembershipOrg.orgId };
};
const updateUa = async ({
accessTokenMaxTTL,
identityId,
accessTokenNumUsesLimit,
accessTokenTTL,
accessTokenTrustedIps,
clientSecretTrustedIps,
actorId,
actor,
actorOrgScope
}: TUpdateUaDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral)
throw new BadRequestError({
message: "Failed to updated universal auth"
});
const uaIdentityAuth = await identityUaDAL.findOne({ identityId });
if (
(accessTokenMaxTTL || uaIdentityAuth.accessTokenMaxTTL) > 0 &&
(accessTokenTTL || uaIdentityAuth.accessTokenMaxTTL) > (accessTokenMaxTTL || uaIdentityAuth.accessTokenMaxTTL)
) {
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
}
const { permission } = await permissionService.getOrgPermission(actor, actorId, identityMembershipOrg.orgId, actorOrgScope);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
const reformattedClientSecretTrustedIps = clientSecretTrustedIps?.map((clientSecretTrustedIp) => {
if (
!plan.ipAllowlisting &&
clientSecretTrustedIp.ipAddress !== "0.0.0.0/0" &&
clientSecretTrustedIp.ipAddress !== "::/0"
)
throw new BadRequestError({
message:
"Failed to add IP access range to service token due to plan restriction. Upgrade plan to add IP access range."
});
if (!isValidIpOrCidr(clientSecretTrustedIp.ipAddress))
throw new BadRequestError({
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
});
return extractIPDetails(clientSecretTrustedIp.ipAddress);
});
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 service 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 updatedUaAuth = await identityUaDAL.updateById(uaIdentityAuth.id, {
clientSecretTrustedIps: reformattedClientSecretTrustedIps
? JSON.stringify(reformattedClientSecretTrustedIps)
: undefined,
accessTokenMaxTTL,
accessTokenTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps: reformattedAccessTokenTrustedIps
? JSON.stringify(reformattedAccessTokenTrustedIps)
: undefined
});
return { ...updatedUaAuth, orgId: identityMembershipOrg.orgId };
};
const getIdentityUa = async ({ identityId, actorId, actor, actorOrgScope }: TGetUaDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral)
throw new BadRequestError({
message: "The identity does not have universal auth"
});
const uaIdentityAuth = await identityUaDAL.findOne({ identityId });
const { permission } = await permissionService.getOrgPermission(actor, actorId, identityMembershipOrg.orgId, actorOrgScope);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
return { ...uaIdentityAuth, orgId: identityMembershipOrg.orgId };
};
const createUaClientSecret = async ({
actor,
actorId,
actorOrgScope,
identityId,
ttl,
description,
numUsesLimit
}: TCreateUaClientSecretDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral)
throw new BadRequestError({
message: "The identity does not have universal auth"
});
const { permission } = await permissionService.getOrgPermission(actor, actorId, identityMembershipOrg.orgId, actorOrgScope);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity);
const { permission: rolePermission } = await permissionService.getOrgPermission(
ActorType.IDENTITY,
identityMembershipOrg.identityId,
identityMembershipOrg.orgId,
actorOrgScope
);
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasPriviledge)
throw new ForbiddenRequestError({
message: "Failed to add identity to project with more privileged role"
});
const appCfg = getConfig();
const clientSecret = crypto.randomBytes(32).toString("hex");
const clientSecretHash = await bcrypt.hash(clientSecret, appCfg.SALT_ROUNDS);
const identityUniversalAuth = await identityUaDAL.findOne({
identityId
});
const identityUaClientSecret = await identityUaClientSecretDAL.create({
identityUAId: identityUniversalAuth.id,
description,
clientSecretPrefix: clientSecret.slice(0, 4),
clientSecretHash,
clientSecretNumUses: 0,
clientSecretNumUsesLimit: numUsesLimit,
clientSecretTTL: ttl,
isClientSecretRevoked: false
});
return {
clientSecret,
clientSecretData: identityUaClientSecret,
uaAuth: identityUniversalAuth,
orgId: identityMembershipOrg.orgId
};
};
const getUaClientSecrets = async ({ actor, actorId, actorOrgScope, identityId }: TGetUaClientSecretsDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral)
throw new BadRequestError({
message: "The identity does not have universal auth"
});
const { permission } = await permissionService.getOrgPermission(actor, actorId, identityMembershipOrg.orgId, actorOrgScope);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
const { permission: rolePermission } = await permissionService.getOrgPermission(
ActorType.IDENTITY,
identityMembershipOrg.identityId,
identityMembershipOrg.orgId,
actorOrgScope
);
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasPriviledge)
throw new ForbiddenRequestError({
message: "Failed to add identity to project with more privileged role"
});
const identityUniversalAuth = await identityUaDAL.findOne({
identityId
});
const clientSecrets = await identityUaClientSecretDAL.find({
identityUAId: identityUniversalAuth.id,
isClientSecretRevoked: false
});
return { clientSecrets, orgId: identityMembershipOrg.orgId };
};
const revokeUaClientSecret = async ({ identityId, actorId, actor, actorOrgScope, clientSecretId }: TRevokeUaClientSecretDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral)
throw new BadRequestError({
message: "The identity does not have universal auth"
});
const { permission } = await permissionService.getOrgPermission(actor, actorId, identityMembershipOrg.orgId, actorOrgScope);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity);
const { permission: rolePermission } = await permissionService.getOrgPermission(
ActorType.IDENTITY,
identityMembershipOrg.identityId,
identityMembershipOrg.orgId,
actorOrgScope
);
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasPriviledge)
throw new ForbiddenRequestError({
message: "Failed to add identity to project with more privileged role"
});
const clientSecret = await identityUaClientSecretDAL.updateById(clientSecretId, {
isClientSecretRevoked: true
});
return { ...clientSecret, identityId, orgId: identityMembershipOrg.orgId };
};
return {
login,
attachUa,
updateUa,
getIdentityUa,
createUaClientSecret,
getUaClientSecrets,
revokeUaClientSecret
};
};
@@ -0,0 +1,139 @@
import { ForbiddenError } from "@casl/ability";
import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
import { TOrgPermission } from "@app/lib/types";
import { ActorType } from "../auth/auth-type";
import { TIdentityDALFactory } from "./identity-dal";
import { TIdentityOrgDALFactory } from "./identity-org-dal";
import { TCreateIdentityDTO, TDeleteIdentityDTO, TUpdateIdentityDTO } from "./identity-types";
type TIdentityServiceFactoryDep = {
identityDAL: TIdentityDALFactory;
identityOrgMembershipDAL: TIdentityOrgDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">;
};
export type TIdentityServiceFactory = ReturnType<typeof identityServiceFactory>;
export const identityServiceFactory = ({
identityDAL,
identityOrgMembershipDAL,
permissionService
}: TIdentityServiceFactoryDep) => {
const createIdentity = async ({ name, role, actor, orgId, actorId, actorOrgScope }: TCreateIdentityDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgScope);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity);
const { permission: rolePermission, role: customRole } = await permissionService.getOrgPermissionByRole(
role,
orgId
);
const isCustomRole = Boolean(customRole);
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasRequiredPriviledges) throw new BadRequestError({ message: "Failed to create a more privileged identity" });
const identity = await identityDAL.transaction(async (tx) => {
const newIdentity = await identityDAL.create({ name }, tx);
await identityOrgMembershipDAL.create(
{
identityId: newIdentity.id,
orgId,
role: isCustomRole ? OrgMembershipRole.Custom : role,
roleId: customRole?.id
},
tx
);
return newIdentity;
});
return identity;
};
const updateIdentity = async ({ id, role, name, actor, actorId, actorOrgScope }: TUpdateIdentityDTO) => {
const identityOrgMembership = await identityOrgMembershipDAL.findOne({ identityId: id });
if (!identityOrgMembership) throw new BadRequestError({ message: `Failed to find identity with id ${id}` });
const { permission } = await permissionService.getOrgPermission(actor, actorId, identityOrgMembership.orgId, actorOrgScope);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
const { permission: identityRolePermission } = await permissionService.getOrgPermission(
ActorType.IDENTITY,
id,
identityOrgMembership.orgId,
actorOrgScope
);
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission);
if (!hasRequiredPriviledges)
throw new ForbiddenRequestError({ message: "Failed to delete more privileged identity" });
let customRole: TOrgRoles | undefined;
if (role) {
const { permission: rolePermission, role: customOrgRole } = await permissionService.getOrgPermissionByRole(
role,
identityOrgMembership.orgId
);
const isCustomRole = Boolean(customOrgRole);
const hasRequiredNewRolePermission = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasRequiredNewRolePermission)
throw new BadRequestError({ message: "Failed to create a more privileged identity" });
if (isCustomRole) customRole = customOrgRole;
}
const identity = await identityDAL.transaction(async (tx) => {
const newIdentity = name ? await identityDAL.updateById(id, { name }, tx) : await identityDAL.findById(id, tx);
if (role) {
await identityOrgMembershipDAL.update(
{ identityId: id },
{
role: customRole ? OrgMembershipRole.Custom : role,
roleId: customRole?.id
},
tx
);
}
return newIdentity;
});
return { ...identity, orgId: identityOrgMembership.orgId };
};
const deleteIdentity = async ({ actorId, actor, actorOrgScope, id }: TDeleteIdentityDTO) => {
const identityOrgMembership = await identityOrgMembershipDAL.findOne({ identityId: id });
if (!identityOrgMembership) throw new BadRequestError({ message: `Failed to find identity with id ${id}` });
const { permission } = await permissionService.getOrgPermission(actor, actorId, identityOrgMembership.orgId, actorOrgScope);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity);
const { permission: identityRolePermission } = await permissionService.getOrgPermission(
ActorType.IDENTITY,
id,
identityOrgMembership.orgId
);
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission);
if (!hasRequiredPriviledges)
throw new ForbiddenRequestError({ message: "Failed to delete more privileged identity" });
const deletedIdentity = await identityDAL.deleteById(id);
return { ...deletedIdentity, orgId: identityOrgMembership.orgId };
};
const listOrgIdentities = async ({ orgId, actor, actorId, actorOrgScope }: TOrgPermission) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgScope);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
const identityMemberhips = await identityOrgMembershipDAL.findByOrgId(orgId);
return identityMemberhips;
};
return {
createIdentity,
updateIdentity,
deleteIdentity,
listOrgIdentities
};
};
+196
View File
@@ -0,0 +1,196 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import {
TableName,
TOrganizations,
TOrganizationsInsert,
TOrgMemberships,
TOrgMembershipsInsert,
TOrgMembershipsUpdate,
TUserEncryptionKeys
} from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { buildFindFilter, selectAllTableCols, TFindFilter, TFindOpt, withTransaction, ormify } from "@app/lib/knex";
export type TOrgDALFactory = ReturnType<typeof orgDALFactory>;
export const orgDALFactory = (db: TDbClient) => {
const orgOrm = ormify(db, TableName.Organization);
const findOrgById = async (orgId: string) => {
try {
const org = await db(TableName.Organization).where({ id: orgId }).first();
return org;
} catch (error) {
throw new DatabaseError({ error, name: "Find org by id" });
}
};
// special query
const findAllOrgsByUserId = async (userId: string): Promise<TOrganizations[]> => {
try {
const org = await db(TableName.OrgMembership)
.where({ userId })
.join(TableName.Organization, `${TableName.OrgMembership}.orgId`, `${TableName.Organization}.id`)
.select(selectAllTableCols(TableName.Organization));
return org;
} catch (error) {
throw new DatabaseError({ error, name: "Find all org by user id" });
}
};
const findOrgByProjectId = async (projectId: string): Promise<TOrganizations> => {
try {
const [org] = await db(TableName.Project)
.where({ [`${TableName.Project}.id` as "id"]: projectId })
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
.select(selectAllTableCols(TableName.Organization));
return org;
} catch (error) {
throw new DatabaseError({ error, name: "Find org by project id" });
}
};
// special query
const findAllOrgMembers = async (orgId: string) => {
try {
const members = await db(TableName.OrgMembership)
.where({ orgId })
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
.leftJoin<TUserEncryptionKeys>(
TableName.UserEncryptionKey,
`${TableName.UserEncryptionKey}.userId`,
`${TableName.Users}.id`
)
.select(
db.ref("id").withSchema(TableName.OrgMembership),
db.ref("inviteEmail").withSchema(TableName.OrgMembership),
db.ref("orgId").withSchema(TableName.OrgMembership),
db.ref("role").withSchema(TableName.OrgMembership),
db.ref("roleId").withSchema(TableName.OrgMembership),
db.ref("status").withSchema(TableName.OrgMembership),
db.ref("email").withSchema(TableName.Users),
db.ref("firstName").withSchema(TableName.Users),
db.ref("lastName").withSchema(TableName.Users),
db.ref("id").withSchema(TableName.Users).as("userId"),
db.ref("publicKey").withSchema(TableName.UserEncryptionKey)
);
return members.map(({ email, firstName, lastName, userId, publicKey, ...data }) => ({
...data,
user: { email, firstName, lastName, id: userId, publicKey }
}));
} catch (error) {
throw new DatabaseError({ error, name: "Find all org members" });
}
};
const create = async (dto: TOrganizationsInsert, tx?: Knex) => {
try {
const [organization] = await (tx || db)(TableName.Organization).insert(dto).returning("*");
return organization;
} catch (error) {
throw new DatabaseError({ error, name: "Create organization" });
}
};
const deleteById = async (orgId: string, tx?: Knex) => {
try {
const [org] = await (tx || db)(TableName.Organization).where({ id: orgId }).delete().returning("*");
return org;
} catch (error) {
throw new DatabaseError({ error, name: "Update organization" });
}
};
const updateById = async (orgId: string, data: Partial<TOrganizations>) => {
try {
const [org] = await db(TableName.Organization)
.where({ id: orgId })
.update({ ...data })
.returning("*");
return org;
} catch (error) {
throw new DatabaseError({ error, name: "Update organization" });
}
};
// MEMBERSHIP OPERATIONS
// --------------------
// const orgMembershipOrm = ormify(db, TableName.OrgMembership);
const createMembership = async (data: TOrgMembershipsInsert, tx?: Knex) => {
try {
const [membership] = await (tx || db)(TableName.OrgMembership).insert(data).returning("*");
return membership;
} catch (error) {
throw new DatabaseError({ error, name: "Create org membership" });
}
};
const updateMembershipById = async (id: string, data: TOrgMembershipsUpdate, tx?: Knex) => {
try {
const [membership] = await (tx || db)(TableName.OrgMembership).where({ id }).update(data).returning("*");
return membership;
} catch (error) {
throw new DatabaseError({ error, name: "Update org membership" });
}
};
const updateMembership = async (filter: Partial<TOrgMemberships>, data: TOrgMembershipsUpdate, tx?: Knex) => {
try {
const membership = await (tx || db)(TableName.OrgMembership).where(filter).update(data).returning("*");
return membership;
} catch (error) {
throw new DatabaseError({ error, name: "Update org memberships" });
}
};
const deleteMembershipById = async (id: string, orgId: string, tx?: Knex) => {
try {
const [membership] = await (tx || db)(TableName.OrgMembership).where({ id, orgId }).delete().returning("*");
return membership;
} catch (error) {
throw new DatabaseError({ error, name: "Delete org membership" });
}
};
const findMembership = async (
filter: TFindFilter<TOrgMemberships>,
{ offset, limit, sort, tx }: TFindOpt<TOrgMemberships> = {}
) => {
try {
const query = (tx || db)(TableName.OrgMembership)
// eslint-disable-next-line
.where(buildFindFilter(filter))
.join(TableName.Users, `${TableName.Users}.id`, `${TableName.OrgMembership}.userId`)
.select(selectAllTableCols(TableName.OrgMembership), db.ref("email").withSchema(TableName.Users));
if (limit) void query.limit(limit);
if (offset) void query.offset(offset);
if (sort) {
void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls })));
}
const res = await query;
return res;
} catch (error) {
throw new DatabaseError({ error, name: "Find one" });
}
};
return withTransaction(db, {
...orgOrm,
findOrgByProjectId,
findAllOrgMembers,
findOrgById,
findAllOrgsByUserId,
create,
updateById,
deleteById,
findMembership,
createMembership,
updateMembershipById,
deleteMembershipById,
updateMembership
});
};
@@ -0,0 +1,113 @@
import { ForbiddenError } from "@casl/ability";
import { packRules } from "@casl/ability/extra";
import { TOrgRolesInsert, TOrgRolesUpdate } from "@app/db/schemas";
import {
orgAdminPermissions,
orgMemberPermissions,
orgNoAccessPermissions,
OrgPermissionActions,
OrgPermissionSubjects
} from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { BadRequestError } from "@app/lib/errors";
import { TOrgRoleDALFactory } from "./org-role-dal";
type TOrgRoleServiceFactoryDep = {
orgRoleDAL: TOrgRoleDALFactory;
permissionService: TPermissionServiceFactory;
};
export type TOrgRoleServiceFactory = ReturnType<typeof orgRoleServiceFactory>;
export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRoleServiceFactoryDep) => {
const createRole = async (userId: string, orgId: string, data: Omit<TOrgRolesInsert, "orgId">, actorOrgScope?: string) => {
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgScope);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Role);
const existingRole = await orgRoleDAL.findOne({ slug: data.slug, orgId });
if (existingRole) throw new BadRequestError({ name: "Create Role", message: "Duplicate role" });
const role = await orgRoleDAL.create({
...data,
orgId,
permissions: JSON.stringify(data.permissions)
});
return role;
};
const updateRole = async (userId: string, orgId: string, roleId: string, data: Omit<TOrgRolesUpdate, "orgId">, actorOrgScope?: string) => {
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgScope);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Role);
if (data?.slug) {
const existingRole = await orgRoleDAL.findOne({ slug: data.slug, orgId });
if (existingRole && existingRole.id !== roleId)
throw new BadRequestError({ name: "Update Role", message: "Duplicate role" });
}
const [updatedRole] = await orgRoleDAL.update(
{ id: roleId, orgId },
{ ...data, permissions: data.permissions ? JSON.stringify(data.permissions) : undefined }
);
if (!updateRole) throw new BadRequestError({ message: "Role not found", name: "Update role" });
return updatedRole;
};
const deleteRole = async (userId: string, orgId: string, roleId: string, actorOrgScope?: string) => {
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgScope);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Role);
const [deletedRole] = await orgRoleDAL.delete({ id: roleId, orgId });
if (!deleteRole) throw new BadRequestError({ message: "Role not found", name: "Update role" });
return deletedRole;
};
const listRoles = async (userId: string, orgId: string, actorOrgScope?: string) => {
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgScope);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Role);
const customRoles = await orgRoleDAL.find({ orgId });
const roles = [
{
id: "b11b49a9-09a9-4443-916a-4246f9ff2c69", // dummy userid
orgId,
name: "Admin",
slug: "admin",
description: "Complete administration access over the organization",
permissions: packRules(orgAdminPermissions.rules),
createdAt: new Date(),
updatedAt: new Date()
},
{
id: "b11b49a9-09a9-4443-916a-4246f9ff2c70", // dummy user for zod validation in response
orgId,
name: "Member",
slug: "member",
description: "Non-administrative role in an organization",
permissions: packRules(orgMemberPermissions.rules),
createdAt: new Date(),
updatedAt: new Date()
},
{
id: "b10d49a9-09a9-4443-916a-4246f9ff2c72", // dummy user for zod validation in response
orgId,
name: "No Access",
slug: "no-access",
description: "No access to any resources in the organization",
permissions: packRules(orgNoAccessPermissions.rules),
createdAt: new Date(),
updatedAt: new Date()
},
...(customRoles || []).map(({ permissions, ...data }) => ({
...data,
permissions
}))
];
return roles;
};
const getUserPermission = async (userId: string, orgId: string, actorOrgScope?: string) => {
const { permission, membership } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgScope);
return { permissions: packRules(permission.rules), membership };
};
return { createRole, updateRole, deleteRole, listRoles, getUserPermission };
};
+457
View File
@@ -0,0 +1,457 @@
import { ForbiddenError } from "@casl/ability";
import slugify from "@sindresorhus/slugify";
import jwt from "jsonwebtoken";
import { OrgMembershipRole, OrgMembershipStatus } from "@app/db/schemas";
import { TProjects } from "@app/db/schemas/projects";
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 { TSamlConfigDALFactory } from "@app/ee/services/saml-config/saml-config-dal";
import { getConfig } from "@app/lib/config/env";
import { generateAsymmetricKeyPair } from "@app/lib/crypto";
import { generateSymmetricKey, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { isDisposableEmail } from "@app/lib/validator";
import { ActorType, AuthMethod, AuthTokenType } from "../auth/auth-type";
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
import { TokenType } from "../auth-token/auth-token-types";
import { TProjectDALFactory } from "../project/project-dal";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TUserDALFactory } from "../user/user-dal";
import { TIncidentContactsDALFactory } from "./incident-contacts-dal";
import { TOrgBotDALFactory } from "./org-bot-dal";
import { TOrgDALFactory } from "./org-dal";
import { TOrgRoleDALFactory } from "./org-role-dal";
import {
TDeleteOrgMembershipDTO,
TFindAllWorkspacesDTO,
TInviteUserToOrgDTO,
TUpdateOrgMembershipDTO,
TVerifyUserToOrgDTO
} from "./org-types";
type TOrgServiceFactoryDep = {
orgDAL: TOrgDALFactory;
orgBotDAL: TOrgBotDALFactory;
orgRoleDAL: TOrgRoleDALFactory;
userDAL: TUserDALFactory;
projectDAL: TProjectDALFactory;
incidentContactDAL: TIncidentContactsDALFactory;
samlConfigDAL: Pick<TSamlConfigDALFactory, "findOne">;
smtpService: TSmtpService;
tokenService: TAuthTokenServiceFactory;
permissionService: TPermissionServiceFactory;
licenseService: Pick<
TLicenseServiceFactory,
"getPlan" | "updateSubscriptionOrgMemberCount" | "generateOrgCustomerId" | "removeOrgCustomer"
>;
};
export type TOrgServiceFactory = ReturnType<typeof orgServiceFactory>;
export const orgServiceFactory = ({
orgDAL,
userDAL,
orgRoleDAL,
incidentContactDAL,
permissionService,
smtpService,
projectDAL,
tokenService,
orgBotDAL,
licenseService,
samlConfigDAL
}: TOrgServiceFactoryDep) => {
/*
* Get organization details by the organization id
* */
const findOrganizationById = async (userId: string, orgId: string, actorOrgScope?: string) => {
await permissionService.getUserOrgPermission(userId, orgId, actorOrgScope);
const org = await orgDAL.findOrgById(orgId);
if (!org) throw new BadRequestError({ name: "Org not found", message: "Organization not found" });
return org;
};
/*
* Get all organization a user part of
* */
const findAllOrganizationOfUser = async (userId: string) => {
const orgs = await orgDAL.findAllOrgsByUserId(userId);
return orgs;
};
/*
* Get all workspace members
* */
const findAllOrgMembers = async (userId: string, orgId: string, actorOrgScope?: string) => {
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgScope);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
const members = await orgDAL.findAllOrgMembers(orgId);
return members;
};
const findAllWorkspaces = async ({ actor, actorId, actorOrgScope, orgId }: TFindAllWorkspacesDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgScope);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace);
const organizationWorkspaceIds = new Set((await projectDAL.find({ orgId })).map((workspace) => workspace.id));
let workspaces: (TProjects & { organization: string } & {
environments: {
id: string;
slug: string;
name: string;
}[];
})[];
if (actor === ActorType.USER) {
workspaces = await projectDAL.findAllProjects(actorId);
} else if (actor === ActorType.IDENTITY) {
workspaces = await projectDAL.findAllProjectsByIdentity(actorId);
} else {
throw new BadRequestError({ message: "Invalid actor type" });
}
return workspaces.filter((workspace) => organizationWorkspaceIds.has(workspace.id));
};
/*
* Update organization settings
* */
const updateOrgName = async (userId: string, orgId: string, name: string, actorOrgScope?: string) => {
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgScope);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
const org = await orgDAL.updateById(orgId, { name });
if (!org) throw new BadRequestError({ name: "Org not found", message: "Organization not found" });
return org;
};
/*
* Create organization
* */
const createOrganization = async (userId: string, userEmail: string, orgName: string) => {
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);
const customerId = await licenseService.generateOrgCustomerId(orgName, userEmail);
const organization = await orgDAL.transaction(async (tx) => {
// akhilmhdh: for now this is auto created. in future we can input from user and for previous users just modifiy
const org = await orgDAL.create(
{ name: orgName, customerId, slug: slugify(`${orgName}-${alphaNumericNanoId(4)}`) },
tx
);
await orgDAL.createMembership(
{
userId,
orgId: org.id,
role: OrgMembershipRole.Admin,
status: OrgMembershipStatus.Accepted
},
tx
);
await orgBotDAL.create(
{
name: org.name,
publicKey,
privateKeyIV,
encryptedPrivateKey,
symmetricKeyIV,
symmetricKeyTag,
encryptedSymmetricKey,
symmetricKeyAlgorithm,
orgId: org.id,
privateKeyTag,
privateKeyAlgorithm,
privateKeyKeyEncoding,
symmetricKeyKeyEncoding
},
tx
);
return org;
});
return organization;
};
/*
* Delete organization by id
* */
const deleteOrganizationById = async (userId: string, orgId: string, actorOrgScope?: string) => {
const { membership } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgScope);
if ((membership.role as OrgMembershipRole) !== OrgMembershipRole.Admin)
throw new UnauthorizedError({ name: "Delete org by id", message: "Not an admin" });
const organization = await orgDAL.deleteById(orgId);
if (organization.customerId) {
await licenseService.removeOrgCustomer(organization.customerId);
}
return organization;
};
/*
* Org membership management
* Not another service because it has close ties with how an org works doesn't make sense to seperate them
* */
const updateOrgMembership = async ({ role, orgId, userId, membershipId, actorOrgScope }: TUpdateOrgMembershipDTO) => {
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgScope);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Member);
const isCustomRole = !Object.values(OrgMembershipRole).includes(role as OrgMembershipRole);
if (isCustomRole) {
const customRole = await orgRoleDAL.findOne({ slug: role, orgId });
if (!customRole) throw new BadRequestError({ name: "Update membership", message: "Role not found" });
const plan = await licenseService.getPlan(orgId);
if (!plan?.rbac)
throw new BadRequestError({
message: "Failed to assign custom role due to RBAC restriction. Upgrade plan to assign custom role to member."
});
const [membership] = await orgDAL.updateMembership(
{ id: membershipId, orgId },
{
role: OrgMembershipRole.Custom,
roleId: customRole.id
}
);
return membership;
}
const [membership] = await orgDAL.updateMembership({ id: membershipId, orgId }, { role, roleId: null });
return membership;
};
/*
* Invite user to organization
*/
const inviteUserToOrganization = async ({ orgId, userId, inviteeEmail, actorOrgScope }: TInviteUserToOrgDTO) => {
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgScope);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
const samlCfg = await samlConfigDAL.findOne({ orgId });
if (samlCfg && samlCfg.isActive) {
throw new BadRequestError({
message: "Failed to invite member due to SAML SSO configured for organization"
});
}
const plan = await licenseService.getPlan(orgId);
if (plan.memberLimit !== null && plan.membersUsed >= plan.memberLimit) {
// case: limit imposed on number of members allowed
// case: number of members used exceeds the number of members allowed
throw new BadRequestError({
message: "Failed to invite member due to member limit reached. Upgrade plan to invite more members."
});
}
const invitee = await orgDAL.transaction(async (tx) => {
const inviteeUser = await userDAL.findUserByEmail(inviteeEmail, tx);
if (inviteeUser) {
// if user already exist means its already part of infisical
// Thus the signup flow is not needed anymore
const [inviteeMembership] = await orgDAL.findMembership({ orgId, userId: inviteeUser.id }, { tx });
if (inviteeMembership && inviteeMembership.status === OrgMembershipStatus.Accepted) {
throw new BadRequestError({
message: "Failed to invite an existing member of org",
name: "Invite user to org"
});
}
if (!inviteeMembership) {
await orgDAL.createMembership(
{
userId: inviteeUser.id,
inviteEmail: inviteeEmail,
orgId,
role: OrgMembershipRole.Member,
status: OrgMembershipStatus.Invited
},
tx
);
}
return inviteeUser;
}
const isEmailInvalid = await isDisposableEmail(inviteeEmail);
if (isEmailInvalid) {
throw new BadRequestError({
message: "Provided a disposable email",
name: "Org invite"
});
}
// not invited before
const user = await userDAL.create(
{
email: inviteeEmail,
isAccepted: false,
authMethods: [AuthMethod.EMAIL]
},
tx
);
await orgDAL.createMembership(
{
inviteEmail: inviteeEmail,
orgId,
userId: user.id,
role: OrgMembershipRole.Member,
status: OrgMembershipStatus.Invited
},
tx
);
return user;
});
const token = await tokenService.createTokenForUser({
type: TokenType.TOKEN_EMAIL_ORG_INVITATION,
userId: invitee.id,
orgId
});
const org = await orgDAL.findOrgById(orgId);
const user = await userDAL.findById(userId);
const appCfg = getConfig();
await smtpService.sendMail({
template: SmtpTemplates.OrgInvite,
subjectLine: "Infisical organization invitation",
recipients: [inviteeEmail],
substitutions: {
inviterFirstName: user.firstName,
inviterEmail: user.email,
organizationName: org?.name,
email: inviteeEmail,
organizationId: org?.id.toString(),
token,
callback_url: `${appCfg.SITE_URL}/signupinvite`
}
});
await licenseService.updateSubscriptionOrgMemberCount(orgId);
if (!appCfg.isSmtpConfigured) {
return `${appCfg.SITE_URL}/signupinvite?token=${token}&to=${inviteeEmail}&organization_id=${org?.id}`;
}
};
/**
* Organization invitation step 2: Verify that code [code] was sent to email [email] as part of
* magic link and issue a temporary signup token for user to complete setting up their account
*/
const verifyUserToOrg = async ({ orgId, email, code }: TVerifyUserToOrgDTO) => {
const user = await userDAL.findUserByEmail(email);
if (!user) {
throw new BadRequestError({ message: "Invalid request", name: "Verify user to org" });
}
const [orgMembership] = await orgDAL.findMembership({
userId: user.id,
status: OrgMembershipStatus.Invited,
orgId
});
if (!orgMembership)
throw new BadRequestError({
message: "Failed to find invitation",
name: "Verify user to org"
});
await tokenService.validateTokenForUser({
type: TokenType.TOKEN_EMAIL_ORG_INVITATION,
userId: user.id,
orgId: orgMembership.orgId,
code
});
if (user.isAccepted) {
// this means user has already completed signup process
// isAccepted is set true when keys are exchanged
await orgDAL.updateMembershipById(orgMembership.id, {
orgId,
status: OrgMembershipStatus.Accepted
});
await licenseService.updateSubscriptionOrgMemberCount(orgId);
return { user };
}
const appCfg = getConfig();
const token = jwt.sign(
{
authTokenType: AuthTokenType.SIGNUP_TOKEN,
userId: user.id
},
appCfg.AUTH_SECRET,
{
expiresIn: appCfg.JWT_SIGNUP_LIFETIME
}
);
return { token, user };
};
const deleteOrgMembership = async ({ orgId, userId, membershipId, actorOrgScope }: TDeleteOrgMembershipDTO) => {
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgScope);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Member);
const membership = await orgDAL.deleteMembershipById(membershipId, orgId);
await licenseService.updateSubscriptionOrgMemberCount(orgId);
return membership;
};
/*
* CRUD operations of incident contacts
* */
const findIncidentContacts = async (userId: string, orgId: string, actorOrgScope?: string) => {
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgScope);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.IncidentAccount);
const incidentContacts = await incidentContactDAL.findByOrgId(orgId);
return incidentContacts;
};
const createIncidentContact = async (userId: string, orgId: string, email: string, actorOrgScope?: string) => {
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgScope);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.IncidentAccount);
const doesIncidentContactExist = await incidentContactDAL.findOne(orgId, { email });
if (doesIncidentContactExist) {
throw new BadRequestError({
message: "Incident contact already exist",
name: "Incident contact exist"
});
}
const incidentContact = await incidentContactDAL.create(orgId, email);
return incidentContact;
};
const deleteIncidentContact = async (userId: string, orgId: string, id: string, actorOrgScope?: string) => {
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgScope);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.IncidentAccount);
const incidentContact = await incidentContactDAL.deleteById(id, orgId);
return incidentContact;
};
return {
findOrganizationById,
findAllOrgMembers,
findAllOrganizationOfUser,
inviteUserToOrganization,
verifyUserToOrg,
updateOrgName,
createOrganization,
deleteOrganizationById,
deleteOrgMembership,
findAllWorkspaces,
updateOrgMembership,
// incident contacts
findIncidentContacts,
createIncidentContact,
deleteIncidentContact
};
};
+36
View File
@@ -0,0 +1,36 @@
import { ActorType } from "../auth/auth-type";
export type TUpdateOrgMembershipDTO = {
userId: string;
orgId: string;
membershipId: string;
role: string;
actorOrgScope?: string;
};
export type TDeleteOrgMembershipDTO = {
userId: string;
orgId: string;
membershipId: string;
actorOrgScope?: string;
};
export type TInviteUserToOrgDTO = {
userId: string;
orgId: string;
actorOrgScope?: string;
inviteeEmail: string;
};
export type TVerifyUserToOrgDTO = {
email: string;
orgId: string;
code: string;
};
export type TFindAllWorkspacesDTO = {
actor: ActorType;
actorId: string;
actorOrgScope?: string;
orgId: string;
};
@@ -0,0 +1,156 @@
import { ForbiddenError } from "@casl/ability";
import slugify from "@sindresorhus/slugify";
import { ProjectMembershipRole } 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 { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env";
import { createSecretBlindIndex } from "@app/lib/crypto";
import { BadRequestError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
import { TSecretBlindIndexDALFactory } from "../secret-blind-index/secret-blind-index-dal";
import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TProjectDALFactory } from "./project-dal";
import { TCreateProjectDTO, TDeleteProjectDTO, TGetProjectDTO } from "./project-types";
export const DEFAULT_PROJECT_ENVS = [
{ name: "Development", slug: "dev" },
{ name: "Staging", slug: "staging" },
{ name: "Production", slug: "prod" }
];
type TProjectServiceFactoryDep = {
projectDAL: TProjectDALFactory;
folderDAL: Pick<TSecretFolderDALFactory, "insertMany">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "insertMany">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create">;
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "create">;
permissionService: TPermissionServiceFactory;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
};
export type TProjectServiceFactory = ReturnType<typeof projectServiceFactory>;
export const projectServiceFactory = ({
projectDAL,
permissionService,
folderDAL,
secretBlindIndexDAL,
projectMembershipDAL,
projectEnvDAL,
licenseService
}: TProjectServiceFactoryDep) => {
/*
* Create workspace. Make user the admin
* */
const createProject = async ({ orgId, actor, actorId, actorOrgScope, workspaceName }: TCreateProjectDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgScope);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
const appCfg = getConfig();
const blindIndex = createSecretBlindIndex(appCfg.ROOT_ENCRYPTION_KEY, appCfg.ENCRYPTION_KEY);
const plan = await licenseService.getPlan(orgId);
if (plan.workspaceLimit !== null && plan.workspacesUsed >= plan.workspaceLimit) {
// case: limit imposed on number of workspaces allowed
// case: number of workspaces used exceeds the number of workspaces allowed
throw new BadRequestError({
message: "Failed to create workspace due to plan limit reached. Upgrade plan to add more workspaces."
});
}
const newProject = projectDAL.transaction(async (tx) => {
const project = await projectDAL.create(
{ name: workspaceName, orgId, slug: slugify(`${workspaceName}-${alphaNumericNanoId(4)}`) },
tx
);
// set user as admin member for proeject
await projectMembershipDAL.create(
{
userId: actorId,
role: ProjectMembershipRole.Admin,
projectId: project.id
},
tx
);
// generate the blind index for project
await secretBlindIndexDAL.create(
{
projectId: project.id,
keyEncoding: blindIndex.keyEncoding,
saltIV: blindIndex.iv,
saltTag: blindIndex.tag,
algorithm: blindIndex.algorithm,
encryptedSaltCipherText: blindIndex.ciphertext
},
tx
);
// set default environments and root folder for provided environments
const envs = await projectEnvDAL.insertMany(
DEFAULT_PROJECT_ENVS.map((el, i) => ({ ...el, projectId: project.id, position: i + 1 })),
tx
);
await folderDAL.insertMany(
envs.map(({ id }) => ({ name: ROOT_FOLDER_NAME, envId: id, version: 1 })),
tx
);
// _id for backward compat
return { ...project, environments: envs, _id: project.id };
});
return newProject;
};
const deleteProject = async ({ actor, actorId, projectId }: TDeleteProjectDTO) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Project);
const deletedProject = await projectDAL.deleteById(projectId);
return deletedProject;
};
const getProjects = async (actorId: string) => {
const workspaces = await projectDAL.findAllProjects(actorId);
return workspaces;
};
const getAProject = async ({ actorId, projectId, actor }: TGetProjectDTO) => {
await permissionService.getProjectPermission(actor, actorId, projectId);
return projectDAL.findProjectById(projectId);
};
const toggleAutoCapitalization = async ({
projectId,
actor,
actorId,
autoCapitalization
}: TGetProjectDTO & { autoCapitalization: boolean }) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
const updatedProject = await projectDAL.updateById(projectId, { autoCapitalization });
return updatedProject;
};
const updateName = async ({ projectId, actor, actorId, name }: TGetProjectDTO & { name: string }) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
const updatedProject = await projectDAL.updateById(projectId, { name });
return updatedProject;
};
return {
createProject,
deleteProject,
getProjects,
getAProject,
toggleAutoCapitalization,
updateName
};
};
@@ -0,0 +1,21 @@
import { ActorType } from "../auth/auth-type";
export type TCreateProjectDTO = {
actor: ActorType;
actorId: string;
actorOrgScope?: string;
orgId: string;
workspaceName: string;
};
export type TDeleteProjectDTO = {
actor: ActorType;
actorId: string;
projectId: string;
};
export type TGetProjectDTO = {
actor: ActorType;
actorId: string;
projectId: string;
};
@@ -0,0 +1,116 @@
import { TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas";
import { BadRequestError } from "@app/lib/errors";
import { TAuthLoginFactory } from "../auth/auth-login-service";
import { AuthMethod } from "../auth/auth-type";
import { TOrgServiceFactory } from "../org/org-service";
import { TUserDALFactory } from "../user/user-dal";
import { TSuperAdminDALFactory } from "./super-admin-dal";
import { TAdminSignUpDTO } from "./super-admin-types";
type TSuperAdminServiceFactoryDep = {
serverCfgDAL: TSuperAdminDALFactory;
userDAL: TUserDALFactory;
authService: Pick<TAuthLoginFactory, "generateUserTokens">;
orgService: Pick<TOrgServiceFactory, "createOrganization">;
};
export type TSuperAdminServiceFactory = ReturnType<typeof superAdminServiceFactory>;
let serverCfg: Readonly<TSuperAdmin>;
export const getServerCfg = () => {
if (!serverCfg) throw new BadRequestError({ name: "Get server cfg", message: "Server cfg not initialized" });
return serverCfg;
};
export const superAdminServiceFactory = ({
serverCfgDAL,
userDAL,
authService,
orgService
}: TSuperAdminServiceFactoryDep) => {
const initServerCfg = async () => {
serverCfg = await serverCfgDAL.findOne({});
if (!serverCfg) {
const newCfg = await serverCfgDAL.create({ initialized: false, allowSignUp: true });
serverCfg = newCfg;
return newCfg;
}
return serverCfg;
};
const updateServerCfg = async (data: TSuperAdminUpdate) => {
const cfg = await serverCfgDAL.updateById(serverCfg.id, data);
serverCfg = cfg;
Object.freeze(serverCfg);
return cfg;
};
const adminSignUp = async ({
lastName,
firstName,
salt,
email,
verifier,
publicKey,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
ip,
userAgent
}: TAdminSignUpDTO) => {
const existingUser = await userDAL.findOne({ email });
if (existingUser) throw new BadRequestError({ name: "Admin sign up", message: "User already exist" });
const userInfo = await userDAL.transaction(async (tx) => {
const newUser = await userDAL.create(
{
firstName,
lastName,
email,
superAdmin: true,
isAccepted: true,
authMethods: [AuthMethod.EMAIL]
},
tx
);
const userEnc = await userDAL.createUserEncryption(
{
salt,
encryptionVersion: 2,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
verifier,
userId: newUser.id
},
tx
);
return { user: newUser, enc: userEnc };
});
await orgService.createOrganization(userInfo.user.id, userInfo.user.email, "Admin Org");
await updateServerCfg({ initialized: true });
const token = await authService.generateUserTokens({
user: userInfo.user,
ip,
userAgent,
organizationId: undefined
});
// TODO(akhilmhdh-pg): telemetry service
return { token, user: userInfo };
};
return {
initServerCfg,
updateServerCfg,
adminSignUp
};
};