feat: first iteration changes for sub org routers

This commit is contained in:
=
2025-10-20 18:57:03 +05:30
parent 00c19455af
commit ef6282968b
23 changed files with 529 additions and 77 deletions
@@ -3,17 +3,19 @@ import { Knex } from "knex";
import { AccessScope, TAuthTokens, TAuthTokenSessions } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto/cryptography";
import { ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { AuthModeJwtTokenPayload, AuthModeRefreshJwtTokenPayload, AuthTokenType } from "../auth/auth-type";
import { TMembershipUserDALFactory } from "../membership-user/membership-user-dal";
import { TUserDALFactory } from "../user/user-dal";
import { TTokenDALFactory } from "./auth-token-dal";
import { TCreateTokenForUserDTO, TIssueAuthTokenDTO, TokenType, TValidateTokenForUserDTO } from "./auth-token-types";
import { TOrgDALFactory } from "../org/org-dal";
type TAuthTokenServiceFactoryDep = {
tokenDAL: TTokenDALFactory;
userDAL: Pick<TUserDALFactory, "findById" | "transaction">;
orgDAL: Pick<TOrgDALFactory, "findOne">;
membershipUserDAL: Pick<TMembershipUserDALFactory, "findOne">;
};
@@ -80,7 +82,7 @@ export const getTokenConfig = (tokenType: TokenType) => {
}
};
export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL }: TAuthTokenServiceFactoryDep) => {
export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL, orgDAL }: TAuthTokenServiceFactoryDep) => {
const createTokenForUser = async ({ type, userId, orgId, aliasId, payload }: TCreateTokenForUserDTO) => {
const { token, ...tkCfg } = getTokenConfig(type);
const appCfg = getConfig();
@@ -194,7 +196,7 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL }: TA
};
// to parse jwt identity in inject identity plugin
const fnValidateJwtIdentity = async (token: AuthModeJwtTokenPayload) => {
const fnValidateJwtIdentity = async (token: AuthModeJwtTokenPayload, subOrganizationSelector?: string) => {
const session = await tokenDAL.findOneTokenSession({
id: token.tokenVersionId,
userId: token.userId
@@ -207,22 +209,53 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL }: TA
const user = await userDAL.findById(session.userId);
if (!user || !user.isAccepted) throw new NotFoundError({ message: `User with ID '${session.userId}' not found` });
let orgId = "";
let parentOrgId = "";
if (token.organizationId) {
const orgMembership = await membershipUserDAL.findOne({
actorUserId: user.id,
scopeOrgId: token.organizationId,
scope: AccessScope.Organization
});
if (subOrganizationSelector) {
const subOrganization = await orgDAL.findOne({
parentOrgId: token.organizationId,
slug: subOrganizationSelector
});
if (!subOrganization)
throw new BadRequestError({ message: `Sub organization ${subOrganizationSelector} not found` });
if (!orgMembership) {
throw new ForbiddenRequestError({ message: "User not member of organization" });
}
if (!orgMembership.isActive) {
throw new ForbiddenRequestError({ message: "User organization membership is inactive" });
const orgMembership = await membershipUserDAL.findOne({
actorUserId: user.id,
scopeOrgId: subOrganization.id,
scope: AccessScope.Organization
});
if (!orgMembership) {
throw new ForbiddenRequestError({ message: "User not member of organization" });
}
if (!orgMembership.isActive) {
throw new ForbiddenRequestError({ message: "User organization membership is inactive" });
}
orgId = subOrganization.id;
parentOrgId = token.organizationId;
} else {
const orgMembership = await membershipUserDAL.findOne({
actorUserId: user.id,
scopeOrgId: token.organizationId,
scope: AccessScope.Organization
});
if (!orgMembership) {
throw new ForbiddenRequestError({ message: "User not member of organization" });
}
if (!orgMembership.isActive) {
throw new ForbiddenRequestError({ message: "User organization membership is inactive" });
}
orgId = token.organizationId;
parentOrgId = token.organizationId;
}
}
return { user, tokenVersionId: token.tokenVersionId, orgId: token.organizationId };
return { user, tokenVersionId: token.tokenVersionId, orgId, parentOrgId };
};
return {
@@ -19,6 +19,7 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.IdentityAccessToken}.identityId`)
.select(selectAllTableCols(TableName.IdentityAccessToken))
.select(db.ref("name").withSchema(TableName.Identity))
.select(db.ref("orgId").withSchema(TableName.Identity).as("identityScopeOrgId"))
.first();
return doc;
@@ -10,6 +10,7 @@ import { TIdentityDALFactory } from "../identity/identity-dal";
import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal";
import { TIdentityAccessTokenDALFactory } from "./identity-access-token-dal";
import { TIdentityAccessTokenJwtPayload, TRenewAccessTokenDTO } from "./identity-access-token-types";
import { TOrgDALFactory } from "../org/org-dal";
type TIdentityAccessTokenServiceFactoryDep = {
identityAccessTokenDAL: TIdentityAccessTokenDALFactory;
@@ -19,6 +20,7 @@ type TIdentityAccessTokenServiceFactoryDep = {
"updateIdentityAccessTokenStatus" | "getIdentityTokenDetailsInCache"
>;
membershipIdentityDAL: Pick<TMembershipIdentityDALFactory, "findOne">;
orgDAL: Pick<TOrgDALFactory, "findOne">;
};
export type TIdentityAccessTokenServiceFactory = ReturnType<typeof identityAccessTokenServiceFactory>;
@@ -27,7 +29,8 @@ export const identityAccessTokenServiceFactory = ({
identityAccessTokenDAL,
accessTokenQueue,
identityDAL,
membershipIdentityDAL
membershipIdentityDAL,
orgDAL
}: TIdentityAccessTokenServiceFactoryDep) => {
const validateAccessTokenExp = async (identityAccessToken: TIdentityAccessTokens) => {
const {
@@ -181,7 +184,11 @@ export const identityAccessTokenServiceFactory = ({
return { revokedToken };
};
const fnValidateIdentityAccessToken = async (token: TIdentityAccessTokenJwtPayload, ipAddress?: string) => {
const fnValidateIdentityAccessToken = async (
token: TIdentityAccessTokenJwtPayload,
subOrganizationSelector?: string,
ipAddress?: string
) => {
const identityAccessToken = await identityAccessTokenDAL.findOne({
[`${TableName.IdentityAccessToken}.id` as "id"]: token.identityAccessTokenId,
isAccessTokenRevoked: false
@@ -202,13 +209,36 @@ export const identityAccessTokenServiceFactory = ({
trustedIps: trustedIps as TIp[]
});
}
const identityOrgMembership = await membershipIdentityDAL.findOne({
scope: AccessScope.Organization,
actorIdentityId: identityAccessToken.identityId
});
let orgId = "";
const parentOrgId = identityAccessToken.identityScopeOrgId;
if (!identityOrgMembership) {
throw new BadRequestError({ message: "Identity does not belong to any organization" });
if (subOrganizationSelector) {
const subOrganization = await orgDAL.findOne({ parentOrgId, slug: subOrganizationSelector });
if (!subOrganizationSelector)
throw new BadRequestError({ message: `Sub organization ${subOrganizationSelector} not found` });
const identityOrgMembership = await membershipIdentityDAL.findOne({
scope: AccessScope.Organization,
actorIdentityId: identityAccessToken.identityId,
scopeOrgId: subOrganization.id
});
if (!identityOrgMembership) {
throw new BadRequestError({ message: "Identity does not belong to any organization" });
}
orgId = subOrganization.id;
} else {
const identityOrgMembership = await membershipIdentityDAL.findOne({
scope: AccessScope.Organization,
actorIdentityId: identityAccessToken.identityId,
scopeOrgId: parentOrgId
});
if (!identityOrgMembership) {
throw new BadRequestError({ message: "Identity does not belong to any organization" });
}
orgId = parentOrgId;
}
let { accessTokenNumUses } = identityAccessToken;
@@ -219,7 +249,7 @@ export const identityAccessTokenServiceFactory = ({
await validateAccessTokenExp({ ...identityAccessToken, accessTokenNumUses });
await accessTokenQueue.updateIdentityAccessTokenStatus(identityAccessToken.id, Number(accessTokenNumUses) + 1);
return { ...identityAccessToken, orgId: identityOrgMembership.scopeOrgId };
return { ...identityAccessToken, orgId, parentOrgId };
};
return { renewAccessToken, revokeAccessToken, fnValidateIdentityAccessToken };
+44
View File
@@ -27,6 +27,7 @@ import {
import { generateKnexQueryFromScim } from "@app/lib/knex/scim";
import { OrgAuthMethod } from "./org-types";
import { ActorType } from "../auth/auth-type";
export type TOrgDALFactory = ReturnType<typeof orgDALFactory>;
@@ -64,6 +65,7 @@ export const orgDALFactory = (db: TDbClient) => {
const buildBaseQuery = (orgIdSubquery: Knex.QueryBuilder) => {
return db
.replicaNode()(TableName.Organization)
.whereNull(`${TableName.Organization}.parentOrgId`)
.whereIn(`${TableName.Organization}.id`, orgIdSubquery)
.leftJoin(TableName.Project, `${TableName.Organization}.id`, `${TableName.Project}.orgId`)
.leftJoin(TableName.Membership, `${TableName.Organization}.id`, `${TableName.Membership}.scopeOrgId`)
@@ -154,11 +156,47 @@ export const orgDALFactory = (db: TDbClient) => {
}
};
const listSubOrganizations = async (dto: {
actorId: string;
actorType: ActorType;
orgId: string;
isAccessible?: boolean;
limit?: number;
offset?: number;
}) => {
try {
// TODO(sub-org:group): check this when implement group support
const query = db
.replicaNode()(TableName.Organization)
.where(`${TableName.Organization}.parentOrgId`, dto.orgId)
.select(selectAllTableCols(TableName.Organization));
if (dto.isAccessible) {
void query.leftJoin(`${TableName.Membership}`, (qb) => {
void qb.on(`${TableName.Membership}.scope`, AccessScope.Organization);
if (dto.actorType === ActorType.IDENTITY) {
void qb.andOn(`${TableName.Membership}.actorIdentityId`, dto.actorId);
} else {
void qb.andOn(`${TableName.Membership}.actorUserId`, dto.actorId);
}
});
}
if (dto.limit) void query.limit(dto.limit);
if (dto.offset) void query.offset(dto.offset);
const orgs = await query;
return orgs;
} catch (error) {
throw new DatabaseError({ error, name: "List sub organization" });
}
};
const findOrgById = async (orgId: string) => {
try {
const org = (await db
.replicaNode()(TableName.Organization)
.where({ [`${TableName.Organization}.id` as "id"]: orgId })
.whereNull(`${TableName.Organization}.parentOrgId`)
.leftJoin(TableName.SamlConfig, (qb) => {
qb.on(`${TableName.SamlConfig}.orgId`, "=", `${TableName.Organization}.id`).andOn(
`${TableName.SamlConfig}.isActive`,
@@ -195,6 +233,7 @@ export const orgDALFactory = (db: TDbClient) => {
try {
const org = (await db
.replicaNode()(TableName.Organization)
.whereNull(`${TableName.Organization}.parentOrgId`)
.where({ [`${TableName.Organization}.slug` as "slug"]: orgSlug })
.leftJoin(TableName.SamlConfig, (qb) => {
qb.on(`${TableName.SamlConfig}.orgId`, "=", `${TableName.Organization}.id`).andOn(
@@ -240,6 +279,7 @@ export const orgDALFactory = (db: TDbClient) => {
.whereNotNull(`${TableName.Membership}.actorUserId`)
.join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`)
.join(TableName.Organization, `${TableName.Membership}.scopeOrgId`, `${TableName.Organization}.id`)
.whereNull(`${TableName.Organization}.parentOrgId`)
.leftJoin(TableName.SamlConfig, (qb) => {
qb.on(`${TableName.SamlConfig}.orgId`, "=", `${TableName.Organization}.id`).andOn(
`${TableName.SamlConfig}.isActive`,
@@ -337,6 +377,7 @@ export const orgDALFactory = (db: TDbClient) => {
}
};
// TODO(sub-org): updated this logic later
const countAllOrgMembers = async (orgId: string) => {
try {
interface CountResult {
@@ -610,6 +651,7 @@ export const orgDALFactory = (db: TDbClient) => {
})
.join(TableName.Users, `${TableName.Users}.id`, `${TableName.Membership}.actorUserId`)
.join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.Membership}.scopeOrgId`)
.whereNull(`${TableName.Organization}.parentOrgId`)
.leftJoin(TableName.UserAliases, function joinUserAlias() {
this.on(`${TableName.UserAliases}.userId`, "=", `${TableName.Membership}.actorUserId`)
.andOn(`${TableName.UserAliases}.orgId`, "=", `${TableName.Membership}.scopeOrgId`)
@@ -648,6 +690,7 @@ export const orgDALFactory = (db: TDbClient) => {
.replicaNode()(TableName.Membership)
.where({ actorIdentityId: identityId })
.where(`${TableName.Membership}.scope`, AccessScope.Organization)
.whereNull(`${TableName.Organization}.parentOrgId`)
.whereNotNull(`${TableName.Membership}.actorIdentityId`)
.join(TableName.MembershipRole, `${TableName.Membership}.id`, `${TableName.MembershipRole}.membershipId`)
.join(TableName.Organization, `${TableName.Membership}.scopeOrgId`, `${TableName.Organization}.id`)
@@ -667,6 +710,7 @@ export const orgDALFactory = (db: TDbClient) => {
findOrgByProjectId,
findAllOrgMembers,
countAllOrgMembers,
listSubOrganizations,
findOrgById,
findOrgBySlug,
findAllOrgsByUserId,