1- import base64
2- from rest_framework import permissions
1+ import requests
2+ import logging
3+ import urllib .parse
34from django .conf import settings
5+ from django .core .cache import cache
6+ from rest_framework import permissions
7+
48from api import manager
59from api .models import blocklist
610from api .models .workspace import Workspace , WorkspaceMember
711
12+ logger = logging .getLogger (__name__ )
13+
814
915def get_app_status (app ):
1016 block = blocklist .Blocklist .get_blocklist (app )
@@ -41,8 +47,9 @@ def has_object_permission(self, request, view, obj):
4147 return True
4248 elif getattr (obj , "user" , None ) == request .user :
4349 return True
44- elif isinstance (obj , Workspace ) or hasattr (obj , 'workspace' ):
45- workspace = obj if isinstance (obj , Workspace ) else obj .workspace
50+ elif isinstance (obj , Workspace ) or hasattr (obj , 'workspace' ) or hasattr (obj , 'app' ):
51+ workspace = obj if isinstance (obj , Workspace ) else getattr (
52+ obj , "workspace" , None ) or getattr (getattr (obj , 'app' , None ), 'workspace' , None )
4653 if request .method in ["GET" , "HEAD" , "OPTIONS" ]:
4754 allowed_roles = ["viewer" , "member" , "admin" ]
4855 elif request .method in ["POST" , "PUT" , "PATCH" ]:
@@ -55,34 +62,43 @@ def has_object_permission(self, request, view, obj):
5562 return False
5663
5764
58- class IsServiceToken (permissions .BasePermission ):
65+ class HasOAuthScope (permissions .BasePermission ):
5966 """
60- The service token is used for internal communication between Drycc components,
61- such as the builder and Quickwit.
67+ Object-level permission to allow only requests with specific OAuth scopes.
68+ The required scopes are defined on the view as `required_oauth_scopes = ['scope1', 'scope2']`
6269 """
63-
6470 def has_permission (self , request , view ):
65- """
66- Return `True` if permission is granted, `False` otherwise.
67- """
68- auth_header = request .META .get ('HTTP_X_DRYCC_SERVICE_KEY' )
69- if not auth_header :
70- return False
71- return auth_header == settings .SERVICE_KEY
71+ required_oauth_scopes = getattr (view , 'required_oauth_scopes' , [])
72+ if not required_oauth_scopes :
73+ return True
7274
75+ auth_header = request .META .get ('HTTP_AUTHORIZATION' , '' )
76+ parts = auth_header .split ()
77+ if len (parts ) == 2 and parts [0 ].lower () == 'bearer' :
78+ token = parts [1 ]
79+ else :
80+ return False
7381
74- class IsWorkflowManager (permissions .BasePermission ):
75- """
76- View permission to allow workflow manager to perform actions
77- with a special HTTP header
78- """
82+ scopes = self .get_token_scopes (token )
83+ return set (required_oauth_scopes ).issubset (scopes )
7984
80- def has_permission (self , request , view ):
81- if request .META .get ("HTTP_AUTHORIZATION" ):
82- token = request .META .get (
83- "HTTP_AUTHORIZATION" ).split (" " )[1 ].encode ("utf8" )
84- access_key , secret_key = base64 .b85decode (token ).decode ("utf8" ).split (":" )
85- if settings .WORKFLOW_MANAGER_ACCESS_KEY == access_key :
86- if settings .WORKFLOW_MANAGER_SECRET_KEY == secret_key :
87- return True
88- return False
85+ def get_token_scopes (self , token ):
86+ def _get_scopes ():
87+ endpoint = getattr (settings , 'SOCIAL_AUTH_DRYCC_OIDC_ENDPOINT' , None )
88+ if not endpoint :
89+ return set ()
90+ oauth_introspect_url = urllib .parse .urljoin (endpoint + "/" , "introspect/" )
91+ key = getattr (settings , 'SOCIAL_AUTH_DRYCC_KEY' , '' )
92+ secret = getattr (settings , 'SOCIAL_AUTH_DRYCC_SECRET' , '' )
93+ try :
94+ resp = requests .post (
95+ oauth_introspect_url , auth = (key , secret ), data = {'token' : token }, timeout = 5 )
96+ if resp .status_code == 200 :
97+ data = resp .json ()
98+ if data .get ("active" ):
99+ return set (data .get ("scope" , "" ).split ())
100+ except Exception as e :
101+ logger .info (f"Error introspecting token: { e } " )
102+ return set ()
103+ return cache .get_or_set (
104+ f"drycc_oauth_scopes_v2_{ token } " , _get_scopes , settings .DRYCC_CACHE_USER_TIME )
0 commit comments