Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Many-to-many dependency condition check #85

Open
anotheri opened this issue Oct 4, 2024 · 2 comments
Open

Many-to-many dependency condition check #85

anotheri opened this issue Oct 4, 2024 · 2 comments

Comments

@anotheri
Copy link

anotheri commented Oct 4, 2024

I use Prisma and I have a problem to make a consistent solution for the "is any related item" check with both prisma-plan-adaptor and cerbos.checkResources implementations.

So I have N:M relations between User and Role models.

And policy like this:

# yaml-language-server: $schema=https://api.cerbos.dev/v0.38.1/cerbos/policy/v1/Policy.schema.json
---
apiVersion: api.cerbos.dev/v1
disabled: false

resourcePolicy:
  version: default
  resource: role
  variables:
    local:
      hasUsers1: ({} in R.attr.users)
      # hasUsers2: size(R.attr.users) > 0
      # hasUsers3: R.attr._count.users > 0

  rules:
    # admin can read the role(s)
    - actions:
        - read
      effect: EFFECT_ALLOW
      roles:
        - admin

    # admin can delete the role
    - actions:
        - delete
      effect: EFFECT_ALLOW
      roles:
        - admin

    #  but admin can't delete the role which has users
    - actions:
        - delete
      effect: EFFECT_DENY
      roles:
        - admin
      condition:
        match:
          expr: V.hasUsers1

Option 1

hasUsers1: ({} in R.attr.users) it works as expected with Prisma plan adapter and returns KIND_CONDITIONAL plan with {"NOT":{"users":{"some":{}}}} filters, but when i check the resource for delete action permissions, it returns EFFECT_ALLOW. I populate and pass the list of users into the checkResources request (ideally, i'd like to avoid this population if possible):

const { results } = await cerbos.checkResources({
  resources: [
    {
      "actions": [
        "read",
        "delete"
      ],
      "resource": {
        "kind": "role",
        "id": "1",
        "attr": {
          "id": 1,
          "name": "user",
          "users": [
            {
              "id": 1
            }
          ]
        }
      }
    }
  ]
});

and i'm get back results as:

{
  "resource": {
    "id": "1",
    "kind": "role",
    "policyVersion": "",
    "scope": ""
  },
  "actions": {
    "delete": "EFFECT_ALLOW", // <------- but i expect to have EFFECT_DENY here
    "read": "EFFECT_ALLOW"
  },
  "validationErrors": [],
  "outputs": []
}

Option 2

hasUsers2: size(R.attr.users) > 0 - i've tried to use it as alternative solution and it works as expected with cerbos.checkResources and the same payload as I mentioned above but it seems that Prisma plan adapter doesn't support size method and it throws the error with the following error stack:

exception: Error: Unexpected variable [object Object],[object Object]
    at mapOperand (/usr/src/app/node_modules/@cerbos/orm-prisma/lib/cjs/index.js:123:15)
    at /usr/src/app/node_modules/@cerbos/orm-prisma/lib/cjs/index.js:114:42
    at Array.map (<anonymous>)
    at mapOperand (/usr/src/app/node_modules/@cerbos/orm-prisma/lib/cjs/index.js:114:31)
    at queryPlanToPrisma (/usr/src/app/node_modules/@cerbos/orm-prisma/lib/cjs/index.js:40:26)
    at AccessControlService.getQueryPlan (/usr/src/app/src/access-control/access-control.service.ts:241:37)
    at processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async CerbosInterceptor.intercept (/usr/src/app/src/access-control/interceptors/cerbos.interceptor.ts:144:18)

And the query plan i pass into queryPlanToPrisma looks like this:

"kind": "KIND_CONDITIONAL",
"condition": {
  "operator": "not",
  "operands": [
    {
      "operator": "gt",
      "operands": [
        {
          "operator": "size",
          "operands": [
            {
              "name": "request.resource.attr.users"
            }
          ]
        },
        {
          "value": 0
        }
      ]
    }
  ]
}

Option 3

hasUsers3: R.attr._count.users > 0 i've tried it as another approach (instead of actual population of the users, just to count the related users) and check this number, in this case it works as expected till the DB request. Prisma throws the error because Role model has no _count field, which exists in plan-adapter filters: KIND_CONDITIONAL {"NOT":{"_count":{"users":{"gt":0}}}}.

The error says:


Invalid `this.prisma.role.findUnique()` invocation in
/usr/src/app/src/roles/roles.service.ts:249:46

  246   });
  247 }
  248 
→ 249 const isAllowed = await this.prisma.role.findUnique({
        where: {
          id: 1,
          AND: {
            NOT: {
              _count: {
              ~~~~~~
                users: {
                  gt: 0
                }
              },
      ?       AND?: RoleWhereInput | RoleWhereInput[],
      ?       OR?: RoleWhereInput[],
      ?       NOT?: RoleWhereInput | RoleWhereInput[],
      ?       id?: IntFilter | Int,
      ?       name?: StringFilter | String,
      ?       users?: UserListRelationFilter
            }
          }
        }
      })

Unknown argument `_count`. Available options are marked with ?.
    at In (/usr/src/app/node_modules/@prisma/client/runtime/library.js:114:7526)
    at Ln.handleRequestError (/usr/src/app/node_modules/@prisma/client/runtime/library.js:121:7396)
    at Ln.handleAndLogRequestError (/usr/src/app/node_modules/@prisma/client/runtime/library.js:121:7061)
    at Ln.request (/usr/src/app/node_modules/@prisma/client/runtime/library.js:121:6745)
    at async l (/usr/src/app/node_modules/@prisma/client/runtime/library.js:130:9633)
    at async RolesService.checkAccessById (/usr/src/app/src/roles/roles.service.ts:249:23)
    at async RolesService.findOne (/usr/src/app/src/roles/roles.service.ts:108:5) {
  clientVersion: '5.19.0'
}

Solution?

I've tried to combine the options, like this hasUsers1or3: ({} in R.attr.users) || (R.attr._count.users > 0) which kind of makes sense to me, but i didn't find a way to say queryPlanToPrisma via fieldMapper/relationMapper that _count condition should be ignorred in this case.
So I'm looking for advice on how to make it properly?

@alexolivier
Copy link
Contributor

Hey! Thanks for the detailed issue. I want to setup a test case for this and then work from there - could you share the relevant parts of your Prisma schema (or a representative example)?

@anotheri
Copy link
Author

anotheri commented Oct 4, 2024

@alexolivier sure,

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"

  // https://github.com/prisma/prisma-client-js/issues/616#issuecomment-616107821
  binaryTargets = ["native", "darwin", "debian-openssl-3.0.x", "linux-musl", "linux-musl-openssl-3.0.x"]

  previewFeatures = ["tracing"]
}

model User {
  id             Int            @id @default(autoincrement()) @db.UnsignedInt
  email          String         @unique @db.VarChar(50)
  passwordHash   String         @map("password_hash") @db.VarChar(255)

  // relation to many Roles
  roles          Role[]

  @@map("users")
}

model Role {
  id          Int      @id @default(autoincrement()) @map("ur_id") @db.UnsignedInt
  name        String   @unique @map("ur_name") @db.VarChar(50)

  // relation to many Users
  users       User[]

  @@map("roles")
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants