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

App specific scopes #395

Closed

Conversation

JensTimmerman
Copy link
Contributor

@JensTimmerman JensTimmerman commented Jun 29, 2016

rebased code from honestbleeps on latest master.
fixes #216 #130
possibly also helps to resolve issues like #318

from django.db import migrations, models


class Migration(migrations.Migration):
Copy link
Member

Choose a reason for hiding this comment

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

Should there also be a data migration that sets the scopes for existing applications?

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 believe so, if no custom scope is set, the default scopes will still be used.

@JensTimmerman
Copy link
Contributor Author

I'm not sure if it makes sense to try to include this in the AuthorizationView, it usually doesn't work in the initial_data, because when you don't know the client_id, you can't do anything yet,
and it doesn't seem in it's place in the validation. Can I get some feedback on this?

@honestbleeps
Copy link
Contributor

Thanks so much for doing the work to rebase my PR! I've been extremely busy and ended up getting tied up too much to do the work that I felt might be necessary before vetting it as a PR to the original master branch, which is why it was just a PR against my own.

It'd be great to see this make it in!

@honestbleeps
Copy link
Contributor

I should add, just in case it's not obvious from the code:

There's no mechanism built into this to handle granting an application access to the requested scopes. In my actual use of this code, I utilized the django admin for manually granting scopes to apps. I'd trigger an email for any app that requested scopes we considered "elevated", and I'd manually copy them over in the django admin.

A cleaner and more universally appropriate UX for app creators to request specific scopes (and see which are considered "elevated") and for site admins to grant those scopes would be optimal. However, this PR is still sufficient for people to use as long as they understand that they need to roll their own solution for granting scopes (and tracking requests of said scopes!)

Jens Timmerman added 2 commits July 5, 2016 14:32
    - filter scopes in both the get and validate functions, so the skip_authorization apps also get filtered scopes
    - added documentation
    - added a filter scope method to the application model
    - also filter the scopes in the the validate_scopes function in the provider
@mkjpryor-stfc
Copy link

@JensTimmerman: I am also keen on this functionality.

It seems that the problem with the Travis build is the use of application rather than self...

===================
You can restrict each individual application to a separate subset of scopes.
Using the ``allowed_scopes = 'scope1 scope2'`` field on the ``Application`` model you can limit the scopes an application
can get. This is usefull if you trust an application owner only for e.g. read access and don't want
Copy link
Contributor

Choose a reason for hiding this comment

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

s/usefull/useful

@JensTimmerman
Copy link
Contributor Author

@mkjpryor-stfc I'll look into fixing some more issues I encountered while working on this. But I'm awaiting some feedback from the original developers on this.

@mkjpryor-stfc
Copy link

@JensTimmerman: No worries. Let me know if there's anything I can do to help this along.

"""
get an application from a client_id, this caches the applications
"""
app = self._application_cache.get(client_id, get_application_model().objects.get(client_id=client_id))
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this doesn't actually cache it, it is still recreated every time

@honestbleeps
Copy link
Contributor

@JensTimmerman are you waiting on me for feedback? I'm the original author but I don't recall seeing any open questions... perhaps I missed it?

or do you mean the original authors of django-oauth-toolkit?

@JensTimmerman
Copy link
Contributor Author

I mean the original authors of django-oauth-toolkit.

@coveralls
Copy link

coveralls commented Nov 8, 2016

Coverage Status

Coverage decreased (-0.03%) to 97.461% when pulling 5027287 on JensTimmerman:app-specific-scopes into 707a601 on evonove:master.

@coveralls
Copy link

coveralls commented Nov 8, 2016

Coverage Status

Coverage decreased (-0.03%) to 97.461% when pulling 1a90b03 on JensTimmerman:app-specific-scopes into 707a601 on evonove:master.

@@ -6,7 +6,7 @@
from django.conf import settings
from django.core.urlresolvers import reverse
from django.db import models, transaction
from django.utils import timezone
from django.utils import timezone, six, six
Copy link
Contributor

Choose a reason for hiding this comment

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

You're importing six twice.

Copy link
Contributor

Choose a reason for hiding this comment

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

Perhaps you should use import six to avoid issues if/when Django moves/removes six.

Copy link
Contributor

Choose a reason for hiding this comment

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

Considering DOT already depends on six being available, it would make the most sense to just import six instead.

oauth2_provider/models.py Show resolved Hide resolved
@@ -216,6 +238,8 @@ def allow_scopes(self, scopes):
"""
if not scopes:
return True
if isinstance(scopes, six.string_types):
Copy link
Contributor

Choose a reason for hiding this comment

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

When is scopes not a string? Are there steps that can be taken to ensure that it is always a string?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

if it is not a string it has to be a list, so then there is no need to split it into a list.

Copy link
Contributor

Choose a reason for hiding this comment

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

I get that. Looking more closely at the existing code and docstring, this method expects an iterable of strings. You are weakening that contract to allow for a single string. However, the only place where you are consuming this is in tests. This change seems unnecessary. To keep the contract clean, if you do use allow_scopes, I would ensure that any new usage sends an iterable of strings rather than a delimited string.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

well a string is in fact an iterable, using it by accident might show up weird errors, this is a bit more developer friendly in that it will do the thing that you wanted to happen when you pass a string. But you are also right that it might encourage the use of a single string and confuse more people when they see inconsitent usage of 'scopes'

def test_get_application(self):
"""Test the get_application method from the AuthorizationView"""
self.assertEqual(self.app_foo_1, AuthorizationView().get_application(self.app_foo_1.client_id))
# test twice to test the cache
Copy link
Contributor

Choose a reason for hiding this comment

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

Unless you're checking DB query counts or calls to the cache, you aren't truly validating caching behavior.

@@ -76,6 +77,12 @@ def setUp(self):
self.app_bar_1 = self._create_application('app bar_user 1', self.bar_user)
self.app_bar_2 = self._create_application('app bar_user 2', self.bar_user)

def test_get_application(self):
"""Test the get_application method from the AuthorizationView"""
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be helpful if the docstring explained what about the method you are testing. For example, Verify the method returns the application corresponding to the given client ID.

@@ -41,6 +41,77 @@ def test_allow_scopes(self):
self.assertTrue(access_token.allow_scopes(['write', 'read', 'read']))
self.assertTrue(access_token.allow_scopes([]))
self.assertFalse(access_token.allow_scopes(['write', 'destroy']))
self.assertTrue(access_token.allow_scopes('read write'))
Copy link
Contributor

Choose a reason for hiding this comment

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

This test is a great candidate for parameterization.

self.assertFalse(access_token.allow_scopes('write destroy'))


def test_get_allowed_scopes_from_scopes(self):
Copy link
Contributor

Choose a reason for hiding this comment

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

This test is also a great candidate for parameterization.

Copy link
Contributor

Choose a reason for hiding this comment

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

This test also needs a docstring, and, based on the length (60 lines!), should be split into multiple test cases.


skip_authorization_completely = False
def getscopes(self):
Copy link
Contributor

Choose a reason for hiding this comment

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

get_scopes

"""
get an application from a client_id, this caches the applications
"""
if client_id not in self._application_cache:
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested alternative:

application = self._application_cache.get(client_id)

if not application:
  application = get_application_model().objects.get(client_id=client_id)
  self._application_cache[client_id] = application

return application

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 think that's better. But that aside this cache can be pulled into its own PR.

@clintonb
Copy link
Contributor

@synasius @Girbons @JensTimmerman what is needed to get this merged?

@JensTimmerman
Copy link
Contributor Author

@clintonb I'm still awaiting a reply from [email protected]

@synasius
Copy link
Contributor

This is definitely a feature we want to merge but I do not like the current implementation. I've been working recently on a feature that adds support for scopes backends. You can see the code here https://github.com/evonove/django-oauth-toolkit/compare/scopes-backend and I wrote about it on a blog post: https://evonove.it/blog/django-oauth-toolkit-0110-released/

At the moment scopes can be changed through settings, but there is no way to implement a custom flow to obtain scopes (for instance I'd like to determine the available scopes based on the groups to which the user belongs).
So I introduced a scopes backend that expose an API to work with scopes (see https://github.com/evonove/django-oauth-toolkit/compare/scopes-backend#diff-8959a5c87f26bd1ebe41c94fa5d65c1cR7).
The default implementation uses the settings so it doesn't change the default DOT behavior (see https://github.com/evonove/django-oauth-toolkit/compare/scopes-backend#diff-8959a5c87f26bd1ebe41c94fa5d65c1cR40).

I'd love to base the App-specific scope feature on this new API.

What do you think about it?

@clintonb
Copy link
Contributor

@synasius this is great! It meets the immediate need, and allows for extensions that are not immediately known. When can we have it!? :)

@honestbleeps
Copy link
Contributor

I wish the distaste with my initial approach was voiced long ago, I would have gladly redone / reworked it in a better way...

in any case, I hope it eventually gets added.

@jleclanche
Copy link
Member

@honestbleeps I'm helping evonove maintain the library now, I'll be working with @synasius to land an improved version of this.

@JensTimmerman
Copy link
Contributor Author

@synasius Great, I like that approach even more! Hope to see more of it soon 👍

@jaddison
Copy link

@synasius Great suggestion, improved flexibility. What needs to be done to move it forward?

@clintonb
Copy link
Contributor

@jleclanche how can we get this merged and released?

@jleclanche
Copy link
Member

I was waiting on @synasius to finish what he mentioned before touching this.

@synasius do you need help finishing it off/taking that part over?

@jleclanche
Copy link
Member

Ok, since @synasius isn't replying, I'd like to suggest taking over his design from the scopes-backend branch.

I really like the plan outlined above. The backwards-compatible default scopes backend is also really cool.

I can't see at first glance what's missing from that commit, so I'll be testing it in a few days (I'm currently trying to release something else on the side so I'm unable to fully focus on this just yet). @clintonb, I'd love your help. If you can help testing the scopes-backend branch (which I have just rebased), verify what's left to implement, I can finish it off myself.

@synasius
Copy link
Contributor

synasius commented Feb 7, 2017

@jleclanche 😅 😅 😅
The only thing I can say is "thank you!!".

@synasius
Copy link
Contributor

synasius commented Feb 7, 2017

@jleclanche If it helps I can say that the code in scopes-backend branch it's used in a production application deployed months ago with no problems. Proper documentation is missing

@jleclanche
Copy link
Member

Excellent, good to know!

In that case I'll play around with it, write some documentation and land it hopefully this week (if you're reading this in the future and I haven't, nudge me).

Once it lands, I'm in favour of doing a 0.12 release. We can then look into builtin app-specific scopes functionality for a 1.0 release (Seeing that DOT is widely used, works well and we're already careful with backwards compatibility, I don't see much point in staying on 0.x much longer).

@synasius
Copy link
Contributor

synasius commented Feb 7, 2017

In that case I'll play around with it, write some documentation and land it hopefully this week (if you're reading this in the future and I haven't, nudge me).

👍

Once it lands, I'm in favour of doing a 0.12 release.

👍

We can then look into builtin app-specific scopes functionality for a 1.0 release (Seeing that DOT is widely used, works well and we're already careful with backwards compatibility, I don't see much point in staying on 0.x much longer).

Totally agree! We've been careful since 0.1 😄

@JensTimmerman
Copy link
Contributor Author

Great! Sounds really good to me.
So I guess I can just close this pr?

@synasius
Copy link
Contributor

synasius commented Feb 7, 2017

Guess you can close it now and reference it later, after the scopes-backend branch has been merged

@tximpa91
Copy link

tximpa91 commented Mar 1, 2018

Someone can upload an example of this beacause i am trying i cant doit . thanks

@prafulbagai
Copy link

@tximpa91 - were you able to achieve it? @anyone - A simple example would be really helpful.

@tximpa91
Copy link

Yes i managed to do it, working perfectly in production

@prafulbagai
Copy link

@tximpa91 - Could please share a sample snippet of the same. I'm not able to understand the flow of it.

@prafulbagai
Copy link

@tximpa91 - I've seen this scope backed. How do I connect it to oauth-toolkit?

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.

feature request: application specific scope restrictions