Browse Source

[sso] require sso permissions on API session auth (#4448)

This enforces the SSO permissions that were previously implemented in the server-rendered views.
David Cramer 8 years ago
parent
commit
902e2f8394

+ 1 - 0
CHANGES

@@ -18,6 +18,7 @@ API Changes
 - Add ``/organizations/{org}/repositories/`` endpoint.
 - Add ``/organizations/{org}/repositories/{repo}/commits/`` endpoint.
 - Add ``/projects/{org}/{project}/releases/{version}/commits/`` endpoint.
+- SSO restrictions are now applied across session-based API authentication.
 
 Schema Changes
 ~~~~~~~~~~~~~~

+ 17 - 0
src/sentry/api/bases/organization.py

@@ -1,5 +1,7 @@
 from __future__ import absolute_import
 
+from rest_framework.exceptions import NotAuthenticated
+
 from sentry.api.base import Endpoint
 from sentry.api.exceptions import ResourceDoesNotExist
 from sentry.api.permissions import ScopedPermission
@@ -7,6 +9,7 @@ from sentry.app import raven
 from sentry.auth import access
 from sentry.models import Organization, OrganizationStatus
 from sentry.models.apikey import ROOT_KEY
+from sentry.utils import auth
 
 
 class OrganizationPermission(ScopedPermission):
@@ -17,6 +20,17 @@ class OrganizationPermission(ScopedPermission):
         'DELETE': ['org:delete'],
     }
 
+    def needs_sso(self, request, organization):
+        # XXX(dcramer): this is very similar to the server-rendered views
+        # logic for checking valid SSO
+        if not request.access.requires_sso:
+            return False
+        if not auth.has_completed_sso(request, organization.id):
+            return True
+        if not request.access.sso_is_valid:
+            return True
+        return False
+
     def has_object_permission(self, request, view, organization):
         if request.user and request.user.is_authenticated() and request.auth:
             request.access = access.from_request(
@@ -30,6 +44,9 @@ class OrganizationPermission(ScopedPermission):
 
         else:
             request.access = access.from_request(request, organization)
+            # session auth needs to confirm various permissions
+            if request.user.is_authenticated() and self.needs_sso(request, organization):
+                raise NotAuthenticated(detail='Must login via SSO')
 
         allowed_scopes = set(self.scope_map.get(request.method, []))
         return any(request.access.has_scope(s) for s in allowed_scopes)

+ 9 - 24
src/sentry/api/bases/project.py

@@ -2,14 +2,13 @@ from __future__ import absolute_import
 
 from sentry.api.base import Endpoint
 from sentry.api.exceptions import ResourceDoesNotExist
-from sentry.api.permissions import ScopedPermission
 from sentry.app import raven
-from sentry.auth import access
 from sentry.models import Project, ProjectStatus
-from sentry.models.apikey import ROOT_KEY
 
+from .team import TeamPermission
 
-class ProjectPermission(ScopedPermission):
+
+class ProjectPermission(TeamPermission):
     scope_map = {
         'GET': ['project:read', 'project:write', 'project:delete'],
         'POST': ['project:write', 'project:delete'],
@@ -18,24 +17,8 @@ class ProjectPermission(ScopedPermission):
     }
 
     def has_object_permission(self, request, view, project):
-        if request.user and request.user.is_authenticated() and request.auth:
-            request.access = access.from_request(
-                request, project.organization, scopes=request.auth.get_scopes(),
-            )
-
-        elif request.auth:
-            if request.auth is ROOT_KEY:
-                return True
-            return request.auth.organization_id == project.organization_id
-
-        else:
-            request.access = access.from_request(request, project.organization)
-
-        allowed_scopes = set(self.scope_map.get(request.method, []))
-        return any(
-            request.access.has_team_scope(project.team, s)
-            for s in allowed_scopes
-        )
+        return super(ProjectPermission, self).has_object_permission(
+            request, view, project.team)
 
 
 class ProjectReleasePermission(ProjectPermission):
@@ -71,16 +54,18 @@ class ProjectEndpoint(Endpoint):
 
     def convert_args(self, request, organization_slug, project_slug, *args, **kwargs):
         try:
-            project = Project.objects.get_from_cache(
+            project = Project.objects.filter(
                 organization__slug=organization_slug,
                 slug=project_slug,
-            )
+            ).select_related('organization', 'team').get()
         except Project.DoesNotExist:
             raise ResourceDoesNotExist
 
         if project.status != ProjectStatus.VISIBLE:
             raise ResourceDoesNotExist
 
+        project.team.organization = project.organization
+
         self.check_object_permissions(request, project)
 
         raven.tags_context({

+ 11 - 14
src/sentry/api/bases/team.py

@@ -2,14 +2,14 @@ from __future__ import absolute_import
 
 from sentry.api.base import Endpoint
 from sentry.api.exceptions import ResourceDoesNotExist
-from sentry.api.permissions import ScopedPermission
 from sentry.app import raven
-from sentry.auth import access
 from sentry.models import Team, TeamStatus
 from sentry.models.apikey import ROOT_KEY
 
+from .organization import OrganizationPermission
 
-class TeamPermission(ScopedPermission):
+
+class TeamPermission(OrganizationPermission):
     scope_map = {
         'GET': ['team:read', 'team:write', 'team:delete'],
         'POST': ['team:write', 'team:delete'],
@@ -18,18 +18,15 @@ class TeamPermission(ScopedPermission):
     }
 
     def has_object_permission(self, request, view, team):
-        if request.user and request.user.is_authenticated() and request.auth:
-            request.access = access.from_request(
-                request, team.organization, scopes=request.auth.get_scopes(),
-            )
+        result = super(TeamPermission, self).has_object_permission(
+            request, view, team.organization)
+        if not result:
+            return result
 
-        elif request.auth:
+        if not (request.user and request.user.is_authenticated()) and request.auth:
             if request.auth is ROOT_KEY:
                 return True
-            return request.auth.organization_id == team.organization_id
-
-        else:
-            request.access = access.from_request(request, team.organization)
+            return request.auth.organization_id == team.organization.id
 
         allowed_scopes = set(self.scope_map.get(request.method, []))
         return any(
@@ -43,10 +40,10 @@ class TeamEndpoint(Endpoint):
 
     def convert_args(self, request, organization_slug, team_slug, *args, **kwargs):
         try:
-            team = Team.objects.get(
+            team = Team.objects.filter(
                 organization__slug=organization_slug,
                 slug=team_slug,
-            )
+            ).select_related('organization').get()
         except Team.DoesNotExist:
             raise ResourceDoesNotExist
 

+ 60 - 0
tests/integration/test_api.py

@@ -0,0 +1,60 @@
+from __future__ import absolute_import
+
+import six
+
+from sentry.models import AuthIdentity, AuthProvider
+from sentry.testutils import AuthProviderTestCase
+from sentry.utils.auth import SSO_SESSION_KEY
+
+
+class AuthenticationTest(AuthProviderTestCase):
+    def test_sso_auth_required(self):
+        user = self.create_user('foo@example.com', is_superuser=False)
+        organization = self.create_organization(name='foo')
+        team = self.create_team(name='bar', organization=organization)
+        project = self.create_project(name='baz', organization=organization,
+                                      team=team)
+        member = self.create_member(user=user, organization=organization,
+                                    teams=[team])
+        setattr(member.flags, 'sso:linked', True)
+        member.save()
+
+        auth_provider = AuthProvider.objects.create(
+            organization=organization,
+            provider='dummy',
+            flags=0,
+        )
+
+        AuthIdentity.objects.create(
+            auth_provider=auth_provider,
+            user=user,
+        )
+
+        self.login_as(user)
+
+        paths = (
+            '/api/0/organizations/{}/'.format(organization.slug),
+            '/api/0/projects/{}/{}/'.format(organization.slug, project.slug),
+            '/api/0/teams/{}/{}/'.format(organization.slug, team.slug),
+        )
+
+        for path in paths:
+            # we should be redirecting the user to the authentication form as they
+            # haven't verified this specific organization
+            resp = self.client.get(path)
+            assert resp.status_code == 401, (resp.status_code, resp.content)
+
+        # superuser should still require SSO as they're a member of the org
+        user.update(is_superuser=True)
+        for path in paths:
+            resp = self.client.get(path)
+            assert resp.status_code == 401, (resp.status_code, resp.content)
+
+        # XXX(dcramer): using internal API as exposing a request object is hard
+        self.session[SSO_SESSION_KEY] = six.text_type(organization.id)
+        self.save_session()
+
+        # now that SSO is marked as complete, we should be able to access dash
+        for path in paths:
+            resp = self.client.get(path)
+            assert resp.status_code == 200, (path, resp.status_code, resp.content)

+ 0 - 1
tests/integration/test_sso.py

@@ -7,7 +7,6 @@ from sentry.testutils import AuthProviderTestCase
 from sentry.utils.auth import SSO_SESSION_KEY
 
 
-# TODO(dcramer): this is an integration test
 class OrganizationAuthLoginTest(AuthProviderTestCase):
     def test_sso_auth_required(self):
         user = self.create_user('foo@example.com', is_superuser=False)

+ 5 - 1
tests/sentry/api/endpoints/test_organization_member_details.py

@@ -41,7 +41,11 @@ class UpdateOrganizationMemberTest(APITestCase):
             user=member,
             role='member',
         )
-        AuthProvider.objects.create(organization=organization, provider='dummy')
+        AuthProvider.objects.create(
+            organization=organization,
+            provider='dummy',
+            flags=1,
+        )
 
         path = reverse('sentry-api-0-organization-member-details', args=[organization.slug, member_om.id])