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

Support multi driver #175

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions metagov/metagov/core/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,26 @@ def get_plugin_instance(plugin_name, community, community_platform_id=None):
)


def get_configuration(config_name, **kwargs):

# if multi driver functionality is on, use httpwrapper's version of get_configuration
from django.conf import settings
if hasattr(settings, "MULTI_DRIVER") and settings.MULTI_DRIVER:
from metagov.httpwrapper.utils import get_configuration as multidriver_get_configuration
return multidriver_get_configuration(config_name, **kwargs)

# otherwise just get from environment
from metagov.settings import TESTING
default_val = TESTING if TESTING else None

return env(config_name, default=default_val)


def set_configuration(config_name, config_value, **kwargs):
# TODO: implement this as a helper method for single-driver apps
pass


# def jsonschema_to_parameters(schema):
# #arg_dict["manual_parameters"].extend(jsonschema_to_parameters(meta.input_schema
# schema = convert(schema)
Expand Down
31 changes: 31 additions & 0 deletions metagov/metagov/httpwrapper/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from django.db import IntegrityError, models
from metaov.core.models import Community


class Driver(models.Model):
readable_name = models.CharField(max_length=100, blank=True, help_text="Human-readable name for the driver")
slug = models.SlugField(
max_length=36, default=uuid.uuid4, unique=True, help_text="Unique slug identifier for the driver"
)
webhooks = models.ArrayField(models.CharField(max_length=200, blank=True))


class APIKey(models.Model):
key = models.SlugField(
max_length=36, default=uuid.uuid4, unique=True, help_text="API Key for the driver"
)
driver = models.ForeignKey(Driver, on_delete=models.CASCADE, related_name="api_keys")


class CommunityDriverLink(models.model):
driver = models.ForeignKey(to=Driver, on_delete=models.CASCADE)
community = models.OneToOneField(to=Community, on_delete=models.CASCADE)


class DriverConfig(models.model):
driver = models.ForeignKey(to=Driver, on_delete=models.CASCADE)
config_name = models.CharField(max_length=100)
config_value = models.CharField()

class Meta:
constraints = [models.UniqueConstraint(fields=["driver", "config_name"], name="unique_driver_config")]
45 changes: 45 additions & 0 deletions metagov/metagov/httpwrapper/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,48 @@ def construct_action_url(plugin_name: str, slug: str, is_public=False) -> str:

def construct_process_url(plugin_name: str, slug: str) -> str:
return f"{internal_path}/process/{plugin_name}.{slug}"


def get_driver(**kwargs):
"""Get Driver object given various inputs."""
from metagov.core.models import Community
from httpwrapper.models import Driver, CommunityDriverLink, APIKey
if "driver_instance" in **kwargs:
return kwargs.get("driver_instance")
if "driver_slug" in **kwargs:
return Driver.objects.get(slug=kwargs.get("driver_slug"))
if "api_key" in **kwargs:
api_key_object = APIKey.objects.get(key=kwargs.get("api_key"))
return api_key_object.driver
if "community" in **kwargs:
community_driver_link = CommunityDriverLink.objects.get(community=community)
return community_driver_link.driver
if "community_slug" in **kwargs:
community = Community.objects.get(slug=kwargs.get("community_slug"))
community_driver_link = CommunityDriverLink.objects.get(community=community)
return community_driver_link.driver


def get_configuration(config_name, **kwargs):
"""We look up configurations based on Driver ID. This function checks for a variety of inputs in
kwargs that can be uniquely linked to Driver ID before giving up."""
from httpwrapper.models import DriverConfig
driver = get_driver(**kwargs)
if driver:
return DriverConfig.objects.get(driver=driver, config_name=config_name)
from metagov.settings import TESTING
return TESTING if TESTING else None


def set_configuration(config_name, config_value, **kwargs):
"""For a given driver, looks up a config variable name. If a row already exists, update the value,
otherwise create the row."""
from httpwrapper.models import DriverConfig
driver = get_driver(**kwargs)
if driver:
driver_config = DriverConfig.objects.get(driver=driver, config_name=config_name)
if driver_config:
driver_config.config_value = config_value
driver_config.save()
else:
DriverConfig.objects.create(driver=driver, config_name=config_name, config_value=config_value)
8 changes: 6 additions & 2 deletions metagov/metagov/plugins/github/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def refresh_token(self):
"""Requests a new installation access token from Github using a JWT signed by private key."""
installation_id = self.config["installation_id"]
self.state.set("installation_id", installation_id)
token = get_access_token(installation_id)
token = get_access_token(installation_id, community=self.community)
self.state.set("installation_access_token", token)

def initialize(self):
Expand Down Expand Up @@ -55,7 +55,11 @@ def github_request(self, method, route, data=None, add_headers=None, refresh=Fal
"""Makes request to Github. If status code returned is 401 (bad credentials), refreshes the
access token and tries again. Refresh parameter is used to make sure we only try once."""

authorization = f"Bearer {get_jwt()}" if use_jwt else f"token {self.state.get('installation_access_token')}"
if use_jwt:
authorization = f"Bearer {get_jwt(community=self.community)}"
else:
authorization = f"token {self.state.get('installation_access_token')}"

headers = {
"Authorization": authorization,
"Accept": "application/vnd.github.v3+json"
Expand Down
20 changes: 9 additions & 11 deletions metagov/metagov/plugins/github/utils.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
""" Authentication """

import jwt, datetime, logging, requests
from django.conf import settings

from metagov.core.errors import PluginErrorInternal
from metagov.core.utils import get_configuration

import sys

TEST = 'test' in sys.argv

logger = logging.getLogger(__name__)

github_settings = settings.METAGOV_SETTINGS["GITHUB"]
PRIVATE_KEY_PATH = github_settings["PRIVATE_KEY_PATH"]
APP_ID = github_settings["APP_ID"]


def get_private_key():
def get_private_key(community):
PRIVATE_KEY_PATH = get_configuration("GITHUB_PRIVATE_KEY_PATH", community=community)
with open(PRIVATE_KEY_PATH) as f:
lines = f.readlines()
if len(lines) == 1:
Expand All @@ -24,25 +22,25 @@ def get_private_key():
return "".join(lines)


def get_jwt():
def get_jwt(community):
if TEST: return ""

payload = {
# GitHub App's identifier
"iss": APP_ID,
"iss": get_configuration("GITHUB_APP_ID, community=community),
# issued at time, 60 seconds in the past to allow for clock drift
"iat": int(datetime.datetime.now().timestamp()) - 60,
# JWT expiration time (10 minute maximum)
"exp": int(datetime.datetime.now().timestamp()) + (9 * 60)
}
return jwt.encode(payload, get_private_key(), algorithm="RS256")
return jwt.encode(payload, get_private_key(community), algorithm="RS256")


def get_access_token(installation_id):
def get_access_token(installation_id, community=community):
"""Get installation access token using installation id"""
headers = {
"Accept": "application/vnd.github.v3+json",
"Authorization": f"Bearer {get_jwt()}"
"Authorization": f"Bearer {get_jwt(community)}"
}
url = f"https://api.github.com/app/installations/{installation_id}/access_tokens"
resp = requests.request("POST", url, headers=headers)
Expand Down
22 changes: 11 additions & 11 deletions metagov/metagov/plugins/twitter/models.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,14 @@
import logging

from django.conf import settings
from metagov.core.plugin_manager import AuthorizationType, Registry, Parameters, VotingStandard
import tweepy
from metagov.core.models import AuthType, Plugin
from metagov.core.errors import PluginErrorInternal
from metagov.core.utils import get_configuration

logger = logging.getLogger(__name__)


twitter_settings = settings.METAGOV_SETTINGS["TWITTER"]

class TwitterSecrets:
api_key = twitter_settings["API_KEY"]
api_secret_key = twitter_settings["API_SECRET_KEY"]
access_token = twitter_settings["ACCESS_TOKEN"]
access_token_secret = twitter_settings["ACCESS_TOKEN_SECRET"]


"""
Expand All @@ -41,9 +34,16 @@ class Meta:
def tweepy_api(self):
if getattr(self, "api", None):
return self.api
auth = tweepy.OAuthHandler(TwitterSecrets.api_key, TwitterSecrets.api_secret_key)
auth.set_access_token(TwitterSecrets.access_token, TwitterSecrets.access_token_secret)

api_key = get_configuration("TWITTER_API_KEY", community=self.community)
api_secret_key = get_configuration("TWITTER_API_SECRET_KEY", community=self.community)
access_token = get_configuration("TWITTER_ACCESS_TOKEN", community=self.community)
access_token_secret = get_configuration("TWITTER_ACCESS_TOKEN_SECRET", community=self.community)

auth = tweepy.OAuthHandler(api_key, api_secret_key)
auth.set_access_token(access_token, access_token_secret)
self.api = tweepy.API(auth)

return self.api

def initialize(self):
Expand Down Expand Up @@ -88,7 +88,7 @@ def send_direct_message(self, user_id, text):
slug="get-user-id",
description="Gets user id of a Twitter user",
input_schema={
"type": "object",
"type": "object",
"properties": {"screen_name": {"type": "string"}},
"required": ["screen_name"]
}
Expand Down