diff --git a/.env.example b/.env.example index 95759fe9..df326738 100644 --- a/.env.example +++ b/.env.example @@ -16,4 +16,14 @@ CLAMAV_TCP_ADDR=localhost REDIS_HOST=localhost REDIS_PORT=6379 REDIS_DB= -REDIS_PASSWORD= \ No newline at end of file +REDIS_PASSWORD= + +# eFiling Hub settings +EFILING_HUB_TOKEN_BASE_URL='' +EFILING_HUB_REALM='' +EFILING_HUB_CLIENT_ID='' +EFILING_HUB_CLIENT_SECRET='' +EFILING_HUB_API_BASE_URL='' + +# BCE ID test accounts for localdev +EFILING_BCEID= \ No newline at end of file diff --git a/edivorce/apps/core/efilinghub.py b/edivorce/apps/core/efilinghub.py new file mode 100644 index 00000000..6f2970c7 --- /dev/null +++ b/edivorce/apps/core/efilinghub.py @@ -0,0 +1,241 @@ +import json +import requests +import logging +import uuid + +from django.conf import settings +from django.core.exceptions import PermissionDenied +from django.urls import reverse + +logger = logging.getLogger(__name__) + +PACKAGE_DOCUMENT_FORMAT = { + "name": "string", + "type": "WNC", + "isAmendment": "false", + "isSupremeCourtScheduling": "false", + "data": {}, + "md5": "string" +} + +PACKAGE_PARTY_FORMAT = { + "partyType": "IND", + "roleType": "CLA", + "firstName": "", + "middleName": "", + "lastName": "", +} + +PACKAGE_FORMAT = { + "clientAppName": "Online Divorce Assistant", + "filingPackage": { + "documents": [], + "court": { + "location": "1211", + "level": "P", + "courtClass": "F", + "division": "I", + "fileNumber": "1234", + "participatingClass": "string" + }, + "parties": [] + }, + "navigationUrls": { + "success": "string", + "error": "string", + "cancel": "string" + } +} + + +class EFilingHub: + + def __init__(self): + self.client_id = settings.EFILING_HUB_CLIENT_ID + self.client_secret = settings.EFILING_HUB_CLIENT_SECRET + self.token_base_url = settings.EFILING_HUB_TOKEN_BASE_URL + self.token_realm = settings.EFILING_HUB_REALM + self.api_base_url = settings.EFILING_HUB_API_BASE_URL + + self.submission_id = None + + def _get_token(self, request): + payload = 'client_id={}&grant_type=client_credentials&client_secret={}'.format(self.client_id, + self.client_secret) + headers = {'Content-Type': 'application/x-www-form-urlencoded'} + + url = '{}/auth/realms/{}/protocol/openid-connect/token'.format(self.token_base_url, self.token_realm) + + response = requests.post(url, headers=headers, data=payload) + logging.debug('EFH - Get Token {}'.format(response.status_code)) + if response.status_code == 200: + response = json.loads(response.text) + + # save in session .. lets just assume that current user is authenticated + if 'access_token' in response: + request.session['access_token'] = response['access_token'] + if 'refresh_token' in response: + request.session['refresh_token'] = response['refresh_token'] + + return True + return False + + def _refresh_token(self, request): + refresh_token = request.session.get('refresh_token', None) + if not refresh_token: + return False + + payload = 'client_id={}&grant_type=refresh_token&client_secret={}&refresh_token={}'.format( + self.client_id, + self.client_secret, + refresh_token) + headers = {'Content-Type': 'application/x-www-form-urlencoded'} + + url = '{}/auth/realms/{}/protocol/openid-connect/token'.format(self.token_base_url, self.token_realm) + + response = requests.post(url, headers=headers, data=payload) + logging.debug('EFH - Get Refresh Token {}'.format(response.status_code)) + + response = json.loads(response.text) + + # save in session .. lets just assume that current user is authenticated + if 'access_token' in response: + request.session['access_token'] = response['access_token'] + if 'refresh_token' in response: + request.session['refresh_token'] = response['refresh_token'] + + return True + return False + + def _get_api(self, request, url, transaction_id, bce_id, headers, data=None, files=None): + # make sure we have a session + access_token = request.session.get('access_token', None) + if not access_token: + if not self._get_token(request): + return False + + access_token = request.session.get('access_token', None) + headers.update({ + 'X-Transaction-Id': transaction_id, + 'X-User-Id': bce_id, + 'Authorization': 'Bearer {}'.format(access_token) + }) + + if not data: + data = {} + + response = requests.post(url, headers=headers, data=data, files=files) + logging.debug('EFH - Get API {} {}'.format(response.status_code, response.text)) + + if response.status_code == 401: + # not authorized .. try refreshing token + if self._refresh_token(request): + access_token = request.session.get('access_token', None) + headers.update({ + 'X-Transaction-Id': transaction_id, + 'X-User-Id': bce_id, + 'Authorization': 'Bearer {}'.format(access_token) + }) + + response = requests.post(url, headers=headers, data=data, files=files) + logging.debug('EFH - Get API Retry {} {}'.format(response.status_code, response.text)) + + return response + + def _get_transaction(self, request): + """ + Get the current transaction id stored in session, otherwise generate one. + :param request: + :return: + """ + guid = request.session.get('transaction_id', None) + if not guid: + guid = str(uuid.uuid4()) + request.session['transaction_id'] = guid + return guid + + def _get_bceid(self, request): + + def _get_raw_bceid(request): + is_localdev = settings.DEPLOYMENT_TYPE in ['localdev', 'minishift'] + if is_localdev: + # to integrate with the Test eFiling Hub, we need a valid BCEID which is + # unavailable for a local eDivorce environment. Use an env specified mapping + # to figure out what we should pass through to eFiling Hub. This BCEID username + # needs to match with what you will be logging in with to the Test BCEID environment. + username = request.session.get('login_name', None) + if username: + if username in settings.EFILING_BCEID: + return settings.EFILING_BCEID[username] + return request.session.get('fake_bceid_guid', None) + return request.session.get('smgov_userguid', None) + + guid = _get_raw_bceid(request) + if guid: + return str(uuid.UUID(guid)) + return guid + + def _format_package(self, request, files, parties): + documents = [] + for file in files: + document = PACKAGE_DOCUMENT_FORMAT.copy() + document['name'] = file[1][0] + documents.append(document) + package = PACKAGE_FORMAT.copy() + package['filingPackage']['documents'] = documents + if parties: + package['filingPackage']['parties'] = parties + # update return urls + package['navigationUrls']['success'] = request.build_absolute_uri( + reverse('dashboard_nav', args=['check_with_registry'])) + package['navigationUrls']['error'] = request.build_absolute_uri( + reverse('dashboard_nav', args=['check_with_registry'])) + package['navigationUrls']['cancel'] = request.build_absolute_uri( + reverse('dashboard_nav', args=['check_with_registry'])) + + return package + + # -- EFILING HUB INTERFACE -- + + def upload(self, request, files, parties=None): + """ + Does an initial upload of documents and gets the generated eFiling Hub url. + :param parties: + :param request: + :param files: Files need to be a list of tuples in the form ('files': (filename, filecontent)) + :return: The url for redirect and any error messages + """ + # Find the transaction id .. this will be a unique guid generated by eDivorce thats passed to Efiling Hub. We + # will tie it to the session. + + transaction_id = self._get_transaction(request) + bce_id = self._get_bceid(request) + + # if bce_id is None .. we basically have an anonymous user so raise an error + if bce_id is None: + raise PermissionDenied() + + response = self._get_api(request, '{}/submission/documents'.format(self.api_base_url), transaction_id, bce_id, + headers={}, files=files) + if response.status_code == 200: + response = json.loads(response.text) + + if "submissionId" in response and response['submissionId'] != "": + # get the redirect url + headers = { + 'Content-Type': 'application/json' + } + package_data = self._format_package(request, files, parties=parties) + url = '{}/submission/{}/generateUrl'.format(self.api_base_url, response['submissionId']) + response = self._get_api(request, url, transaction_id, bce_id, headers=headers, + data=json.dumps(package_data)) + + if response.status_code == 200: + response = json.loads(response.text) + return response['efilingUrl'], 'success' + + response = json.loads(response.text) + + return None, '{} - {}'.format(response['error'], response['message']) + + return None, '{} - {}'.format(response.status_code, response.text) diff --git a/edivorce/apps/core/tests/test_efiling_hub.py b/edivorce/apps/core/tests/test_efiling_hub.py new file mode 100644 index 00000000..affe54c9 --- /dev/null +++ b/edivorce/apps/core/tests/test_efiling_hub.py @@ -0,0 +1,232 @@ +import json +import requests +from unittest import mock + +from django.contrib.sessions.middleware import SessionMiddleware +from django.core.exceptions import PermissionDenied +from django.test import TransactionTestCase +from django.test.client import RequestFactory +from django.core.files.uploadedfile import SimpleUploadedFile + +from edivorce.apps.core.efilinghub import EFilingHub, PACKAGE_PARTY_FORMAT + +SAMPLE_TOKEN_RESPONSE = { + "access_token": "klkadlfjadsfkj", + "expires_in": 300, + "refresh_expires_in": 1800, + "refresh_token": "ljasdfjaofijwekfjadslkjf", + "token_type": "bearer", + "not-before-policy": 0, + "session_state": "bed88a31-4d73-4f31-a4ee-dd8aa225d801", + "scope": "email profile" +} + +INITIAL_DOC_UPLOAD_RESPONSE = { + "submissionId": "70fc9ce1-0cd6-4170-b842-bbabb88452a9", + "received": 1 +} + +GENERATE_URL_RESPONSE = { + "expiryDate": 1597775531035, + "efilingUrl": "http://efiling.gov.bc.ca/efiling?submissionId=adfadsf&transactionId=adsfadsf" +} + +GENERATE_URL_RESPONSE_ERROR = { + "error": "403", + "message": "Request does not meet criteria" +} + + +class EFilingHubTests(TransactionTestCase): + + def setUp(self): + # Every test needs access to the request factory. + self.factory = RequestFactory() + + self.request = self.factory.get('/') + middleware = SessionMiddleware() + middleware.process_request(self.request) + self.request.session.save() + + self.hub = EFilingHub() + + def _mock_response(self, status=200, text="Text", json_data=None, raise_for_status=None): + mock_resp = mock.Mock() + mock_resp.raise_for_status = mock.Mock() + if raise_for_status: + mock_resp.raise_for_status.side_effect = raise_for_status + mock_resp.status_code = status + mock_resp.text = text + if json_data: + mock_resp.json = mock.Mock( + return_value=json_data + ) + return mock_resp + + @mock.patch('requests.post') + def test_get_token(self, mock_request_post): + mock_request_post.return_value = self._mock_response(text=json.dumps(SAMPLE_TOKEN_RESPONSE)) + + self.assertTrue(self.hub._get_token(self.request)) + self.assertEqual(self.request.session['access_token'], SAMPLE_TOKEN_RESPONSE['access_token']) + + @mock.patch('requests.post') + def test_get_token_error(self, mock_request_post): + mock_request_post.return_value = self._mock_response(text=json.dumps(SAMPLE_TOKEN_RESPONSE), status=401) + + self.assertFalse(self.hub._get_token(self.request)) + self.assertFalse("access_token" in self.request.session) + + @mock.patch('requests.post') + def test_renew_token(self, mock_request_post): + mock_request_post.return_value = self._mock_response(text=json.dumps(SAMPLE_TOKEN_RESPONSE)) + self.request.session['refresh_token'] = 'alskdfjadlfads' + + self.assertTrue(self.hub._refresh_token(self.request)) + self.assertEqual(self.request.session['access_token'], SAMPLE_TOKEN_RESPONSE['access_token']) + + @mock.patch('requests.post') + def test_renew_token_anon(self, mock_request_post): + # if we don't have a refresh token in session, this should fail + mock_request_post.return_value = self._mock_response(text=json.dumps(SAMPLE_TOKEN_RESPONSE)) + + self.assertFalse(self.hub._refresh_token(self.request)) + self.assertFalse("access_token" in self.request.session) + + @mock.patch('requests.post') + def test_renew_token_error(self, mock_request_post): + mock_request_post.return_value = self._mock_response(text=json.dumps(SAMPLE_TOKEN_RESPONSE), status=401) + self.request.session['refresh_token'] = 'alskdfjadlfads' + + self.assertTrue(self.hub._refresh_token(self.request)) + self.assertEqual(self.request.session['access_token'], SAMPLE_TOKEN_RESPONSE['access_token']) + + @mock.patch('requests.post') + def test_get_api_success(self, mock_request_post): + mock_request_post.return_value = self._mock_response(text=json.dumps(INITIAL_DOC_UPLOAD_RESPONSE)) + self.request.session['access_token'] = 'aslkfjadskfjd' + + response = self.hub._get_api(self.request, 'https://somewhere.com', 'alksdjfa', 'kasdkfd', {}) + + self.assertTrue(response) + self.assertEqual(response.status_code, 200) + response = json.loads(response.text) + self.assertTrue("submissionId" in response) + + @mock.patch('requests.post') + def test_get_api_expired_token(self, mock_request_post): + self.request.session['access_token'] = 'aslkfjadskfjd' + self.request.session['refresh_token'] = 'alskdfjadlfads' + + # we want 3 mock side effects for post .. a 401 on the first and success on token renewal + mock_request_post.side_effect = [ + self._mock_response(text=json.dumps(INITIAL_DOC_UPLOAD_RESPONSE), status=401), + self._mock_response(text=json.dumps(SAMPLE_TOKEN_RESPONSE)), + self._mock_response(text=json.dumps(INITIAL_DOC_UPLOAD_RESPONSE)) + ] + + response = self.hub._get_api(self.request, 'https://somewhere.com', 'alksdjfa', 'kasdkfd', {}) + + self.assertTrue(response) + self.assertEqual(response.status_code, 200) + response = json.loads(response.text) + self.assertTrue("submissionId" in response) + + @mock.patch('requests.post') + def test_get_api_no_token(self, mock_request_post): + mock_request_post.return_value = self._mock_response(text=json.dumps(INITIAL_DOC_UPLOAD_RESPONSE)) + + response = self.hub._get_api(self.request, 'https://somewhere.com', 'alksdjfa', 'kasdkfd', {}) + + self.assertFalse(response) + + def test_transaction_id_current(self): + self.request.session['transaction_id'] = 'alksdjflaskdjf' + guid = self.hub._get_transaction(self.request) + + self.assertEqual(guid, 'alksdjflaskdjf') + + def test_transaction_id_empty(self): + self.assertFalse('transaction_id' in self.request.session) + guild = self.hub._get_transaction(self.request) + + self.assertTrue('transaction_id' in self.request.session) + + def test_bceid_get_current(self): + self.request.session['smgov_userguid'] = '70fc9ce1-0cd6-4170-b842-bbabb88452a9' + with self.settings(DEPLOYMENT_TYPE='prod'): + bceid = self.hub._get_bceid(self.request) + self.assertEqual(bceid, '70fc9ce1-0cd6-4170-b842-bbabb88452a9') + + def test_bceid_get_local_fake(self): + self.request.session['fake_bceid_guid'] = '70fc9ce1-0cd6-4170-b842-bbabb88452a9' + with self.settings(DEPLOYMENT_TYPE='localdev'): + bceid = self.hub._get_bceid(self.request) + self.assertEqual(bceid, '70fc9ce1-0cd6-4170-b842-bbabb88452a9') + + def test_bceid_anonymous_user(self): + with self.settings(DEPLOYMENT_TYPE='prod'): + bceid = self.hub._get_bceid(self.request) + self.assertFalse(bceid) + + def test_format_package(self): + files = [] + for i in range(0, 2): + file = SimpleUploadedFile('form_{}.pdf'.format(i), b'test content') + files.append(('files', (file.name, file.read()))) + parties = [] + for i in range(0, 2): + party = PACKAGE_PARTY_FORMAT.copy() + party['firstName'] = 'Party {}'.format(i) + party['lastName'] = 'Test' + parties.append(party) + package = self.hub._format_package(self.request, files, parties=parties) + + self.assertTrue(package) + self.assertEqual(package['filingPackage']['documents'][0]['name'], 'form_0.pdf') + self.assertEqual(package['filingPackage']['documents'][1]['name'], 'form_1.pdf') + self.assertEqual(package['filingPackage']['parties'][0]['firstName'], 'Party 0') + self.assertEqual(package['filingPackage']['parties'][1]['firstName'], 'Party 1') + + def test_upload_anonymous_user(self): + with self.settings(DEPLOYMENT_TYPE='prod'): + with self.assertRaises(PermissionDenied): + redirect, msg = self.hub.upload(self.request, None) + + @mock.patch('edivorce.apps.core.efilinghub.EFilingHub._get_api') + def test_upload_success(self, mock_get_api): + self.request.session['smgov_userguid'] = '70fc9ce1-0cd6-4170-b842-bbabb88452a9' + with self.settings(DEPLOYMENT_TYPE='prod'): + mock_get_api.side_effect = [ + self._mock_response(text=json.dumps(INITIAL_DOC_UPLOAD_RESPONSE)), + self._mock_response(text=json.dumps(GENERATE_URL_RESPONSE)) + ] + redirect, msg = self.hub.upload(self.request, {}) + + self.assertTrue(redirect) + self.assertEqual(redirect, GENERATE_URL_RESPONSE['efilingUrl']) + self.assertEqual(msg, 'success') + + @mock.patch('edivorce.apps.core.efilinghub.EFilingHub._get_api') + def test_upload_failed_initial_upload(self, mock_get_api): + self.request.session['smgov_userguid'] = '70fc9ce1-0cd6-4170-b842-bbabb88452a9' + with self.settings(DEPLOYMENT_TYPE='prod'): + mock_get_api.side_effect = [ + self._mock_response(text=json.dumps(INITIAL_DOC_UPLOAD_RESPONSE), status=401), + self._mock_response(text=json.dumps(GENERATE_URL_RESPONSE)) + ] + redirect, msg = self.hub.upload(self.request, {}) + + self.assertFalse(redirect) + + @mock.patch('edivorce.apps.core.efilinghub.EFilingHub._get_api') + def test_upload_failed_generate_url(self, mock_get_api): + self.request.session['smgov_userguid'] = '70fc9ce1-0cd6-4170-b842-bbabb88452a9' + with self.settings(DEPLOYMENT_TYPE='prod'): + mock_get_api.side_effect = [ + self._mock_response(text=json.dumps(INITIAL_DOC_UPLOAD_RESPONSE)), + self._mock_response(text=json.dumps(GENERATE_URL_RESPONSE_ERROR), status=403) + ] + redirect, msg = self.hub.upload(self.request, {}) + + self.assertFalse(redirect) diff --git a/edivorce/apps/poc/templates/hub.html b/edivorce/apps/poc/templates/hub.html new file mode 100644 index 00000000..ef8e7010 --- /dev/null +++ b/edivorce/apps/poc/templates/hub.html @@ -0,0 +1,46 @@ +{% extends 'base.html' %} +{% load input_field %} +{% load step_order %} +{% load load_json %} + +{% block title %}{{ block.super }}: POC{% endblock %} + +{% block progress %}{% include "poc-sidebar.html" %}{% endblock %} + +{% block content %} +

Proof of Concept:Efiling Hub Integration

+ +
+ {% csrf_token %} + +
+

Specify files to upload to the eFiling Hub.

+
+
{{ form.upload_file }}
+ {% if form.upload_file.errors %} + + {% for err in form.upload_file.errors %}{{ err }}{% endfor %} + + {% endif %} +
+
+ +
+ +
+ +
+ +{% endblock %} + +{% block formbuttons %} + +{% endblock %} + +{% block sidebarNav %} + +{% endblock %} + +{% block sidebar %} + +{% endblock %} diff --git a/edivorce/apps/poc/templates/poc-sidebar.html b/edivorce/apps/poc/templates/poc-sidebar.html index 7d53d9c5..3901d09b 100644 --- a/edivorce/apps/poc/templates/poc-sidebar.html +++ b/edivorce/apps/poc/templates/poc-sidebar.html @@ -12,4 +12,8 @@ Redis Storage + + + eFiling Hub + diff --git a/edivorce/apps/poc/urls.py b/edivorce/apps/poc/urls.py index b5e53724..0567ac76 100644 --- a/edivorce/apps/poc/urls.py +++ b/edivorce/apps/poc/urls.py @@ -5,6 +5,7 @@ from ..core.decorators import bceid_required urlpatterns = [ url(r'scan', bceid_required(views.UploadScan.as_view()), name="poc-scan"), + url(r'hub', bceid_required(views.EfilingHubUpload.as_view()), name="poc-hub"), url(r'storage/doc/(?P\d+)', bceid_required(views.view_document_file), name="poc-storage-download"), url(r'storage/delete/(?P\d+)', bceid_required(views.UploadStorageDelete.as_view()), name="poc-storage-delete"), url(r'storage', bceid_required(views.UploadStorage.as_view()), name="poc-storage"), diff --git a/edivorce/apps/poc/views.py b/edivorce/apps/poc/views.py index 3994e362..7a8243da 100644 --- a/edivorce/apps/poc/views.py +++ b/edivorce/apps/poc/views.py @@ -1,3 +1,5 @@ +import logging + from django.shortcuts import render from django.utils.decorators import method_decorator from django.views.decorators.csrf import csrf_exempt @@ -6,9 +8,12 @@ from django import forms from django.http import HttpResponse from django.conf import settings +from edivorce.apps.core.efilinghub import EFilingHub, PACKAGE_PARTY_FORMAT from edivorce.apps.core.validators import file_scan_validation from edivorce.apps.poc.models import Document +logger = logging.getLogger(__name__) + """ Everything in this file is considered as proof of concept work and should not be used for production code. """ @@ -18,6 +23,10 @@ class UploadScanForm(forms.Form): upload_file = forms.FileField(validators=[file_scan_validation]) +class MultipleUploadForm(forms.Form): + upload_file = forms.FileField(widget=forms.ClearableFileInput(attrs={'multiple': True})) + + class UploadScan(FormView): form_class = UploadScanForm template_name = "scan.html" @@ -56,4 +65,41 @@ def view_document_file(request, document_id): response = HttpResponse(doc.file.read(), content_type=content_type) response['Content-Disposition'] = 'attachment; filename={}'.format(doc.filename) - return response \ No newline at end of file + return response + + +class EfilingHubUpload(FormView): + form_class = MultipleUploadForm + template_name = 'hub.html' + success_url = '/poc/hub' + + def post(self, request, *args, **kwargs): + form_class = self.get_form_class() + form = self.get_form(form_class) + files = request.FILES.getlist('upload_file') + if form.is_valid(): + # NOTE: this does not do any validation for file types .. make sure that is done + # prior to sending to eFiling Hub + post_files = [] + if files: + for file in files: + post_files.append(('files', (file.name, file.read()))) + + # generate the list of parties to send to eFiling Hub + parties = [] + for i in range(0, 2): + party = PACKAGE_PARTY_FORMAT.copy() + party['firstName'] = 'Party {}'.format(i) + party['lastName'] = 'Test' + parties.append(party) + + hub = EFilingHub() + redirect, msg = hub.upload(request, post_files, parties=parties) + if redirect: + self.success_url = redirect + + return self.form_valid(form) + + form.add_error('upload_file', msg) + + return self.form_invalid(form) diff --git a/edivorce/settings/base.py b/edivorce/settings/base.py index c45e6d49..e988e6dd 100644 --- a/edivorce/settings/base.py +++ b/edivorce/settings/base.py @@ -102,6 +102,22 @@ REST_FRAMEWORK = { ] } +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + }, + }, + 'loggers': { + '': { + 'handlers': ['console'], + 'level': os.getenv('DJANGO_LOG_LEVEL', 'INFO'), + }, + }, +} + # Internationalization # https://docs.djangoproject.com/en/1.8/topics/i18n/ @@ -156,20 +172,23 @@ SECURE_BROWSER_XSS_FILTER = True LOGOUT_URL = '/accounts/logout/' -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'handlers': { - 'console':{ - 'level':'DEBUG', - 'class':'logging.StreamHandler', - }, - }, - 'loggers': { - 'django.request': { - 'handlers':['console'], - 'propagate': True, - 'level':'DEBUG', - } - }, -} + +# CLAMAV settings +CLAMAV_ENABLED = env.bool('CLAMAV_ENABLED', True) +CLAMAV_TCP_PORT = env.int('CLAMAV_TCP_PORT', 3310) +CLAMAV_TCP_ADDR = env('CLAMAV_TCP_ADDR', 'localhost') + +# Redis settings +REDIS_HOST = env('REDIS_HOST', 'localhost') +REDIS_PORT = env.int('REDIS_PORT', 6379) +REDIS_DB = env('REDIS_DB', '') +REDIS_PASSWORD = env('REDIS_PASSWORD', '') + +# eFiling Hub settings +EFILING_HUB_TOKEN_BASE_URL=env('EFILING_HUB_TOKEN_BASE_URL') +EFILING_HUB_REALM=env('EFILING_HUB_REALM') +EFILING_HUB_CLIENT_ID=env('EFILING_HUB_CLIENT_ID') +EFILING_HUB_CLIENT_SECRET=env('EFILING_HUB_CLIENT_SECRET') +EFILING_HUB_API_BASE_URL=env('EFILING_HUB_API_BASE_URL') + +EFILING_BCEID=env.dict('EFILING_BCEID', subcast=str) \ No newline at end of file