Specific abilities does not work as expected - nestjs

I'm using `casl` with a `Nest.js` project and I have a problem.
I want to deny users to read collections unless they are public.
My entity class looks like
class Collection {
isPrivate?: boolean
}
My casl factory looks like
allow(Action.READ, Collection, { isPublic: true })`
So I want to allow reading collection ONLY if it is public.
Currently, when I try ability.can(Action.READ, Collection) (so for a list) it returns true. I want it to return false because it's not public.
Now I give you complete code so you are able to understand the context.
import { Injectable } from '#nestjs/common';
import {
AbilityBuilder,
createMongoAbility,
ExtractSubjectType,
InferSubjects,
MongoAbility,
} from '#casl/ability';
import { JwtPayload, Role } from '../../authentication/types';
import { Action } from '../constants/action';
import { Collection } from '../../collection/entities/collection.entity';
import { User } from '../../user/entities/user.entity';
import { Media } from '../../media/entities/media.entity';
type Subjects =
| InferSubjects<
typeof Collection | typeof User | typeof Media | Collection | User | Media
>
| 'all';
export type AppAbility = MongoAbility<[Action, Subjects]>;
#Injectable()
export class AuthorizationAbilityFactory {
createForUser(user?: JwtPayload) {
const {
can: allow,
cannot: deny,
build,
} = new AbilityBuilder<AppAbility>(createMongoAbility);
allow(Action.READ, Collection, { isPublic: true }).because(
'Public collections can be read',
);
allow(Action.READ, Media);
if (user?.role !== Role.USER) {
allow(Action.CREATE, User);
}
return build({
// Read https://casl.js.org/v5/en/guide/subject-type-detection#use-classes-as-subject-types for details
detectSubjectType: (item) =>
item.constructor as ExtractSubjectType<Subjects>,
});
}
}
And the failing test
const ability = authorizationAbilityFactory.createForUser(); // <- anonymous user
expect(ability.can(Action.READ, Collection)).toBe(false); // <- this fails

Related

How to pass default parameters to the #Query class in Nest.Js?

I'm trying to pass the default parameters maxnodes=3 and addstats=false to the controller via the #Query parameter in Nest.Js.
The code works fine, but the default parameters are not used. When I pass on the query parameters the ones that are passed are shown, but if none are passed, the default values (3 and false) are not used.
How to fix that?
context.contructor.ts:
import { CreateContextQuery } from './context.query';
import { CreateContextDto } from './context.dto';
#Post('graph')
public async createGraphForContext(
#Body('context') contextData: CreateContextDto,
#Query()
contextQuery: CreateContextQuery,
) {
const before = Date.now();
const { context } = await this.contextService.createContext(contextData);
const graph = await this.contextService.getGraphOfContext(
context.id,
contextQuery.maxnodes,
contextQuery.addstats,
);
}
context.query.ts:
import { ApiProperty } from '#nestjs/swagger';
export class CreateContextQuery {
#ApiProperty({
description: 'Maximum number of nodes to show on the graph',
})
maxnodes;
#ApiProperty({
description: 'Include graph statistics',
})
addstats;
constructor(maxnodes = 3, addstats = false) {
this.maxnodes = maxnodes;
this.addstats = addstats;
}
}
So basically in your DTO, you can give default values.
export class CreateContextQuery {
#IsOptional()
#Type(() => Number)
#IsNumber()
#Min(0)
maxnodes?: number = 3;
#IsOptional()
#Type(() => Boolean)
#IsBoolean()
addstats?: boolean = false;
constructor(maxnodes = 3, addstats = false) {
this.maxnodes = maxnodes;
this.addstats = addstats;
}
}
// as you can see i am using validation too
And in your controller :
#Post('graph')
#UsePipes(new ValidationPipe({ transform: true }))
// you need to add this for tansformation
public async createGraphForContext(
#Body('context') contextData: CreateContextDto,
#Query()
contextQuery: CreateContextQuery,
) {
const before = Date.now();
const { context } = await this.contextService.createContext(contextData);
const graph = await this.contextService.getGraphOfContext(
context.id,
contextQuery.maxnodes,
contextQuery.addstats,
);
}
PS
Also if you want you can add custom decorators, in your case:
// add this decorator
export const GetContextQuery = createParamDecorator((_data: unknown, ctx: ExecutionContext): CreateContextDto => {
const request = ctx.switchToHttp().getRequest();
const query = request.query;
const maxnodes = parseInt(query.maxnodes) || 3;//default values here in case it fails to parse
const addstats = Boolean(query.addstats) || 0;
return { addstats, addstats };
});
and in your controller, you can call the decorator instead of #Query
just add your decorator #GetContextQuery() context: CreateContextDto, and now you do not need the UsePipes
What you receive in the query param is a plain object. You can achieve what you want putting a pipe in your query param and applying a class transform to instantiate the class.
Read this: https://docs.nestjs.com/pipes#providing-defaults
contextQuery isn't an instance of CreateContextQuery because, without any configuration, Nest won't call new CreateContextQuery any time. This is why you end up using pipes (read this https://docs.nestjs.com/techniques/validation#transform-payload-objects)

Dynamic Dependency injection with Typescript using tsyringe

I am trying to build and example to understand how the DI framework/library works, but i am encountering some problems.
I have this interface with two possible implementations:
export interface Operation {
calculate(a: number, b: number): number;
}
sub.ts
import { Operation } from "./operation.interface";
export class Sub implements Operation {
calculate(a: number, b: number): number {
return Math.abs(a - b);
}
}
sum.ts
import { Operation } from "./operation.interface";
export class Sum implements Operation {
calculate(a: number, b: number): number {
return a + b;
}
}
calculator.ts
import { Operation } from "./operation.interface";
import {injectable, inject} from "tsyringe";
#injectable()
export class Calculator {
constructor(#inject("Operation") private operation?: Operation){}
operate(a: number, b: number): number {
return this.operation.calculate(a, b);
}
}
index.ts
import "reflect-metadata";
import { container } from "tsyringe";
import { Calculator } from "./classes/calculator";
import { Sub } from "./classes/sub";
import { Sum } from "./classes/sum";
container.register("Operation", {
useClass: Sum
});
container.register("OperationSub", {
useClass: Sub
});
const calculatorSum = container.resolve(Calculator);
const result = calculatorSum.operate(4,6);
console.log(result);
// const calculatorSub = ???
is there a way where I could have two calculators with different behaviours or am I doing it completely wrong?
Since OperationSub isn’t used anywhere, it cannot affect injected Operation value.
Calculators with different dependency sets should be represented with multiple containers. Summing calculator can be considered a default implementation and use root container, or both implementations can be represented by children containers while root container remains abstract.
// common deps are registered on `container`
const sumContainer = container.createChildContainer();
const subContainer = container.createChildContainer();
sumContainer.register("Operation", { useClass: Sum });
subContainer.register("Operation", { useClass: Sub });
const calculatorSum = sumContainer.resolve(Calculator);
const calculatorSub = subContainer.resolve(Calculator);

Objection.js add data from joined table to parent json

I have the following Objection.js models:
Appointment:
'use strict'
const { Model } = require('objection')
class Appointment extends Model {
// Table name is the only required property.
static get tableName() {
return 'appointment'
}
static get idColumn() {
return 'appointmentId';
}
// This object defines the relations to other models.
static get relationMappings() {
// One way to prevent circular references
// is to require the model classes here.
const AppointmentType = require('./AppointmentType')
return {
appointmentType: {
relation: Model.BelongsToOneRelation,
// The related model. This can be either a Model subclass constructor or an
// absolute file path to a module that exports one.
modelClass: AppointmentType,
join: {
from: 'appointment.appointmentTypeId',
to: 'appointmentType.appointmentTypeId'
}
},
}
}
}
module.exports = Appointment
AppointmentType:
'use strict'
const { Model } = require('objection')
class AppointmentType extends Model {
// Table name is the only required property.
static get tableName() {
return 'appointmentType'
}
static get idColumn() {
return 'appointmentTypeId';
}
}
module.exports = AppointmentType
Using the following query:
await Appointment.query().withGraphJoined({appointmentType: true})
I get the following results:
{
"appointmentId": 1,
"duration": 12,
"appointmentTypeId": 2,
"appointmentType": {
"appointmentTypeId": 2,
"appointmentTypeName": "Type Name"
}
....
}
In most cases, the default return from objection is useful but in this one not so much. Would it be possible to return something like:
{
"appointmentId": 1,
"duration": 12,
"appointmentTypeName": "Type Name" // or "typeName": "Type Name"
...
}
I think this is not possible yet. I ll just parse the object again, or use it just like that. I'll leave this here in case someone has found a nice way
you can select the columns you want and they'll return as one flat object
const appointments = await Appointment.query().select('appointmentId','duration', 'appointmentTypeName').leftJoinRelated('appointmentType');
but this is vulnerable to duplicates only if an appointment has many types. be careful

undefined is not iterable (cannot read property Symbol(Symbol.iterator))

I'm trying to loop through a fileList in order to perform a delete query. First i fetched data from table "files" in database where attribute "postnumber"=user input. Then it is saved into the "fileList:Files[]". Then a loop through this fileList in order to perform a delete query. but it keeps saying that
"ERROR TypeError: undefined is not iterable (cannot read property
Symbol(Symbol.iterator))". See this image =>
forum-admin-list.component.ts
import { FileService } from 'src/app/shared/file.service';
import { Files } from 'src/app/shared/files.model';
export class ForumAdminListComponent {
fileList:Files[];
onDelete(pNo:string){
this.fservice.getPost(pNo).subscribe(actionArray => {
this.fileList = actionArray.map(item => {
return {
id: item.payload.doc.id,
...item.payload.doc.data()
} as Files;
})
});
for(let i of this.fileList){
this.storage.storage.refFromURL(i.path).delete();
this.firestore.doc("files/"+i.id).delete();
}
}
}
files.model.ts
export class Files {
id:string;
pNo:string;
downloadURL:string;
path:string;
}
file.service.ts
export class FileService {
formData: Files;
constructor(private firestore: AngularFirestore) { }
getPost(userRef){
return this.firestore.collection('files',ref=>ref.where('pNo','==',userRef)).snapshotChanges();
}
}
You're looping through the fileList outside the subscribe(), meaning it won't actually wait for the Observable to be resolved. Try to loop inside your subscribe().
onDelete(pNo:string){
this.fservice.getPost(pNo).subscribe(actionArray => {
this.fileList = actionArray.map(item => {
return {
id: item.payload.doc.id,
...item.payload.doc.data()
} as Files[];
for(let i of this.fileList){
this.storage.storage.refFromURL(i.path).delete();
this.firestore.doc("files/"+i.id).delete();
}
})
});
}
Also you might wanna mark the result of the subscription as Files[] instead of as File

How to Filter Data in Grid when Route Changed

Hi I'm very new to angular.
I have an url like http://localhost:3000/prizes/brand1 , brand2, brand3
I want to reload my grid items and filter them base on that brand.
{ path: 'prizes/:brand_name', component: PrizeComponent }
This is the method that I have to get the items
getPrizes() {
return this._http.get('/prizes')
.map(data => data.json()).toPromise()
}
Question
How do I reload/call back the get method base on the new route and how do I get the brand name and pass that one as a parameter to the get method.
Regards
If to use dataService to fetch data:
#Injectable()
export class daraService {
private brandName: string = '';
constructor(aroute: ActivatedRoute) {
aroute.url.subscribe((data) => {
console.log('params ', data); //check for brand here
this.brandName = data[data.length - 1]
});
}
getPrizes() {
// you can access this.brandName value here
return this._http.get('/prizes')
.map(data => data.json()).toPromise()
}
}

Resources