I have two different roles in my project: ROLE_USER and ROLE_ADMIN.
I want to get list of all users through REST API's url '/users', but some fields (for example email) can see only those person, who authenticated with ROLE_ADMIN.
So, I have generally 2 questions:
1) On which abstraction level (in MVC pattern) should I decide which information can be returned based on ROLE
2) Which is the best way to implement such a Role-based REST API in Symfony?
Thanks
If you are using JMSSerializer you can use groups to decide what can be seen or not. Then in your controller, or where ever, you could set the group based on the role.
For example with the mapping (in YAML)..
Fully\Qualified\Class\Name:
exclusion_policy: ALL
properties:
id:
groups: [user]
userAndAdmin:
groups: [user]
adminOnly:
groups: [admin]
And then in your controller you would set the group like...
public function getUsersAction(Request $request)
{
$users = $this->getRepository()->findAll();
$serializer = $this->get('jms_serializer.serializer');
$json = $serializer->serialize(
$users,
'json',
SerializationContext::create()->setGroups($this->generateGroups())
);
return new Response($json);
// If you are using FOSRestBundle, which I would recommend, then you would just need to do...
$view = $this
->view($this->getRepository()->findAll();)
->setExclusionGroups($this->generateGroups())
;
return $this->handleView($view);
}
private function generateGroups()
{
$securityContext = $this->get('security.context');
$groups = array();
if ($securityContext->isGranted('ROLE_USER')) {
$groups[] = 'user';
}
if ($securityContext->isGranted('ROLE_ADMIN')) {
$groups[] = 'admin';
}
return $groups;
}
Although the whole "generateGroups" and setting the groups would be better placed in a customer view handler or response generator.
Assuming your hierarchy has ROLE_ADMIN as a parent of ROLE_USER you would get the following results.
ROLE_USER
{
"users": [
{
"id": 1,
"userAndAdmin": "val"
}
]
}
ROLE_ADMIN
{
"users": [
{
"id": 1,
"userAndAdmin": "val",
"adminOnly": "val"
}
]
}
Since the API is dependent on user who is making the request, each request will have to carry the information about the current user. Usually all authorization related tasks are processed within the controller. So, answer to your first question is that you should process the roles in the controller and based on the roles, you should filter out the fields from the data returned from the repository. For example,
//users is the array of user objects returned by your repository
data = [];
if ($this->get('security.context')->isGranted('ROLE_ADMIN')) {
foreach($users as $user){
// Add ROLE_ADMIN specific data
data[][] = array(
'name' => $user->getName(),
'email' => $user->getEmail(),
);
}
}
if ($this->get('security.context')->isGranted('ROLE_USER')) {
foreach($users as $user){
// Add ROLE_USER specific data
data[][] = array(
'name' => $user->getName(),
'address' => $user->getAddress(),
);
}
}
then, JSON encode the data array and return as the response. To reduce the number of queries, you can add a __toArray method in your User class.
Related
I have got a couple of tables looking like this:
1) article
id
name
2) article_options
id
article_id
option_id
3) option
id
name
An article can have none, one or many options and I want to return in the RestAPI a JSON structure that looks like this:
[{
"id": 1,
"name": "Article1",
"options": [10]
},
{
"id": 2,
"name": "Article2",
"options": [3, 10]
},
{
"id": 3,
"name": "Article3",
"options": []
}]
whereby 3 and 10 would be the option ids.
I tried a few solutions but none worked. Here is what I currently have:
Article Model:
class Article extends Model
{
# protected $with = ['Article_Option'];
public function options()
{
return $this->hasMany('App\Article_Option');
}
}
Article Controller:
class ArticleController extends Controller
{
public function index()
{
$articles = Article::all();
return response()->json(
$articles
, 200);
}
}
I just can't get my head around how the model and the controller should be configured to achieve this.
Please help.
Thanks
Goppi
[UPDATE]
Thanks to Hides help. With some modification I got it to work the way I wanted. Here is all the code...
Article model:
namespace App;
use Illuminate\Database\Eloquent\Model;
class Article extends Model
{
public function options()
{
return $this->belongsToMany('App\Option');
}
}
ArticleController:
namespace App\Http\Controllers;
use App\Article;
use App\Http\Resources\Article as ArticleResource;
use Illuminate\Http\Request;
class ArticleController extends Controller
{
public function index()
{
$articles = ArticleResource::collection(Article::all());
return response()->json ($articles, 200);
}
}
and the Resource:
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class Article extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'options' => $this->options->pluck('id')->all()
];
}
}
article_options is your pivot table and the relationship between article and options is a many to many relationship which laravel provides a function for here. So all you will have to do is add the relationship to your article model.
public function options() {
return $this->belongsToMany(Option::class);
}
then when getting the articles you can call that relationship.
$articles = Article::with('options')->get();
This will grab all articles with all option relations and format it in the way that you desired. Laravel can automatically work out which fields it needs to use to form the relationship between the tables so don't worry about that though it does have an option to supply them in the belongsToMany function.
If you are only wanting the id you can pass specific columns to the with function as below.
$articles = Articles::with('options:id')->get()
To then return in a json format you can use resources. Resources help better format the collection of the model which can be used to achieve what you want. You will need to create a resource for articles. In the article resource you can format it how you wanted and to format the options you can use collection methods to transform it into an array of ids by plucking the field from the collection.
Article Resource
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'options' => $this->options->pluck('id')->all(),
];
}
So once you have got the articles you can return them like so
return new ArticlesResource::collection($articles);
If later on you can wanting to pass additional columns through for options you can create an options resource and pass that to the options key in you article resource.
'options' => new OptionsResource::collection($this->options)
I am trying to develop a notification system, but I am not sure if I do certain parts correctly. To simplify the case I will use some generic naming.
How the system should work:
A registered user can subscribe for notifications based on chosen filters from a data table grid. ( for example, notify me when the quantity of an item is X, or have multiple filters set up like set1.slug.quantity > X, some_value = false and some_int = 52)
How I store such preferences:
Example object
"O:8:"stdClass":2:{s:9:"set1.slug";a:1:{s:3:"$eq";s:10:"item_slug1";}s:13:"set1.quantity";a:1:{s:4:"$gte";i:1;}}"
Generation simplified
$object = new \stdClass();
$object->{'set1.slug'} = ['$eq' => 'item_slug1'];
$object->{'set1.quantity'} = ['$gte' => 1];
$object = serialize($object);
It attaches also the user_id and all the data serialized from the form to a partial MongoDB raw query.
Database stored object - predefined filter set for a user. Md is an md5 hash of the object for easier access and edit.
+----+-----------------------------------------------------------------------------------------------------------------+---------+----------------------------------+
| id | object | user_id | md |
+----+-----------------------------------------------------------------------------------------------------------------+---------+----------------------------------+
| 1 | O:8:"stdClass":2:{s:9:"set1.slug";a:1:{s:3:"$eq";s:10:"item_slug1";}s:13:"set1.quantity";a:1:{s:4:"$gte";i:1;}} | 22 | d5003ba3227c4db3189827329815b053 |
+----+-----------------------------------------------------------------------------------------------------------------+---------+----------------------------------+
This is how I would use it - my vision of how it would work.
I would call findByFilterMD in the API parser in a loop, while the table Items is populated.
// MongoDB query ( items database )
protected function executeUserFilter($array)
{
$list = Items::raw(function ($collection) use (&$array) {
return $collection->find(
$array, ["typeMap" => ['root' => 'array', 'document' => 'array']])
->toArray();
});
return $list;
}
// MySQL query - stored filters
protected function findByFilterMD($id)
{
$user = Auth::user();
$filter = PredefinedFilters::where('md', '=', $id)->first();
$deserialize = unserialize($filter->object);
$results = $this->executeUserFilter($deserialize);
// here would probably be a notification function like pusher or onesignal
}
I am aware that my attempt of achieving this might be totally wrong and I might reinvent the wheel since some tools might do that already.
Here is an example Item MongoDB object
{
"_id": {
"$oid": "5c0406abdc04e7007f17f4ef"
},
"name": "myObject",
"inner_ID": "0db0b19a-9c01-4c21-8e10-6879dbcb37f1",
"some_value": false,
"some_int": 52,
"set1": [
{
"slug": "item_slug1",
"quantity": 88,
"extra": {
"value": 0
}
},
],
"set2": [
{
"slug": "item_slug2",
"quantity": 88,
"extra": {
"value": 0
}
},
{
"slug": "item_slug3",
"quantity": 88,
"extra": {
"value": 0
}
}
],
"expires": "2018-12-02 22:21:30"
}
Here comes my questions
Is this way of doing it proper?
Where should the notification system kick in? I assume it might be in the place where I parse the api of items, then I should loop over the user filter data and run the stored object query - or should it be a separate system called with cron?
I am open to any suggestions, redesigns.
I developed an app kinda like this. My approach is to make use of Laravel notification found here:
https://laravel.com/docs/5.7/notifications#creating-notifications
Let's say, in your case, if someone modify/create data, the other users who subscribe will get notification.
Create notification
php artisan make:notification UserUpdateQuantity
Make User model notifiable, also create scope that subscribe for something
<?php
namespace App;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
use Notifiable;
public function scopeSubscribe($query)
{
return $query->where('subscribe', true);
}
}
the $query in method scopeSubscribe needs to be adjusted based on your business logic
Send notification
$subscriber = User::subscribe()->get();
Notification::send($subscriber, new UserUpdateQuantity());
Create event & listener
You can find the event and listener here https://laravel.com/docs/5.7/events
In the EventServiceProvider
protected $listen = [
'App\Events\QuantityUpdated' => [
'App\Listeners\SendUpdateQuantiryNotification',
],
];
Then run the command php artisan event:generate
Event listener
In the event listener, we send notification
public function handle(QuantityUpdated $event)
{
$subscriber = User::subscribe()->get();
Notification::send($subscriber, new UserUpdateQuantity());
}
Eloquent event
Add event on your eloquent model, so when someone update quantity, it triggers event and the listener will send notification to subscribed users
// In your model
protected $dispatchesEvents = [
'updated' => App\Events\QuantityUpdated::class
];
When I query Meteor.users I do not receive the services field or any other custom fields I have created outside of profile. Why is it that I only receive _id and profile on the client and how can I receive the entire Meteor.users object?
Thanks.
From the DOcs
By default, the current user's username, emails and profile are published to the client. You can publish additional fields for the current user with:
As said above If you want other fields you need to publish them
// server
Meteor.publish("userData", function () {
if (this.userId) {
return Meteor.users.find({_id: this.userId},
{fields: {'services': 1, 'others': 1}});
} else {
this.ready();
}
});
// client
Meteor.subscribe("userData");
The above answer does work, but it means you have to subscribe to said data, which you should do if you are getting data from users other than the currently logged in one.
But if all you care about is the logged in user's data, then you can instead use a null publication to get the data without subscribing.
On the server do,
Meteor.publish(null, function () {
if (! this.userId) {
return null;
}
return Meteor.users.find(this.userId, {
fields: {
services: 1,
profile: 1,
roles: 1,
username: 1,
},
});
});
And this is actually what the accounts package does under the hood
Getting into sails.js - enjoying the cleanliness of models, routes, and the recent addition of associations. My dilemma:
I have Users, and Groups. There is a many-many relationship between the two.
var User = {
attributes: {
username: 'string',
groups: {
collection: 'group',
via: 'users'
}
}
};
module.exports = User;
...
var Group = {
attributes: {
name: 'string',
users: {
collection: 'user',
via: 'groups',
dominant: true
}
}
};
module.exports = Group;
I'm having difficulty understanding how I would save a user and it's associated groups.
Can I access the 'join table' directly?
From an ajax call, how should I be sending in the list of group ids to my controller?
If via REST URL, is this already accounted for in blueprint functions via update?
If so - what does the URL look like? /user/update/1?groups=1,2,3 ?
Is all of this just not supported yet? Any insight is helpful, thanks.
Documentation for these blueprints is forthcoming, but to link two records that have a many-to-many association, you can use the following REST url:
POST /user/[userId]/groups
where the body of the post is:
{id: [groupId]}
assuming that id is the primary key of the Group model. Starting with v0.10-rc5, you can also simultaneously create and a add a new group to a user by sending data about the new group in the POST body, without an id:
{name: 'myGroup'}
You can currently only add one linked entity at a time.
To add an entity programmatically, use the add method:
User.findOne(123).exec(function(err, user) {
if (err) {return res.serverError(err);}
// Add group with ID 1 to user with ID 123
user.groups.add(1);
// Add brand new group to user with ID 123
user.groups.add({name: 'myGroup'});
// Save the user, committing the additions
user.save(function(err, user) {
if (err) {return res.serverError(err);}
return res.json(user);
});
});
Just to answer your question about accessing the join tables directly,
Yes you can do that if you are using Model.query function. You need to check the namees of the join tables from DB itself. Not sure if it is recommended or not but I have found myself in such situations sometimes when it was unavoidable.
There have been times when the logic I was trying to implement involved a lot many queries and it was required to be executed as an atomic transaction.
In those case, I encapsulated all the DB logic in a stored function and executed that using Model.query
var myQuery = "select some_db_function(" + <param> + ")";
Model.query(myQuery, function(err, result){
if(err) return res.json(err);
else{
result = result.rows[0].some_db_function;
return res.json(result);
}
});
postgres has been a great help here due to json datatype which allowed me to pass params as JSON and also return values as JSON
I'm getting ready to implement stripped down version of role based access control in my application and I'm contemplating how/what to model in my document store which happens to be mongodb with mongoose.js as my "convenience lib". But this question should apply to any document store.
It seems quite common that deciding between using embedded objects vs refs is a challenge when using a document store given the competing factors of duplication vs performance and what not. I'm trying to keep the RBAC as simple as possible and not go too crazy on nested Collections/Ref IDs which would mean a ton of loops, over using mongoose's populate, etc.
Question:
I'm already leaning toward having collections for User, Permission, and Role; but does it make sense to model Operations and Resources, or, just use key/vals for these?
See code example below or jsfiddle which should help to reason about the problem. Note it's not at all the way I want to implement this but just a way to examine to relationships!
/*
Imagine this being used in a CMS ;)
User: have a role property (e.g. role:"admin" or role:"writer")
Operation: Create,Read,Update,Delete,etc.
Resource: Page,Post,User, etc.
* For simplicity, we can represent operations and resource with simple strings.
Permission: A permission is an allowable "Operation" on a "Resource"
Role: A Role is just an abstraction of a set of possible "Permissions"
*/
// I could see this as a Permission model in mongo
var adminPerms = {
create: ['pages','posts', 'users'],
update: ['posts','pages','users'],
update_others: ['posts','pages'],
delete: ['posts','pages','users'],
read:['pages','posts','users']
};
// I could see this as a Role model in mongo
var admin = {
perms: adminPerms
};
var writerPerms = {
create: ['pages','posts'],
update: ['pages','posts'],
update_others: [],
delete: [],
read:['pages','posts']
};
var writer = {
perms: writerPerms
};
// Now we can just see if that user's perms has the operation on resource defined
function hasPerms(user, operation, resource) {
var i, len, op;
if(!user || !user.role || !operation || !resource) return false;
if(typeof rolemap[user.role] !== 'undefined' &&
typeof rolemap[user.role]['perms'] !== 'undefined' &&
typeof rolemap[user.role]['perms'][operation] !== 'undefined') {
op = rolemap[user.role]['perms'][operation];
for(i=0, len=op.length; i<len; i++) {
if(op[i] === resource) {
return true;
}
}
}
return false;
}
var rolemap = {"admin":admin, "writer":writer}
var user_admin = {name:'Rob Levin', role:'admin'}
var user_jack = {name:'Jack Black', role:'writer'}
hasPerms(user_jack, 'create', 'users')
// false
hasPerms(user_admin, 'create', 'users')
// true
hasPerms(user_admin, 'update_others', 'posts')
// true
hasPerms(user_jack, 'update_others', 'posts')
// false
EDIT: Assume that roles must be editable on a per app basis so I might want to allow admin users to control access rights; this is why I want to use a database.
Regarding doing it all inside the application that won't work given the requirement to persist and possibly change. However, one compromise in this direction is I could just the role collection:
db.role.find({name:'writer'}).pretty()
{
"_id" : ObjectId("4f4c2a510785b51c7b11bc45"),
"name" : "writer",
"perms" : {
"create" : [
"posts",
"pages"
],
"update" : [
"posts",
"pages"
],
"update_others" : [ ],
"delete" : [ ],
"read" : [
"posts",
"pages"
]
}
}
And than I could make changes like removal, etc., like the following (assuming I already have a reference to a role object retrieved from mongo at point of call):
function removePerm(role, op, resource) {
if(!role || !role.perms || !role.perms[op]) {
console.log("Something not defined!");
return false;
}
var perm = role.perms[op];
for(var i=0, len=perm.length; i<len; i++) {
if(perm[i] === resource) {
perm.splice(i,1);
break;
}
}
}
I recently used mongoosejs with a user/roles/permissions need I had with Drywall.js - the actual permissions are key/value based. They can be shared by group and also overridden granularly on the administrator level.
Although it's not exactly RBAC I'm hoping that reviewing another approach helps you get closer to achieving your goal.
Project Overview:
http://jedireza.github.com/drywall/
Mongoose Schemas:
https://github.com/jedireza/drywall/tree/master/schema
Specifically Look At:
/schema/User.js
/schema/Admin.js
/schema/AdminGroup.js
/schema/Account.js
I'm interested to see what you come up with.
Your design is almost entirely dependent on the behavior of your application. What I would recommend based on the information you've provided here is to keep the data in key/values, not in the database--CRUD operations aren't going to change, so there's no reason to put that in the db. The 'resources' are classes that you've already built into your code, so you don't need to duplicate it in the db either.