Skip to content

Commit 131fa08

Browse files
authored
16527 - check token cache endpoint before generating new service account token (#2369)
1 parent ef23906 commit 131fa08

22 files changed

+174
-2
lines changed

auth-api/requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ pyrsistent==0.19.3
5757
python-dotenv==1.0.0
5858
python-jose==3.3.0
5959
pytz==2022.7.1
60+
python-memcached==1.59
61+
redis==4.3.6
6062
requests==2.31.0
6163
rsa==4.9
6264
semver==2.13.0

auth-api/requirements/dev.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,4 @@ FreezeGun
2323
lovely-pytest-docker
2424
Faker
2525
pytest-asyncio==0.21.0
26+
mock

auth-api/requirements/prod.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,5 @@ launchdarkly-server-sdk
3131
protobuf~=3.19.5
3232
aiohttp
3333
orjson
34+
python-memcached
35+
redis

auth-api/src/auth_api/config.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,17 @@ class _Config: # pylint: disable=too-few-public-methods
9797
KEYCLOAK_ADMIN_USERNAME = os.getenv('SBC_AUTH_ADMIN_CLIENT_ID')
9898
KEYCLOAK_ADMIN_SECRET = os.getenv('SBC_AUTH_ADMIN_CLIENT_SECRET')
9999

100+
# keycloak service account token lifepan
101+
try:
102+
CACHE_DEFAULT_TIMEOUT = int(os.getenv('ACCESS_TOKEN_LIFESPAN'))
103+
except: # pylint:disable=bare-except # noqa: B901, E722
104+
CACHE_DEFAULT_TIMEOUT = 300
105+
106+
CACHE_MEMCACHED_SERVERS = os.getenv('CACHE_MEMCACHED_SERVERS')
107+
108+
CACHE_REDIS_HOST = os.getenv('CACHE_REDIS_HOST')
109+
CACHE_REDIS_PORT = os.getenv('CACHE_REDIS_PORT')
110+
100111
# Service account details
101112
KEYCLOAK_SERVICE_ACCOUNT_ID = os.getenv('SBC_AUTH_ADMIN_CLIENT_ID')
102113
KEYCLOAK_SERVICE_ACCOUNT_SECRET = os.getenv('SBC_AUTH_ADMIN_CLIENT_SECRET')

auth-api/src/auth_api/services/rest_service.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030

3131
from auth_api.exceptions import ServiceUnavailableException
3232
from auth_api.utils.enums import AuthHeaderType, ContentType
33+
from auth_api.utils.cache import cache
3334

3435
RETRY_ADAPTER = HTTPAdapter(max_retries=Retry(total=5, backoff_factor=1, status_forcelist=[404]))
3536

@@ -163,11 +164,13 @@ def get(endpoint, token=None, # pylint: disable=too-many-arguments
163164
return response
164165

165166
@staticmethod
167+
@cache.cached(query_string=True)
166168
def get_service_account_token(config_id='KEYCLOAK_SERVICE_ACCOUNT_ID',
167169
config_secret='KEYCLOAK_SERVICE_ACCOUNT_SECRET') -> str:
168170
"""Generate a service account token."""
169171
kc_service_id = current_app.config.get(config_id)
170172
kc_secret = current_app.config.get(config_secret)
173+
171174
issuer_url = current_app.config.get('JWT_OIDC_ISSUER')
172175
token_url = issuer_url + '/protocol/openid-connect/token'
173176
auth_response = requests.post(token_url, auth=(kc_service_id, kc_secret), headers={

auth-api/src/auth_api/utils/cache.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,23 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414
"""Bring in the common cache."""
15+
import os
1516
from flask_caching import Cache
1617

18+
cache_servers = os.environ.get('CACHE_MEMCACHED_SERVERS')
1719

18-
# lower case name as used by convention in most Flask apps
19-
cache = Cache(config={'CACHE_TYPE': 'simple'}) # pylint: disable=invalid-name
20+
if cache_servers:
21+
cache = Cache(config={'CACHE_TYPE': 'MemcachedCache',
22+
'CACHE_MEMCACHED_SERVERS': cache_servers.split(',')})
23+
else:
24+
25+
redis_host = os.environ.get('CACHE_REDIS_HOST')
26+
redis_port = os.environ.get('CACHE_REDIS_PORT')
27+
28+
if redis_host and redis_port:
29+
cache = Cache(config={'CACHE_TYPE': 'RedisCache',
30+
'CACHE_REDIS_HOST': redis_host,
31+
'CACHE_REDIS_PORT': redis_port})
32+
33+
else:
34+
cache = Cache(config={'CACHE_TYPE': 'simple'}) # pylint: disable=invalid-name

auth-api/tests/conftest.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@
2828
from auth_api.models import db as _db
2929

3030

31+
def mock_token(config_id='', config_secret=''):
32+
"""Mock token generator."""
33+
return 'TOKEN....'
34+
35+
3136
@pytest.fixture(scope='session')
3237
def app():
3338
"""Return a session-wide application configured in TEST mode."""

auth-api/tests/unit/api/test_task.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
Test-Suite to ensure that the /tasks endpoint is working as expected.
1717
"""
1818
import json
19+
import mock
1920

2021
import datetime as dt
2122
import pytest
@@ -35,6 +36,8 @@
3536
from tests.utilities.factory_utils import (
3637
factory_auth_header, factory_task_model, factory_task_service, factory_user_model, factory_user_model_with_contact,
3738
patch_token_info)
39+
from tests.conftest import mock_token
40+
3841

3942
current_dt = dt.datetime.now()
4043
current_date_str = current_dt.strftime('%Y-%m-%d')
@@ -128,6 +131,7 @@ def test_fetch_tasks_end_of_day(client, jwt, session):
128131
assert rv.status_code == http_status.HTTP_200_OK
129132

130133

134+
@mock.patch('auth_api.services.affiliation_invitation.RestService.get_service_account_token', mock_token)
131135
def test_put_task_org(client, jwt, session, keycloak_mock, monkeypatch): # pylint:disable=unused-argument
132136
"""Assert that the task can be updated."""
133137
# 1. Create User
@@ -185,6 +189,7 @@ def test_put_task_org(client, jwt, session, keycloak_mock, monkeypatch): # pyli
185189
assert rv.json.get('orgStatus') == OrgStatus.ACTIVE.value
186190

187191

192+
@mock.patch('auth_api.services.affiliation_invitation.RestService.get_service_account_token', mock_token)
188193
def test_put_task_org_on_hold(client, jwt, session, keycloak_mock, monkeypatch): # pylint:disable=unused-argument
189194
"""Assert that the task can be updated."""
190195
# 1. Create User
@@ -244,6 +249,7 @@ def test_put_task_org_on_hold(client, jwt, session, keycloak_mock, monkeypatch):
244249
assert rv.json.get('orgStatus') == OrgStatus.PENDING_STAFF_REVIEW.value
245250

246251

252+
@mock.patch('auth_api.services.affiliation_invitation.RestService.get_service_account_token', mock_token)
247253
def test_put_task_product(client, jwt, session, keycloak_mock, monkeypatch): # pylint:disable=unused-argument
248254
"""Assert that the task can be updated."""
249255
# 1. Create User
@@ -344,6 +350,7 @@ def test_fetch_task(client, jwt, session): # pylint:disable=unused-argument
344350
(TestJwtClaims.public_bceid_user, AccessType.GOVN.value, TaskAction.AFFIDAVIT_REVIEW.value),
345351
(TestJwtClaims.public_user_role, AccessType.GOVN.value, TaskAction.ACCOUNT_REVIEW.value),
346352
])
353+
@mock.patch('auth_api.services.affiliation_invitation.RestService.get_service_account_token', mock_token)
347354
def test_tasks_on_account_creation(client, jwt, session, keycloak_mock, # pylint:disable=unused-argument
348355
monkeypatch, user_token, access_type, expected_task_action):
349356
"""Assert that tasks are created."""

auth-api/tests/unit/api/test_user.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import pytest
2222
import time
2323
import uuid
24+
import mock
2425

2526
from sqlalchemy import event
2627

@@ -47,6 +48,7 @@
4748
factory_invitation_anonymous, factory_membership_model, factory_org_model, factory_product_model,
4849
factory_user_model, patch_token_info)
4950
from tests.utilities.sqlalchemy import clear_event_listeners
51+
from tests.conftest import mock_token
5052

5153
KEYCLOAK_SERVICE = KeycloakService()
5254

@@ -59,6 +61,7 @@ def test_add_user(client, jwt, session): # pylint:disable=unused-argument
5961
assert schema_utils.validate(rv.json, 'user_response')[0]
6062

6163

64+
@mock.patch('auth_api.services.affiliation_invitation.RestService.get_service_account_token', mock_token)
6265
def test_add_user_staff_org(client, jwt, session, keycloak_mock, monkeypatch):
6366
"""Assert that adding and removing membership to a staff org occurs."""
6467
# Create a user and org
@@ -686,6 +689,7 @@ def test_delete_unknown_user_returns_404(client, jwt, session): # pylint:disabl
686689

687690

688691
@pytest.mark.parametrize('environment', ['test', None])
692+
@mock.patch('auth_api.services.affiliation_invitation.RestService.get_service_account_token', mock_token)
689693
def test_delete_user_as_only_admin_returns_400(client, jwt, session, keycloak_mock,
690694
monkeypatch, environment): # pylint:disable=unused-argument
691695
"""Test if the user is the only owner of a team assert status is 400."""
@@ -717,6 +721,7 @@ def test_delete_user_as_only_admin_returns_400(client, jwt, session, keycloak_mo
717721

718722

719723
@pytest.mark.parametrize('environment', ['test', None])
724+
@mock.patch('auth_api.services.affiliation_invitation.RestService.get_service_account_token', mock_token)
720725
def test_delete_user_is_member_returns_204(client, jwt, session, keycloak_mock,
721726
monkeypatch, environment): # pylint:disable=unused-argument
722727
"""Test if the user is the member of a team assert status is 204."""

auth-api/tests/unit/api/test_user_settings.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
Test-Suite to ensure that the /users endpoint is working as expected.
1818
"""
1919
import copy
20+
import mock
2021

2122
from auth_api import status as http_status
2223
from auth_api.models import ContactLink as ContactLinkModel
@@ -25,8 +26,10 @@
2526
from tests.utilities.factory_scenarios import TestJwtClaims, TestOrgInfo, TestUserInfo
2627
from tests.utilities.factory_utils import (
2728
factory_auth_header, factory_contact_model, factory_user_model, patch_token_info)
29+
from tests.conftest import mock_token
2830

2931

32+
@mock.patch('auth_api.services.affiliation_invitation.RestService.get_service_account_token', mock_token)
3033
def test_get_user_settings(client, jwt, session, keycloak_mock, monkeypatch): # pylint:disable=unused-argument
3134
"""Assert that get works and adhere to schema."""
3235
user_model = factory_user_model(user_info=TestUserInfo.user_test)

auth-api/tests/unit/services/test_affidavit.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
1616
Test suite to ensure that the affidavit service routines are working as expected.
1717
"""
18+
import mock
1819
from auth_api.services import Affidavit as AffidavitService
1920
from auth_api.models import Task as TaskModel
2021
from auth_api.services import Org as OrgService
@@ -23,6 +24,7 @@
2324
TaskRelationshipStatus)
2425
from tests.utilities.factory_scenarios import TestAffidavit, TestJwtClaims, TestOrgInfo, TestUserInfo # noqa: I005
2526
from tests.utilities.factory_utils import factory_user_model, factory_user_model_with_contact, patch_token_info
27+
from tests.conftest import mock_token
2628

2729

2830
def test_create_affidavit(session, keycloak_mock, monkeypatch): # pylint:disable=unused-argument
@@ -56,6 +58,7 @@ def test_create_affidavit_duplicate(session, keycloak_mock, monkeypatch): # pyl
5658
assert affidavit3.as_dict().get('status', None) == AffidavitStatus.PENDING.value
5759

5860

61+
@mock.patch('auth_api.services.affiliation_invitation.RestService.get_service_account_token', mock_token)
5962
def test_approve_org(session, keycloak_mock, monkeypatch): # pylint:disable=unused-argument
6063
"""Assert that an Affidavit can be approved."""
6164
user = factory_user_model_with_contact(user_info=TestUserInfo.user_bceid_tester)
@@ -82,6 +85,7 @@ def test_approve_org(session, keycloak_mock, monkeypatch): # pylint:disable=unu
8285
assert affidavit['status'] == AffidavitStatus.APPROVED.value
8386

8487

88+
@mock.patch('auth_api.services.affiliation_invitation.RestService.get_service_account_token', mock_token)
8589
def test_task_creation(session, keycloak_mock, monkeypatch): # pylint:disable=unused-argument
8690
"""Assert that affidavit reupload creates new task."""
8791
user = factory_user_model_with_contact()
@@ -102,6 +106,7 @@ def test_task_creation(session, keycloak_mock, monkeypatch): # pylint:disable=u
102106
assert TaskModel.find_by_task_for_account(org_id, TaskStatus.OPEN.value) is not None
103107

104108

109+
@mock.patch('auth_api.services.affiliation_invitation.RestService.get_service_account_token', mock_token)
105110
def test_reject_org(session, keycloak_mock, monkeypatch): # pylint:disable=unused-argument
106111
"""Assert that an Affidavit can be rejected."""
107112
user = factory_user_model_with_contact(user_info=TestUserInfo.user_bceid_tester)

auth-api/tests/unit/services/test_affiliation.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"""
1818
from unittest.mock import ANY, patch
1919

20+
import mock
2021
import pytest
2122

2223
from auth_api.exceptions import BusinessException
@@ -33,6 +34,7 @@
3334
from tests.utilities.factory_utils import (
3435
convert_org_to_staff_org, factory_entity_service, factory_membership_model, factory_org_service,
3536
factory_user_model_with_contact, patch_get_firms_parties, patch_token_info)
37+
from tests.conftest import mock_token
3638

3739

3840
@pytest.mark.parametrize('environment', ['test', None])
@@ -163,6 +165,7 @@ def test_create_affiliation_exists(session, auth_mock, environment): # pylint:d
163165
assert affiliation
164166

165167

168+
@mock.patch('auth_api.services.affiliation_invitation.RestService.get_service_account_token', mock_token)
166169
@pytest.mark.parametrize('environment', ['test', None])
167170
def test_create_affiliation_firms(session, auth_mock, monkeypatch, environment): # pylint:disable=unused-argument
168171
"""Assert that an Affiliation can be created."""
@@ -223,6 +226,7 @@ def test_create_affiliation_staff_sbc_staff(
223226
affiliation = AffiliationService.create_affiliation(org_id, business_identifier, environment)
224227

225228

229+
@mock.patch('auth_api.services.affiliation_invitation.RestService.get_service_account_token', mock_token)
226230
@pytest.mark.parametrize('environment', ['test', None])
227231
def test_create_affiliation_firms_party_with_additional_space(session, auth_mock,
228232
monkeypatch, environment):
@@ -245,6 +249,7 @@ def test_create_affiliation_firms_party_with_additional_space(session, auth_mock
245249
assert affiliation.as_dict()['organization']['id'] == org_dictionary['id']
246250

247251

252+
@mock.patch('auth_api.services.affiliation_invitation.RestService.get_service_account_token', mock_token)
248253
@pytest.mark.parametrize('environment', ['test', None])
249254
def test_create_affiliation_firms_party_not_valid(session, auth_mock, monkeypatch,
250255
environment):

0 commit comments

Comments
 (0)