Field-Level Security in SAP CAP

 

Introduction

Here we look at SAP CAP service-level authentication and controlling which fields different users can see.

Original article here.

Project Setup

cds init authorization-demo
cd authorization-demo
npm install

Data Model

Create db/schema.cds as follows:

namespace com.test;
using { managed } from ‘@sap/cds/common’;
entity Data: managed {
  key ID : UUID;
  x: Integer;
  y: Integer;
}

Edit package.json to use Sqlite as follows:

{
  ...
  “cds”: {
    “requires”: {
      “db”: {
        “kind”: “sqlite”,
        “credentials”: {
          “database”: “db.sqlite”
        }
      }
    }
  }
}

Service

Create srv/test-service.cds:

using { com.test as db } from ‘../db/schema’;
service TestService @(requires: ‘authenticated-user’) {
 entity Data @(restrict: [
  { grant: ‘*’,  to: ‘admin’ },
  { grant: ‘READ’, to: ‘authenticated-user’ }
 ]) as projection on db.Data {
  ID,
  x,
  y
 };
}

Users

Create .cdsrc.json and add some test users:

{
  “requires”: {
    “auth”: {
      “kind”: “mocked”,
      “users”: {
        “alice”: {
          “password”: “123”,
          “roles”: [”admin”]
        },
        “bob”: {
          “password”: “123”,
          “roles”: [”authenticated-user”]
        }
      }
    }
  }
}

Deploy

You can now deploy and run the application as follows:

cds deploy
cds watch

Testing Basic Service-Level Authentication

You can now test reading as admin and authenticated-user as follows:

curl -u “alice:123” “http://localhost:4004/odata/v4/test/Data”
curl -u “bob:123” “http://localhost:4004/odata/v4/test/Data”

Both should work. However, updating or creating an entity as anything other than admin will not work, for example:

#!/bin/bash
curl -u “bob:123” -X POST http://localhost:4004/odata/v4/test/Data\
     -H “Content-Type: application/json” \
     -d ‘{
  “x”:’$RANDOM’,
  “y”:’$RANDOM’
         }’

This will result in the following error response:

{
  “error”: {
    “message”: “Forbidden”,
    “code”: “403”,
    “@Common.numericSeverity”: 4
  }
}

For reading, notice that the entity will return both the x and y fields. You can deal with leaving out certain fields in different ways.

Separate Entities per Role

The declarative approach can be used to expose two different projections of the same underlying database entity, each restricted to a different role:

service TestService @(requires: ‘authenticated-user’) {
entity DataAdmin @(restrict: [
    { grant: ‘*’, to: ‘admin’ }
  ]) as projection on db.Data {
    ID, x, y
  };
  entity Data @(restrict: [
    { grant: ‘READ’, to: ‘authenticated-user’ }
  ]) as projection on db.Data {
    ID, x
  };
}

CAP will enforce access at the framework level automatically for you. The downside is that you now have two OData endpoints (/Data and /DataAdmin), which means your frontend or API clients need to know which one to call based on the logged-in user’s role.

This is the cleaner and more auditable approach, and is recommended when separate endpoints are fine.

Handler-Based Field Stripping

If you want a single /Data endpoint and hide fields for different roles, you can handle that part the handler code.

The service definition is back to the way it was before:

service TestService @(requires: ‘authenticated-user’) {
  entity Data @(restrict: [
    { grant: ‘*’,    to: ‘admin’ },
    { grant: ‘READ’, to: ‘authenticated-user’ }
  ]) as projection on db.Data {
    ID, x, y
  };
}

Implement srv/test-service.js as follows

const cds = require(’@sap/cds’);
const LOG = cds.log(”test”);
module.exports = async function() {
 const { Data } = this.entities;
 const { AdminData } = this.entities;
this.before(’READ’, ‘Data’, (req) => {
  if (!req.user.is(’admin’)) {
    const cols = req.query.SELECT.columns

    if (cols && cols.length === 1 && cols[0] === ‘*’) {
      req.query.SELECT.columns = [
        { ref: [’ID’] },
        { ref: [’x’] }
      ]
    } else {
      req.query.SELECT.columns = cols.filter(c => c.ref?.[0] !== ‘y’)
    }
  }
})
}

By intercepting in the before hook, y is excluded from the database query entirely.

Here the request user is checked, if it is not admin , for scenarios where all the fields are selected the y column is removed, and for scenarios where specific fields are selected, this is also done.

References

Comments

Popular Posts