From b4973736e0a5845e5764fd2c466e645b7ff52ef7 Mon Sep 17 00:00:00 2001 From: Shauna Gordon-McKeon Date: Fri, 17 Dec 2021 15:39:56 -0500 Subject: [PATCH 1/4] Add tentative driver models --- metagov/metagov/httpwrapper/models.py | 31 +++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 metagov/metagov/httpwrapper/models.py diff --git a/metagov/metagov/httpwrapper/models.py b/metagov/metagov/httpwrapper/models.py new file mode 100644 index 00000000..21bd1958 --- /dev/null +++ b/metagov/metagov/httpwrapper/models.py @@ -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")] From 3d3744af0315af091cf2707ee4ee45989a4a0c1e Mon Sep 17 00:00:00 2001 From: Shauna Gordon-McKeon Date: Fri, 17 Dec 2021 15:40:33 -0500 Subject: [PATCH 2/4] add get_config utils in core and httpwrapper --- metagov/metagov/core/utils.py | 20 +++++++++++++ metagov/metagov/httpwrapper/utils.py | 45 ++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/metagov/metagov/core/utils.py b/metagov/metagov/core/utils.py index c6573b43..5abacbda 100644 --- a/metagov/metagov/core/utils.py +++ b/metagov/metagov/core/utils.py @@ -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) diff --git a/metagov/metagov/httpwrapper/utils.py b/metagov/metagov/httpwrapper/utils.py index 63676797..51aea716 100644 --- a/metagov/metagov/httpwrapper/utils.py +++ b/metagov/metagov/httpwrapper/utils.py @@ -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) \ No newline at end of file From d8037251f0625004f4bb85ba33720347c82cb46d Mon Sep 17 00:00:00 2001 From: Shauna Gordon-McKeon Date: Fri, 17 Dec 2021 15:41:05 -0500 Subject: [PATCH 3/4] Swittch twitter and github plugins to use new get_config method --- metagov/metagov/plugins/github/models.py | 8 ++++++-- metagov/metagov/plugins/github/utils.py | 20 +++++++++++--------- metagov/metagov/plugins/twitter/models.py | 22 +++++++++++----------- 3 files changed, 28 insertions(+), 22 deletions(-) diff --git a/metagov/metagov/plugins/github/models.py b/metagov/metagov/plugins/github/models.py index 376521d1..42d5332b 100644 --- a/metagov/metagov/plugins/github/models.py +++ b/metagov/metagov/plugins/github/models.py @@ -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): @@ -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" diff --git a/metagov/metagov/plugins/github/utils.py b/metagov/metagov/plugins/github/utils.py index 6be9837b..0e1378ed 100644 --- a/metagov/metagov/plugins/github/utils.py +++ b/metagov/metagov/plugins/github/utils.py @@ -1,8 +1,9 @@ """ 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 @@ -10,12 +11,13 @@ 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: @@ -24,25 +26,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_PRIVATE_KEY_PATH", 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) diff --git a/metagov/metagov/plugins/twitter/models.py b/metagov/metagov/plugins/twitter/models.py index 37e29a34..194faf83 100644 --- a/metagov/metagov/plugins/twitter/models.py +++ b/metagov/metagov/plugins/twitter/models.py @@ -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"] """ @@ -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): @@ -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"] } From a072cbccafde6b8f759daf010b6b2cc59c1cd387 Mon Sep 17 00:00:00 2001 From: Shauna Gordon-McKeon Date: Fri, 17 Dec 2021 15:42:40 -0500 Subject: [PATCH 4/4] Cleanup --- metagov/metagov/plugins/github/utils.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/metagov/metagov/plugins/github/utils.py b/metagov/metagov/plugins/github/utils.py index 0e1378ed..e58136a7 100644 --- a/metagov/metagov/plugins/github/utils.py +++ b/metagov/metagov/plugins/github/utils.py @@ -11,10 +11,6 @@ logger = logging.getLogger(__name__) -APP_ID = github_settings["APP_ID"] - - - def get_private_key(community): PRIVATE_KEY_PATH = get_configuration("GITHUB_PRIVATE_KEY_PATH", community=community) @@ -31,7 +27,7 @@ def get_jwt(community): payload = { # GitHub App's identifier - "iss": get_configuration("GITHUB_PRIVATE_KEY_PATH", community=community), + "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)