@casl/ability with typegoose result - type Subject = DocumentType<Model> #680
-
I'm trying to make Casl recognize the subject, but I can't work it out. typegoose.findOneById(id).then((user: DocumentType<UserModel>) => {
// ...
type Actions = 'update' | 'manage'
type Subjects = 'UserModel'
type AppAbility = Ability<[Actions, Subjects]>
const { can: allow, cannot: forbid, build } = new AbilityBuilder<AppAbility>(Ability)
// ...
keysToUpdate.forEach((keyToUpdate) => {
CaslForbiddenError.from(ability).throwUnlessCan('update', subject('UserModel', user), keyToUpdate) // error
})
// ...
} I'm getting the following error: I tried dozens of different types, but couldn't find the right way. If you succeeded to make casl work with typegoose I would be grateful to listen to your guidance. |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 3 replies
-
Typegoose is just over complicated thing. I’d recommend to use pure mongoose, its TS support is much better nowadays In your case to make it work with mongoose use mongoose model names or extend Subjects to be |
Beta Was this translation helpful? Give feedback.
-
After some discussion with others: I got typegoose working at this codesandbox. Relevant code: import { AbilityBuilder, createMongoAbility } from "@casl/ability";
import mongoose from "mongoose";
import { prop, getModelForClass, DocumentType } from "@typegoose/typegoose";
import {
AnyParamConstructor,
IModelOptions,
BeAnObject,
Ref,
} from "@typegoose/typegoose/lib/types";
import { accessibleRecordsPlugin, AccessibleRecordModel } from "@casl/mongoose";
// Usually you'd use a real mongodb database this is just an in memory version.
import { MongoMemoryServer } from "mongodb-memory-server";
// This will create an new instance of "MongoMemoryServer" and automatically start it
const mongod = await MongoMemoryServer.create();
const uri = mongod.getUri();
mongoose.set("strictQuery", false);
await mongoose.connect(uri);
/// Setup CASL's plugin
mongoose.plugin(accessibleRecordsPlugin);
enum StatusEnum {
review = "review",
published = "published",
draft = "draft",
}
class User {}
interface IArticle {
status: StatusEnum;
}
class Article implements IArticle {
@prop({
enum: Object.values(StatusEnum),
type: String,
})
status!: IArticle["status"];
@prop({
ref: User,
})
owner!: Ref<User>;
}
/// Setup CASL's plugin to work with typegoose's getModelForClass according to:
/// https://github.com/stalniy/casl/discussions/549#discussioncomment-3796299
const getMyModelForClass = <
U extends AnyParamConstructor<any>,
QueryHelpers = BeAnObject
>(
cl: U,
options?: IModelOptions
): AccessibleRecordModel<
InstanceType<U> & DocumentType<U, QueryHelpers>,
QueryHelpers
> &
U => getModelForClass(cl, options) as any;
/// Use our new getMyModelForClass which provides the CASL types
const ArticleModel = getMyModelForClass(Article);
const UserModel = getMyModelForClass(User);
/** Defining an ability using mongo ability */
function defineAbilityFor(user: DocumentType<User>) {
// YOU DO NOT NEED A DEPENDENCY ON MONGODB TO USE CREATEMONGOABILITY.
// For proof of the above fact check the package.json of this sandbox.
// You don't need MongoDB cause under the hood the createMongoAbility
// uses the great ucast library from the maintainers of CASL.
const { can, cannot, build } = new AbilityBuilder(createMongoAbility);
// Mongo ability allows Mongo like queries like '$in'
// Here are more supported query operators:
// https://casl.js.org/v6/en/guide/conditions-in-depth#supported-operators
can("read", Article.name, {
status: { $in: [StatusEnum.published, StatusEnum.review] },
});
// In case the owner ref is populated.
can("read", Article.name, {
"owner._id": user._id,
});
// In case the owner ref is not populated
// e.g. when the article is searched straight from the DB.
can("read", Article.name, {
owner: user._id,
});
return build();
}
/// Setup a user.
const user = await UserModel.create({});
/// Make an ability for our user, so that we can check their permissions.
const ability = defineAbilityFor(user);
/// Check Abilities on single records that our user can see.
console.log(
ability.can(
"read",
await ArticleModel.create({ status: StatusEnum.published })
)
);
console.log(
ability.can("read", await ArticleModel.create({ status: StatusEnum.draft }))
);
console.log(
ability.can(
"read",
await ArticleModel.create({ status: StatusEnum.draft, owner: user._id })
)
);
console.log(
ability.can("read", await ArticleModel.create({ status: StatusEnum.review }))
);
/// Or, query the database for only the records the user can see.
console.log(await ArticleModel.accessibleBy(ability));
/// Or, query the database for only drafted articles the user can see.
console.log(
await ArticleModel.find({
status: StatusEnum.draft,
}).accessibleBy(ability)
);
// Stop the mongoose server, so the script doesn't hang.
// You may not need to do this if you are using a server :D
await mongod.stop(); |
Beta Was this translation helpful? Give feedback.
Typegoose is just over complicated thing. I’d recommend to use pure mongoose, its TS support is much better nowadays
In your case to make it work with mongoose use mongoose model names or extend Subjects to be
UserModel | ForcedSubject<‘UserModel’>