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

How to deal with conditions on ref field (mongoose) #383

Closed
SebUndefined opened this issue Aug 30, 2020 · 6 comments
Closed

How to deal with conditions on ref field (mongoose) #383

SebUndefined opened this issue Aug 30, 2020 · 6 comments
Labels

Comments

@SebUndefined
Copy link

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

@stalniy
Copy link
Owner

stalniy commented Aug 30, 2020

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

@stalniy
Copy link
Owner

stalniy commented Aug 30, 2020

Close as the issue doesn’t follow suggested templates

@SebUndefined
Copy link
Author

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

@stalniy
Copy link
Owner

stalniy commented 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: #220

@gterras
Copy link

gterras commented Aug 14, 2022

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

@SebUndefined
Copy link
Author

Here is my post from stackoverflow

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 {
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 {
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 {
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

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 ?

I don't know if it can help, but if I change the permissions to :

"manage" and "all" (without condition)
"read" and "User" (without condition)

It works.

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

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

No branches or pull requests

3 participants