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/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")] 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 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..e58136a7 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,9 @@ 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 +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) 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"] }