question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

How to deal with conditions on ref field (mongoose)

See original GitHub issue

After using Casl for some simple project, I am trying to implement something more complicated. I am trying to mix Roles with persisted permissions with JWT described on the website.

For this basic example, I try to give read action permissions to User subject but only on users entries that are part of an organization:

My User model

interface UserAttrs {
  email: string;
  firstName: string;
  lastName: string;
  password: string;
  role: RoleDoc;
  organization: OrganizationDoc;
}
interface UserModel extends mongoose.Model<UserDoc> {
  build(attrs: UserAttrs): UserDoc;
}
interface UserDoc extends mongoose.Document {
  email: string;
  firstName: string;
  lastName: string;
  active: boolean;
  password: string;
  createdAt: Date;
  updatedAt: Date;
  role: RoleDoc;
  organization: OrganizationDoc;
}

const userSchema = new mongoose.Schema(
  {
    email: {
      type: String,
      required: true,
      unique: true,
      trim: true,
      match: [/.+\@.+\..+/, 'Please fill a valid email address'],
    },
    firstName: {
      type: String,
      required: true,
      trim: true,
    },
    lastName: {
      type: String,
      required: true,
      trim: true,
    },
    active: {
      type: Boolean,
      default: true,
      required: true,
    },
    password: {
      type: String,
      required: true,
    },
    createdAt: {
      type: Date,
      required: true,
      default: Date.now,
    },
    updatedAt: {
      type: Date,
      required: true,
      default: Date.now,
    },
    role: {
      type: mongoose.Schema.Types.ObjectId,
      required: true,
      ref: 'Role',
    },
    organization: {
      type: mongoose.Schema.Types.ObjectId,
      ref: 'Organization',
    },
  },
  {
    toJSON: {
      transform(_doc, ret) {
        ret.id = ret._id;
        delete ret._id;
        delete ret.password;
        delete ret.__v;
      },
    },
  }
);

userSchema.pre('save', async function (done) {
  if (this.isModified('password')) {
    const hashed = await Password.toHash(this.get('password'));
    this.set('password', hashed);
    this.set('updatedAt', Date.now);
  }
  done();
});

userSchema.statics.build = (attrs: UserAttrs) => {
  return new User(attrs);
};

const User = mongoose.model<UserDoc, UserModel>('User', userSchema);

export { User };

Organization Model

interface OrganizationAttrs {
  id: string;
  name: string;
}
interface OrganizationModel extends mongoose.Model<OrganizationDoc> {
  build(attrs: OrganizationAttrs): OrganizationDoc;
}

export interface OrganizationDoc extends mongoose.Document {
  name: string;
  version: number;
}

const organizationSchema = new mongoose.Schema(
  {
    name: {
      type: String,
      required: true,
      unique: true,
      trim: true,
    },
  },
  {
    toJSON: {
      transform(_doc, ret) {
        ret.id = ret._id;
        delete ret._id;
      },
    },
  }
);

organizationSchema.set('versionKey', 'version');
organizationSchema.plugin(updateIfCurrentPlugin);

organizationSchema.statics.findByEvent = (event: {
  id: string;
  version: number;
}) => {
  return Organization.findOne({
    _id: event.id,
    version: event.version - 1,
  });
};

organizationSchema.statics.build = (attrs: OrganizationAttrs) => {
  return new Organization({
    _id: attrs.id,
    name: attrs.name,
  });
};

const Organization = mongoose.model<OrganizationDoc, OrganizationModel>(
  'Organization',
  organizationSchema
);

export { Organization };

Role model

interface RoleAttrs {
  name: string;
  permissions: string;
  organization: OrganizationDoc;
}

interface RoleModel extends mongoose.Model<RoleDoc> {
  build(attrs: RoleAttrs): RoleDoc;
}

export interface RoleDoc extends mongoose.Document {
  name: string;
  permissions: string;
  organization: OrganizationDoc;
}

const roleSchema = new mongoose.Schema(
  {
    name: {
      type: String,
      required: true,
      unique: true,
      trim: true,
    },
    permissions: {
      type: String,
      required: true,
      trim: true,
    },
    organization: {
      type: mongoose.Schema.Types.ObjectId,
      required: true,
      ref: 'Organization',
    },
  },
  {
    toJSON: {
      transform(_doc, ret) {
        ret.id = ret._id;
        delete ret._id;
        delete ret.__v;
      },
    },
  }
);

roleSchema.pre('save', async function (done) {
  done();
});

roleSchema.statics.build = (attrs: RoleAttrs) => {
  return new Role(attrs);
};

const Role = mongoose.model<RoleDoc, RoleModel>('Role', roleSchema);

export { Role };

The permissions field from role is stored as string. As soon as a user logged in, I add the permissions to the JWT token

const existingUser = await User.findOne({ email, active: true })
      .populate('role')
      .populate('organization');

// Check if user is valid...

// userPermissions = [{ action: 'read', subject: 'User', conditions: { organization: '{{organization.id}}' }, }, ](as a string)
const userPermissions = Mustache.render(
      existingUser.role.permissions,
      existingUser
    );
    console.log(userPermissions); 
// result ==> [{"action":"read","subject":"User","conditions":{"organization":"5f4bc664e27664265cb033d7"}}]
    // Genereate json web token JWT
    const userJWT = jwt.sign(
      {
        id: existingUser.id,
        email: existingUser.email,
        organizationId: existingUser.organization.id,
        userRolePermissions: userPermissions,
      },
      process.env.JWT_KEY!
    );
`

Then in a middleware, I create the abilities similar to [here](https://casl.js.org/v4/en/cookbook/roles-with-persisted-permissions ) 

```typescript
const { id, email, organizationId, userRolePermissions } = jwt.verify(
        req.session.jwt,
        process.env.JWT_KEY!
      ) as Token;
const currentUser: UserPayload = {
        id: id,
        email: email,
        organizationId: organizationId,
        userRolePermissions: createAbility(JSON.parse(userRolePermissions)),
      };

The result of createAbility is

i {
        s: false,
        v: [Object: null prototype] {},
        p: [Object: null prototype] {},
        g: [Object: null prototype] {
          User: [Object: null prototype] {
            read: [Object: null prototype] {
              '0': t {
                t: [Function],
                i: undefined,
                action: 'read',
                subject: 'User',
                inverted: false,
                conditions: { organization: '5f4bc8d85dc07d269e4f303d' },
                reason: undefined,
                fields: undefined
              }
            }
          }
        },
        j: [
          {
            action: 'read',
            subject: 'User',
            conditions: { organization: '5f4bc8d85dc07d269e4f303d' }
          }
        ],
        O: {
          conditionsMatcher: [Function],
          fieldMatcher: [Function: j],
          resolveAction: [Function: u]
        }
      }

if I execute

const organizationId = req.params.organizationId as Object;

const users = await User.find({ organization: organizationId });

// req.currentUser contain the user with the userRolePermissions above
ForbiddenError.from(req.currentUser!.userRolePermissions).throwUnlessCan(
        'read',
        subject('User', users)
      );

I get message: ‘Cannot execute “read” on “User”’. How ca we deal with ref fields ?

How casl works in case I populate the organization field on user (I will then have an object) ? Should I make two rules in permissions field in role (one if populated one if not) ?

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Comments:6 (3 by maintainers)

github_iconTop GitHub Comments

1reaction
gterrascommented, Aug 14, 2022

Hi the post was removed from stackoverflow, is it possible to get a summary here? Short answer is “not possible” right?

1reaction
stalniycommented, Aug 30, 2020

Ok, I’ll give a detailed answer a bit later. Meanwhile you can read my answer to the similar question in past: https://github.com/stalniy/casl/issues/220

Read more comments on GitHub >

github_iconTop Results From Across the Web

How to find in one document on the basis of ... - Stack Overflow
How to find in one document on the basis of condition in the second document based on ref _Id mongoose ... records =...
Read more >
Can a reference field in a Mongoose schema pertain to more ...
I want the type to be an ObjectId of some document; it's just that I want to be able to reference the id's...
Read more >
Mongoose v6.8.2: Query Population
Arrays of refs work the same way. Just call the populate method on the query and an array of documents will be returned...
Read more >
Populating with .match condition doesn't filter documents #6451
When you populate a query and use .match to filter the results, documents which violate the match condition...
Read more >
MongoDB Schema using Mongoose - Arun Rajeevan - Medium
As you can see, if you want more control over the attribute, use an object while defining the schema of an attribute. Example:...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found