Repository: dj-stripe/dj-stripe

Issue:
Is dj-stripe suitable, when each user needs to have different stripe API keys?
Suppose, you're building something like ERP system with Django, where you've got to support each user to have their different Stripe API keys.

From what it looks, dj-stripe simply hard-codes the API keys to the settings via environment variables, so it seems unsuitable for cases, where each user will have different Stripe API keys. Is it true? Or is there a way to use dj-stripe with different Stripe API keys for each user?


Discussion:
This is an issue for me as well. My first thought was to use connected accounts but there are some webhook issues #740 that seem like more than a quick fix.

I am attempting to build an app using django-tenants which routes database schemas for each client by their domain. This would work well if the settings were dynamic... If i could just tell djstripe which settings to use. This is similar to the old issue #542 which had the plans in the settings file.

Django settings are supposed to be immutable so changing them dynamically isn't recommend. I thought about overriding the djstripe_settings.py with my own version but that will require somehow passing in the request from each view... ugh.

Is there an approach I am not considering? I noticed #740 was just moved from milestone 2.3.0 to 2.x.0. 

django-tenants and dj-stripe would be a real power couple if they were compatible. 
Yeah, dj-stripe currently doesn't have any support for multiple API keys.

I haven't thought too much about how to implement it, but a first step would be supporting multiple keys from a single stripe account.

You both seem to be talking about multi-tenancy though, would be is another extract step of complication.
#1262 has a working prototype of what a multi-api-key architecture looks like. I'm actively using it in a test environment.

The way it works is by relying on the new APIKey model I implemented a while back: API keys are in the database. API keys have an account. Objects fetched using specific API keys will gain their account as owner account. By default, the API key used to retrieve/refresh items will be filtered by account.

Resolving patch:
diff --git a/djstripe/models/account.py b/djstripe/models/account.py
--- a/djstripe/models/account.py
+++ b/djstripe/models/account.py
@@ -3,6 +3,7 @@
 
 from .. import enums
 from .. import settings as djstripe_settings
+from ..enums import APIKeyType
 from ..fields import JSONField, StripeCurrencyCodeField, StripeEnumField
 from .api import APIKey
 from .base import StripeModel
@@ -85,6 +86,18 @@ class Account(StripeModel):
         help_text="Details on the acceptance of the Stripe Services Agreement",
     )
 
+    @property
+    def default_api_key(self) -> str:
+        return self.get_default_api_key()
+
+    def get_default_api_key(self) -> str:
+        api_key = APIKey.objects.filter(
+            djstripe_owner_account=self, type=APIKeyType.secret
+        ).first()
+        if api_key:
+            return api_key.secret
+        return djstripe_settings.get_default_api_key(self.livemode)
+
     @property
     def business_url(self):
         """
diff --git a/djstripe/models/base.py b/djstripe/models/base.py
--- a/djstripe/models/base.py
+++ b/djstripe/models/base.py
@@ -93,6 +93,10 @@ def get_stripe_dashboard_url(self):
 
     @property
     def default_api_key(self):
+        # If the class is abstract (StripeModel), fall back to default key.
+        if not self._meta.abstract:
+            if self.djstripe_owner_account:
+                return self.djstripe_owner_account.get_default_api_key()
         return djstripe_settings.get_default_api_key(self.livemode)
 
     def _get_stripe_account_id(self, api_key=None):
@@ -148,14 +152,13 @@ def api_retrieve(self, api_key=None, stripe_account=None):
             for which this request is being made.
         :type stripe_account: string
         """
-        api_key = api_key or self.default_api_key
         # Prefer passed in stripe_account if set.
         if not stripe_account:
             stripe_account = self._get_stripe_account_id(api_key)
 
         return self.stripe_class.retrieve(
             id=self.id,
-            api_key=api_key,
+            api_key=api_key or self.default_api_key,
             expand=self.expand_fields,
             stripe_account=stripe_account,
         )


Test patch:
diff --git a/tests/test_api_keys.py b/tests/test_api_keys.py
--- a/tests/test_api_keys.py
+++ b/tests/test_api_keys.py
@@ -19,9 +19,7 @@ def test_global_api_keys_live_mode(self):
         self.assertEqual(djstripe_settings.STRIPE_LIVE_MODE, True)
         self.assertEqual(djstripe_settings.STRIPE_SECRET_KEY, "sk_live_foo")
         self.assertEqual(djstripe_settings.LIVE_API_KEY, "sk_live_foo")
-        self.assertEqual(
-            models.StripeModel(livemode=True).default_api_key, "sk_live_foo"
-        )
+        self.assertEqual(models.Account(livemode=True).default_api_key, "sk_live_foo")
 
     @override_settings(
         STRIPE_TEST_SECRET_KEY="sk_test_foo",
@@ -33,9 +31,7 @@ def test_global_api_keys_test_mode(self):
         self.assertEqual(djstripe_settings.STRIPE_LIVE_MODE, False)
         self.assertEqual(djstripe_settings.STRIPE_SECRET_KEY, "sk_test_foo")
         self.assertEqual(djstripe_settings.TEST_API_KEY, "sk_test_foo")
-        self.assertEqual(
-            models.StripeModel(livemode=False).default_api_key, "sk_test_foo"
-        )
+        self.assertEqual(models.Account(livemode=False).default_api_key, "sk_test_foo")
 
     @override_settings(
         STRIPE_TEST_SECRET_KEY="sk_test_foo",
@@ -52,9 +48,7 @@ def test_api_key_live_mode(self):
         self.assertEqual(djstripe_settings.STRIPE_SECRET_KEY, "sk_live_foo")
         self.assertEqual(djstripe_settings.STRIPE_PUBLIC_KEY, "pk_live_foo")
         self.assertEqual(djstripe_settings.LIVE_API_KEY, "sk_live_foo")
-        self.assertEqual(
-            models.StripeModel(livemode=True).default_api_key, "sk_live_foo"
-        )
+        self.assertEqual(models.Account(livemode=True).default_api_key, "sk_live_foo")
 
     @override_settings(
         STRIPE_TEST_SECRET_KEY="sk_test_foo",
@@ -71,9 +65,7 @@ def test_secret_key_test_mode(self):
         self.assertEqual(djstripe_settings.STRIPE_SECRET_KEY, "sk_test_foo")
         self.assertEqual(djstripe_settings.STRIPE_PUBLIC_KEY, "pk_test_foo")
         self.assertEqual(djstripe_settings.TEST_API_KEY, "sk_test_foo")
-        self.assertEqual(
-            models.StripeModel(livemode=False).default_api_key, "sk_test_foo"
-        )
+        self.assertEqual(models.Account(livemode=False).default_api_key, "sk_test_foo")
 
     def tearDown(self):
         reload(djstripe_settings)
diff --git a/tests/test_apikey.py b/tests/test_apikey.py
--- a/tests/test_apikey.py
+++ b/tests/test_apikey.py
@@ -1,6 +1,9 @@
 """
 dj-stripe APIKey model tests
 """
+from copy import deepcopy
+from unittest.mock import patch
+
 import pytest
 from django.test import TestCase
 
@@ -8,32 +11,24 @@
 from djstripe.models import Account, APIKey
 from djstripe.models.api import get_api_key_details_by_prefix
 
-from . import default_account
+from . import FAKE_ACCOUNT, FAKE_FILEUPLOAD_ICON, default_account
 
+# avoid literal api keys to prevent git secret scanners false-positives
+SK_TEST = "sk_test_" + "XXXXXXXXXXXXXXXXXXXX1234"
+SK_LIVE = "sk_live_" + "XXXXXXXXXXXXXXXXXXXX5678"
+RK_TEST = "rk_test_" + "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX9876"
+RK_LIVE = "rk_live_" + "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX5432"
+PK_TEST = "pk_test_" + "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXAAAA"
+PK_LIVE = "pk_live_" + "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXBBBB"
 
-def test_get_api_key_details_by_prefix():
-    # avoid literal api keys to prevent git secret scanners false-positives
-    assert get_api_key_details_by_prefix("sk_test_" + "XXXXXXXXXXXXXXXXXXXX1234") == (
-        APIKeyType.secret,
-        False,
-    )
 
-    assert get_api_key_details_by_prefix("sk_live_" + "XXXXXXXXXXXXXXXXXXXX1234") == (
-        APIKeyType.secret,
-        True,
-    )
-    assert get_api_key_details_by_prefix(
-        "rk_test_" + "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX9876"
-    ) == (APIKeyType.restricted, False)
-    assert get_api_key_details_by_prefix(
-        "rk_live_" + "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX9876"
-    ) == (APIKeyType.restricted, True)
-    assert get_api_key_details_by_prefix(
-        "pk_test_" + "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXAAAA"
-    ) == (APIKeyType.publishable, False)
-    assert get_api_key_details_by_prefix(
-        "pk_live_" + "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXBBBB"
-    ) == (APIKeyType.publishable, True)
+def test_get_api_key_details_by_prefix():
+    assert get_api_key_details_by_prefix(SK_TEST) == (APIKeyType.secret, False)
+    assert get_api_key_details_by_prefix(SK_LIVE) == (APIKeyType.secret, True)
+    assert get_api_key_details_by_prefix(RK_TEST) == (APIKeyType.restricted, False)
+    assert get_api_key_details_by_prefix(RK_LIVE) == (APIKeyType.restricted, True)
+    assert get_api_key_details_by_prefix(PK_TEST) == (APIKeyType.publishable, False)
+    assert get_api_key_details_by_prefix(PK_LIVE) == (APIKeyType.publishable, True)
 
 
 def test_get_api_key_details_by_prefix_bad_values():
@@ -45,20 +40,27 @@ def test_get_api_key_details_by_prefix_bad_values():
         get_api_key_details_by_prefix("rk_nope_1234")
 
 
+def test_clean_public_apikey():
+    key = APIKey(type=APIKeyType.publishable, livemode=False, secret=PK_TEST)
+    assert not key.djstripe_owner_account
+    key.clean()
+    assert not key.djstripe_owner_account
+
+
 class APIKeyTest(TestCase):
     def setUp(self):
         self.account = default_account()
         self.apikey_test = APIKey.objects.create(
             type=APIKeyType.secret,
             name="Test Secret Key",
-            secret="sk_test_" + "0123456789XXXXXXXXXX1234",
+            secret=SK_TEST,
             livemode=False,
             djstripe_owner_account=self.account,
         )
         self.apikey_live = APIKey.objects.create(
             type=APIKeyType.secret,
             name="Live Secret Key",
-            secret="sk_live_" + "0123456789XXXXXXXXXX9876",
+            secret=SK_LIVE,
             livemode=True,
             djstripe_owner_account=self.account,
         )
@@ -75,7 +77,7 @@ def test_get_stripe_dashboard_url(self):
 
     def test_secret_redacted(self):
         self.assertEqual(self.apikey_test.secret_redacted, "sk_test_...1234")
-        self.assertEqual(self.apikey_live.secret_redacted, "sk_live_...9876")
+        self.assertEqual(self.apikey_live.secret_redacted, "sk_live_...5678")
 
     def test_secret_not_in_str(self):
         assert self.apikey_test.secret not in str(self.apikey_test)
@@ -84,3 +86,14 @@ def test_secret_not_in_str(self):
     def test_get_account_by_api_key(self):
         account = Account.get_or_retrieve_for_api_key(self.apikey_test.secret)
         assert account == self.account
+
+    @patch(
+        "stripe.Account.retrieve",
+        return_value=deepcopy(FAKE_ACCOUNT),
+    )
+    @patch("stripe.FileUpload.retrieve", return_value=deepcopy(FAKE_FILEUPLOAD_ICON))
+    def test_refresh_account(self, fileupload_retrieve_mock, account_retrieve_mock):
+        self.apikey_test.djstripe_owner_account = None
+        self.apikey_test.save()
+        self.apikey_test.clean()
+        assert self.apikey_test.djstripe_owner_account.id == FAKE_ACCOUNT["id"]