Getting the right permitted fields of conditions - casl

Currently, I'm building an app with with following similar logic:
...
const user = {
isAdmin: true,
company: '5faa6a847b42bf47b8f785a1',
projects: ['5faa6a847b42bf47b8f785a2']
}
function defineAbilityForUser(user) {
return defineAbility((can) => {
if (user.isAdmin) {
can('create', 'ProjectTime', {
company: user.company,
}
);
}
can(
'create',
'ProjectTime',
["company", "project", "user", "start", "end"],
{
company: user.company,
project: {
$in: user.projects
}
}
);
});
}
const userAbility = defineAbilityForUser(user); //
console.log( permittedFieldsOf(userAbility, 'create', 'ProjectTime') );
// console output: ['company', 'project', 'user', 'start', 'end']
Basically an admin should be allowed to create a project time with no field restrictions.
And a none admin user should only be allowed to set the specified fields for projects to which he belongs.
The problem is that I would expect to get [] as output because an admin should be allowed to set all fields for a project time.
The only solution I found was to set all fields on the admin user condition. But this requires a lot of migration work later when new fields are added to the project time model. (also wrapping the second condition in an else-block is not possible in my case)
Is there any other better way to do this? Or maybe, would it be better if the permittedFieldsOf-function would prioritize the condition with no field restrictions?

There is actually no way for casl to know what means all fields in context of your models. It knows almost nothing about their shapes and relies on conditions you provide it to check that objects later. So, it does not have full information.
What you need to do is to pass the 4th argument to override fieldsFrom callback. Check the api docs and reference implementation in #casl/mongoose
In casl v5, that parameter is mandatory. So, this confusion will disappear very soon

Related

AWS Amplify AppSync Subscription: data returning null

I was working on my Amplify App and I had subscriptions working fine with this:
graphql:
type Item #model(subscriptions: null)
#auth(rules: [
{allow: owner},
{allow: groups, groups: ["Admin"], operations: [create, update, read, delete] }
]) {
id: ID!
name: String
files: String
}
type Subscription {
itemUpdated(id: ID): Item #aws_subscribe(mutations: ["updateItem"])
}
js:
const handleSubscription = (data) => {
if (data.value.data.itemUpdated) {
setItemObj(data.value.data.itemUpdated);
}
};
useEffect(() => {
const subscription = API.graphql(
graphqlOperation(subscriptions.itemUpdated, {
id,
}),
).subscribe({
next: handleSubscription,
});
return () => subscription.unsubscribe();
}, []);
In the handleSubscription method, when the app made a mutation call to the Item, the return data (data.value.data.itemUpdated) would have the correct data.
Now, for reasons I am obviously unclear about, I can still see the subscription event fire when a mutation occurs, but the return data (data.value.data.itemUpdated) is consistently null.
I have tried to remove the {allow: owner} rule from the graphql schema's auth field as This Question suggests - which did not work (aside: I am still curious as to why that would work in the first place, but I do not have enough rep to comment).
While writing this, my thoughts were that I am going to try to create a new Item without the {allow: owner} rule and try again, if that works I will report back, but my question will pivot to asking why and asking then how can I ensure Items are private to the owner still? Lastly, I am almost positive I had the {allow: owner} rule in there when it was working too, but I could be mistaken.
I have also tried:
tested with updating different Item fields
let amplify cli rebuild my graphql js files
changed code around, i.e
removed the return () => subscription.unsubscribe();
made the input more specific API.graphql(graphqlOperation(subscriptions.itemUpdated, {input: { id: id },}) (which I am sure does not matter, but I wanted to try.)
I am just not sure what is going on here. This all seems so simple and it must be something dumb I am missing...I know I will figure it out eventually, but I wanted to tap anyone here in case.
Versions:
"aws-amplify": "^3.0.24"
"#aws-amplify/ui-react": "^0.2.15"
"react": "^16.13.1"
amplify-cli: 4.29.0
Please let me know if I left any important information out. Thanks in advance for any help.
Ok.. just a dumb thing like I thought. My bad for wasting anyone's time!
API.graphql(
graphqlOperation(subscriptions.itemUpdated, {
id: Id,
}),
).subscribe({
next: handleSubscription,
});
it was the id: Id, parameter. I had the Id var before named as id and js allows for shorting {name: name} to { name } - I must have changed the id var and went right to {input: { id: Id },} which is the incorrect syntax for subscriptions.
Real bonehead move and I am appropriately embarrassed. Good lesson in bad naming even during testing.

Best way to make ReactiveAggregate reactive when data changes on a user

I am currently using ReactiveAggregate to find a subset of Product data, like this:
ReactiveAggregate(this, Products, [
{ $match: {}},
{ $project: {
title: true,
image: true,
variants: {
$filter: {
input: "$variants",
as: "variant",
cond: {
$setIsSubset: [['$$variant.id'], user.variantFollowing]
}
}
}
}}
], { clientCollection: 'aggregateVariants' }
As you can see, a variant is returned if user.variantFollowing matches. When a user 'follows' a product, the ID is added to their object. However, if I understand correctly, this is not triggering ReactiveAggregate to get the new subset when this happens. Only on a full page refresh do I get the correct (latest) data.
Is this the correct way to approach this?
I could store the user's ID as part of the Product object, but the way this would be stored would be nested two places, and I think I would need the Mongo 3.5 updates to then be able to accurately update this. So i'm looking for how to do this in Meteor 1.5+ / Mongo 3.2.12
So, I've been able to get there by adding autorun to the subscription of the aggregate collection, like this:
Template.followedProducts.onCreated(function() {
Meteor.subscribe('products');
this.autorun(() => {
Meteor.subscribe('productsFollowed');
});
... rest of function
For context, productsFollowed is the subscription to retrieve aggregateVariants from the original question.
Thanks to robfallows in this post: https://forums.meteor.com/t/when-and-how-to-use-this-autorun/26075/6

Meteor Framework Subscribe/Publish according to document variables

I have a game built on Meteor framework. One game document is something like this:
{
...
participants : [
{
"name":"a",
"character":"fighter",
"weapon" : "sword"
},
{
"name":"b",
"character":"wizard",
"weapon" : "book"
},
...
],
...
}
I want Fighter character not to see the character of the "b" user. (and b character not to see the a's) There are about 10 fields like character and weapon and their value can change during the game so as the restrictions.
Right now I am using Session variables not to display that information. However, it is not a very safe idea. How can I subscribe/publish documents according to the values based on characters?
There are 2 possible solutions that come to mind:
1. Publishing all combinations for different field values and subscribing according to the current state of the user. However, I am using Iron Router's waitOn feature to load subscriptions before rendering the page. So I am not very confident that I can change subscriptions during the game. Also because it is a time-sensitive game, I guess changing subscriptions would take time during the game and corrupt the game pleasure.
My problem right now is the user typing
Collection.find({})
to the console and see fields of other users. If I change my collection name into something difficult to find, can somebody discover the collection name? I could not find a command to find collections on the client side.
The way this is usually solved in Meteor is by using two publications. If your game state is represented by a single document you may have problem implementing this easily, so for the sake of an example I will temporarily assume that you have a Participants collection in which you're storing the corresponding data.
So anyway, you should have one subscription with data available to all the players, e.g.
Meteor.publish('players', function (gameId) {
return Participants.find({ gameId: gameId }, { fields: {
// exclude the "character" field from the result
character: 0
}});
});
and another subscription for private player data:
Meteor.publish('myPrivateData', function (gameId) {
// NOTE: not excluding anything, because we are only
// publishing a single document here, whose owner
// is the current user ...
return Participants.find({
userId: this.userId,
gameId: gameId,
});
});
Now, on the client side, the only thing you need to do is subscribe to both datasets, so:
Meteor.subscribe('players', myGameId);
Meteor.subscribe('myPrivateData', myGameId);
Meteor will be clever enough to merge the incoming data into a single Participants collection, in which other players' documents will not contain the character field.
EDIT
If your fields visibility is going to change dynamically I suggest the following approach:
put all the restricted properties in a separated collection that tracks exactly who can view which field
on client side use observe to integrate that collection into your local player representation for easier access to the data
Data model
For example, the collection may look like this:
PlayerProperties = new Mongo.Collection('playerProperties');
/* schema:
userId : String
gameId : String
key : String
value : *
whoCanSee : [String]
*/
Publishing data
First you will need to expose own properties to each player
Meteor.publish('myProperties', function (gameId) {
return PlayerProperties.find({
userId: this.userId,
gameId: gameId
});
});
then the other players properties:
Meteor.publish('otherPlayersProperties', function (gameId) {
if (!this.userId) return [];
return PlayerProperties.find({
gameId: gameId,
whoCanSee: this.userId,
});
});
Now the only thing you need to do during the game is to make sure you add corresponding userId to the whoCanSee array as soon as the user gets ability to see that property.
Improvements
In order to keep your data in order I suggest having a client-side-only collection, e.g. IntegratedPlayerData, which you can use to arrange the player properties into some manageable structure:
var IntegratedPlayerData = new Mongo.Collection(null);
var cache = {};
PlayerProperties.find().observe({
added: function (doc) {
IntegratedPlayerData.upsert({ _id : doc.userId }, {
$set: _.object([ doc.key ], [ doc.value ])
});
},
changed: function (doc) {
IntegratedPlayerData.update({ _id : doc.userId }, {
$set: _.object([ doc.key ], [ doc.value ])
});
},
removed: function (doc) {
IntegratedPlayerData.update({ _id : doc.userId }, {
$unset: _.object([ doc.key ], [ true ])
});
}
});
This data "integration" is only a draft and can be refined in many different ways. It could potentially be done on server-side with a custom publish method.

Meteor filtering data when asserting fields from queries

Why does running this code
var userId = Meteor.userId();
var user = Users.findOne(userId, { fields: { earnings: 1 } });
Return
{ _id: 'Co5bMySeaqySgDP6h', earnings: { period: 0.6, total: 52.5 } }
Instead of returning all the fields on the user, including the earnings (custom field)
Also, is there a way to make user queries automatically return custom specified fields, so I dont have to manually specify it each time I need it?
Much appreciated
The reason that you only get the specified field (plus the id) is given in the docs:
To include only specific fields in the result documents, use 1 as the value. The _id field is still included in the result.
If instead you just call Meteor.users.findOne(userId) it will return all of the available fields. If this is called on the server, that will be the entire document, but if you use it on the client, it will only return the fields that have been published from the server, which by default is just the username and the emails and profile fields. Again, per the docs:
On the client, this will be the subset of the fields in the document that are published from the server (other fields won't be available on the client). By default the server publishes username, emails, and profile (writable by user). See Meteor.users for more on the fields used in user documents.
This means that if you have added a new field to you user docs, you need to explicitly publish it for it to be available on the client (assuming autopublish has been removed). Note that it's fine to do this using the previously discussed fields specifier as the other required details (username, profile) will not be overwritten by another publish function unless you try to publish the same top-level field again.
Meteor.publish('earnings', function() {
return Meteor.users.find(this.userId, { fields: { earnings: 1 } });
};
(Publish functions expect you to return a cursor rather than an array, so you need to use find rather than findOne even if there will only be one result).
Finally, it's easy to add your own methods to a collection to make finding stuff you want more concise.
Meteor.users.findSimple = function(selector, options) {
options = options || {};
options.fields = options.fields || {};
options.fields.earnings = 1;
\\ same thing for any other fields you want to limit this find to;
return this.find(selector, options);
};

How to honour user privacy settings in Meteor

I have a set of users defined like this:
Accounts.createUser({
username:'Simon',
email:'simon#email.com',
profile:{
firstname:'Simon',
lastname:'Surname',
location:'Home Address',
privacy: {
location:0,
emails:0 } //Location and emails are private and should not be disclosed
}
});
My question is how can I publish this user's record for other users to view, taking into account the profile privacy settings. In this example, I have set the privacy for location and emails to zero with the intention that this information is not published for this user.
I would like to publish it using the standard method:
Meteor.publish("usersWithPublicEmails", function () {
return Meteor.users.find();
});
But I cannot see a way to specify the selector or fields in such a way that only public information will be published.
I have tried adding additional publications of the form:
Meteor.publish("allUsers", function () {
return Meteor.users.find( {}, {fields:{username:1}} );
});
Meteor.publish("usersWithPublicEmails", function () {
return Meteor.users.find( {"profile.privacy.emails":1}, {fields:{username:1, emails:1}} );
});
but the selector does not seem to be returning the emails as I expected. I am looking for optimal way to do this from a performance point of view.
Mongodb is not a relational database so whenever I want to join or query based on metadata I remember I have to do things differently. In your case I would make a separate Collection for user privacy if I wanted to query on user privacy. In addition, if I cared about performance I probably would never want "all of x", I would just want enough to show the user, thus paginate. With these two ideas in mind you can easily get what you want: query based on privacy settings and performance.
Privacy = new Mongo.Collection("privacy");
Whenever we want to add privacy to an account:
Privacy.insert({
emails: 1,
userId: account._id,
});
Then later, one page at a time, showing ten results each page, tracking with currentPage:
Meteor.publish("usersWithPublicEmails function (currentPage) {
var results = []
var privacyResults = Privacy.find({"emails":1}, {skip: currentPage,
limit: 10});
var result;
while (privacyResults.hasNext() ) {
result = privacyResult.next();
results.append(Meteor.users.find({_id: result.userId});
}
return result;
});
I didn't test this code, it may have errors, but it should give you the general idea. The drawback here is that you have to keep privacy and users in sync, but these are the kinds of problems you run into when you're not using a relational database.
Mongodb has a way to do this kind of reference lookup with less code, but it still happens on demand and I prefer the flexibility of doing it myself. If you're interested take a look at Database references
That's because you have a typo in your publish function's fields object, instead of email you've typed emails
So the correct function would be:
Meteor.publish("usersWithPublicEmails", function () {
return Meteor.users.find( {"profile.privacy.emails":1}, {fields:{username:1, email:1}} );
});
Furthermore, you're already publishing all usernames in your allUsers publication, therefore, in order to add the missing data for relevant public users, you'll just need this:
Meteor.publish("usersWithPublicEmails", function () {
return Meteor.users.find( {"profile.privacy.emails":1}, {fields:{email:1}} );
});
and Meteor will automatically merge those records for you.
A simple solution in the end. I had missed the additional subscription in my router:
Router.route('/users', {
name: 'userList',
waitOn: function(){
return Meteor.subscribe('allUsers') &&
Meteor.subscribe('usersWithPublicEmails');
},
data: function(){
return Meteor.users.find();
}
});
A basic mistake:-(

Resources