From 82a1f554b5e87b1fb484a854f9ed8df211f3a106 Mon Sep 17 00:00:00 2001 From: Ruben Talstra Date: Sat, 22 Feb 2025 11:00:02 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20Enhance=20Group=20and=20Use?= =?UTF-8?q?r=20Schemas=20with=20OpenID=20Support=20and=20Documentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/models/schema/groupSchema.js | 36 +++++++++++++++++++++----------- api/models/schema/userSchema.js | 12 +++++------ api/strategies/openidStrategy.js | 27 +++++++++++++++++++++++- 3 files changed, 56 insertions(+), 19 deletions(-) diff --git a/api/models/schema/groupSchema.js b/api/models/schema/groupSchema.js index b2dc988c7e..acbf5ac000 100644 --- a/api/models/schema/groupSchema.js +++ b/api/models/schema/groupSchema.js @@ -1,6 +1,16 @@ const mongoose = require('mongoose'); -const groupSchema = new mongoose.Schema( +/** + * @typedef {Object} MongoGroup + * @property {ObjectId} [_id] - MongoDB Document ID + * @property {string} name - The group's name + * @property {string} [description] - A brief description of the group + * @property {string} [externalId] - External identifier for the group (required for non-local groups) + * @property {string} provider - The provider of the group. Defaults to 'local'. For external groups (e.g., 'openid') the externalId is required. + * @property {Date} [createdAt] - Date when the group was created (added by timestamps) + * @property {Date} [updatedAt] - Date when the group was last updated (added by timestamps) + */ +const groupSchema = mongoose.Schema( { name: { type: String, @@ -10,19 +20,21 @@ const groupSchema = new mongoose.Schema( description: { type: String, }, - allowedEndpoints: [ - { - type: String, - required: true, - + externalId: { + type: String, + unique: true, + required: function () { + return this.provider !== 'local'; }, - ], - allowedModels: [ - { - type: String, - }, - ], + }, + provider: { + type: String, + required: true, + default: 'local', + enum: ['local', 'openid'], + }, }, + { timestamps: true }, ); diff --git a/api/models/schema/userSchema.js b/api/models/schema/userSchema.js index 32e8a62314..7d62623b53 100644 --- a/api/models/schema/userSchema.js +++ b/api/models/schema/userSchema.js @@ -27,6 +27,7 @@ const { SystemRoles } = require('librechat-data-provider'); * @property {Array} [plugins=[]] - List of plugins used by the user * @property {Array.} [refreshToken] - List of sessions with refresh tokens * @property {Date} [expiresAt] - Optional expiration date of the file + * @property {Array.} [groups] - List of group IDs the user belongs to (references to the Group model) * @property {Date} [createdAt] - Date when the user was created (added by timestamps) * @property {Date} [updatedAt] - Date when the user was last updated (added by timestamps) */ @@ -143,12 +144,11 @@ const userSchema = mongoose.Schema( type: Boolean, default: false, }, - groups: [ - { - type: mongoose.Schema.Types.ObjectId, - ref: 'Group', - }, - ], + groups: { + type: [mongoose.Schema.Types.ObjectId], + ref: 'Group', + default: [], + }, }, { timestamps: true }, diff --git a/api/strategies/openidStrategy.js b/api/strategies/openidStrategy.js index b26b11efed..f32d271652 100644 --- a/api/strategies/openidStrategy.js +++ b/api/strategies/openidStrategy.js @@ -8,6 +8,7 @@ const { findUser, createUser, updateUser } = require('~/models/userMethods'); const { hashToken } = require('~/server/utils/crypto'); const { isEnabled } = require('~/server/utils'); const { logger } = require('~/config'); +const Group = require('~/models/group'); let crypto; try { @@ -146,7 +147,7 @@ async function setupOpenId() { async (tokenset, userinfo, done) => { try { logger.info(`[openidStrategy] verify login openidId: ${userinfo.sub}`); - logger.debug('[openidStrategy] very login tokenset and userinfo', { tokenset, userinfo }); + logger.debug('[openidStrategy] verify login tokenset and userinfo', { tokenset, userinfo }); let user = await findUser({ openidId: userinfo.sub }); logger.info( @@ -192,6 +193,30 @@ async function setupOpenId() { message: `You must have the "${requiredRole}" role to log in.`, }); } + // Synchronize the user's groups for OpenID. + const userGroupIds = user.groups.map(id => id.toString()); + + // Remove existing OpenID group references. + const currentOpenIdGroups = await Group.find({ + _id: { $in: userGroupIds }, + provider: 'openid', + }); + const currentOpenIdGroupIds = new Set(currentOpenIdGroups.map(g => g._id.toString())); + user.groups = user.groups.filter(id => !currentOpenIdGroupIds.has(id.toString())); + + // Look up groups matching the roles. + const matchingGroups = await Group.find({ + provider: 'openid', + externalId: { $in: roles }, + }); + const userGroupSet = new Set(user.groups.map(id => id.toString())); + for (const group of matchingGroups) { + const groupIdStr = group._id.toString(); + if (!userGroupSet.has(groupIdStr)) { + user.groups.push(group._id); + userGroupSet.add(groupIdStr); + } + } } let username = '';