Ported obp app from API Tester #31

Also enables possibility to use DirectLogin and GatewayLogin
This commit is contained in:
Sebastian Henschel 2017-10-24 13:19:33 +02:00
parent 5fc4e45577
commit 2358d8eed6
29 changed files with 778 additions and 350 deletions

View File

@ -25,14 +25,8 @@ apimanager/
│   ├── requirements.txt
│   └── supervisor.apimanager.conf
├── db.sqlite3
├── logs [error opening dir]
├── logs
├── static-collected
│   ├── admin
│   ├── consumers
│   ├── css
│   ├── img
│   ├── js
│   └── users
└── venv
├── bin
└── lib
@ -54,7 +48,7 @@ Edit `apimanager/apimanager/local_settings.py`:
# Used internally by Django, can be anything of your choice
SECRET_KEY = '<random string>'
# API hostname, e.g. https://api.openbankproject.com
OAUTH_API = '<hostname>'
API_HOST = '<hostname>'
# Consumer key + secret to authenticate the _app_ against the API
OAUTH_CONSUMER_KEY = '<key>'
OAUTH_CONSUMER_SECRET = '<secret>'

View File

@ -45,7 +45,7 @@ INSTALLED_APPS = [
'django.contrib.staticfiles',
'base',
'oauth',
'obp',
'consumers',
'users',
'customers',
@ -187,13 +187,18 @@ API_DATETIMEFORMAT = '%Y-%m-%dT%H:%M:%SZ'
API_DATEFORMAT = '%Y-%m-%d'
OAUTH_API = 'http://127.0.0.1:8080'
API_HOST = 'http://127.0.0.1:8080'
API_BASE_PATH = '/obp/v3.0.0'
# For some reason, swagger is not available at the latest API version
API_SWAGGER_BASE_PATH = '/obp/v1.4.0'
# Always save session$
SESSION_SAVE_EVERY_REQUEST = True
OAUTH_TOKEN_PATH = '/oauth/initiate'
OAUTH_AUTHORIZATION_PATH = '/oauth/authorize'
OAUTH_ACCESS_TOKEN_PATH = '/oauth/token'
OAUTH_API_BASE_PATH = '/obp/v3.0.0'
# Set OAuth client key/secret in apimanager/local_settings.py
OAUTH_CONSUMER_KEY = None

View File

@ -6,11 +6,19 @@ URLs for apimanager
from django.conf.urls import url, include
from base.views import HomeView
from obp.views import OAuthInitiateView, OAuthAuthorizeView, LogoutView
urlpatterns = [
url(r'^$', HomeView.as_view(), name="home"),
url(r'^oauth/', include('oauth.urls')),
# Defining authentication URLs here and not including oauth.urls for
# backward compatibility
url(r'^oauth/initiate$',
OAuthInitiateView.as_view(), name='oauth-initiate'),
url(r'^oauth/authorize$',
OAuthAuthorizeView.as_view(), name='oauth-authorize'),
url(r'^logout$',
LogoutView.as_view(), name='oauth-logout'),
url(r'^consumers/', include('consumers.urls')),
url(r'^users/', include('users.urls')),
url(r'^customers/', include('customers.urls')),

View File

@ -1,116 +0,0 @@
# -*- coding: utf-8 -*-
"""
Module to handle the OBP API
It instantiates a convenience api object for imports, e.g.:
from base.api import api
"""
from datetime import datetime
import logging
import time
from requests.exceptions import ConnectionError
from requests_oauthlib import OAuth1Session
from django.conf import settings
from django.contrib.auth import logout
DATE_FORMAT = '%d/%b/%Y %H:%M:%S'
LOGGER = logging.getLogger(__name__)
def log(level, message):
"""Logs a given message on a given level to log facility"""
now = datetime.now().strftime(DATE_FORMAT)
msg = '[{}] API: {}'.format(now, message)
LOGGER.log(level, msg)
class APIError(Exception):
"""Exception class for API errors"""
pass
class API(object):
"""Implements an interface to the OBP API"""
def get(self, request, urlpath=''):
"""Gets data from the API"""
return self.call(request, 'GET', urlpath)
def delete(self, request, urlpath):
"""Deletes data from the API"""
return self.call(request, 'DELETE', urlpath)
def post(self, request, urlpath, payload):
"""Posts data to the API"""
return self.call(request, 'POST', urlpath, payload)
def put(self, request, urlpath, payload):
"""Puts data onto the API"""
return self.call(request, 'PUT', urlpath, payload)
def handle_response_404(self, response, prefix):
# Stripping HTML body ...
if response.text.find('body'):
msg = response.text.split('<body>')[1].split('</body>')[0]
msg = '{} {}: {}'.format(
prefix, response.status_code, msg)
log(logging.ERROR, msg)
raise APIError(msg)
def handle_response_500(self, response, prefix):
msg = '{} {}: {}'.format(
prefix, response.status_code, response.text)
log(logging.ERROR, msg)
raise APIError(msg)
def handle_response_error(self, request, prefix, error):
if 'Invalid or expired access token' in error:
logout(request)
msg = '{} {}'.format(prefix, error)
raise APIError(msg)
def handle_response(self, request, response):
"""Handles the response, e.g. errors or conversion to JSON"""
prefix = 'APIError'
if response.status_code == 404:
self.handle_response_404(response, prefix)
elif response.status_code == 500:
self.handle_response_500(response, prefix)
elif response.status_code in [204]:
return response.text
else:
data = response.json()
if 'error' in data:
self.handle_response_error(request, prefix, data['error'])
return data
def call(self, request, method='GET', urlpath='', payload=None):
"""Workhorse which actually calls the API"""
url = settings.OAUTH_API + settings.OAUTH_API_BASE_PATH + urlpath
log(logging.INFO, '{} {}'.format(method, url))
if payload:
log(logging.INFO, 'Payload: {}'.format(payload))
if not hasattr(request, 'api'):
request.api = OAuth1Session(
settings.OAUTH_CONSUMER_KEY,
client_secret=settings.OAUTH_CONSUMER_SECRET,
resource_owner_key=request.session['oauth_token'],
resource_owner_secret=request.session['oauth_secret']
)
time_start = time.time()
try:
if payload:
response = request.api.request(method, url, json=payload)
else:
response = request.api.request(method, url)
except ConnectionError as err:
raise APIError(err)
time_end = time.time()
elapsed = int((time_end - time_start) * 1000)
log(logging.INFO, 'Took {} ms'.format(elapsed))
return self.handle_response(request, response)
api = API()

View File

@ -1,32 +0,0 @@
# -*- coding: utf-8 -*-
"""
Helpers to reuse common API calls
"""
from django.contrib import messages
from .api import api, APIError
def get_bank_id_choices(request):
"""Gets a list of bank ids and bank ids as used by form choices"""
choices = [('', 'Choose ...')]
try:
result = api.get(request, '/banks')
for bank in result['banks']:
choices.append((bank['id'], bank['id']))
except APIError as err:
messages.error(request, err)
return choices
def get_user_id_choices(request):
"""Gets a list of user ids and usernames as used by form choices"""
choices = [('', 'Choose ...')]
try:
result = api.get(request, '/users')
for user in result['users']:
choices.append((user['user_id'], user['username']))
except APIError as err:
messages.error(request, err)
return choices

View File

@ -6,13 +6,13 @@ Context processors for base app
from django.conf import settings
from django.contrib import messages
from base.api import api, APIError
from obp.api import API, APIError
def api_root(request):
"""Returns the configured API_ROOT"""
return {'API_ROOT': settings.OAUTH_API + settings.OAUTH_API_BASE_PATH}
return {'API_ROOT': settings.API_HOST + settings.API_BASE_PATH}
@ -21,7 +21,8 @@ def api_username(request):
username = 'not authenticated'
if request.user.is_authenticated:
try:
data = api.get(request, '/users/current')
api = API(request.session.get('obp'))
data = api.get('/users/current')
username = data['username']
except APIError as err:
messages.error(request, err)

View File

@ -6,7 +6,7 @@
<div class="well" id="intro">
<p>
This app gives you access to management functionality for the sandbox at <a href="{{ OAUTH_API }}">{{ OAUTH_API }}</a>. You have to <a href="{{ OAUTH_API }}/user_mgt/sign_up" title="Register at {{ OAUTH_API }}">register</a> an account before being able to proceed. The logged-in user needs to have specific roles granted to use the functionality.
This app gives you access to management functionality for the sandbox at <a href="{{ API_HOST }}">{{ API_HOST }}</a>. You have to <a href="{{ API_HOST }}/user_mgt/sign_up" title="Register at {{ API_HOST }}">register</a> an account before being able to proceed. The logged-in user needs to have specific roles granted to use the functionality.
</p>
</div>

View File

@ -13,5 +13,5 @@ class HomeView(TemplateView):
def get_context_data(self, **kwargs):
context = super(HomeView, self).get_context_data(**kwargs)
context['OAUTH_API'] = settings.OAUTH_API
context['API_HOST'] = settings.API_HOST
return context

View File

@ -9,9 +9,7 @@ from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import FormView, TemplateView, View
from base.api import api, APIError
from obp.api import API, APIError
class IndexView(LoginRequiredMixin, TemplateView):
@ -20,9 +18,10 @@ class IndexView(LoginRequiredMixin, TemplateView):
def get_context_data(self, **kwargs):
context = super(IndexView, self).get_context_data(**kwargs)
api = API(self.request.session.get('obp'))
try:
urlpath = '/config'
config = api.get(self.request, urlpath)
config = api.get(urlpath)
except APIError as err:
messages.error(self.request, err)
config = {}

View File

@ -11,7 +11,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse
from django.views.generic import TemplateView, RedirectView
from base.api import api, APIError
from obp.api import API, APIError
from base.filters import BaseFilter, FilterTime
@ -68,10 +68,10 @@ class IndexView(LoginRequiredMixin, TemplateView):
def get_context_data(self, **kwargs):
context = super(IndexView, self).get_context_data(**kwargs)
consumers = []
api = API(self.request.session.get('obp'))
try:
urlpath = '/management/consumers'
consumers = api.get(self.request, urlpath)
consumers = api.get(urlpath)
consumers = FilterEnabled(context, self.request.GET)\
.apply(consumers['list'])
consumers = FilterAppType(context, self.request.GET)\
@ -98,10 +98,11 @@ class DetailView(LoginRequiredMixin, TemplateView):
def get_context_data(self, **kwargs):
context = super(DetailView, self).get_context_data(**kwargs)
api = API(self.request.session.get('obp'))
try:
urlpath = '/management/consumers/{}'.format(kwargs['consumer_id'])
consumer = api.get(self.request, urlpath)
consumer = api.get(urlpath)
consumer['created'] = datetime.strptime(
consumer['created'], settings.API_DATETIMEFORMAT)
except APIError as err:
@ -120,10 +121,11 @@ class EnableDisableView(LoginRequiredMixin, RedirectView):
success = None
def get_redirect_url(self, *args, **kwargs):
api = API(self.request.session.get('obp'))
try:
urlpath = '/management/consumers/{}'.format(kwargs['consumer_id'])
payload = {'enabled': self.enabled}
api.put(self.request, urlpath, payload)
api.put(urlpath, payload)
messages.success(self.request, self.success)
except APIError as err:
messages.error(self.request, err)

View File

@ -12,8 +12,7 @@ from django.http import HttpResponseRedirect
from django.urls import reverse_lazy
from django.views.generic import FormView
from base.api import api, APIError
from base.api_helper import get_bank_id_choices, get_user_id_choices
from obp.api import API, APIError
from .forms import CreateCustomerForm
@ -24,53 +23,56 @@ class CreateView(LoginRequiredMixin, FormView):
template_name = 'customers/create.html'
success_url = reverse_lazy('customers-create')
def dispatch(self, request, *args, **kwargs):
self.api = API(request.session.get('obp'))
return super(CreateView, self).dispatch(request, *args,**kwargs)
def get_form(self, *args, **kwargs):
form = super(CreateView, self).get_form(*args, **kwargs)
fields = form.fields
fields['bank_id'].choices = get_bank_id_choices(self.request)
fields['user_id'].choices = get_user_id_choices(self.request)
fields['bank_id'].choices = self.api.get_bank_id_choices()
fields['user_id'].choices = self.api.get_user_id_choices()
fields['last_ok_date'].initial =\
datetime.datetime.now().strftime(settings.API_DATETIMEFORMAT)
return form
def form_valid(self, form):
data = form.cleaned_data
urlpath = '/banks/{}/customers'.format(data['bank_id'])
payload = {
'user_id': data['user_id'],
'customer_number': data['customer_number'],
'legal_name': data['legal_name'],
'mobile_phone_number': data['mobile_phone_number'],
'email': data['email'],
'face_image': {
'url': data['face_image_url'],
'date': data['face_image_date'],
},
'date_of_birth': data['date_of_birth'],
'relationship_status': data['relationship_status'],
'dependants': data['dependants'],
'dob_of_dependants': data['dob_of_dependants'],
'credit_rating': {
'rating': data['credit_rating_rating'],
'source': data['credit_rating_source'],
},
'credit_limit': {
'currency': data['credit_limit_currency'],
'amount': data['credit_limit_amount'],
},
'highest_education_attained':
data['highest_education_attained'],
'employment_status': data['employment_status'],
'kyc_status': data['kyc_status'],
'last_ok_date':
data['last_ok_date'].strftime(settings.API_DATETIMEFORMAT),
}
try:
data = form.cleaned_data
urlpath = '/banks/{}/customers'.format(data['bank_id'])
payload = {
'user_id': data['user_id'],
'customer_number': data['customer_number'],
'legal_name': data['legal_name'],
'mobile_phone_number': data['mobile_phone_number'],
'email': data['email'],
'face_image': {
'url': data['face_image_url'],
'date': data['face_image_date'],
},
'date_of_birth': data['date_of_birth'],
'relationship_status': data['relationship_status'],
'dependants': data['dependants'],
'dob_of_dependants': data['dob_of_dependants'],
'credit_rating': {
'rating': data['credit_rating_rating'],
'source': data['credit_rating_source'],
},
'credit_limit': {
'currency': data['credit_limit_currency'],
'amount': data['credit_limit_amount'],
},
'highest_education_attained':
data['highest_education_attained'],
'employment_status': data['employment_status'],
'kyc_status': data['kyc_status'],
'last_ok_date':
data['last_ok_date'].strftime(settings.API_DATETIMEFORMAT),
}
result = api.post(self.request, urlpath, payload=payload)
result = self.api.post(urlpath, payload=payload)
except APIError as err:
messages.error(self.request, err)
return super(CreateView, self).form_invalid(form)
msg = 'Customer number {} has been created successfully!'.format(
result['customer_number'])
messages.success(self.request, msg)

View File

@ -15,7 +15,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import FormView, TemplateView
from django.utils.http import urlquote
from base.api import api, APIError
from obp.api import API, APIError
from .forms import APIMetricsForm, ConnectorMetricsForm
@ -117,8 +117,9 @@ class MetricsView(LoginRequiredMixin, TemplateView):
metrics = []
params = self.to_api(cleaned_data)
urlpath = '{}?{}'.format(self.api_urlpath, params)
api = API(self.request.session.get('obp'))
try:
metrics = api.get(self.request, urlpath)
metrics = api.get(urlpath)
metrics = self.to_django(metrics['metrics'])
except APIError as err:
messages.error(self.request, err)

View File

@ -1,11 +0,0 @@
# -*- coding: utf-8 -*-
"""
App config for OAuth 1 app
"""
from django.apps import AppConfig
class OauthConfig(AppConfig):
"""Config for OAuth 1"""
name = 'oauth'

View File

@ -1,14 +0,0 @@
# -*- coding: utf-8 -*-
"""
URLs for OAuth 1 app
"""
from django.conf.urls import url
from .views import InitiateView, AuthorizeView, LogoutView
urlpatterns = [
url(r'^initiate$', InitiateView.as_view(), name='oauth-initiate'),
url(r'^authorize$', AuthorizeView.as_view(), name='oauth-authorize'),
url(r'^logout$', LogoutView.as_view(), name='oauth-logout'),
]

View File

@ -1,102 +0,0 @@
# -*- coding: utf-8 -*-
"""
Views for OAuth 1 app
"""
import hashlib
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import login, logout
from django.contrib.auth.models import User
from django.urls import reverse
from django.views.generic import RedirectView
from requests_oauthlib import OAuth1Session
from requests_oauthlib.oauth1_session import TokenRequestDenied
from base.api import api
class InitiateView(RedirectView):
"""View to initiate OAuth 1 session"""
def get_callback_uri(self, request):
"""
Gets the callback URI to where the user shall be returned after
authorization at OAuth 1 server
"""
base_url = '{}://{}'.format(
request.scheme, request.environ['HTTP_HOST'])
uri = base_url + reverse('oauth-authorize')
if 'next' in request.GET:
uri = '{}?next={}'.format(uri, request.GET['next'])
return uri
def get_redirect_url(self, *args, **kwargs):
callback_uri = self.get_callback_uri(self.request)
session = OAuth1Session(
settings.OAUTH_CONSUMER_KEY,
client_secret=settings.OAUTH_CONSUMER_SECRET,
callback_uri=callback_uri,
)
try:
url = settings.OAUTH_API + settings.OAUTH_TOKEN_PATH
response = session.fetch_request_token(url)
except (ValueError, TokenRequestDenied) as err:
messages.error(self.request, err)
return reverse('home')
url = settings.OAUTH_API + settings.OAUTH_AUTHORIZATION_PATH
authorization_url = session.authorization_url(url)
self.request.session['oauth_token'] = response.get('oauth_token')
self.request.session['oauth_secret'] = response.get('oauth_token_secret')
self.request.session.modified = True
return authorization_url
class AuthorizeView(RedirectView):
"""View to authorize user"""
def login_to_django(self):
"""
Logs the user into Django
Kind of faking it to establish if a user is authenticated later on
"""
data = api.get(self.request, '/users/current')
userid = data['user_id'] or data['email']
username = hashlib.sha256(userid.encode('utf-8')).hexdigest()
password = username
user, _ = User.objects.get_or_create(
username=username, password=password,
)
login(self.request, user)
def get_redirect_url(self, *args, **kwargs):
session = OAuth1Session(
settings.OAUTH_CONSUMER_KEY,
settings.OAUTH_CONSUMER_SECRET,
resource_owner_key=self.request.session.get('oauth_token'),
resource_owner_secret=self.request.session.get('oauth_secret')
)
session.parse_authorization_response(self.request.build_absolute_uri())
url = settings.OAUTH_API + settings.OAUTH_ACCESS_TOKEN_PATH
try:
response = session.fetch_access_token(url)
self.request.session['oauth_token'] = response.get('oauth_token')
self.request.session['oauth_secret'] = response.get('oauth_token_secret')
self.request.session.modified = True
self.login_to_django()
except TokenRequestDenied as err:
messages.error(self.request, err)
redirect_url = self.request.GET.get('next', reverse('home'))
return redirect_url
class LogoutView(RedirectView):
"""View to logout"""
def get_redirect_url(self, *args, **kwargs):
logout(self.request)
return reverse('home')

175
apimanager/obp/api.py Normal file
View File

@ -0,0 +1,175 @@
# -*- coding: utf-8 -*-
"""
Module to handle the OBP API
It instantiates a convenience api object for imports, e.g.:
from obp.api import api
"""
from datetime import datetime
import importlib
import logging
import time
import requests
from requests.exceptions import ConnectionError
from django.conf import settings
DATE_FORMAT = '%d/%b/%Y %H:%M:%S'
LOGGER = logging.getLogger(__name__)
def log(level, message):
"""Logs a given message on a given level to log facility"""
now = datetime.now().strftime(DATE_FORMAT)
msg = '[{}] API: {}'.format(now, message)
LOGGER.log(level, msg)
class APIError(Exception):
"""Exception class for API errors"""
pass
class API(object):
"""Implements an interface to the OBP API"""
session = None
swagger = None
def __init__(self, session_data=None):
self.set_base_path()
if session_data:
self.start_session(session_data)
self.session_data = session_data
def set_base_path(self, base_path=None):
"""Sets the basepath for API calls"""
if base_path:
self.base_path = settings.API_HOST + base_path
else:
self.base_path = settings.API_HOST + settings.API_BASE_PATH
def call(self, method='GET', urlpath='', payload=None):
"""Workhorse which actually calls the API"""
url = self.base_path + urlpath
log(logging.INFO, '{} {}'.format(method, url))
if payload:
log(logging.INFO, 'Payload: {}'.format(payload))
# use `requests` if no session has been started
session = self.session or requests
time_start = time.time()
try:
if payload:
response = session.request(method, url, json=payload)
else:
response = session.request(method, url)
except ConnectionError as err:
raise APIError(err)
time_end = time.time()
elapsed = int((time_end - time_start) * 1000)
log(logging.INFO, 'Took {} ms'.format(elapsed))
response.execution_time = elapsed
return response
def get(self, urlpath=''):
"""Gets data from the API"""
response = self.call('GET', urlpath)
return self.handle_response(response)
def delete(self, urlpath):
"""Deletes data from the API"""
response = self.call('DELETE', urlpath)
return self.handle_response(response)
def post(self, urlpath, payload):
"""Posts data to the API"""
response = self.call('POST', urlpath, payload)
return self.handle_response(response)
def put(self, urlpath, payload):
"""Puts data onto the API"""
response = self.call('PUT', urlpath, payload)
return self.handle_response(response)
def handle_response_404(self, response, prefix):
# Stripping HTML body ...
if response.text.find('body'):
msg = response.text.split('<body>')[1].split('</body>')[0]
msg = '{} {}: {}'.format(
prefix, response.status_code, msg)
log(logging.ERROR, msg)
raise APIError(msg)
def handle_response_500(self, response, prefix):
msg = '{} {}: {}'.format(
prefix, response.status_code, response.text)
log(logging.ERROR, msg)
raise APIError(msg)
def handle_response_error(self, prefix, error):
if 'Invalid or expired access token' in error:
raise APIError(error)
msg = '{} {}'.format(prefix, error)
raise APIError(msg)
def handle_response(self, response):
"""Handles the response, e.g. errors or conversion to JSON"""
prefix = 'APIError'
if response.status_code == 404:
self.handle_response_404(response, prefix)
elif response.status_code == 500:
self.handle_response_500(response, prefix)
elif response.status_code in [204]:
return response.text
else:
data = response.json()
if 'error' in data:
self.handle_response_error(prefix, data['error'])
return data
def start_session(self, session_data):
"""
Starts a session with given session_data:
- Authenticator class name (e.g. obp.oauth.OAuthAuthenticator)
- Token data
for subsequent requests to the API
"""
if 'authenticator' in session_data and\
'authenticator_kwargs' in session_data:
mod_name, cls_name = session_data['authenticator'].rsplit('.', 1)
log(logging.INFO, 'Authenticator {}'.format(cls_name))
cls = getattr(importlib.import_module(mod_name), cls_name)
authenticator = cls(**session_data['authenticator_kwargs'])
self.session = authenticator.get_session()
return self.session
else:
return None
def get_swagger(self):
"""Gets the swagger definition from the API"""
if not self.session_data.get('swagger'):
self.set_base_path(settings.API_SWAGGER_BASE_PATH)
urlpath = '/resource-docs/v3.0.0/swagger'
response = self.get(urlpath)
# Set base path back
self.set_base_path()
self.session_data['swagger'] = response
return self.session_data.get('swagger')
def get_bank_id_choices(self):
"""Gets a list of bank ids and bank ids as used by form choices"""
choices = [('', 'Choose ...')]
result = self.get('/banks')
for bank in result['banks']:
choices.append((bank['id'], bank['id']))
return choices
def get_user_id_choices(self):
"""Gets a list of user ids and usernames as used by form choices"""
choices = [('', 'Choose ...')]
result = self.get('/users')
for user in result['users']:
choices.append((user['user_id'], user['username']))
return choices

11
apimanager/obp/apps.py Normal file
View File

@ -0,0 +1,11 @@
# -*- coding: utf-8 -*-
"""
App config for OBP app
"""
from django.apps import AppConfig
class OBPConfig(AppConfig):
"""Config for OBP"""
name = 'obp'

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
"""
Base authenticator for OBP app
"""
import hashlib
from django.contrib import messages
from django.contrib.auth import login
from django.contrib.auth.models import User
class AuthenticatorError(Exception):
"""Exception class for Authenticator errors"""
pass
class Authenticator(object):
"""Generic authenticator to the API"""
pass

View File

@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
"""
DirectLogin authenticator for OBP app
"""
import requests
from django.conf import settings
from .authenticator import Authenticator, AuthenticatorError
class DirectLoginAuthenticator(Authenticator):
"""Implements a DirectLogin authenticator to the API"""
token = None
def __init__(self, token=None):
self.token = token
def login_to_api(self, data):
"""
Logs into the API and returns the token
data is a dict which contains keys username, password and consumer_key
"""
url = settings.API_HOST + settings.DIRECTLOGIN_PATH
authorization = 'DirectLogin username="{}",password="{}",consumer_key="{}"'.format( # noqa
data['username'],
data['password'],
data['consumer_key'])
headers = {'Authorization': authorization}
try:
response = requests.get(url, headers=headers)
except requests.exceptions.ConnectionError as err:
raise AuthenticatorError(err)
result = response.json()
if response.status_code != 200:
raise AuthenticatorError(result['error'])
else:
self.token = result['token']
def get_session(self):
"""Returns a session object to make authenticated requests"""
headers = {'Authorization': 'DirectLogin token={}'.format(self.token)}
session = requests.Session()
session.headers.update(headers)
return session

53
apimanager/obp/forms.py Normal file
View File

@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
"""
Forms for OBP app
"""
from django import forms
from .authenticator import AuthenticatorError
from .directlogin import DirectLoginAuthenticator
from .gatewaylogin import GatewayLoginAuthenticator
class DirectLoginForm(forms.Form):
username = forms.CharField(widget=forms.TextInput(
attrs={'class': 'form-control'}))
password = forms.CharField(widget=forms.PasswordInput(
attrs={'class': 'form-control'}))
consumer_key = forms.CharField(widget=forms.TextInput(
attrs={'class': 'form-control'}))
def clean(self):
"""
Stores an authenticator in cleaned_data after successful login to API
"""
cleaned_data = super(DirectLoginForm, self).clean()
authenticator = DirectLoginAuthenticator()
try:
authenticator.login_to_api(cleaned_data)
cleaned_data['authenticator'] = authenticator
except AuthenticatorError as err:
raise forms.ValidationError(err)
return cleaned_data
class GatewayLoginForm(forms.Form):
username = forms.CharField(widget=forms.TextInput(
attrs={'class': 'form-control'}))
secret = forms.CharField(widget=forms.PasswordInput(
attrs={'class': 'form-control'}))
def clean(self):
"""
Stores an authenticator in cleaned_data after successful login to API
"""
cleaned_data = super(GatewayLoginForm, self).clean()
authenticator = GatewayLoginAuthenticator()
try:
authenticator.login_to_api(cleaned_data)
cleaned_data['authenticator'] = authenticator
except AuthenticatorError as err:
raise forms.ValidationError(err)
return cleaned_data

View File

@ -0,0 +1,67 @@
# -*- coding: utf-8 -*-
"""
GatewayLogin authenticator for OBP app
"""
import jwt
import requests
from django.conf import settings
from .authenticator import Authenticator, AuthenticatorError
class GatewayLoginAuthenticator(Authenticator):
"""Implements a GatewayLogin authenticator to the API"""
token = None
def __init__(self, token=None):
self.token = token
def create_jwt(self, data):
"""
Creates a JWT used for future requests tothe API
data is a dict which contains keys username, secret
"""
url = settings.API_HOST + settings.DIRECTLOGIN_PATH
message = {
'username': data['username'],
'timestamp': 'unused',
'consumer_id': '', # Do not create new consumer
'consumer_name': '', # Do not create new consumer
}
if settings.GATEWAYLOGIN_HAS_CBS:
# Not sure if that is the right thing to do
message['is_first'] = True
else:
# Fake when there is no core banking system
message.update({
'is_first': False,
'CBS_auth_token': 'dummy',
})
token = jwt.encode(message, data['secret'], 'HS256')
self.token = token.decode('utf-8')
return self.token
def login_to_api(self, data):
token = self.create_jwt(data)
# Make a test call to see if the token works
url = settings.API_HOST + settings.API_BASE_PATH + '/users/current'
api = self.get_session()
try:
response = api.get(url)
except requests.exceptions.ConnectionError as err:
raise AuthenticationError(err)
if response.status_code != 200:
raise AuthenticatorError(response.json()['error'])
else:
return token
def get_session(self):
"""Returns a session object to make authenticated requests"""
headers = {'Authorization': 'GatewayLogin token="{}"'.format(self.token)}
session = requests.Session()
session.headers.update(headers)
return session

77
apimanager/obp/oauth.py Normal file
View File

@ -0,0 +1,77 @@
# -*- coding: utf-8 -*-
"""
OAuth authenticator for OBP app
"""
import logging
import time
from django.conf import settings
from requests.exceptions import ConnectionError
from requests_oauthlib import OAuth1Session
from requests_oauthlib.oauth1_session import TokenRequestDenied
from .authenticator import Authenticator, AuthenticatorError
LOGGER = logging.getLogger(__name__)
class OAuthAuthenticator(Authenticator):
"""Implements an OAuth authenticator to the API"""
token = None
secret = None
def __init__(self, token=None, secret=None):
self.token = token
self.secret = secret
def get_authorization_url(self, callback_uri):
session = OAuth1Session(
settings.OAUTH_CONSUMER_KEY,
client_secret=settings.OAUTH_CONSUMER_SECRET,
callback_uri=callback_uri,
)
try:
url = settings.API_HOST + settings.OAUTH_TOKEN_PATH
response = session.fetch_request_token(url)
except (ValueError, TokenRequestDenied, ConnectionError) as err:
raise AuthenticatorError(err)
else:
self.token = response.get('oauth_token')
self.secret = response.get('oauth_token_secret')
url = settings.API_HOST + settings.OAUTH_AUTHORIZATION_PATH
authorization_url = session.authorization_url(url)
LOGGER.log(logging.INFO, 'Initial token {}, secret {}'.format(
self.token, self.secret))
return authorization_url
def set_access_token(self, authorization_url):
session = OAuth1Session(
settings.OAUTH_CONSUMER_KEY,
settings.OAUTH_CONSUMER_SECRET,
resource_owner_key=self.token,
resource_owner_secret=self.secret,
)
session.parse_authorization_response(authorization_url)
url = settings.API_HOST + settings.OAUTH_ACCESS_TOKEN_PATH
try:
response = session.fetch_access_token(url)
except (TokenRequestDenied, ConnectionError) as err:
raise AuthenticatorError(err)
else:
self.token = response.get('oauth_token')
self.secret = response.get('oauth_token_secret')
LOGGER.log(logging.INFO, 'Updated token {}, secret {}'.format(
self.token, self.secret))
def get_session(self):
session = OAuth1Session(
settings.OAUTH_CONSUMER_KEY,
client_secret=settings.OAUTH_CONSUMER_SECRET,
resource_owner_key=self.token,
resource_owner_secret=self.secret,
)
return session

View File

@ -0,0 +1,25 @@
{% extends "base.html" %}
{% block content %}
<h1>DirectLogin</h1>
<form action="{% url 'directlogin' %}" method="post">
{% if form.non_field_errors %}<div class="alert alert-danger">{{ form.non_field_errors }}</div>{% endif %}
{% csrf_token %}
<div class="form-group">
{% if form.username.errors %}<div class="alert alert-danger">{{ form.username.errors }}</div>{% endif %}
<label for="username">Username:</label>
{{ form.username }}
</div>
<div class="form-group">
{% if form.password.errors %}<div class="alert alert-danger">{{ form.password.errors }}</div>{% endif %}
<label for="password">Password:</label>
{{ form.password }}
</div>
<div class="form-group">
{% if form.consumer_key.errors %}<div class="alert alert-danger">{{ form.consumer_key.errors }}</div>{% endif %}
<label for="consumer-key">Consumer Key:</label>
{{ form.consumer_key }}
</div>
<button class="btn btn-primary">Login</button>
</form>
{% endblock content %}

View File

@ -0,0 +1,20 @@
{% extends "base.html" %}
{% block content %}
<h1>GatewayLogin</h1>
<form action="{% url 'gatewaylogin' %}" method="post">
{% if form.non_field_errors %}<div class="alert alert-danger">{{ form.non_field_errors }}</div>{% endif %}
{% csrf_token %}
<div class="form-group">
{% if form.username.errors %}<div class="alert alert-danger">{{ form.username.errors }}</div>{% endif %}
<label for="username">Username:</label>
{{ form.username }}
</div>
<div class="form-group">
{% if form.secret.errors %}<div class="alert alert-danger">{{ form.secret.errors }}</div>{% endif %}
<label for="secret">Secret:</label>
{{ form.secret }}
</div>
<button class="btn btn-primary">Login</button>
</form>
{% endblock content %}

27
apimanager/obp/urls.py Normal file
View File

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
"""
URLs for OBP app
"""
from django.conf.urls import url
from .views import (
OAuthInitiateView, OAuthAuthorizeView,
DirectLoginView,
GatewayLoginView,
LogoutView,
)
urlpatterns = [
url(r'^oauth/initiate$',
OAuthInitiateView.as_view(), name='oauth-initiate'),
url(r'^oauth/authorize$',
OAuthAuthorizeView.as_view(), name='oauth-authorize'),
url(r'^directlogin$',
DirectLoginView.as_view(), name='directlogin'),
url(r'^gatewaylogin$',
GatewayLoginView.as_view(), name='gatewaylogin'),
url(r'^logout$',
LogoutView.as_view(), name='oauth-logout'),
]

160
apimanager/obp/views.py Normal file
View File

@ -0,0 +1,160 @@
# -*- coding: utf-8 -*-
"""
Views for OBP app
"""
import hashlib
from django.contrib import messages
from django.contrib.auth import login, logout
from django.contrib.auth.models import User
from django.urls import reverse
from django.views.generic import RedirectView, FormView
from .api import API, APIError
from .authenticator import AuthenticatorError
from .forms import DirectLoginForm, GatewayLoginForm
from .oauth import OAuthAuthenticator
class LoginToDjangoMixin(object):
"""Mixin to login to Django from views."""
def login_to_django(self):
"""
Logs the user into Django
Kind of faking it to establish if a user is authenticated later on
"""
api = API(self.request.session.get('obp'))
try:
data = api.get('/users/current')
except APIError as err:
messages.error(self.request, err)
return False
else:
userid = data['user_id'] or data['email']
username = hashlib.sha256(userid.encode('utf-8')).hexdigest()
password = username
user, _ = User.objects.get_or_create(
username=username, password=password,
)
login(self.request, user)
return True
class OAuthInitiateView(RedirectView):
"""View to initiate OAuth session"""
def get_callback_uri(self, request):
"""
Gets the callback URI to where the user shall be returned after
initiation at OAuth server
"""
base_url = '{}://{}'.format(
request.scheme, request.environ['HTTP_HOST'])
uri = base_url + reverse('oauth-authorize')
if 'next' in request.GET:
uri = '{}?next={}'.format(uri, request.GET['next'])
return uri
def get_redirect_url(self, *args, **kwargs):
callback_uri = self.get_callback_uri(self.request)
try:
authenticator = OAuthAuthenticator()
authorization_url = authenticator.get_authorization_url(
callback_uri)
except AuthenticatorError as err:
messages.error(self.request, err)
return reverse('home')
else:
self.request.session['obp'] = {
'authenticator': 'obp.oauth.OAuthAuthenticator',
'authenticator_kwargs': {
'token': authenticator.token,
'secret': authenticator.secret,
}
}
return authorization_url
class OAuthAuthorizeView(RedirectView, LoginToDjangoMixin):
"""View to authorize user after OAuth 1 initiation"""
def get_redirect_url(self, *args, **kwargs):
session_data = self.request.session.get('obp')
authenticator_kwargs = session_data.get('authenticator_kwargs')
authenticator = OAuthAuthenticator(**authenticator_kwargs)
authorization_url = self.request.build_absolute_uri()
try:
authenticator.set_access_token(authorization_url)
except AuthenticatorError as err:
messages.error(self.request, err)
else:
session_data['authenticator_kwargs'] = {
'token': authenticator.token,
'secret': authenticator.secret,
}
self.login_to_django()
messages.success(self.request, 'OAuth login successful!')
redirect_url = self.request.GET.get('next', reverse('home'))
return redirect_url
class DirectLoginView(FormView, LoginToDjangoMixin):
"""View to login via DirectLogin"""
form_class = DirectLoginForm
template_name = 'obp/directlogin.html'
def get_success_url(self):
messages.success(self.request, 'DirectLogin successful!')
return reverse('runtests-index')
def form_valid(self, form):
"""
Stores a DirectLogin token in the request's session for use in
future requests. It also logs in to Django.
"""
authenticator = form.cleaned_data['authenticator']
self.request.session['obp'] = {
'authenticator': 'obp.directlogin.DirectLoginAuthenticator',
'authenticator_kwargs': {
'token': authenticator.token,
}
}
self.login_to_django()
return super(DirectLoginView, self).form_valid(form)
class GatewayLoginView(FormView, LoginToDjangoMixin):
"""View to login via GatewayLogin"""
form_class = GatewayLoginForm
template_name = 'obp/gatewaylogin.html'
def get_success_url(self):
messages.success(self.request, 'GatewayLogin successful!')
return reverse('runtests-index')
def form_valid(self, form):
"""
Stores a GatewayLogin token in the request's session for use in
future requests. It also logs in to Django.
"""
authenticator = form.cleaned_data['authenticator']
self.request.session['obp'] = {
'authenticator': 'obp.gatewaylogin.GatewayLoginAuthenticator',
'authenticator_kwargs': {
'token': authenticator.token,
}
}
self.login_to_django()
return super(GatewayLoginView, self).form_valid(form)
class LogoutView(RedirectView):
"""View to logout"""
def get_redirect_url(self, *args, **kwargs):
logout(self.request)
if 'obp' in self.request.session:
del self.request.session['obp']
return reverse('home')

View File

@ -10,8 +10,7 @@ from django.urls import reverse, reverse_lazy
from django.views.generic import FormView, TemplateView, View
from base.filters import BaseFilter
from base.api import api, APIError
from base.api_helper import get_bank_id_choices
from obp.api import API, APIError
from .forms import AddEntitlementForm
@ -42,9 +41,10 @@ class IndexView(LoginRequiredMixin, TemplateView):
def get_users_rolenames(self, context):
users = []
api = API(self.request.session.get('obp'))
try:
urlpath = '/users'
users = api.get(self.request, urlpath)
users = api.get(urlpath)
except APIError as err:
messages.error(self.request, err)
return [], []
@ -85,9 +85,13 @@ class DetailView(LoginRequiredMixin, FormView):
form_class = AddEntitlementForm
template_name = 'users/detail.html'
def dispatch(self, request, *args, **kwargs):
self.api = API(request.session.get('obp'))
return super(DetailView, self).dispatch(request, *args,**kwargs)
def get_form(self, *args, **kwargs):
form = super(DetailView, self).get_form(*args, **kwargs)
form.fields['bank_id'].choices = get_bank_id_choices(self.request)
form.fields['bank_id'].choices = self.api.get_bank_id_choices()
return form
def form_valid(self, form):
@ -99,7 +103,7 @@ class DetailView(LoginRequiredMixin, FormView):
'bank_id': data['bank_id'],
'role_name': data['role_name'],
}
entitlement = api.post(self.request, urlpath, payload=payload)
entitlement = self.api.post(urlpath, payload=payload)
except APIError as err:
messages.error(self.request, err)
return super(DetailView, self).form_invalid(form)
@ -112,13 +116,12 @@ class DetailView(LoginRequiredMixin, FormView):
def get_context_data(self, **kwargs):
context = super(DetailView, self).get_context_data(**kwargs)
# NOTE: assuming there is just one user with that email address
# The API needs a call 'get user by id'!
user = {}
try:
urlpath = '/users/user_id/{}'.format(self.kwargs['user_id'])
user = api.get(self.request, urlpath)
user = self.api.get(urlpath)
context['form'].fields['user_id'].initial = user['user_id']
except APIError as err:
messages.error(self.request, err)
@ -134,10 +137,11 @@ class DeleteEntitlementView(LoginRequiredMixin, View):
def post(self, request, *args, **kwargs):
"""Deletes entitlement from API"""
api = API(self.request.session.get('obp'))
try:
urlpath = '/users/{}/entitlement/{}'.format(
kwargs['user_id'], kwargs['entitlement_id'])
api.delete(request, urlpath)
api.delete(urlpath)
msg = 'Entitlement with role {} has been deleted.'.format(
request.POST.get('role_name', '<undefined>'))
messages.success(request, msg)

View File

@ -2,4 +2,5 @@ Django==1.11.5
oauthlib==2.0.0
requests==2.11.1
requests-oauthlib==0.6.2
PyJWT==1.5.3
gunicorn==19.6.0