Is there any way of using ACL in Parse Server to deny access to items for a specific user or role?
Say I have a social networking app where users post updates. I have a role called all_users which all registered users are added to. All updates are readable by this role, EXCEPT users that the author has blocked.
I can grant read and write access to users/roles, but removing both read and write access through the Parse Dashboard removes the entry completely.
Tips would be greatly appreciated.
I wasn't 100% sure the answer, so I did what I usually do, make a unit test to figure it out.
As it happens, I am working on a PR to create an 'all user role' that will improve on your current solution for that, especially if your social network takes off and you have many many users.
See the issue: https://github.com/parse-community/parse-server/issues/4107
You can track the current state of my solution (which currently works, but isn't ready to merge just yet) here: https://github.com/parse-community/parse-server/pull/4111
But all I'm working on is the 'all user role' case, not the 'deny a user' case you need.
What I did to test your question was extend the unit test from my pr to address your particular (interesting) use case:
it('should respect for read.', function (done) {
let role, user;
const userP = new Parse.User()
.set('username', 'userA')
.set('password', 'password')
.save()
.then((user) => Parse.User.logIn(user.get('username'), 'password'));
const roleP = new Parse.Role('aRole', new Parse.ACL())
.save();
Parse.Promise.when(userP, roleP)
.then((newUser, newrole) => {
user = newUser;
role = newrole;
const acl = new Parse.ACL();
acl.setRoleReadAccess(role, true);
return new Parse.Object('Foo')
.setACL(acl)
.save();
})
.then(() => new Parse.Query('Foo').first())
.then((obj) => {
expect(obj).not.toBeDefined();
return new Parse.Query(Parse.Role)
.equalTo('name', '_All_Role')
.first()
})
.then((allRole) => {
expect(allRole).toBeDefined();
const roles = role.relation('roles');
roles.add(allRole);
return role.save(null, { useMasterKey: true });
})
.then(() => new Parse.Query('Foo').first())
.then((obj) => {
expect(obj).toBeDefined();
const acl = obj.getACL();
acl.setReadAccess(user.id, false); // <--- this is what you want!!!
console.log(acl);
const valid = obj.setACL(acl);
expect(valid).toBe(true);
return obj.save();
})
.then(() => new Parse.Query('Foo').first())
.then((obj) => {
expect(obj).not.toBeDefined(); // <--- but this fails :(....
done();
})
.catch(done.fail);
});
As I suspected, the test failed. I'm not an expert on parse permission (though learning) but my current understanding is that in there is no concept of precedence, so once you have added the permission through your everyone group, there is no way to 'deny'. In other words, the current permission model is that permissions are added, not explicitly denied.
Your use case is compelling though so finding a way to accommodate your use case would be interesting. As is often the case though, either you're going to need to figure out how to add it in such a way that it would be acceptable for the general use cases too or recruit someone who can (not volunteering, my dance card is full :).
Related
I have this retarded amount of product in a collection on Shopify (over 50k products) and I would need to delete them all, is there a way I can automate that? All I can find on the internet is to use the "bulk edit tool" which is the most useless thing I've ever seen as it can only grab 50 products at a time.
I've tried automating a script to update the rows with the CSV export file, but it takes over 6 hours for 20K products to import. Plus, since there are hashtags in the title and handle, it apparently doesn't overwrite the products for some reason. So I just can't use the archive anymore...
Has anyone ran into this issue and found a solution?
Thank you!
When it comes to this kinds of tasks I usually write myself a quick dev console script that will do the job for me instead of relying on an app.
Here is a script that you can use in the dev console of your shopify admin page (just copy /paste):
let productsArray = [];
// Recursive function that will grab all products from a collection
const requestCollection = (collectionId, url = `https://${window.location.host}/admin/api/2020-10/collections/${collectionId}/products.json?limit=250`) => {
fetch(url).then(async res => {
const link = res.headers.get('link');
const data = await res.json();
productsArray = [...productsArray, ...data.products];
if(link && link.match(/<([^\s]+)>;\srel="next"/)){
const nextLink = link.match(/<([^\s]+)>;\srel="next"/)[1];
requestCollection(collectionId, nextLink)
} else {
initDelete(productsArray)
}
})
}
// Get CSRF token or the request will require password
const getCSRFToken = () => fetch('/admin/settings/files',{
headers: {
"x-requested-with": "XMLHttpRequest",
"x-shopify-web": 1,
"x-xhr-referer": `https://${window.location.host}/admin/settings/files`
}
}).then(res => res.text()).then(res => {
const parser = new DOMParser();
const doc = parser.parseFromString(res, 'text/html');
return doc.querySelector('meta[name="csrf-token"]').getAttribute('content')
})
// The function that will start the deleting process
const initDelete = async (products) => {
const csrfToken = await getCSRFToken();
products.forEach(item => {
fetch(`https://${window.location.host}/admin/api/2020-10/products/${item.id}.json`, {
method: "delete",
credentials: 'include',
headers: {
"x-csrf-token": csrfToken,
"x-requested-with": "XMLHttpRequest",
"x-shopify-web": 1,
"x-xhr-referer": `https://${window.location.host}/admin/settings/files`
}
})
})
}
And you start it by using requestCollection(ADD_YOUR_COLLECTION_ID_HERE).
To clarify the script, there are 3 main functions:
requestCollection - this handles the product grabbing from the collection. It's a recursive function since we can't grab more than 250 products at the same time.
getCSRFToken - this grabs the CSRF token since most of the post/update/delete request requires it or they will fail (I grab it from the files page)
initDelete - this function start the delete process where we stack all the request one of the other without waiting, you may want to await each request, but even if you crash your browser I think it will be still faster to repeat the process rather then wait for each request to finish.
If you plan to use this script please TEST IT BEFORE USING IT. Create a collection with a few products and run against that, in case there are issues. I've tested it on my side and it's working but it's a code I wrote in 10 minutes after midnight, there can be issues there.
Have in mind that this script will delete ALL products in the collection you specify in the requestCollection(1231254125) method.
PS: All of this can be done using a Private App as well with the products scope set to read/write, using a back-end language of your choice. The main difference will be that you won't need the CSRF token and most of the headers that I set above. But I like quick solutions that doesn't require you to pull out the big guns.
My thing is a small project.
In main what it does is that the "server" will get a call from the link directly what will run some functions that will update the database and the data that has to be shown.
I will show what I mean:
function updateData(){
connection.query(`SELECT * FROM muzica WHERE melodie = "${updateList()}"`, function (error, rezultat, fields) {
if (error) {console.log('err la selectare')};
//express output
let data = {
melodie: rezultat[0].melodie,
likes: rezultat[0].likes
}
console.log(data.likes);
app.get('/like', (req,res) =>{
res.json(`${data.likes}`);
});
}
setInterval(()=>{
updateData();
}, 20000)
Uhh, how to explain it, I'm so bad at this...
So, in main, I'm new to back-end work, everything that I did was based on their Documentation as I learn way faster by my needs than some guides and so on.
So, when I or someone does my http://website/like it should show just data.likes, cause that is all that I need, don't count data.melodie (i will clean that later on) after I finish all the code.
Anyway, whenever I do website/like data.likes is not updating to the new database data.likes.
For example, data.likes before were 5, in a few minutes it can be 2 but whenever I call website/like show "5" than its new value 2.
Don't be hash on me, I'm new and I want to learn as much as I can, but I can't understand the above case, by my logic it should ALWAYS show what its in database when it refreshes each 10 seconds(I run this in localhost so I will not stress any online server).
But if there is any better way to check for databases update than "setInterval" please notice me.
It's hard to learn alone without a mentor or someone else to talk about this domain.
Thank you for your time!
Kind regards,
Pulsy
You have things a bit inside out. A request handler such as app.get('/like', ...) goes at the top level and you only ever call it once. What that statement does is register an event handler for any incoming requests with the /like path. When the server receives an incoming request for /like, it will then call the function for this route handler.
You then put inside that route handler the code that you want to run to generate the response and send the response back to the client.
app.get('/like', (req, res) => {
connection.query(`SELECT * FROM muzica WHERE melodie = "${updateList()}"`, function (error, rezultat, fields) {
if (error) {
console.log(error);
res.sendStatus(500);
} else {
//express output
let data = {
melodie: rezultat[0].melodie,
likes: rezultat[0].likes
}
res.json(data);
}
});
});
The endpoints need to be outside of any functions in express.
For example, if you look at the express "hello world" example here, you will see that they have a basic app that only has a single GET endpoint defined which is "/" so you would access it by running "localhost/" or "127.0.0.1/".
In your case, you want your endpoint to be "/like", so you must define something like:
const express = require('express')
const app = express()
const port = 3000
app.listen(port, () => console.log(`Example app listening at http://localhost:${port}`))
app.get('/like', (req, res) => {
// do database stuff and assign data variable
// res.json(data);
}
I am trying to achieve a board that enables real-time editing for cooperating users. I am running a parse server using Sashido, where I have LiveQueries enabled for among other things 'Sticky'. For the frontend I have the following code:
const query = new Parse.Query(Sticky);
... some query constraints
this.subscription = await query.subscribe();
this.subscription.on('open', () => {
console.log('SUBSCRIPTION: opened');
}
this.subscription.on('create', (sticky) => {
console.log('SUBSCRIPTION: created');
}
this.subscription.on('update', (sticky) => {
console.log('SUBSCRIPTION: updated');
}
this.subscription.on('enter', (sticky) => {
console.log('SUBSCRIPTION: entered');
}
this.stickies = await query.find();
When I open my application in two different browser tabs, I get the 'SUBSCRIPTION: opened'. When I edit or create Sticky instances, I expect to get the corresponding events and see changes in the Sashido database.
However, I always see the changes in the database, but half of the times when I create or edit Sticky instances, I do not get the update/create/enter events. Note: Sometimes they do get triggered, but I have not found a sequence of events that leads to them being triggered or not, it seems to happen at random.
Can someone see what I'm doing wrong?
I am using this filter hook in my Auth0 Delegated Administration Extension.
function(ctx, callback) {
// Get the company from the current user's metadata.
var company = ctx.request.user.app_metadata && ctx.request.user.app_metadata.company;
if (!company || !company.length) {
return callback(new Error('The current user is not part of any company.'));
}
// The GREEN company can see all users.
if (company === 'GREEN') {
return callback();
}
// Return the lucene query.
return callback(null, 'app_metadata.company:"' + company + '"');
}
When user logged in whose company is GREEN can see all users. But when user logged in whose company is RED can't see any users whose company is RED.
I need to make this when user logged in, user should only be able to access users within his company. (except users from GREEN company).
But above code is not giving expected result. What could be the issue?
This might be related to a little warning note on the User Search documentation page
Basically they don't let you search for properties in the app_metadata field anymore. Unfortunately, this change was breaking and unannounced.
We had to make changes to our API so that we keep a copy of the app_metadatas in a separate database and convert lucene syntax to MongoDB queries, so that we can query by a chain of user_id:"<>" OR user_id:"<>" OR ....
One caveat though, you can't pass a query that's longer than 72 user_ids long. This number is so far undocumented and obtained empirically.
Also, you can't rely on Auth0's hooks to add new users to your database, as these don't fire for social logins, only for Username-Password-Authentication connections.
I hope this gave you some explanation as for why it wasn't working as well as a possible solution.
If I were you, I would look for an alternative for Auth0, which is what we are currently doing.
I finally ended up with this solution.
Used search functionality to filter users. I had to change below two files.
fetchUsers function in client\actions\user.js
changed
export function fetchUsers(search = '', reset = false, page = 0)
to
export function fetchUsers(search = '#red.com', reset = false,
page = 0)
AND
onReset function in client\containers\Users\Users.jsx
changed
onReset = () => { this.props.fetchUsers('', true); }
to
onReset = () => { this.props.fetchUsers('#red.com', true); }
How do I delete data when my path contains multiple objects? gun.path('saving_accounts').put(null) would delete all savings accounts.
Or, do you have a way to handle the null errors when iterating over data that has a 'deleted' object? I'm providing fully working examples to try to help in answering. Say I create gun data with this:
// localStorage.clear();
var gun = Gun();
////////////////////////////////////////////////////////////////// create record
var saving1 = gun.put({
name: "Bank of America",
accType: "Saving",
last4: "5555",
favorite: true,
status: true,
created: "some date created"
});
var saving2 = gun.put({
name: "Bank of America",
accType: "Saving",
last4: "4123",
favorite: true,
status: true,
created: "some date created"
});
var saving_accounts = gun.get('saving_accounts')
saving_accounts.set(saving1);
saving_accounts.set(saving2);
Then i can query all savings accounts with something like this:
const queryMultiple = (data_path) => {
console.log("Query for: " + data_path);
gun.get(data_path).map().val((name, ID) => {
// console.log(ID);
console.log(name.name, name.accType, ID);
});
};
queryMultiple('saving_accounts');
I tried to delete a record based on the gundb question here and wiki gun.path('object3').put(null) but I'm not sure how to change it for my application. On the savings account path, there are multiple savings accounts. So if i want to delete a specific savings account, i delete it by id but I think i'm doing it wrong. Say the id of the account i want to delete is FesxPaup8gzuNSsLFlWXMKaL:
// delete record
const deletebyID = (data_path, qID) => {
console.log("DELETE record");
gun.get(data_path).path(qID).put(null);
};
deletebyID('saving_accounts', 'FesxPaup8gzuNSsLFlWXMKaL');
But the .put(null) above will make the object FesxPaup8gzuNSsLFlWXMKaL point to null and when i list all savings accounts again with queryMultiple('saving_accounts'); I get a cannot read property name of null.
How do I delete data when my path contains multiple objects?
Side note: eventually i will nest multi transactions for a savings account under each savings account so I will have to do the same thing when deleting a account transaction that was made by mistake. Also hopefully when i delete a savings account, it automatically deletes/nulls all of that accounts transactions too but i haven't gotten past playing playing with data at this first layer.
#jtlindsey great question! You are correct though on how to delete data, even about how to delete an item inside a list/collection/table. But here is how to get the results you want:
Quick Solution:
Change your query to this:
const queryMultiple = (data_path) => {
console.log("Query for: " + data_path);
gun.get(data_path).map().val((name, ID) => {
if(!name){ return }
// console.log(ID);
console.log(name.name, name.accType, ID);
});
};
queryMultiple('saving_accounts');
And it will work. Why? Because it filters out any nulled account.
Why All the Nulls?
Deletes in GUN work like Mac OSX or Windows or Linux. The nulling tells every machine to "Put this data in the trash/recycle bin". The reason this is useful is because it lets you change your mind about deleting something, so you can recover it later if you want. (Recovering deleted content/files happens a LOT, but it something most people don't think about).
The null data also is useful for notifications! This is very applicable when you are designing frontend websites and you are rendering HTML. Let's go over a simple example:
Example
Imagine your user is checking the site on his phone, and realizes he needs to get clear up some issues that are a little bit more complicated so he logs on with his laptop. After checking the details on the laptop he decides to delete the account. Underneath his "click" action causes your run code to run:
// delete record
const deletebyID = (data_path, qID) => {
console.log("DELETE record");
gun.get(data_path).path(qID).put(null);
};
deletebyID('saving_accounts', 'FesxPaup8gzuNSsLFlWXMKaL');
Which is correct. However, if he then closes his laptop and picks his phone back up... he'll notice his account is still there!!! Which is not a good experience. But with GUN fixing this is easy because of the null notification:
gun.get(data_path).map().on((account, ID) => {
var UI = $("#account-" + ID);
if(!account){
UI.remove();
return;
}
updateUI(ID, account);
});
Now when they pick up their phone it will reflect the current state of their accounts! They'll see that it had been removed on all their devices because the null got synced to all devices.
Does that make sense? Does that your answer your question? Need help with anything else? As always, https://gitter.im/amark/gun and https://github.com/amark/gun/wiki/delete and http://gun.js.org/ .