I've been evaluating Strongloop (& Loopback) over the past couple of days. I've written a couple of helper endpoints for me to get information about my models in order to generate a CMS frontend for the REST API.
I've create a boot script that returns the public models that are in use, and a bit about them, as well as a couple of custom fields to do with the display of models etc. Here it is:
module.exports = function mountModelDiscoveryService(server) {
server.get('/api/RemoteModules', function(req, res) {
var models = server.models();
var modelObject = [];
models.forEach(function(Model) {
if(Model.shared) {
modelObject.push({
name: Model.modelName,
plural: (Model.settings.plural || Model.modelName),
attributes: Model.definition.rawProperties,
uri: (Model.settings.plural || Model.modelName).toLowerCase(),
displaySettings: (Model.settings.display || {list: ["id", "title"]})
})
}
});
return res.send(modelObject);
})
}
This is working well and I've been pleased with the progress. However, I'd like to be able to get a list of models (+ endpoints) that I have access to as the currently logged in user.
I've implemented the ACL stuff as the tutorial describes, and this correctly allows me or denies me access based on my current permission level, but this is only at the point of making the call - I was essentially wondering if anyone had tried to use the permissions system on the frontend - I'd like to hide certain elements if a user doesn't have access to create new objects, for example.
Do you think I should add some sort of property to this object that just returns a cut-down version of the ACL object with a complete list of what everyone can do? Is there an internal Loopback method I can use to achieve this result?
Thanks a lot for your time.
Related
I'm just starting learning node.js, express and mongoose.
In some tutorial I see some project structure like they have different folder for controller, different for services , different for data access layer.
Now my question is what is the difference in services and data access layer file, what we keep there? And where we keep data access layer file in my project structure?
Also what exactly the task of controller and routes ?
The routes files are where you place the application endpoints. Those will refer to the specific application methods you define. This is called routing.
app.post('/users', userController);
For clarification, in the example above we're calling the POST HTTP method for the /users route and redirecting the request to the userController method.
The controller layer is more specific. He is responsible for parsing the HTTP request data and sending it to the service layer.
For example:
async function userController(request, response) {
const { name, age } = request.body;
const serviceRequestBody = { name, age };
const serviceResponse = await userService(serviceRequestBody);
return response.json(serviceResponse);
}
The service layer is normally where you place the project rules (domain specific rules). For example: calculating the birth year based on the user age.
async function createUserService(userData) {
const birthYear = new Date().getFullYear() - userData.age;
const userFormatedData = {...userData, birthYear }; // the three dots means that we're getting all the information inside userData and placing it inside the new variable.
const dbResult = await userRepository(userFormatedData);
return dbResult;
}
The data access layer (can also be called "repository") is responsible for getting, posting, updating or deleting the information from the database.
async function userRepository(userInfo) {
const dbResult = await db.post(userInfo);
return dbResult;
}
For the project structure, its more up to you. I like to structure my projects like this:
-src
|-modules
| | -user // domain specific entities. If you have other entities, they will be inside another folder with the same structure as this one
| |-domain
| |-controllers
| |-repositories
| |-routes
| |-services
|-shared // can be used across any module
|-utils
|-providers
-package.json
-configFile.json
PS: Those abstractions may vary over time as you scale your application. It's the engineer's responsibility to figure out a better structure according to the case he's facing.
If you want to learn more about software engineering, search for Domain Driven Design (DDD), which is set of architecture rules that composes a good and scalable project.
I'm still a novice web developer, so please bear with me if I miss something fundamental !
I'm creating a backoffice for a Strapi backend, using react-admin.
React-admin library uses a 'data provider' to link itself with an API. Luckily someone already wrote a data provider for Strapi. I had no problem with step 1 and 2 of this README, and I can authenticate to Strapi within my React app.
I now want to fetch and display my Strapi data, starting with Users. In order to do that, quoting Step 3 of this readme : 'In controllers I need to set the Content-Range header with the total number of results to build the pagination'.
So far I tried to do this in my User controller, with no success.
What I try to achieve:
First, I'd like it to simply work with the ctx.set('Content-Range', ...) hard-coded in the controller like aforementioned Step 3.
Second, I've thought it would be very dirty to c/p this logic in every controller (not to mention in any future controllers), instead of having some callback function dynamically appending the Content-Range header to any fetchAll request. Ultimately that's what I aim for, because with ~40 Strapi objects to administrate already and plenty more to come, it has to scale.
Technical infos
node -v: 11.13.0
npm -v: 6.7.0
strapi version: 3.0.0-alpha.25.2
uname -r output: Linux 4.14.106-97.85.amzn2.x86_64
DB: mySQL v2.16
So far I've tried accessing the count() method of User model like aforementioned step3, but my controller doesn't look like the example as I'm working with users-permissions plugin.
This is the action I've tried to edit (located in project/plugins/users-permissions/controllers/User.js)
find: async (ctx) => {
let data = await strapi.plugins['users-permissions'].services.user.fetchAll(ctx.query);
data.reduce((acc, user) => {
acc.push(_.omit(user.toJSON ? user.toJSON() : user, ['password', 'resetPasswordToken']));
return acc;
}, []);
// Send 200 `ok`
ctx.send(data);
},
From what I've gathered on Strapi documentation (here and also here), context is a sort of wrapper object. I only worked with Express-generated APIs before, so I understood this snippet as 'use fetchAll method of the User model object, with ctx.query as an argument', but I had no luck logging this ctx.query. And as I can't log stuff, I'm kinda blocked.
In my exploration, I naively tried to log the full ctx object and work from there:
// Send 200 `ok`
ctx.send(data);
strapi.log.info(ctx.query, ' were query');
strapi.log.info(ctx.request, 'were request');
strapi.log.info(ctx.response, 'were response');
strapi.log.info(ctx.res, 'were res');
strapi.log.info(ctx.req, 'were req');
strapi.log.info(ctx, 'is full context')
},
Unfortunately, I fear I miss something obvious, as it gives me no input at all. Making a fetchAll request from my React app with these console.logs print this in my terminal:
[2019-09-19T12:43:03.409Z] info were query
[2019-09-19T12:43:03.410Z] info were request
[2019-09-19T12:43:03.418Z] info were response
[2019-09-19T12:43:03.419Z] info were res
[2019-09-19T12:43:03.419Z] info were req
[2019-09-19T12:43:03.419Z] info is full context
[2019-09-19T12:43:03.435Z] debug GET /users?_sort=id:DESC&_start=0&_limit=10& (74 ms)
While in my frontend I get the good ol' The Content-Range header is missing in the HTTP Response message I'm trying to solve.
After writing this wall of text I realize the logging issue is separated from my original problem, but if I was able to at least log ctx properly, maybe I'd be able to find the solution myself.
Trying to summarize:
Actual problem is, how do I set my Content-Range properly in my strapi controller ? (partially answered cf. edit 3)
Collateral problem n°1: Can't even log ctx object (cf. edit 2)
Collateral problem n°2: Once I figure out the actual problem, is it feasible to address it dynamically (basically some callback function for index/fetchAll routes, in which the model is a variable, on which I'd call the appropriate count() method, and finally append the result to my response header)? I'm not asking for the code here, just if you think it's feasible and/or know a more elegant way.
Thank you for reading through and excuse me if it was confuse; I wasn't sure which infos would be relevant, so I thought the more the better.
/edit1: forgot to mention, in my controller I also tried to log strapi.plugins['users-permissions'].services.user object to see if it actually has a count() method but got no luck with that either. Also tried the original snippet (Step 3 of aforementioned README), but failed as expected as afaik I don't see the User model being imported anywhere (the only import in User.js being lodash)
/edit2: About the logs, my bad, I just misunderstood the documentation. I now do:
ctx.send(data);
strapi.log.info('ctx should be : ', {ctx});
strapi.log.info('ctx.req = ', {...ctx.req});
strapi.log.info('ctx.res = ', {...ctx.res});
strapi.log.info('ctx.request = ', {...ctx.request});
ctrapi.log.info('ctx.response = ', {...ctx.response});
Ctx logs this way; also it seems that it needs the spread operator to display nested objects ({ctx.req} crash the server, {...ctx.req} is okay). Cool, because it narrows the question to what's interesting.
/edit3: As expected, having logs helps big time. I've managed to display my users (although in the dirty way). Couldn't find any count() method, but watching the data object that is passed to ctx.send(), it's equivalent to your typical 'res.data' i.e a pure JSON with my user list. So a simple .length did the trick:
let data = await strapi.plugins['users-permissions'].services.user.fetchAll(ctx.query);
data.reduce((acc, user) => {
acc.push(_.omit(user.toJSON ? user.toJSON() : user, ['password', 'resetPasswordToken']));
return acc;
}, []);
ctx.set('Content-Range', data.length) // <-- it did the trick
// Send 200 `ok`
ctx.send(data);
Now starting to work on the hard part: the dynamic callback function that will do that for any index/fetchAll call. Will update once I figure it out
I'm using React Admin and Strapi together and installed ra-strapi-provider.
A little boring to paste Content-Range header into all of my controllers, so I searched for a better solution. Then I've found middleware concept and created one that fits my needs. It's probably not the best solution, but do its job well:
const _ = require("lodash");
module.exports = strapi => {
return {
// can also be async
initialize() {
strapi.app.use(async (ctx, next) => {
await next();
if (_.isArray(ctx.response.body))
ctx.set("Content-Range", ctx.response.body.length);
});
}
};
};
I hope it helps
For people still landing on this page:
Strapi has been updated from #alpha to #beta. Care, as some of the code in my OP is no longer valid; also some of their documentation is not up to date.
I failed to find a "clever" way to solve this problem; in the end I copy/pasted the ctx.set('Content-Range', data.length) bit in all relevant controllers and it just worked.
If somebody comes with a clever solution for that problem I'll happily accept his answer. With the current Strapi version I don't think it's doable with policies or lifecycle callbacks.
The "quick & easy fix" is still to customize each relevant Strapi controller.
With strapi#beta you don't have direct access to controller's code: you'll first need to "rewrite" one with the help of this doc. Then add the ctx.set('Content-Range', data.length) bit. Test it properly with RA, so for the other controllers, you'll just have to create the folder, name the file, copy/paste your code + "Search & Replace" on model name.
The "longer & cleaner fix" would be to dive into the react-admin source code and refactorize so the lack of "Content-Range" header doesn't break pagination.
You'll now have to maintain your own react-admin fork, so make sure you're already committed into this library and have A LOT of tables to manage through it (so much that customizing every Strapi controller will be too tedious).
Before forking RA, please remember all the stuff you can do with the Strapi backoffice alone (including embedding your custom React app into it) and ensure it will be worth the trouble.
Hi please can anyone help me. I want user to be able to access only what they are permitted to access.
I have been looking at several Access Control List packages. I have not made a final decision.
A restaurant which would have several levels of permission.
The customer can place several orders and can see what foods he has ordered
He can also modify the order only within a specified time period e.g. before the order is being processed.
The customer can only view his own order and the stage which the order is.
A staff can only check the order than is under his menu and state how much the order would cost and how long the order would take.
Another staff would be in charge of the stores and how things goes in and goes out.
A Staff can be in charge of a department and at the same time allow input to a menu which is under another department.
I have been looking at how I can go about putting this into Express.js and mongodb
I have looked at the following
https://github.com/optimalbits/node_acl main focus
https://www.npmjs.com/package/acl
https://www.npmjs.com/package/express-acl
But I have not got the granularity and the mix which I stated above.
The permission would be based majorly on data. It has been a little confusing as to how I can go about that.
Any help will be useful
I use mongoose as my driver
As I said in my comments, this design has a bit of business logic that might make it not the best fit for regular ACL-type security controls. On the surface, it seems like the easier to figure out solution would be to just implement your business rules in your Mongoose models or Controller code, depending on your preference. That said, a key piece of doing any of this with an ACL-like approach comes down to your URL design. For example, it's tempting to make your API such that all orders are available through /api/orders and maybe a person would query their own orders via /api/orders?userId=12345. But that makes most ACL-based approaches fail. Instead you have to think about the API in terms of the hierarchy as you want it secured (regardless of if all orders are stored in the Orders Mongoose model, and persist in the orders collection).
So using your first requirement as an example
The customer can place several orders and can see what foods he has ordered
The focus here is that you are securing things by the customer 'owner' of the orders, so to secure it that way you need to setup your route that way, eg (assuming you're using the first middleware you asked about):
app.post('/api/customer/:customerId/orders', acl.middleware(), (req, res, next) => {
const order = new Order(req.body); // TODO: whitelist what info you take in here
order.customerId = req.user.id; // assuming you have a logged-in user that does this
order.save(e => {
if (e) return next(e);
return res.status(201).send(order);
});
});
To support this, you'd register your ACL info as such:
acl.allow('12345', '/api/customer/12345/orders', ['post']);
Minimally, you'd do that. You would likely provide more options such as 'get', etc. As you can guess, this means that you'll need to register permissions for individual users whenever you create them (to support the concept of 'ownership').
For your second requirement,
He can also modify the order only within a specified time period e.g. before the order is being processed.
Despite what I said before, you could arguably do this in an ACL if you really wanted to. For example, you could make the URL account for the status, like '/api/customers/12345/orders/modifiable/6789', but that becomes hard to maintain in my experience. You're better off putting that logic in the controller or the Mongoose logic. It's probably simpler to do it in the controller, unless you plan on using your Mongoose models outside of the Express app. Something like this (note, not using the ACL in this case, though you could if you wanted):
app.param('orderId', (req, res, next, id) => {
Order.findById(id, (err, order) => {
if (err) return next(err);
if (order) {
req.order = order;
return next();
}
const notFound = new Error('Order not found');
notFound.status = 404;
return next(notFound);
});
});
app.put('/api/orders/:orderId', (req, res, next) => {
if (req.order.status !== 'pending') {// or whatever your code setup is
const notProcessable = new Error('Cannot modify an order in process');
notProcessable.status = 422;
return next(notProcessable);
}
// handle the modification and save stuff
});
I'm using koa2 and koa-router together with sequelize on top. I want to be able to control user access based on their roles in the database, and it's been working somewhat so far. I made my own RBAC implementation, but I'm having some trouble.
I need to quit execution BEFORE any endpoint is hit if the user doesn't have access, considering endpoints can do any action (like inserting a new item etc.). This makes perfect sense, I realize I could potentially use transactions with Sequelize, but I find that would add more overhead and deadline is closing in.
My implementation so far looks somewhat like the following:
// initialize.js
initalizeRoutes()
initializeServerMiddleware()
Server middleware is registered after routes.
// function initializeRoutes
app.router = require('koa-router')
app.router.use('*', access_control(app))
require('./routes_init')
routes_init just runs a function which recursively parses a folder and imports all middleware definitions.
// function initializeServerMiddleware
// blah blah bunch of middleware
app.server.use(app.router.routes()).use(app.router.allowedMethods())
This is just regular koa-router.
However, the issue arises in access_control.
I have one file (access_control_definitions.js) where I specify named routes, their respective sequelize model name, and what rules exists for the route. (e.g. what role, if the owner is able to access their own resource...) I calculate whether the requester owns a resource by a route param (e.g. resource ID is ctx.params.id). However, in this implementation, params don't seem to be parsed. I don't think it's right that I have to manually parse the params before koa-router does it. Is anyone able to identify a better way based on this that would solve ctx.params not being filled with the actual named parameter?
edit: I also created a GitHub issue for this, considering it seems to me like there's some funny business going on.
So if you look at router.js
layerChain = matchedLayers.reduce(function(memo, layer) {
memo.push(function(ctx, next) {
ctx.captures = layer.captures(path, ctx.captures);
ctx.params = layer.params(path, ctx.captures, ctx.params);
ctx.routerName = layer.name;
return next();
});
return memo.concat(layer.stack);
}, []);
return compose(layerChain)(ctx, next);
What it does is that for every route function that you have, it add its own capturing layer to generate the params
Now this actually does make sense because you can have two middleware for same url with different parameters
router.use('/abc/:did', (ctx, next) => {
// ctx.router available
console.log('my request came here too', ctx.params.did)
if (next)
next();
});
router.get('/abc/:id', (ctx, next) => {
console.log('my request came here', ctx.params.id)
});
Now for the first handler a parameter id makes no sense and for the second one parameter did doesn't make any sense. Which means these parameters are specific to a handler and only make sense inside the handler. That is why it makes sense to not have the params that you expect to be there. I don't think it is a bug
And since you already found the workaround
const fromRouteId = pathToRegexp(ctx._matchedRoute).exec(ctx.captures[0])
You should use the same. Or a better one might be
var lastMatch = ctx.matched[ctx.matched.length-1];
params = lastMatch.params(ctx.originalUrl, lastMatch.captures(ctx.originalUrl), {})
I'm using GetStream's Laravel integration (github.com/GetStream/stream-laravel) but noticed that 'actor' is just a swappable Model. I'm trying to allow logged in users to post, but I want to allow "anonymous" (unauthenticated) users to like the post using UserAgent/fingerprint/ip/session etc.
Has anybody done something similar or know of a method to hot swap the actors or allow the actor_id in specific situations to use another model?
You should be able to use the ActivityTrait (https://github.com/GetStream/stream-laravel/blob/master/src/GetStream/StreamLaravel/Eloquent/ActivityTrait.php) without using the activityActor method, which is called here: https://github.com/GetStream/stream-laravel/blob/master/src/GetStream/StreamLaravel/Eloquent/ActivityTrait.php#L130.
For example:
class Model
{
use ActivityTrait {activityActor as traitActivityActor;}
public function activityActor()
{
if (/* check if authenticated user */) {
return $this->traitActivityActor(); // Reusing the same method.
}
return /* some other logic to return identifier for anonymous user */;
}
}
There are other places to hook into and adjust the functionality by this package. But I think in your case this might be the easiest for this model.
I hope this help you in the right direction.