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.

3 Likes

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

1 Like