Global queries are not recommended because they are very hard to secure. If you are not interested in exposing an API for your clients or expose a public database, continue reading next part
But they are very interesting in what they offer and they can prove to be very useful. You can expose an API that has access to all or certain parts of your database, without defining a named query for each.
The difference between a Named Query
and a Global Query
is that the later
does not have their form defined on the server, the client can query for anything that he wishes
as long as the query is exposed and respects the security restrictions.
A Global Query
is as almost feature rich as a Named Query
with the exception of caching.
- You have a public database, you just want to expose it
- You have a multi-tenant system, and you want to give full database access to the tenant admin
- Other cases as well
In order to query for a collection from the client-side and fetch it or subscribe to it. You must expose it.
Exposing a collection does the following things:
- Creates a method called: exposure_{collectionName} which accepts a query
- Creates a publication called: exposure_{collectionName} which accepts a query and uses reywood:publish-composite to achieve reactive relationships.
- If firewall is specified, it extends find method of your collection, allowing an extra parameter:
Collection.find(filters, options, userId);
If userId is undefined, the firewall and constraints will not be applied. If the userId is null, the firewall will be applied. This is to allows server-side fetching without any restrictions.
// server-side
Meteor.users.expose();
This means that any user, from the client can do:
createQuery({
users: {
services: 1, // yes, everything becomes exposed
anyLink: {
anySubLink: {
// and it can go on and on and on
}
}
}
})
Ok this is very bad. Let's secure it.
Meteor.users.expose({
restrictedFields: ['services'],
restrictLinks: ['anyLink'],
});
Phiew, that's better. But is it? You'll have to keep track of all the link restrictions.
// server-side
Collection.expose({
firewall(filters, options, userId) {
if (!userId) {
throw new Meteor.Error('...');
}
}
});
Collection.expose({
// it can also be an array of functions
firewall(filters, options, userId) {
filters.userId = userId;
},
// Allow reactive query-ing
publication: true,
// Allow static query-in
method: true,
// Unblock() the method/publication
blocking: false,
// The publication/method will not allow data fetching for more than 100 items.
maxLimit: 100,
// The publication/method will not allow a query with more than 3 levels deep.
maxDepth: 3,
// This will clean up filters, options.sort and options.fields and remove those fields from there.
// It even removes it from deep filters with $or, $nin, etc
restrictedFields: ['services', 'secretField'],
// Array of strings or a function that has userId
restrictLinks: ['link1', 'link2']
});
When querying for a data-graph like:
{
users: {
comments: {}
}
}
It is not necessary to have an exposure for comments, however if you do have it, and it has a firewall. The firewall rules will be applied. The reason for this is security.
Don't worry about performance. We went great lengths to retrieve data in as few MongoDB requests as possible, in the scenario above, if you do have a firewall for users and comments, both will be called only once, because we only make 2 MongoDB requests.
import { Exposure } from 'meteor/cultofcoders:grapher';
// Make sure you do this before exposing any collections.
Exposure.setConfig({
firewall,
method,
publication,
blocking,
maxLimit,
maxDepth,
restrictedFields
});
When you expose a collection, it will extend the global exposure methods. The reason for this is you may want a global limit of 100, or you may want a maximum graph depth of 5 for all your exposed collections, without having to specify this for each.
Important: if global exposure has a firewall and the collection exposure has a firewall defined as well, the collection exposure firewall will be applied.
// Apply filters based on userId
Collection.expose({
firewall(filters, options, userId) {
if (!isAdmin(userId)) {
filters.isVisible = true;
}
}
});
// Make certain fields invisible for certain users
import { Exposure } from 'meteor/cultofcoders:grapher'
Collection.expose({
firewall(filters, options, userId) {
if (!isAdmin(userId)) {
Exposure.restrictFields(filters, options, ['privateData']);
// it will remove all specified fields from filters, options.sort, options.fields
// this way you will not expose unwanted data.
}
}
});
Compute restricted links when fetching the query:
Collection.expose({
restrictLinks(userId) {
return ['privateLink', 'anotherPrivateLink']
}
});
If body is specified, it is first applied on the requested body and then the subsequent rules such as restrictedFields, restrictLinks will apply still.
This is for advanced usage and it completes the security of exposure.
By using body, Grapher automatically assumes you have control over what you give, meaning all firewalls from other exposures for linked elements in this body will be bypassed.
The firewall of the current exposure still executes of course.
Meteor.users.expose({
body: {
firstName: 1,
groups: {
name: 1
}
}
})
If you query from the client-side something like:
createQuery({
users: {
firstName: 1,
lastName: 1,
groups: {
name: 1,
createdAt: 1,
}
}
})
The intersected body will look like:
{
firstName: 1,
groups: {
name: 1,
}
}
Ok, but what if I want to have a different body based on the userId?
Body can also be a function that takes in an userId
, and returns an actual body, an Object
.
Collection.expose({
body(userId) {
let body = { firstName: 1 };
if (isAdmin(userId)) {
_.extend(body, { lastName: 1 })
}
return body;
}
})
Deep nesting with other links not be allowed unless your body specifies it.
The special fields $filters
and $options
are allowed at any link level (including root). However, they will go through a phase of cleaning,
meaning it will only allow you to filter
and sort
for fields that exist in the body.
This check goes deeply to verify "$and", "$or", "$nin" and "$not" special MongoDB selectors. This way you are sure you do not expose data you don't want to.
Because, given enough requests, a hacker playing with $filters
and $sort
options can figure out a field that you may not want to give him access to.
If the body contains functions they will be computed before intersection. Each function will receive userId.
{
linkName(userId) { return {test: 1} }
}
// transforms into
{
linkName: {
test: 1
}
}
You can return undefined or false in your function if you want to disable the field/link for intersection.
{
linkName(userId) {
if (isAdmin(userId)) {
return object;
}
}
}
Now things start to get crazy around here!
You can link bodies in your own way and also reference other bodies'links. Functions are computed on-demand, meaning you can have self-referencing body functions:
// Comments ONE link to Users as 'user'
// Users INVERSED 'user' from Comments AS 'comments'
const commentBody = function(userId) {
return {
user: userBody,
text: 1
}
}
const userBody = function(userId) {
if (isAdmin(userId)) {
return {
comments: commentBody
};
}
return somethingElse;
}
Users.expose({
body: userBody
})
Comments.expose({
body: commentBody
})
This will allow requests like:
{
users: {
comments: {
user: {
// It doesn't make much sense for this case
// but you can :)
}
}
}
}
The global queries are a very powerful tool to expose your full database, but unlike Named Queries
they do
not benefit of caching
.