Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/endpoint authorization #849

Closed

Conversation

luizgribeiro
Copy link
Contributor

Hey @mcollina, @ivan-tymoshenko.

Have a look on the initial draft for endpoint based access control.
Let me know if I'm missing out something.

@luizgribeiro
Copy link
Contributor Author

I think the structure is not quite there.
After having a deeper look on fastify I think a valid approach would be:

  1. Use the "onReady" hook that is alredy creating hooks for entities to create a "onRequest" hook
  2. This onRequest hook would check on the request content and enpoint rules to allow or deny access to route handler
  3. The "onReady" hook function would have to be refactor because if a rule that has no entity on it is processed it throws

@mcollina
Copy link
Member

mcollina commented Apr 6, 2023

I would follow a slightly different structure:

  1. create an req.authorize(permissions) method as described in the issue first. This allows you to develop things in baby-steps.
  2. Use the onRoute hook to add a onRequest hook that calls req.authorize()

Wdyt?

@luizgribeiro
Copy link
Contributor Author

You mean to use a basic structure like this in platformatic.db.json?

{
  "rules": [
    {
      "endpoint": "/someCustomEndpoint",
      "authorizer": "<path to exported authorizer>"
    }
  ]
}

I was thinking about using a structure like the one used for entities for endpoints as well, but it seems reasonable to have different forms for both cases.

I'd like to hear your thoughts about how to be specific for a route (considering that a route is made of both HTTP Method and Path). This way only the Path is being considered.

More general questions are:

  • I think of authorizers returning either a boolean or throwing. Do you see any case in witch this is a problem?
  • Authorizer should actually be authorizers. This way an array can be passed and if any of the authorizers provided returns true the request handler is used.

@mcollina
Copy link
Member

mcollina commented Apr 7, 2023

I don't really get your previous comment.

The first step I recommended is to add a single req.authorize(permissions) function that would check a bunch of user-specified permissions before progressing. It should throw if the current user does not have the rights to perform the operation described in permissions. I would represent permission in a similar way we have represented all other rules.

@luizgribeiro
Copy link
Contributor Author

Alright! I've just pushed with what I imagine that is the first step using the approach that you seem to be talking about.
Turns out the issue had a lot of different and incompatible ideas and I focused on what I think that would be easier to the end user/developer.

I followed the approach suggested by @ivan-tymoshenko to provide a method to be called inside the handler, but in terms of developer experience I think it would be better to do it before that and make it invisible for developers. It might be a bit more difficult to implement because of the way that we deal with request hooks and plugins at fastify core (I don't know much, it's my first time using it).

I still have a doubt about using on onRoute hook. In the way that I implemented it it looks like whenever a new route is registered by fastify a new onRequest hook is also created. Wouldn't it be enough just to use one single onRequest hook to check authorization?

@@ -61,6 +61,37 @@ async function auth (app, opts) {
}
}

app.addHook('onRoute', function () {
app.addHook('onRequest', function (request, reply, done) {
request.authorize = async () => {
Copy link
Member

Choose a reason for hiding this comment

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

I think this could be implemented as

app.decorateRequest('authorize', authorizeFn)

Instead of using the hooks.

Signed-off-by: Luiz Ribeiro <ltrindaderibeiro@gmail.com>
Signed-off-by: Luiz Ribeiro <ltrindaderibeiro@gmail.com>
@mcollina
Copy link
Member

wdyt @ivan-tymoshenko ?

packages/db-authorization/fixtures/plugin.js Show resolved Hide resolved
await request.extractUser()

const authorizationMatch = endpointRules.find(rule => {
if (rule.role === request.user['X-PLATFORMATIC-ROLE'] && rule[restMapper[request.method]]) {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if (rule.role === request.user['X-PLATFORMATIC-ROLE'] && rule[restMapper[request.method]]) {
if (rule.role === request.user[rokeKey] && rule[restMapper[request.method]]) {

return !!request.raw.url.match(endPointRegEx)
}
return false
})
Copy link
Member

Choose a reason for hiding this comment

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

The whole point of adding a decorator is to support not doing this in here. We want to provide a generic utility folks could use manually.

Then we use the data in rule.endpoint to automatically add a preHandler that call this endpoint with the given rule.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't get the part about using it manually. Do you mean something like passing the rules to req.authorize() like @ivan-tymoshenko said?

It looks a bit confusing to me to be honest. I like the idea of using preHandler to prevent calling req.authorize() inside the plugin, but I'm not sure if it is possible if it's not using all the authorization rules from a configuration files

Copy link

Choose a reason for hiding this comment

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

I find this idea confusing too... @mcollina ..😉🙂

Copy link
Member

@ivan-tymoshenko ivan-tymoshenko left a comment

Choose a reason for hiding this comment

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

I see the idea of defining all rules in one file, but I don't like the idea of mapping custom routes to the rules/entities manually. It would be impossible to maintain it with a real application. I would prefer an explicit request.authorize(rules) call.

const endpointRules = opts.rules.filter(rule => {
if (rule?.endpoint) {
const endPointRegEx = new RegExp(rule.endpoint)
return !!request.raw.url.match(endPointRegEx)
Copy link
Member

Choose a reason for hiding this comment

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

This would not work for all possible fastify routes. It should work for parametric and regexp routes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The idea behind this was to use a root/base endpoint to cover the parametric routes. I agree that it is not ideal though. A rule using the endpoint /pages would also cover /pages/pageId.
I did not think about how to cover regexp rules.

return false
})

const restMapper = {
Copy link
Member

Choose a reason for hiding this comment

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

Why only these methods?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Still wip, there is still a lot to be done on this

roleKey: 'X-PLATFORMATIC-ROLE',
anonymousRole: 'anonymous',
rules: [{
role: 'user',
Copy link
Member

Choose a reason for hiding this comment

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

I don't understand what this rule means. find, save, and delete should be related to some entity.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am not a big fan of this either. It was implemented like that to look like the rules provided for entities

Co-authored-by: Matteo Collina <matteo.collina@gmail.com>
@luizgribeiro
Copy link
Contributor Author

Hey folks!

I was still working on this but it was nice to have a review from both of you.
At this point I think that there might be a few different ideas about how this should work.
I believe that it can be worked out before advancing on this.

If the spec becomes to big it might be worth to split it in a few features to have it fully covered.

My main concerns at this point are:

  1. Where should authorization rules be kept? If it's passed to the authorize call I'd expect an array with all the roles that are allowed to access that specific endpoint (seems clean to me).
  2. What should be the structure used for securing an endpoint? In the case above an array would be enough, but with rules coming from a config file this wold be important. As @ivan-tymoshenko said, using find, save, delete for a plugin sounds weird

@mcollina
Copy link
Member

closing for no updates

@mcollina mcollina closed this May 22, 2024
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.

None yet

3 participants