Skip to content

[Issue #70] Allow setting the list of allowed permissions for a client#71

Merged
ferjm merged 1 commit intomozilla-sensorweb:masterfrom
ferjm:client.scopes
Jan 26, 2017
Merged

[Issue #70] Allow setting the list of allowed permissions for a client#71
ferjm merged 1 commit intomozilla-sensorweb:masterfrom
ferjm:client.scopes

Conversation

@ferjm
Copy link
Member

@ferjm ferjm commented Jan 23, 2017

This will allow us to set a list of permissions a client is allowed to ask when requesting an auth token.

@ferjm ferjm requested a review from julienw January 23, 2017 18:56
@coveralls
Copy link

Coverage Status

Coverage increased (+1.1%) to 86.732% when pulling d791ffa on ferjm:client.scopes into 47530b7 on mozilla-sensorweb:master.

@ferjm
Copy link
Member Author

ferjm commented Jan 23, 2017

r? @julienw

Copy link
Collaborator

@julienw julienw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mostly nits
main question is: why should the permissions be in the config ?

associate: db => {
Client.belongsToMany(db.Permissions, { through: 'ClientPermissions' });
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

quite a good idea to have it here instead of defining it below, so that it's closer to the definition.

state = READY;

// Load default permissions.
const permissions = config.get('permissions').map(permission => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this really belong in config ?

I'm not sure what you have in mind for the future. How will the permissions be required ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just though the config would be a good place cause it allows overriding the list of default permissions (for future testing purposes for example) and it provides an easy way to access the list (config.get('permissions').

I don't have a strong about this, so we could use an independent json file if you think is more appropriate.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think it's better; so that we can also ship it in git.

Alternatively you can provide the default list in the default property for this, but I find it's clumsy.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, I missed it. I still find it clumsy but up to you :) maybe just file an issue if you prefer to move faster.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll file a follow-up.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#74

timestamps: false,
classMethods: {
associate: db => {
Permission.belongsToMany(db.Clients, { through: 'ClientPermissions' });
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: do we need the bidierectional association ? Maybe we only need it in Clients.

Copy link
Member Author

@ferjm ferjm Jan 24, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A permission can be applied to multiple clients and a client can have multiple permissions. And in the future we may want to be able to query all clients with a specific permission. But maybe the single direction is enough for this.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, I'm fine with belongsToMany, but maybe it's not needed to be bidirectional (I mean: both here and in the definition for Clients). Adding it here will add "addClient" and such methods to Permission, and I don't think it's useful. I think only addPermission and such in Clients are useful.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just tried using a single association here and it does not work for what we need. We need to be able to assign the same permission to multiple clients, so we need a n:m relation. With a Client.hasMany(Permissions) (1:n relation) we get a Permissions table with a Client's foreign key, which is not enough for our needs. For example, with this kind of relation, trying to set the same permission (admin) to two different clients ends up with this result:

[{
    "key": "acb91322e69ed852",
    "name": "anotherClientName",
    "authRedirectUrls": [ "http://domain.org" ],
    "authFailureRedirectUrls": [ "http://domain.org" ],
    "permissions": [ "admin" ]
  }, {
    "key": "f8913a3a75deffdf",
    "name": "clientName",
    "authRedirectUrls": ["http://domain.org"],
    "authFailureRedirectUrls": ["http://domain.org"],
    "permissions": []
  }
]

As you can see, the permissions list of the second client is empty.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry if I was not clear. I totally agree with the belongsToMany relation.

What I mean is: I think it's enough to define it in Clients (https://github.com/mozilla-sensorweb/sensorweb-server/pull/71/files#diff-4727579a8668deee0b4103f2a0aa458bR48) and that it's (maybe) not useful to define it here as well.

The relation is already effective with the first command (I tried it myself -- I correctly get the relation table, and tests are passing). The second command only adds methods like addClients to a Permission object, or maybe find all clients that has a permission. I don't know if that's useful, but you know better.

That's all I'm saying here: asking if the bidirectional relation is useful :) Again, I agree with you thinking that belongsToMany is the right relation type.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok! Got it :) Sorry I wasn't understanding you properly. I'll remove the bidirectional association.

return;
}
return client.addPermissions(req.body.permissions, { transaction });
});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we can simplify with a create with include, see http://docs.sequelizejs.com/en/latest/docs/associations/#creating-elements-of-a-hasmany-or-belongstomany-association
so that we don't need to be explicit with the transaction

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was my first approach, but somehow I failed to apply it... so if you don't mind, I'll stick with the current approach.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oki

db().then(models => {
models.Clients.create(req.body).then(client => {
let client;
models.sequelize.transaction(transaction => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: should we return the result of transaction ? Currently I think we return the 201 before the data is actually written in the DB.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The transaction is not committed/rolled back until the promise chain returned within the models.sequelize.transaction callback resolves/rejects.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know, but the function we're in still returns before this, and the next promise in the chain (the one sending the response) runs -- all this possibly before the transaction is commited.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... that's not how I understood the doc. IIUC line 94 is not executed until the Promise returned at line 86 resolves.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you need to return at line 85 too :) There is nothing to do with sequelize, this is just how promises work !

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I mix the indentations, let me look again.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I was confused with indentation because I thought the response was done on the upper chain. That said, I think it would be better at this location ;)
I mean:

db().then(models => {
  return transaction(transaction => {
    return create().then(() => addPermissions())
  });
}).then(() => res.status(XXX).send())
.catch(...)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, fair enough.


const normalizeClient = client => {
if (client.Permissions) {
client.dataValues.permissions = client.Permissions.map(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it really needed to use dataValues here ? Can't you just assign to client.permissions ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, unfortunately we need to modify dataValues. Modifying client.permissions directly didn't work for me.

return db().then(models => {
models.Clients.destroy({ where: {} });
});
});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need to return destroy because otherwise the beforeEach finishes before the destroy is done.

using co-mocha you can do it like this:

beforeEach(function*() {
  const { Clients } = yield db();
  yield Clients.destroy({ where: {} });
});

res.status.should.be.equal(401);
res.body.code.should.be.equal(401);
res.body.errno.should.be.equal(errnos[ERRNO_UNAUTHORIZED]);
res.body.error.should.be.equal(errors[UNAUTHORIZED]);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you use postError ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We add the auth header within postError, adding yet another parameter to postError to remove the auth header for a single test didn't seem worthy... But it's not a big deal anyway.

};

it('should respond 400 BadRequest if name param is missing', done => {
const postError = (done, code, error, errno, body) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you use an object as parameter instead of the very long parameter list ?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or maybe (done, body, { code, error, errno }) so that done, body is the same as for postSuccess


describe('POST ' + endpointPrefix + '/clients', () => {
it('should respond 401 Unauthorized if there is no auth header', done => {
const postSuccess = (done, body) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about postSuccess -> postAndCheckSuccess (same for postError) ?

@coveralls
Copy link

Coverage Status

Coverage increased (+1.1%) to 86.797% when pulling f25d22f on ferjm:client.scopes into 17797c2 on mozilla-sensorweb:master.

@ferjm ferjm requested a review from julienw January 25, 2017 17:23
@ferjm
Copy link
Member Author

ferjm commented Jan 25, 2017

r? @julienw

Copy link
Collaborator

@julienw julienw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bidirectional relation comment is a nit -- the code is working like this. I still thing it's better with only the one-directional belongsToMany relation, but up to you :)

Also 1 very small nit in the test, and 1 nit in how you send back the client to the client (pun not intended).

}].forEach(test => {
it('should respond 400 BadRequest if ' + test.reason, done => {
postError(done, 400, BAD_REQUEST, test.errno, test.body);
it('should respond ' + test.code + ' if ' + test.reason, done => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: you can use template strings

if (!req.body.permissions) {
return;
}
return client.addPermissions(req.body.permissions, { transaction });
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you could do:

return client.addPermissions(req.body.permissions, { transaction })
  .then(() => client);

and then below:

.then(client => {
  res.status(201).send(client);
});

So that you don't need to have the client temp variable. The object is just passed along with the promise.

@coveralls
Copy link

Coverage Status

Coverage increased (+1.09%) to 86.765% when pulling a130ff4 on ferjm:client.scopes into 17797c2 on mozilla-sensorweb:master.

@ferjm ferjm merged commit 7cf93a7 into mozilla-sensorweb:master Jan 26, 2017
@ferjm ferjm deleted the client.scopes branch January 26, 2017 17:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants