How to deal with conditions on ref field (mongoose)
See original GitHub issueAfter using Casl for some simple project, I am trying to implement something more complicated. I am trying to mix Roles with persisted permissions with JWT described on the website.
For this basic example, I try to give read action permissions to User subject but only on users entries that are part of an organization:
My User model
interface UserAttrs {
email: string;
firstName: string;
lastName: string;
password: string;
role: RoleDoc;
organization: OrganizationDoc;
}
interface UserModel extends mongoose.Model<UserDoc> {
build(attrs: UserAttrs): UserDoc;
}
interface UserDoc extends mongoose.Document {
email: string;
firstName: string;
lastName: string;
active: boolean;
password: string;
createdAt: Date;
updatedAt: Date;
role: RoleDoc;
organization: OrganizationDoc;
}
const userSchema = new mongoose.Schema(
{
email: {
type: String,
required: true,
unique: true,
trim: true,
match: [/.+\@.+\..+/, 'Please fill a valid email address'],
},
firstName: {
type: String,
required: true,
trim: true,
},
lastName: {
type: String,
required: true,
trim: true,
},
active: {
type: Boolean,
default: true,
required: true,
},
password: {
type: String,
required: true,
},
createdAt: {
type: Date,
required: true,
default: Date.now,
},
updatedAt: {
type: Date,
required: true,
default: Date.now,
},
role: {
type: mongoose.Schema.Types.ObjectId,
required: true,
ref: 'Role',
},
organization: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Organization',
},
},
{
toJSON: {
transform(_doc, ret) {
ret.id = ret._id;
delete ret._id;
delete ret.password;
delete ret.__v;
},
},
}
);
userSchema.pre('save', async function (done) {
if (this.isModified('password')) {
const hashed = await Password.toHash(this.get('password'));
this.set('password', hashed);
this.set('updatedAt', Date.now);
}
done();
});
userSchema.statics.build = (attrs: UserAttrs) => {
return new User(attrs);
};
const User = mongoose.model<UserDoc, UserModel>('User', userSchema);
export { User };
Organization Model
interface OrganizationAttrs {
id: string;
name: string;
}
interface OrganizationModel extends mongoose.Model<OrganizationDoc> {
build(attrs: OrganizationAttrs): OrganizationDoc;
}
export interface OrganizationDoc extends mongoose.Document {
name: string;
version: number;
}
const organizationSchema = new mongoose.Schema(
{
name: {
type: String,
required: true,
unique: true,
trim: true,
},
},
{
toJSON: {
transform(_doc, ret) {
ret.id = ret._id;
delete ret._id;
},
},
}
);
organizationSchema.set('versionKey', 'version');
organizationSchema.plugin(updateIfCurrentPlugin);
organizationSchema.statics.findByEvent = (event: {
id: string;
version: number;
}) => {
return Organization.findOne({
_id: event.id,
version: event.version - 1,
});
};
organizationSchema.statics.build = (attrs: OrganizationAttrs) => {
return new Organization({
_id: attrs.id,
name: attrs.name,
});
};
const Organization = mongoose.model<OrganizationDoc, OrganizationModel>(
'Organization',
organizationSchema
);
export { Organization };
Role model
interface RoleAttrs {
name: string;
permissions: string;
organization: OrganizationDoc;
}
interface RoleModel extends mongoose.Model<RoleDoc> {
build(attrs: RoleAttrs): RoleDoc;
}
export interface RoleDoc extends mongoose.Document {
name: string;
permissions: string;
organization: OrganizationDoc;
}
const roleSchema = new mongoose.Schema(
{
name: {
type: String,
required: true,
unique: true,
trim: true,
},
permissions: {
type: String,
required: true,
trim: true,
},
organization: {
type: mongoose.Schema.Types.ObjectId,
required: true,
ref: 'Organization',
},
},
{
toJSON: {
transform(_doc, ret) {
ret.id = ret._id;
delete ret._id;
delete ret.__v;
},
},
}
);
roleSchema.pre('save', async function (done) {
done();
});
roleSchema.statics.build = (attrs: RoleAttrs) => {
return new Role(attrs);
};
const Role = mongoose.model<RoleDoc, RoleModel>('Role', roleSchema);
export { Role };
The permissions field from role is stored as string. As soon as a user logged in, I add the permissions to the JWT token
const existingUser = await User.findOne({ email, active: true })
.populate('role')
.populate('organization');
// Check if user is valid...
// userPermissions = [{ action: 'read', subject: 'User', conditions: { organization: '{{organization.id}}' }, }, ](as a string)
const userPermissions = Mustache.render(
existingUser.role.permissions,
existingUser
);
console.log(userPermissions);
// result ==> [{"action":"read","subject":"User","conditions":{"organization":"5f4bc664e27664265cb033d7"}}]
// Genereate json web token JWT
const userJWT = jwt.sign(
{
id: existingUser.id,
email: existingUser.email,
organizationId: existingUser.organization.id,
userRolePermissions: userPermissions,
},
process.env.JWT_KEY!
);
`
Then in a middleware, I create the abilities similar to [here](https://casl.js.org/v4/en/cookbook/roles-with-persisted-permissions )
```typescript
const { id, email, organizationId, userRolePermissions } = jwt.verify(
req.session.jwt,
process.env.JWT_KEY!
) as Token;
const currentUser: UserPayload = {
id: id,
email: email,
organizationId: organizationId,
userRolePermissions: createAbility(JSON.parse(userRolePermissions)),
};
The result of createAbility is
i {
s: false,
v: [Object: null prototype] {},
p: [Object: null prototype] {},
g: [Object: null prototype] {
User: [Object: null prototype] {
read: [Object: null prototype] {
'0': t {
t: [Function],
i: undefined,
action: 'read',
subject: 'User',
inverted: false,
conditions: { organization: '5f4bc8d85dc07d269e4f303d' },
reason: undefined,
fields: undefined
}
}
}
},
j: [
{
action: 'read',
subject: 'User',
conditions: { organization: '5f4bc8d85dc07d269e4f303d' }
}
],
O: {
conditionsMatcher: [Function],
fieldMatcher: [Function: j],
resolveAction: [Function: u]
}
}
if I execute
const organizationId = req.params.organizationId as Object;
const users = await User.find({ organization: organizationId });
// req.currentUser contain the user with the userRolePermissions above
ForbiddenError.from(req.currentUser!.userRolePermissions).throwUnlessCan(
'read',
subject('User', users)
);
I get message: ‘Cannot execute “read” on “User”’. How ca we deal with ref fields ?
How casl works in case I populate the organization field on user (I will then have an object) ? Should I make two rules in permissions field in role (one if populated one if not) ?
Issue Analytics
- State:
- Created 3 years ago
- Comments:6 (3 by maintainers)
Top GitHub Comments
Hi the post was removed from stackoverflow, is it possible to get a summary here? Short answer is “not possible” right?
Ok, I’ll give a detailed answer a bit later. Meanwhile you can read my answer to the similar question in past: https://github.com/stalniy/casl/issues/220