cancel
Showing results for 
Search instead for 
Did you mean: 
Read only

Handle domain-driven authorization in a Node.js CAP app that uses PostgreSQL as database

FedericoBelotti
Explorer
0 Kudos
479

Hello everyone,
I'm developing a CAP application in Node.js (with TypeScript) that needs to check for authorizations on a domain level via associations navigation. If, for example, I have a Foo entity that has an association to Bar entity that associates itself to a Third entity that has a scalar field Field. I want to check if that Field has a specific value (maybe compared to one of the $user attributes that come from the authentication, for the sake of this example) and, if so, I would serve only the data related to a specific role that respect the given conditions.
So far so good, I can leverage the domain-driven authorizations and just navigate through the entity associations to check the value I want:

 

annotate DummyService.Foo with @(
    restrict    : [{
        grant: 'READ',
        to   : 'user',
        where: 'bar.third.field = $user.field'
    }]
);

 

But this didn't seem to work at all, I never got to have any records back. After a lot of trial and error I dug in the CAP documentation and I found out that associations navigation is not (yet, hopefully) possible outside of a SAP HANA context.

I'm using PostgreSQL so, if I didn't misunderstand that warning, I guess I have to handle it by myself in the custom handlers.

Fine, I have set up a class for the DummyService to handle the on.before event for reading Foo records. The idea is to inject the privilege check into the where clause of the req.query.SELECT object to obtain something on the line of:

 

req.query.SELECT.where = [{<query filters>}, "and", {ref: ["bar", "third", "field"]}, "=", {val: req.user.attr.field}]; 

 

The problem is that I can't figure out how to inject data into the where variable, since TypeScript complains about type compatibility with whatever I try to push into the conditions array.
At runtime, the where object is an Array of objects so it wouldn't be difficult at all to push something into it, but at design time stuff is a bit different. In fact, the where object adheres to the following type:

 

Partial<string & SELECT_2 & ref & val & xpr & function_call>'

 

This is what I have tried to do so far:

 

import cds from "@sap/cds";

export default class DummyService extends cds.ApplicationService {
  init() {
    this.before("READ", "Foo", (req) => {
      req.query.SELECT?.where?.push("and");
      req.query.SELECT?.where?.push({xpr: [{ref: ["bar", "third", "field"]}, "=", {val: req.user.attr.field}]});
    });

    return super.init();
  }
}

 

And the specific error is:
Argument of type '{ xpr: (string | { ref: string[]; } | { val: string; })[]; }' is not assignable to parameter of type 'Partial<string & SELECT_2 & ref & val & xpr & function_call>'.

The types returned by 'valueOf()' are incompatible between these types.
Type 'Object' is not assignable to type 'string'.

Has someone ever had to manage this kind of scenario? Is there an actual way to do this?

Thanks in advance for any help,
Federico

View Entire Topic
FedericoBelotti
Explorer

I have found a better way to handle this problem without overriding standard handlers.

The limit with path navigation IS there, the workaround is to flatten the relations so that you end up with entities that have only one-level associations you can use in domain-driven authorizations. Considering the above example, I needed to check that bar.third.field = $user.field, so what I did was simply, thanks to the many ways you can define a CDS entity with:

entity FooExtension as select from Foo {
    key ID as ID;
    bar.third.field as field;
}

So that I can just add an association to Foo inside the DummyService definition (and keep the schema clean): 

using {your.schema.namespace as db} from 'path/to/schema';

service DummyService {
    entity Foo as projection on db.Foo {
        extension: Association to many FooExtension on extension.ID = $self;
    }

    entity FooExtension as select from Foo {
        key ID as ID;
        bar.third.field as field;
    }
}

And lastly, where you have defined privileges:

annotate DummyService.Foo with @(
    restrict: [
        grant: 'READ',
        to: 'user',
        where: 'extension.field = $user.field'
    ]
)