Is there another term for covering permissions that based on “ownership” on top of roles?

This question comes from

Charlie_Today at 2:33 AM

The cookbook covers RBAC, Is there another term for covering permissions that based on “ownership” on top of roles? I.e. Bob has the role of “writer” but can only edit blog posts that he created?

I figure I could have a go at making something like the hoc but does these kinds of ownership checks instead of only roles, but don’t want to re-invent the wheel if something like this already exists for redwood.

First, let me point to an existing topic that can also make for some context:

Thanks for reading the RBAC Cookbook – and you are correct; the roles do not contain any info or rules to answer or enforce: “Bob has the role of “writer” but can only edit blog posts that he created?”

In the RedwoodJS docs for Auth0, I mention:

Role-based access control (RBAC) refers to the idea of assigning permissions to users based on their role within an organization. It provides fine-grained control and offers a simple, manageable approach to access management that is less prone to error than assigning permissions to users individually.

Essentially, a role is a collection of permissions that you can apply to users. A role might be “admin”, “editor” or “publisher”. This differs from permissions an example of which might be “publish:blog”.

These “permissions” or “scopes” further define what a publisher (a role) may publish (blogs, but not alerts).

But that doesn’t yet get the to the question you asked: “Bob has the role of “writer” but can only edit blog posts that he created?”

I haven’t written a Rails app in some years, but when I did and had this problem to solve, I used a gem called CanCan :dancer:

It’s important to note that CanCan itself says:

CanCan is decoupled from how you implement roles in the User model

That’s another job – part of authentication and other rules. Here it is just trying to deal with permissions and sets of permissions based on rules (like role or other info).

CanCan dubbed an Ability as the rule “where all user permissions are defined.”

Here’s an example:

class Ability
  include CanCan::Ability

  def initialize(user)
    user ||= User.new # guest user (not logged in)
    if user.admin?
      can :manage, :all
    else
      can :read, :all
    end
  end
end

You can see it does a few things:

  • are they authenticated?
  • do they have the admin role?
  • if, so then “can” manage everything (ie all crud)
  • no, well then just read

The in code you’d check if user can(:update, something).

OK, closer … but still not the answer.

You can see that it’s not a huge leap to create a rule or ability that says:

  • I am Bob
  • I am a writer
  • I can read and add blog posts
  • But, I cannot delete them
  • and I can only edit/update ones where I can am the author (aka post.authorId = currentUser.id)

And that’s the rule you need to check and enforce.

Enforcing would be at the service level, perhaps going from:

export const updatePost = ({ id, input }) => {
  requireAuth({ roles: UPDATE_POST_ROLES })

  return db.post.update({
    data: {
      ...input,
      editorId: context.currentUser.sub,
      publisherId: context.currentUser.sub,
      updatedAt: new Date(),
    },
    where: { id },
  })
}

to

export const updatePost = ({ id, input }) => {
  requireAuth({ roles: UPDATE_POST_ROLES })

  return db.post.update({
    data: {
      ...input,
      editorId: context.currentUser.sub,
      publisherId: context.currentUser.sub,
      updatedAt: new Date(),
    },
    where: { id, authorId: context.currentUser.sub },
  })
}

Subtle difference is that when fetching the post that is trying to be updated, check that the post has the user’s authorId.

or maybe

export const updatePost = ({ id, input }) => {
  requireAuth({ roles: UPDATE_POST_ROLES })

  const post = db.post.update({
    data: {
      ...input,
      editorId: context.currentUser.sub,
      publisherId: context.currentUser.sub,
      updatedAt: new Date(),
    },
    where: { id, authorId: context.currentUser.sub },
  })

  if (post === undefined) {
    throw new ForbiddenError("You don't have access to do that.")
  }
  
  return post
}

or even

export const updatePost = ({ id, input }) => {
  requireAuth({ roles: UPDATE_POST_ROLES })

  const post = db.post.findOne({
    where: { id, authorId: context.currentUser.sub },
  })

  if (post === undefined) {
    throw new ForbiddenError("You don't have access to do that.")
  }

  return db.post.update({
    data: {
      ...input,
      editorId: context.currentUser.sub,
      publisherId: context.currentUser.sub,
      updatedAt: new Date(),
    },
    where: { id },
  })
}

and at that point you may want to bake in:

requireAuth({ roles: UPDATE_POST_ROLES })

  const post = db.post.findOne({
    where: { id, authorId: context.currentUser.sub },
  })

  if (post === undefined) {
    throw new ForbiddenError("You don't have access to do that.")
  }

into its own canEditPost() method as some ability.

And then you’d of course have to do some of this rule checking on the web side, too. Always do both.

TLDR;

There’s Authentication and Authorization. Authorization can have roles and permissions. Currently RedwoodJS provides the foundation to extract role info from auth providers (or roll your own :smile: ) but does not yet have a pattern for the permissions or scopes because well those rules are very app specific. But, I imagine in near future it will have some opinions and patterns to choose from.

5 Likes

Thanks for such a detailed response. I’ll see how I go :crossed_fingers:

1 Like

What I’ve done to add some “ownership” permissions is, first off I’ve made a requireOwnership util to match requireAuth. The API is passing in an id of something a userId, partId (all optional but needs one), and it checks that the current user “owns” that resource. Here’s an example (https://github.com/Irev-Dev/cadhub/blob/main/api/src/services/parts/parts.js)

export const updatePart = async ({ id, input }) => {
  requireAuth()
  await requireOwnership({ partId: id })
  if (input.title) {
    input.title = enforceAlphaNumeric(input.title)
  }
  return db.part.update({
    data: foreignKeyReplacement(input),
    where: { id },
  })
}

One important thing to point out is that requireOwnership needs to be awaited since it needs to do some db reads to check township.
requireOwnership (https://github.com/Irev-Dev/cadhub/blob/main/api/src/lib/owner.js)

export const requireOwnership = async ({ userId, userName, partId } = {}) => {
  if (!userId && !userName && !partId) {
    throw new ForbiddenError("You don't have access to do that.")
  }
  if (context.currentUser.roles?.includes('admin')) {
    return
  }
  const netlifyUserId = context.currentUser?.sub
  if (userId && userId !== netlifyUserId) {
    throw new ForbiddenError("You don't own this resource.")
  }
  if (userName) {
    const user = await db.user.findOne({
      where: { userName },
    })

    if (!user || user.id !== netlifyUserId) {
      throw new ForbiddenError("You don't own this resource.")
    }
  }
  if (partId) {
    const user = await db.part
      .findOne({
        where: { id: partId },
      })
      .user()
    if (!user || user.id !== netlifyUserId) {
      throw new ForbiddenError("You don't own this resource.")
    }
  }
}

In my app so far everything is public, so I only need to guard against editing something you don’t own, not viewing, so in all of my edit routes I have a redirecting useEffect similar to this, that redirects to to the non-edit page. (https://github.com/Irev-Dev/cadhub/blob/main/web/src/components/PartProfile/PartProfile.js)

const canEdit = currentUser?.sub === partOwner.id
  useEffect(() => {
    !canEdit &&
      navigate(
        routes.part({ userName: partOwner.userName, partTitle: part.title })
      )
  }, [currentUser, canEdit])

That’s says nothing about my db schema, but I think it goes without saying that parts are linked to a user etc.

3 Likes

@Irev-Dev I wonder how you might test a service that makes use of requireOwnership()?

Example:

things.js

export const updateThing = async ({ userId, thingId, input }) => {
  await requireOwnership({ userId, thingId })
  return db.thing.update({
    data: input,
    where: { thingId },
  })
}

thing.test.js

scenario('updates a thing', async (scenario) => {
    const original = await thing({
      thingId: scenario.thing.one.id,
      name: 'Test',
      userId: '1',
    })
    const result = await updateThing({
      userId: '1',
      thingId: original.id,
      input: { name: 'Test2' },
    })

    expect(result.name).toEqual('Test2')
  })

This results in a ForbiddenError because context.currentUser?.sub is undefined when running the test.

Hey @darryl-snow, here’s one way of mocking out current user

You just have to make sure you call it before your describe block.

jest.mock('@redwoodjs/api', () => {
  return {
    context: {
      currentUser: {
        activeSubscription: null,
        id: 'mocked-user-123',
        email: 'mockedemail@example.com',
        name: 'Mockity McMockface',
        teamMember: [
          {
            id: 'team-member-id-123',
            role: 'ADMIN',
            teamId: 'mocked-team-id',
            userId: 'mocked-user-123',
            team: {
              id: 'teamid',
              name: 'mocked team',
            },
          },
        ],
      },
    },
  }
})

Right now I’m doing one mock like this per test file, and one scenario to go along with it. To mock a user per test is a little more hassle because after you call jest.mock you will need to re-import the function you’re testing, otherwise the mocks won’t have been updated.

e.g. tapes.test.ts, tapes.scenario.ts is one set, with one mocked user limits.test.ts, limits.scenario.ts is another

1 Like

Got it, thank you!

1 Like

Hello - just an update on this. v0.37.x will include a fix where you can mock current user in your scenario/api tests using :drum:mockCurrentUser() - no imports necessary!

Alternatively you can also mock the entire context using setContext

1 Like

Is there more planned on this? I haven’t seen anything in the documentation regarding Permissions ‘best practices’ but several forum posts have detailed great ways to get it started.

Permissions or Attributes based Access Control (ABAC) is on there roadmap (which we acknowledge needs better communication).

One library being evaluated is CASL: CASL.js

Which can determine if someone has the ability “to do something”:

import { Ability, AbilityBuilder } from '@casl/ability';

// define abilities
const { can, cannot, rules } = new AbilityBuilder();

can('read', ['Post', 'Comment']);
can('manage', 'Post', { author: 'me' });
can('create', 'Comment');

// check abilities
const ability = new Ability(rules);

ability.can('read', 'Post') // true

Thanks for the reply. I suppose as I’m currently trying to implement permissions I’m stuck wondering if Redwood becomes opinionated about this, will my solution clash with CASL if that becomes the standard? Right now, I know of 3 different ways to do this and I’m not sure which route to pick:

or a library like CASL or Casbin, as you mentioned.

const sub = 'alice'; // the user that wants to access a resource.
const obj = 'data1'; // the resource that is going to be accessed.
const act = 'read'; // the operation that the user performs on the resource.

// Async:
const res = await enforcer.enforce(sub, obj, act);
// Sync:
// const res = enforcer.enforceSync(sub, obj, act);

if (res) {
  // permit alice to read data1
} else {
  // deny the request, show an error
}
const roles = await enforcer.getRolesForUser('alice');

The last permissions solution I came up with for a large app (Ruby on Ralis, naturally) felt amazingly easy to use. I made it available as a gem so anyone could add it into a Rails app: GitHub - workingnotworking/wnw-permissible: Adds roles and permissions to your Rails app

The interface looks something like this:

current_user.can_create_post?
current_user.can_view_admin?

This was handled by a little metaprogramming which kicked in whenever it saw a method call prefixed with can_. It would use the rest of the method name to look up a value in an array of permissions:

current_user.permissions # => ['create_post', 'view_admin']

If the name of the method was in that list then it was allowed, if not then it wasn’t. So the can_ version was really just syntactic sugar for:

current_user.permissions.includes?('view_admin')

And the permissions object was always assigned to current_user on each request, so you could count on it being available anywhere current_user was available. You could do this in Redwood by adding permissions to the getCurrentUser() function in api/src/lib/auth.js as this is what says what data is available in currentUser on the web side and context.currentUser on the api side.

These permissions were stored in the database in a permissions table, which had a foreign key to a roles table. So a user would have one or more roles which conferred on them a union of permissions. Permissions were only additive: you couldn’t have one role which allowed a permissions and then another role which removed it.

When you have permissions it’s bad practice to check for a user’s role because what a user can do is dictated by their permissions now, not their role. You don’t care about what fancy titles a user has (their role), you care what they can and can’t do (their permissions).

There was also the concept of more complex permissions like:

current_user.can_message_user?(other_user)
current_user.can_edit_post?(post)

These methods were create in a standalone class and then mixed into the User model (prefer composition over inheritance!). This way, all permission-like info was accessed through a single interface with the same “feel,” asking the current_user about its capabilities.

This permission scheme also contained the concept of limits, where a user was allowed to do something a certain number of times a month (we defaulted to per-month since that’s how billing was handled, but you could easily include the span of time along with thing being limited). There was a role_limits table which stored how many times a certain role could perform an action, and you’d access it using something like:

current_user.limit(:searches) # => Infinity
current_user.limit(:new_posts) # => 10

In retrospect, these could have been handled with metaprogramming as well:

current_user.searches_limit
current_user.new_posts_limit

On the server side, when it came to picking records out of the database, we’d almost always start from the current_user so that you only pulled data the user was allowed to access. Rails’ ActiveRecord made this really easy. It’s not so simple using Prisma.

# posts the user created
posts = current_user.posts
# comments on posts the user has created
comments = current_user.comments 
# posts the user is allowed to edit
editable_posts = current_user.editable_posts

You can get the first level posts via Prisma, but you can’t drill down to other relationships like comments, or have custom selectors like editable_posts. Note that RedwoodRecord does let you do stuff like this, although you’d need to define the function and the logic for selecting the records, but once you do, you can access everything via the user itself (and you can have getCurrentUser() return RedwoodRecord instances).

Likewise when creating a record, you could create it through the user so they were automatically attached:

post = current_user.create_post(input)

There are going to be plenty of folks that will not like accessing permissions on the user itself. They’d rather have a permissions enforcer (see previous post) that tracks what can and can’t be done by a given user. But for me, I’d much rather ask the object itself about its capabilities than to bundle everything up and ask yet another object. I’ve already got the user, just tell me what you can do.

These kinds of permission models start to fall apart when you have client-side rendering: what’s to stop me from just editing my list of permissions in the browser on currentUser or enforcer and doing things I’m not supposed to do? This has to rely on a much more robust server-side enforcement of what a user can and can’t see and do. This means you can’t just ship down to the client all the data that any user might need, any only expose the information that their permission allows. Likewise on the server, you can’t ever trust that the data coming in is limited to the subset that a certain user is allowed to submit: the user could have edited the payload and sent in admin-only field data. Beware!