🔐 feat: Granular Role-based Permissions + Entra ID Group Discovery (#7804)

This commit is contained in:
Danny Avila 2025-06-23 10:54:25 -04:00
parent 6c9a29b6cf
commit f55cdc9b7f
No known key found for this signature in database
GPG key ID: BF31EEB2C5CA0956
99 changed files with 11321 additions and 621 deletions

View file

@ -0,0 +1,31 @@
import { Schema } from 'mongoose';
import type { IAccessRole } from '~/types';
const accessRoleSchema = new Schema<IAccessRole>(
{
accessRoleId: {
type: String,
required: true,
index: true,
unique: true,
},
name: {
type: String,
required: true,
},
description: String,
resourceType: {
type: String,
enum: ['agent', 'project', 'file'],
required: true,
default: 'agent',
},
permBits: {
type: Number,
required: true,
},
},
{ timestamps: true },
);
export default accessRoleSchema;

View file

@ -0,0 +1,65 @@
import { Schema } from 'mongoose';
import type { IAclEntry } from '~/types';
const aclEntrySchema = new Schema<IAclEntry>(
{
principalType: {
type: String,
enum: ['user', 'group', 'public'],
required: true,
},
principalId: {
type: Schema.Types.ObjectId,
refPath: 'principalModel',
required: function (this: IAclEntry) {
return this.principalType !== 'public';
},
index: true,
},
principalModel: {
type: String,
enum: ['User', 'Group'],
required: function (this: IAclEntry) {
return this.principalType !== 'public';
},
},
resourceType: {
type: String,
enum: ['agent', 'project', 'file'],
required: true,
},
resourceId: {
type: Schema.Types.ObjectId,
required: true,
index: true,
},
permBits: {
type: Number,
default: 1,
},
roleId: {
type: Schema.Types.ObjectId,
ref: 'AccessRole',
},
inheritedFrom: {
type: Schema.Types.ObjectId,
sparse: true,
index: true,
},
grantedBy: {
type: Schema.Types.ObjectId,
ref: 'User',
},
grantedAt: {
type: Date,
default: Date.now,
},
},
{ timestamps: true },
);
aclEntrySchema.index({ principalId: 1, principalType: 1, resourceType: 1, resourceId: 1 });
aclEntrySchema.index({ resourceId: 1, principalType: 1, principalId: 1 });
aclEntrySchema.index({ principalId: 1, permBits: 1, resourceType: 1 });
export default aclEntrySchema;

View file

@ -98,4 +98,6 @@ const agentSchema = new Schema<IAgent>(
},
);
agentSchema.index({ updatedAt: -1, _id: 1 });
export default agentSchema;

View file

@ -0,0 +1,56 @@
import { Schema } from 'mongoose';
import type { IGroup } from '~/types';
const groupSchema = new Schema<IGroup>(
{
name: {
type: String,
required: true,
index: true,
},
description: {
type: String,
required: false,
},
email: {
type: String,
required: false,
index: true,
},
avatar: {
type: String,
required: false,
},
memberIds: [
{
type: String,
},
],
source: {
type: String,
enum: ['local', 'entra'],
default: 'local',
},
/** External ID (e.g., Entra ID) */
idOnTheSource: {
type: String,
sparse: true,
index: true,
required: function (this: IGroup) {
return this.source !== 'local';
},
},
},
{ timestamps: true },
);
groupSchema.index(
{ idOnTheSource: 1, source: 1 },
{
unique: true,
partialFilterExpression: { idOnTheSource: { $exists: true } },
},
);
groupSchema.index({ memberIds: 1 });
export default groupSchema;

View file

@ -138,6 +138,11 @@ const userSchema = new Schema<IUser>(
},
default: {},
},
/** Field for external source identification (for consistency with TPrincipal schema) */
idOnTheSource: {
type: String,
sparse: true,
},
},
{ timestamps: true },
);