Skip to content

Commit

Permalink
Tentative refactoring of core handlers for clarity
Browse files Browse the repository at this point in the history
  • Loading branch information
shaunagm committed Dec 20, 2021
1 parent a072cbc commit 137eb08
Showing 1 changed file with 117 additions and 78 deletions.
195 changes: 117 additions & 78 deletions metagov/metagov/core/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,50 +33,121 @@ def handle_incoming_webhook(self, request) -> Optional[HttpResponse]:


class MetagovRequestHandler:

def __init__(self, app: MetagovApp):
self.app = app

### Incoming Webhook Logic ###

def pass_to_plugin_instance(self, request, community_slug, community_platform_id):
"""Passes incoming request to a specific pluin instance as well as all pending GovernanceProcesses
associated with that plugin."""

# Pass request a specific plugin instance
community = self.app.get_community(community_slug)
plugin = community.get_plugin(plugin_name, community_platform_id)
response = None
if plugin._webhook_receiver_function:
webhook_handler_fn = getattr(plugin, plugin._webhook_receiver_function)
logger.debug(f"Passing webhook request to: {plugin}")
try:
response = webhook_handler_fn(request)
except Exception as e:
logger.error(f"Plugin '{plugin}' failed to process webhook: {e}")

# Pass request to all pending GovernanceProcesses for this plugin, too
for cls in plugin._process_registry.values():
processes = cls.objects.filter(plugin=plugin, status=ProcessStatus.PENDING.value)
if processes.count():
logger.debug(f"{processes.count()} pending processes for plugin instance '{plugin}'")
for process in processes:
try:
process.receive_webhook(request)
except Exception as e:
logger.error(f"Process '{process}' failed to process webhook: {e}")

return response

def pass_to_platformwide_handlers(self, plugin_name, request):
"""Passes request to platform-wide handlers (e.g. Slack, where there is one webhook for all
communities)."""
plugin_handler = self._get_plugin_request_handler(plugin_name)
if not plugin_handler:
logger.error(f"No request handler found for '{plugin_name}'")
else:
try:
return plugin_handler.handle_incoming_webhook(request) or HttpResponse()
except NotImplementedError:
logger.error(f"Webhook handler not implemented for '{plugin_name}'")

def handle_incoming_webhook(
self, request, plugin_name, community_slug=None, community_platform_id=None
) -> HttpResponse:
logger.debug(f"Received webhook request: {plugin_name} ({community_platform_id or 'no community_platform_id'}) ({community_slug or 'no community'})")

if community_slug:
# Pass request a specific plugin instance
community = self.app.get_community(community_slug)
plugin = community.get_plugin(plugin_name, community_platform_id)
response = None
if plugin._webhook_receiver_function:
webhook_handler_fn = getattr(plugin, plugin._webhook_receiver_function)
logger.debug(f"Passing webhook request to: {plugin}")
try:
response = webhook_handler_fn(request)
except Exception as e:
logger.error(f"Plugin '{plugin}' failed to process webhook: {e}")

# Pass request to all pending GovernanceProcesses for this plugin, too
for cls in plugin._process_registry.values():
processes = cls.objects.filter(plugin=plugin, status=ProcessStatus.PENDING.value)
if processes.count():
logger.debug(f"{processes.count()} pending processes for plugin instance '{plugin}'")
for process in processes:
try:
process.receive_webhook(request)
except Exception as e:
logger.error(f"Process '{process}' failed to process webhook: {e}")

response = self.pass_to_plugin_instance(request, community_slug, community_platform_id)
return response or HttpResponse()

# Pass request to platform-wide handlers (e.g. Slack, where there is one webhook for all communities)
response = self.pass_to_platformwide_handlers(request, plugin_name)
return response or HttpResponseNotFound()

### Oauth Logic ###

def get_community_for_installation(self, request, plugin_name, community_slug):

community_slug or request.GET.get("community")
logger.debug(f"Handling {type} authorization request for {plugin_name} to community '{community_slug or 'new community'}'")

if community_slug:
try:
return Community.objects.get(slug=community_slug)
except Community.DoesNotExist:
return HttpResponseBadRequest(f"No such community: {community_slug}")

community = Community.objects.create()
# TODO: delete the community if installation fails.
logger.debug(f"Created new community for installing {plugin_name}: {community}")
return community

def create_state(self, request, redirect_uri, metagov_id, community_slug):

# where to redirect after auth flow is done
redirect_uri = redirect_uri or request.GET.get("redirect_uri")
# metagov_id of logged in user, if exists
metagov_id = metagov_id or request.GET.get("metagov_id")
# state to pass along to final redirect after auth flow is done
received_state = request.GET.get("state")
request.session["received_authorize_state"] = received_state

# Create the state
nonce = utils.generate_nonce()
state = {
nonce: {"community": community_slug, "redirect_uri": redirect_uri, "type": type, "metagov_id": metagov_id}
}
state_str = json.dumps(state).encode("ascii")
state_encoded = base64.b64encode(state_str).decode("ascii")
# Store nonce in the session so we can validate the callback request
request.session["nonce"] = nonce

return state_encoded

def get_plugin_handler(self, plugin_name):
"""Takes a plugin name and finds the associated handler or returns the appropriate
response."""

if not plugin_registry.get(plugin_name):
return None, HttpResponseBadRequest(f"No such plugin: {plugin_name}")

logger.debug(f"Handling {type} authorization request for {plugin_name}'")

plugin_handler = self._get_plugin_request_handler(plugin_name)
if not plugin_handler:
logger.error(f"No request handler found for '{plugin_name}'")
return HttpResponseNotFound()
try:
return plugin_handler.handle_incoming_webhook(request) or HttpResponse()
except NotImplementedError:
logger.error(f"Webhook handler not implemented for '{plugin_name}'")
return HttpResponseNotFound()
return None, HttpResponseNotFound()

return plugin_handler, None


def handle_oauth_authorize(
self,
Expand All @@ -97,54 +168,25 @@ def handle_oauth_authorize(
:param community_slug: community to install to (optional for installation, ignored for user login)
:param metagov_id: metagov_id of logged in user, if exists
"""
if not plugin_registry.get(plugin_name):
return HttpResponseBadRequest(f"No such plugin: {plugin_name}")
# auth type (user login or app installation)

type = type or request.GET.get("type", AuthorizationType.APP_INSTALL)
# community to install to (optional for installation, ignored for user login)
community_slug = community_slug or request.GET.get("community")
# where to redirect after auth flow is done
redirect_uri = redirect_uri or request.GET.get("redirect_uri")
# metagov_id of logged in user, if exists
metagov_id = metagov_id or request.GET.get("metagov_id")
# state to pass along to final redirect after auth flow is done
received_state = request.GET.get("state")
request.session["received_authorize_state"] = received_state
if type != AuthorizationType.APP_INSTALL and type != AuthorizationType.USER_LOGIN:
return HttpResponseBadRequest(
f"Parameter 'type' must be '{AuthorizationType.APP_INSTALL}' or '{AuthorizationType.USER_LOGIN}'"
)
logger.debug(f"Handling {type} authorization request for {plugin_name}'")

logger.debug(f"Handling {type} authorization request for {plugin_name} to community '{community_slug or 'new community'}'")

plugin_handler = self._get_plugin_request_handler(plugin_name)
if not plugin_handler:
logger.error(f"No request handler found for '{plugin_name}'")
return HttpResponseNotFound()
plugin_handler, response = self.get_plugin_handler(plugin_name)
if response: # get_plugin_handler will return either a plugin_handler or a bad response
return response

community = None
if type == AuthorizationType.APP_INSTALL:
if community_slug:
try:
community = Community.objects.get(slug=community_slug)
except Community.DoesNotExist:
return HttpResponseBadRequest(f"No such community: {community_slug}")
else:
community = Community.objects.create()
# TODO: delete the community if installation fails.
logger.debug(f"Created new community for installing {plugin_name}: {community}")
community_slug = str(community.slug)

# Create the state
nonce = utils.generate_nonce()
state = {
nonce: {"community": community_slug, "redirect_uri": redirect_uri, "type": type, "metagov_id": metagov_id}
}
state_str = json.dumps(state).encode("ascii")
state_encoded = base64.b64encode(state_str).decode("ascii")
# Store nonce in the session so we can validate the callback request
request.session["nonce"] = nonce
community = self.get_community_for_installation(request, plugin_name, community_slug)
community_slug = str(community.slug) if community else None

# Create state and return it to redirect url
state_encoded = self.create_state(request, redirect_uri, metagov_id, community_slug)
url = plugin_handler.construct_oauth_authorize_url(type=type, community=community)
logger.debug(f"Redirecting to {url}")
return redirect_with_params(url, state=state_encoded)
Expand All @@ -153,23 +195,20 @@ def handle_oauth_callback(self, request, plugin_name) -> HttpResponse:
"""
Oauth2 callback for installation and/or user login
"""

logger.debug(f"Plugin auth callback received request: {request.GET}")
if not plugin_registry.get(plugin_name):
return HttpResponseBadRequest(f"No such plugin: {plugin_name}")

plugin_handler, response = self.get_plugin_handler(plugin_name)
if response: # get_plugin_handler will return either a plugin_handler or a bad response
return response

# Validate and decode state
state_str = request.GET.get("state")
if not state_str:
return HttpResponseBadRequest("missing state")

# Validate and decode state
nonce = request.session.get("nonce")
if not nonce:
return HttpResponseBadRequest("missing session nonce")

plugin_handler = self._get_plugin_request_handler(plugin_name)
if not plugin_handler:
logger.error(f"No request handler found for '{plugin_name}'")
return HttpResponseNotFound()

state = OAuthState(state_str, nonce)
logger.debug(f"Decoded state: {state.__dict__}")

Expand Down

0 comments on commit 137eb08

Please sign in to comment.