mirror of
https://github.com/fosrl/pangolin.git
synced 2025-08-04 01:55:10 +02:00
add default mapping policy and move auto provision
This commit is contained in:
parent
99188233db
commit
b4fda6a1f6
6 changed files with 475 additions and 257 deletions
|
@ -65,8 +65,7 @@ export enum ActionsEnum {
|
|||
listResourceRules = "listResourceRules",
|
||||
updateResourceRule = "updateResourceRule",
|
||||
listOrgDomains = "listOrgDomains",
|
||||
createNewt = "createNewt",
|
||||
createIdp = "createIdp"
|
||||
createNewt = "createNewt"
|
||||
}
|
||||
|
||||
export async function checkUserActionPermission(
|
||||
|
|
|
@ -425,7 +425,14 @@ export const supporterKey = sqliteTable("supporterKey", {
|
|||
export const idp = sqliteTable("idp", {
|
||||
idpId: integer("idpId").primaryKey({ autoIncrement: true }),
|
||||
name: text("name").notNull(),
|
||||
type: text("type").notNull()
|
||||
type: text("type").notNull(),
|
||||
defaultRoleMapping: text("defaultRoleMapping"),
|
||||
defaultOrgMapping: text("defaultOrgMapping"),
|
||||
autoProvision: integer("autoProvision", {
|
||||
mode: "boolean"
|
||||
})
|
||||
.notNull()
|
||||
.default(false)
|
||||
});
|
||||
|
||||
// Identity Provider OAuth Configuration
|
||||
|
@ -440,11 +447,6 @@ export const idpOidcConfig = sqliteTable("idpOidcConfig", {
|
|||
clientSecret: text("clientSecret").notNull(),
|
||||
authUrl: text("authUrl").notNull(),
|
||||
tokenUrl: text("tokenUrl").notNull(),
|
||||
autoProvision: integer("autoProvision", {
|
||||
mode: "boolean"
|
||||
})
|
||||
.notNull()
|
||||
.default(false),
|
||||
identifierPath: text("identifierPath").notNull(),
|
||||
emailPath: text("emailPath"),
|
||||
namePath: text("namePath"),
|
||||
|
|
|
@ -91,6 +91,7 @@ export async function createOidcIdp(
|
|||
.insert(idp)
|
||||
.values({
|
||||
name,
|
||||
autoProvision,
|
||||
type: "oidc"
|
||||
})
|
||||
.returning();
|
||||
|
@ -103,7 +104,6 @@ export async function createOidcIdp(
|
|||
clientSecret: encryptedSecret,
|
||||
authUrl,
|
||||
tokenUrl,
|
||||
autoProvision,
|
||||
scopes,
|
||||
identifierPath,
|
||||
emailPath,
|
||||
|
|
|
@ -20,16 +20,18 @@ const paramsSchema = z
|
|||
|
||||
const bodySchema = z
|
||||
.object({
|
||||
name: z.string().nonempty(),
|
||||
clientId: z.string().nonempty(),
|
||||
clientSecret: z.string().nonempty(),
|
||||
authUrl: z.string().url(),
|
||||
tokenUrl: z.string().url(),
|
||||
identifierPath: z.string().nonempty(),
|
||||
name: z.string().optional(),
|
||||
clientId: z.string().optional(),
|
||||
clientSecret: z.string().optional(),
|
||||
authUrl: z.string().optional(),
|
||||
tokenUrl: z.string().optional(),
|
||||
identifierPath: z.string().optional(),
|
||||
emailPath: z.string().optional(),
|
||||
namePath: z.string().optional(),
|
||||
scopes: z.string().optional(),
|
||||
autoProvision: z.boolean().optional()
|
||||
autoProvision: z.boolean().optional(),
|
||||
defaultRoleMapping: z.string().optional(),
|
||||
defaultOrgMapping: z.string().optional()
|
||||
})
|
||||
.strict();
|
||||
|
||||
|
@ -92,7 +94,9 @@ export async function updateOidcIdp(
|
|||
emailPath,
|
||||
namePath,
|
||||
name,
|
||||
autoProvision
|
||||
autoProvision,
|
||||
defaultRoleMapping,
|
||||
defaultOrgMapping
|
||||
} = parsedBody.data;
|
||||
|
||||
// Check if IDP exists and is of type OIDC
|
||||
|
@ -115,33 +119,51 @@ export async function updateOidcIdp(
|
|||
}
|
||||
|
||||
const key = config.getRawConfig().server.secret;
|
||||
const encryptedSecret = encrypt(clientSecret, key);
|
||||
const encryptedClientId = encrypt(clientId, key);
|
||||
const encryptedSecret = clientSecret
|
||||
? encrypt(clientSecret, key)
|
||||
: undefined;
|
||||
const encryptedClientId = clientId ? encrypt(clientId, key) : undefined;
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
// Update IDP name
|
||||
await trx
|
||||
.update(idp)
|
||||
.set({
|
||||
name
|
||||
})
|
||||
.where(eq(idp.idpId, idpId));
|
||||
const idpData = {
|
||||
name,
|
||||
autoProvision,
|
||||
defaultRoleMapping,
|
||||
defaultOrgMapping
|
||||
};
|
||||
|
||||
// Update OIDC config
|
||||
await trx
|
||||
.update(idpOidcConfig)
|
||||
.set({
|
||||
clientId: encryptedClientId,
|
||||
clientSecret: encryptedSecret,
|
||||
authUrl,
|
||||
tokenUrl,
|
||||
autoProvision,
|
||||
scopes,
|
||||
identifierPath,
|
||||
emailPath,
|
||||
namePath
|
||||
})
|
||||
.where(eq(idpOidcConfig.idpId, idpId));
|
||||
// only update if at least one key is not undefined
|
||||
let keysToUpdate = Object.keys(idpData).filter(
|
||||
(key) => idpData[key as keyof typeof idpData] !== undefined
|
||||
);
|
||||
|
||||
if (keysToUpdate.length > 0) {
|
||||
await trx.update(idp).set(idpData).where(eq(idp.idpId, idpId));
|
||||
}
|
||||
|
||||
const configData = {
|
||||
clientId: encryptedClientId,
|
||||
clientSecret: encryptedSecret,
|
||||
authUrl,
|
||||
tokenUrl,
|
||||
scopes,
|
||||
identifierPath,
|
||||
emailPath,
|
||||
namePath
|
||||
};
|
||||
|
||||
keysToUpdate = Object.keys(configData).filter(
|
||||
(key) =>
|
||||
configData[key as keyof typeof configData] !== undefined
|
||||
);
|
||||
|
||||
if (keysToUpdate.length > 0) {
|
||||
// Update OIDC config
|
||||
await trx
|
||||
.update(idpOidcConfig)
|
||||
.set(configData)
|
||||
.where(eq(idpOidcConfig.idpId, idpId));
|
||||
}
|
||||
});
|
||||
|
||||
return response<UpdateIdpResponse>(res, {
|
||||
|
|
|
@ -213,188 +213,229 @@ export async function validateOidcCallback(
|
|||
)
|
||||
);
|
||||
|
||||
const idpOrgs = await db
|
||||
.select()
|
||||
.from(idpOrg)
|
||||
.where(eq(idpOrg.idpId, existingIdp.idp.idpId));
|
||||
|
||||
let userOrgInfo: { orgId: string; roleId: number }[] = [];
|
||||
for (const idpOrg of idpOrgs) {
|
||||
let roleId: number | undefined = undefined;
|
||||
|
||||
if (idpOrg.orgMapping) {
|
||||
const orgId = jmespath.search(claims, idpOrg.orgMapping);
|
||||
if (!orgId) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (idpOrg.roleMapping) {
|
||||
const roleName = jmespath.search(claims, idpOrg.roleMapping);
|
||||
|
||||
if (!roleName) {
|
||||
logger.error("Role name not found in the ID token", {
|
||||
roleName
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const [roleRes] = await db
|
||||
.select()
|
||||
.from(roles)
|
||||
.where(
|
||||
and(
|
||||
eq(roles.orgId, idpOrg.orgId),
|
||||
eq(roles.name, roleName)
|
||||
)
|
||||
);
|
||||
|
||||
if (!roleRes) {
|
||||
logger.error("Role not found", {
|
||||
orgId: idpOrg.orgId,
|
||||
roleName
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
roleId = roleRes.roleId;
|
||||
|
||||
userOrgInfo.push({
|
||||
orgId: idpOrg.orgId,
|
||||
roleId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug("User org info", { userOrgInfo });
|
||||
|
||||
let existingUserId = existingUser?.userId;
|
||||
|
||||
// sync the user with the orgs and roles
|
||||
await db.transaction(async (trx) => {
|
||||
let userId = existingUser?.userId;
|
||||
|
||||
// create user if not exists
|
||||
if (!existingUser) {
|
||||
userId = generateId(15);
|
||||
|
||||
await trx.insert(users).values({
|
||||
userId,
|
||||
username: userIdentifier,
|
||||
email: email || null,
|
||||
name: name || null,
|
||||
type: UserType.OIDC,
|
||||
idpId: existingIdp.idp.idpId,
|
||||
emailVerified: true, // OIDC users are always verified
|
||||
dateCreated: new Date().toISOString()
|
||||
});
|
||||
} else {
|
||||
// set the name and email
|
||||
await trx
|
||||
.update(users)
|
||||
.set({
|
||||
username: userIdentifier,
|
||||
email: email || null,
|
||||
name: name || null
|
||||
})
|
||||
.where(eq(users.userId, userId));
|
||||
}
|
||||
|
||||
existingUserId = userId;
|
||||
|
||||
// get all current user orgs
|
||||
const currentUserOrgs = await trx
|
||||
if (existingIdp.idp.autoProvision) {
|
||||
const idpOrgs = await db
|
||||
.select()
|
||||
.from(userOrgs)
|
||||
.where(eq(userOrgs.userId, userId));
|
||||
.from(idpOrg)
|
||||
.where(eq(idpOrg.idpId, existingIdp.idp.idpId));
|
||||
|
||||
// Delete orgs that are no longer valid
|
||||
const orgsToDelete = currentUserOrgs.filter(
|
||||
(currentOrg) =>
|
||||
!userOrgInfo.some(
|
||||
(newOrg) => newOrg.orgId === currentOrg.orgId
|
||||
)
|
||||
);
|
||||
const defaultRoleMapping = existingIdp.idp.defaultRoleMapping;
|
||||
const defaultOrgMapping = existingIdp.idp.defaultOrgMapping;
|
||||
|
||||
if (orgsToDelete.length > 0) {
|
||||
await trx.delete(userOrgs).where(
|
||||
and(
|
||||
eq(userOrgs.userId, userId),
|
||||
inArray(
|
||||
userOrgs.orgId,
|
||||
orgsToDelete.map((org) => org.orgId)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
let userOrgInfo: { orgId: string; roleId: number }[] = [];
|
||||
for (const idpOrg of idpOrgs) {
|
||||
let roleId: number | undefined = undefined;
|
||||
|
||||
// Update roles for existing orgs where the role has changed
|
||||
const orgsToUpdate = currentUserOrgs.filter((currentOrg) => {
|
||||
const newOrg = userOrgInfo.find(
|
||||
(newOrg) => newOrg.orgId === currentOrg.orgId
|
||||
);
|
||||
return newOrg && newOrg.roleId !== currentOrg.roleId;
|
||||
});
|
||||
const orgMapping = idpOrg.orgMapping || defaultOrgMapping;
|
||||
const hydratedOrgMapping = orgMapping
|
||||
?.split("{{orgId}}")
|
||||
.join(idpOrg.orgId);
|
||||
|
||||
if (orgsToUpdate.length > 0) {
|
||||
for (const org of orgsToUpdate) {
|
||||
const newRole = userOrgInfo.find(
|
||||
(newOrg) => newOrg.orgId === org.orgId
|
||||
);
|
||||
if (newRole) {
|
||||
await trx
|
||||
.update(userOrgs)
|
||||
.set({ roleId: newRole.roleId })
|
||||
.where(
|
||||
and(
|
||||
eq(userOrgs.userId, userId),
|
||||
eq(userOrgs.orgId, org.orgId)
|
||||
)
|
||||
);
|
||||
if (hydratedOrgMapping) {
|
||||
const orgId = jmespath.search(claims, hydratedOrgMapping);
|
||||
if (!(orgId === true || orgId === idpOrg.orgId)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const roleMapping = idpOrg.roleMapping || defaultRoleMapping;
|
||||
if (roleMapping) {
|
||||
const roleName = jmespath.search(claims, roleMapping);
|
||||
|
||||
if (!roleName) {
|
||||
logger.error("Role name not found in the ID token", {
|
||||
roleName
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const [roleRes] = await db
|
||||
.select()
|
||||
.from(roles)
|
||||
.where(
|
||||
and(
|
||||
eq(roles.orgId, idpOrg.orgId),
|
||||
eq(roles.name, roleName)
|
||||
)
|
||||
);
|
||||
|
||||
if (!roleRes) {
|
||||
logger.error("Role not found", {
|
||||
orgId: idpOrg.orgId,
|
||||
roleName
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
roleId = roleRes.roleId;
|
||||
|
||||
userOrgInfo.push({
|
||||
orgId: idpOrg.orgId,
|
||||
roleId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Add new orgs that don't exist yet
|
||||
const orgsToAdd = userOrgInfo.filter(
|
||||
(newOrg) =>
|
||||
!currentUserOrgs.some(
|
||||
(currentOrg) => currentOrg.orgId === newOrg.orgId
|
||||
)
|
||||
logger.debug("User org info", { userOrgInfo });
|
||||
|
||||
let existingUserId = existingUser?.userId;
|
||||
|
||||
// sync the user with the orgs and roles
|
||||
await db.transaction(async (trx) => {
|
||||
let userId = existingUser?.userId;
|
||||
|
||||
// create user if not exists
|
||||
if (!existingUser) {
|
||||
userId = generateId(15);
|
||||
|
||||
await trx.insert(users).values({
|
||||
userId,
|
||||
username: userIdentifier,
|
||||
email: email || null,
|
||||
name: name || null,
|
||||
type: UserType.OIDC,
|
||||
idpId: existingIdp.idp.idpId,
|
||||
emailVerified: true, // OIDC users are always verified
|
||||
dateCreated: new Date().toISOString()
|
||||
});
|
||||
} else {
|
||||
// set the name and email
|
||||
await trx
|
||||
.update(users)
|
||||
.set({
|
||||
username: userIdentifier,
|
||||
email: email || null,
|
||||
name: name || null
|
||||
})
|
||||
.where(eq(users.userId, userId));
|
||||
}
|
||||
|
||||
existingUserId = userId;
|
||||
|
||||
// get all current user orgs
|
||||
const currentUserOrgs = await trx
|
||||
.select()
|
||||
.from(userOrgs)
|
||||
.where(eq(userOrgs.userId, userId));
|
||||
|
||||
// Delete orgs that are no longer valid
|
||||
const orgsToDelete = currentUserOrgs.filter(
|
||||
(currentOrg) =>
|
||||
!userOrgInfo.some(
|
||||
(newOrg) => newOrg.orgId === currentOrg.orgId
|
||||
)
|
||||
);
|
||||
|
||||
if (orgsToDelete.length > 0) {
|
||||
await trx.delete(userOrgs).where(
|
||||
and(
|
||||
eq(userOrgs.userId, userId),
|
||||
inArray(
|
||||
userOrgs.orgId,
|
||||
orgsToDelete.map((org) => org.orgId)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Update roles for existing orgs where the role has changed
|
||||
const orgsToUpdate = currentUserOrgs.filter((currentOrg) => {
|
||||
const newOrg = userOrgInfo.find(
|
||||
(newOrg) => newOrg.orgId === currentOrg.orgId
|
||||
);
|
||||
return newOrg && newOrg.roleId !== currentOrg.roleId;
|
||||
});
|
||||
|
||||
if (orgsToUpdate.length > 0) {
|
||||
for (const org of orgsToUpdate) {
|
||||
const newRole = userOrgInfo.find(
|
||||
(newOrg) => newOrg.orgId === org.orgId
|
||||
);
|
||||
if (newRole) {
|
||||
await trx
|
||||
.update(userOrgs)
|
||||
.set({ roleId: newRole.roleId })
|
||||
.where(
|
||||
and(
|
||||
eq(userOrgs.userId, userId),
|
||||
eq(userOrgs.orgId, org.orgId)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add new orgs that don't exist yet
|
||||
const orgsToAdd = userOrgInfo.filter(
|
||||
(newOrg) =>
|
||||
!currentUserOrgs.some(
|
||||
(currentOrg) => currentOrg.orgId === newOrg.orgId
|
||||
)
|
||||
);
|
||||
|
||||
if (orgsToAdd.length > 0) {
|
||||
await trx.insert(userOrgs).values(
|
||||
orgsToAdd.map((org) => ({
|
||||
userId,
|
||||
orgId: org.orgId,
|
||||
roleId: org.roleId,
|
||||
dateCreated: new Date().toISOString()
|
||||
}))
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const token = generateSessionToken();
|
||||
const sess = await createSession(token, existingUserId);
|
||||
const isSecure = req.protocol === "https";
|
||||
const cookie = serializeSessionCookie(
|
||||
token,
|
||||
isSecure,
|
||||
new Date(sess.expiresAt)
|
||||
);
|
||||
|
||||
if (orgsToAdd.length > 0) {
|
||||
await trx.insert(userOrgs).values(
|
||||
orgsToAdd.map((org) => ({
|
||||
userId,
|
||||
orgId: org.orgId,
|
||||
roleId: org.roleId,
|
||||
dateCreated: new Date().toISOString()
|
||||
}))
|
||||
res.appendHeader("Set-Cookie", cookie);
|
||||
|
||||
return response<ValidateOidcUrlCallbackResponse>(res, {
|
||||
data: {
|
||||
redirectUrl: postAuthRedirectUrl
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "OIDC callback validated successfully",
|
||||
status: HttpCode.CREATED
|
||||
});
|
||||
} else {
|
||||
if (!existingUser) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.UNAUTHORIZED,
|
||||
"User not found in the IdP"
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const token = generateSessionToken();
|
||||
const sess = await createSession(token, existingUserId);
|
||||
const isSecure = req.protocol === "https";
|
||||
const cookie = serializeSessionCookie(
|
||||
token,
|
||||
isSecure,
|
||||
new Date(sess.expiresAt)
|
||||
);
|
||||
|
||||
res.appendHeader("Set-Cookie", cookie);
|
||||
|
||||
return response<ValidateOidcUrlCallbackResponse>(res, {
|
||||
data: {
|
||||
redirectUrl: postAuthRedirectUrl
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "OIDC callback validated successfully",
|
||||
status: HttpCode.CREATED
|
||||
});
|
||||
//
|
||||
// const token = generateSessionToken();
|
||||
// const sess = await createSession(token, existingUser.userId);
|
||||
// const isSecure = req.protocol === "https";
|
||||
// const cookie = serializeSessionCookie(
|
||||
// token,
|
||||
// isSecure,
|
||||
// new Date(sess.expiresAt)
|
||||
// );
|
||||
//
|
||||
// res.appendHeader("Set-Cookie", cookie);
|
||||
//
|
||||
// return response<ValidateOidcUrlCallbackResponse>(res, {
|
||||
// data: {
|
||||
// redirectUrl: postAuthRedirectUrl
|
||||
// },
|
||||
// success: true,
|
||||
// error: false,
|
||||
// message: "OIDC callback validated successfully",
|
||||
// status: HttpCode.CREATED
|
||||
// });
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue