Casl: How to deal with conditions on ref field (mongoose)

Created on 30 Aug 2020  路  4Comments  路  Source: stalniy/casl

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) ?

question

All 4 comments

Hello,

The question is quite interesting. But this is a question not a bug and not a feature.

Please, move it to stackoverflow - https://stackoverflow.com/questions/tagged/casl, so others can find my answer on it

Close as the issue doesn鈥檛 follow suggested templates

Ok thanks for your answer and sorry for the bad template.

I follow your advice and post a question on stackoverflow https://stackoverflow.com/questions/63660271/how-to-deal-with-conditions-on-ref-field-mongoose-with-casl

Ok, I鈥檒l 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

Was this page helpful?
0 / 5 - 0 ratings