Skip to content

Commit

Permalink
Merge commit '8507cf9ae558ea4f93940f7284dcbd004c19e065'
Browse files Browse the repository at this point in the history
  • Loading branch information
dill0wn committed Mar 27, 2024
2 parents 0ca5177 + 8507cf9 commit f797888
Show file tree
Hide file tree
Showing 3 changed files with 160 additions and 6 deletions.
105 changes: 104 additions & 1 deletion Allura/allura/controllers/rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@
"""REST Controller"""
import json
import logging
from datetime import datetime, timedelta
from urllib.parse import unquote, urlparse, parse_qs

import oauthlib.oauth1
import oauthlib.oauth2
import oauthlib.common
from paste.util.converters import asbool
from webob import exc
Expand All @@ -41,7 +43,7 @@
from allura.lib.project_create_helpers import make_newproject_schema, deserialize_project, create_project_with_attrs
from allura.lib.security import has_access
import six
from datetime import datetime


log = logging.getLogger(__name__)

Expand All @@ -50,6 +52,7 @@ class RestController:

def __init__(self):
self.oauth = OAuthNegotiator()
self.oauth2 = Oauth2Negotiator()
self.auth = AuthRestController()

def _check_security(self):
Expand Down Expand Up @@ -234,6 +237,70 @@ def dummy_access_token(self) -> str:
return 'dummy-access-token-for-oauthlib'


class Oauth2Validator(oauthlib.oauth2.RequestValidator):
def validate_client_id(self, client_id: str, request: oauthlib.common.Request) -> bool:
return M.OAuth2Client.query.get(client_id=client_id) is not None

def validate_redirect_uri(self, client_id, redirect_uri, request, *args, **kwargs):
return True

def validate_response_type(self, client_id: str, response_type: str, client: oauthlib.oauth2.Client, request: oauthlib.common.Request, *args, **kwargs) -> bool:
res_type = M.OAuth2Client.query.get(client_id=client_id).response_type
return res_type == response_type

def validate_scopes(self, client_id: str, scopes, client: oauthlib.oauth2.Client, request: oauthlib.common.Request, *args, **kwargs) -> bool:
return True

def validate_grant_type(self, client_id: str, grant_type: str, client: oauthlib.oauth2.Client, request: oauthlib.common.Request, *args, **kwargs) -> bool:
return True

def get_default_scopes(self, client_id: str, request: oauthlib.common.Request, *args, **kwargs):
return []

def get_default_redirect_uri(self, client_id: str, request: oauthlib.common.Request, *args, **kwargs) -> str:
return request.uri

def invalidate_authorization_code(self, client_id: str, code: str, request: oauthlib.common.Request, *args, **kwargs) -> None:
return

def authenticate_client(self, request: oauthlib.common.Request, *args, **kwargs) -> bool:
client_id = request.body['client_id']
client = M.OAuth2Client.query.get(client_id=client_id)
if not client:
return False

request.client = client
return True

def validate_code(self, client_id: str, code: str, client: oauthlib.oauth2.Client, request: oauthlib.common.Request, *args, **kwargs) -> bool:
return True

def confirm_redirect_uri(self, client_id: str, code: str, redirect_uri: str, client: oauthlib.oauth2.Client, request: oauthlib.common.Request, *args, **kwargs) -> bool:
return True

def save_authorization_code(self, client_id: str, code, request: oauthlib.common.Request, *args, **kwargs) -> None:
auth_code = M.OAuth2AuthorizationCode(
client_id = client_id,
authorization_code = code['code'],
expires_at = datetime.utcnow() + timedelta(minutes=10)
)
request.client_id = client_id
session(auth_code).flush()
log.info(f'Saving new authorization code for client: {request.client_id}')

def save_bearer_token(self, token, request: oauthlib.common.Request, *args, **kwargs) -> object:
bearer_token = M.OAuth2Token(
client_id = request.client_id,
scopes = token.get('scope', []),
access_token = token.get('access_token'),
refresh_token = token.get('refresh_token'),
expires_at = datetime.utcfromtimestamp(token.get('expires_in'))
)

session(bearer_token).flush()
log.info(f'Saving new bearer token for client: {request.client_id}')


class AlluraOauth1Server(oauthlib.oauth1.WebApplicationServer):
def validate_request_token_request(self, request):
# this is NOT standard OAuth1 (spec requires the param)
Expand Down Expand Up @@ -370,6 +437,42 @@ def access_token(self, **kw):
return body


class Oauth2Negotiator:
@property
def server(self):
return oauthlib.oauth2.WebApplicationServer(Oauth2Validator())

@expose('json:')
def authorize(self, **kwargs):
security.require_authenticated()
json_body = None

if request.body:
# We need to decode the request body and convert it to a dict because Turbogears creates it as bytes
# and oauthlib will treat it as x-www-form-urlencoded format.
decoded_body = str(request.body, 'utf-8')
json_body = json.loads(decoded_body)

try:
scopes, credentials = self.server.validate_authorization_request(uri=request.url, http_method=request.method, headers=request.headers, body=json_body)
headers, body, status = self.server.create_authorization_response(
uri=request.url, http_method=request.method, body=json_body, headers=request.headers, scopes=[], credentials=credentials
)
except Exception as e:
log.exception(e)

@expose('json:')
@require_post()
def token(self, **kwargs):
try:
decoded_body = str(request.body, 'utf-8')
json_body = json.loads(decoded_body)
headers, body, status = self.server.create_token_response(uri=request.url, http_method=request.method, body=json_body, headers=request.headers)
return body
except Exception as e:
log.exception(e)


def rest_has_access(obj, user, perm):
"""
Helper function that encapsulates common functionality for has_access API
Expand Down
11 changes: 6 additions & 5 deletions Allura/allura/model/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
from .repository import Repository, RepositoryImplementation, CommitStatus
from .repository import MergeRequest, GitLikeTree
from .stats import Stats
from .oauth import OAuthToken, OAuthConsumerToken, OAuthRequestToken, OAuthAccessToken
from .oauth import OAuthToken, OAuthConsumerToken, OAuthRequestToken, OAuthAccessToken, OAuth2Client, OAuth2AuthorizationCode, OAuth2Token
from .monq_model import MonQTask
from .webhook import Webhook
from .multifactor import TotpKey
Expand All @@ -56,7 +56,8 @@
'DiscussionAttachment', 'BaseAttachment', 'AuthGlobals', 'User', 'ProjectRole', 'EmailAddress',
'AuditLog', 'AlluraUserProperty', 'File', 'Notification', 'Mailbox', 'Repository',
'RepositoryImplementation', 'CommitStatus', 'MergeRequest', 'GitLikeTree', 'Stats', 'OAuthToken', 'OAuthConsumerToken',
'OAuthRequestToken', 'OAuthAccessToken', 'MonQTask', 'Webhook', 'ACE', 'ACL', 'EVERYONE', 'ALL_PERMISSIONS',
'DENY_ALL', 'MarkdownCache', 'main_doc_session', 'main_orm_session', 'project_doc_session', 'project_orm_session',
'artifact_orm_session', 'repository_orm_session', 'task_orm_session', 'ArtifactSessionExtension', 'repository',
'repo_refresh', 'SiteNotification', 'TotpKey', 'UserLoginDetails', 'main_explicitflush_orm_session']
'OAuthRequestToken', 'OAuthAccessToken', 'OAuth2Client', 'OAuth2AuthorizationCode', 'OAuth2Token', 'MonQTask', 'Webhook',
'ACE', 'ACL', 'EVERYONE', 'ALL_PERMISSIONS', 'DENY_ALL', 'MarkdownCache', 'main_doc_session', 'main_orm_session',
'project_doc_session', 'project_orm_session', 'artifact_orm_session', 'repository_orm_session', 'task_orm_session',
'ArtifactSessionExtension', 'repository', 'repo_refresh', 'SiteNotification', 'TotpKey', 'UserLoginDetails',
'main_explicitflush_orm_session']
50 changes: 50 additions & 0 deletions Allura/allura/model/oauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,56 @@ def can_import_forum(self):
return False


class OAuth2Client(MappedClass):
class __mongometa__:
session = main_orm_session
name = 'oauth2_client'

query: 'Query[OAuth2Client]'

_id = FieldProperty(S.ObjectId)
client_id = FieldProperty(str)
user_id: ObjectId = AlluraUserProperty(if_missing=lambda: c.user._id)
grant_type = FieldProperty(str)
response_type = FieldProperty(str)
scopes = FieldProperty([str])
redirect_uris = FieldProperty([str])

class OAuth2AuthorizationCode(MappedClass):
class __mongometa__:
session = main_orm_session
name = 'oauth2_authorization_code'

query: 'Query[OAuth2AuthorizationCode]'

_id = FieldProperty(S.ObjectId)
client_id = FieldProperty(str)
user_id: ObjectId = AlluraUserProperty(if_missing=lambda: c.user._id)
scopes = FieldProperty([str])
redirect_uri = FieldProperty(str)
authorization_code = FieldProperty(str)
expires_at = FieldProperty(S.DateTime)
# For PKCE support
challenge = FieldProperty(str)
challenge_method = FieldProperty(str)


class OAuth2Token(MappedClass):
class __mongometa__:
session = main_orm_session
name = 'oauth2_token'

query: 'Query[OAuth2Token]'

_id = FieldProperty(S.ObjectId)
client_id = FieldProperty(str)
user_id: ObjectId = AlluraUserProperty(if_missing=lambda: c.user._id)
scopes = FieldProperty([str])
access_token = FieldProperty(str)
refresh_token = FieldProperty(str)
expires_at = FieldProperty(S.DateTime)


def dummy_oauths():
from allura.controllers.rest import Oauth1Validator
# oauthlib implementation NEEDS these "dummy" values. If a request comes in with an invalid param, it runs
Expand Down

0 comments on commit f797888

Please sign in to comment.