-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #65 from thisissoon/feature/allow-known-clients
Feature: Allow Known Clients
- Loading branch information
Showing
10 changed files
with
409 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
#!/usr/bin/env python | ||
# encoding: utf-8 | ||
|
||
""" | ||
fm.auth | ||
======= | ||
Authentication decorators | ||
""" | ||
|
||
# Standard Libs | ||
from functools import wraps | ||
|
||
# First Party Libs | ||
from fm import clients, session | ||
from fm.http import Unauthorized | ||
|
||
|
||
def authenticated(function): | ||
""" This decorator handles allowing access to tesources based on session | ||
or known clients. | ||
""" | ||
|
||
@wraps(function) | ||
def wrapper(*args, **kwargs): | ||
if not is_authenticated_request(): | ||
return Unauthorized() | ||
return function(*args, **kwargs) | ||
|
||
return wrapper | ||
|
||
|
||
def is_authenticated_request(): | ||
""" Retruns if the request is valid | ||
Returns | ||
------- | ||
bool | ||
If the requets is valid | ||
""" | ||
|
||
# Known client? | ||
if clients.valid_request(): | ||
return True | ||
|
||
# Is a user session? | ||
user = session.user_from_session() | ||
if user is not None: | ||
return True | ||
|
||
return False |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
#!/usr/bin/env python | ||
# encoding: utf-8 | ||
|
||
""" | ||
fm.clients | ||
========== | ||
Allow known external clients that are not users access to the API, these could | ||
be the Player or Volume control systems for example which are physical boxes | ||
that run outside of the network perimeter. | ||
Clients should encode their request body with their private key using HMAC, this | ||
should then be sent with the Authorization Header with the username being the | ||
Clients ID and the password the signature. | ||
Example | ||
------- | ||
GET / HTTP/1.1 | ||
Accept: */* | ||
Accept-Encoding: gzip, deflate | ||
Authorization: Basic Y2xpZW50OmZ5S1dEaG5PalNuVnlGWm1mRUV6d1pNNS92NG5XZWJjYTMzZUNZdUxYOVE9 | ||
Connection: keep-alive | ||
Clients should use SHA256 to encode their requests and send a Base 64 encoded | ||
version of the HMAC digest. | ||
""" | ||
|
||
# Standard Libs | ||
import base64 | ||
import hashlib | ||
import hmac | ||
from functools import wraps | ||
|
||
# Third Party Libs | ||
from flask import current_app, request | ||
|
||
# First Party Libs | ||
from fm.http import Unauthorized | ||
|
||
|
||
def know_client_only_required(function): | ||
""" Decorator which requires that the view is accessible by only | ||
known external clients. | ||
""" | ||
|
||
@wraps(function) | ||
def wrapper(*args, **kwargs): | ||
if not valid_request(): | ||
return Unauthorized() | ||
return function(*args, **kwargs) | ||
|
||
return wrapper | ||
|
||
|
||
def valid_request(): | ||
""" Validates an incoming request from and ensures it comes from a trusted | ||
source. | ||
Returns | ||
------- | ||
bool | ||
If the request is from a trust client | ||
""" | ||
|
||
try: | ||
auth_type, auth_creds = request.headers.get('Authorization', '').split() | ||
except ValueError: | ||
return False | ||
|
||
try: | ||
creds = base64.b64decode(auth_creds) | ||
except TypeError: | ||
return False | ||
|
||
try: | ||
cid, sig = creds.split(':') | ||
except ValueError: | ||
return False | ||
|
||
key = get_private_key(cid) | ||
if key is None: | ||
return False | ||
|
||
return validate_signature(key, sig) | ||
|
||
|
||
def get_private_key(client_id): | ||
""" Gets the private key for the client from Flask application | ||
configuration. | ||
Example | ||
------- | ||
>>> EXTERNAL_CLIENTS = { | ||
>>> 'Client-ID': 'PrivateKey' | ||
>>> } | ||
Returns | ||
------- | ||
str or None | ||
The clients private key or None if the client is not found | ||
""" | ||
|
||
clients = current_app.config.get('EXTERNAL_CLIENTS', {}) | ||
key = clients.get(client_id, None) | ||
|
||
return key | ||
|
||
|
||
def validate_signature(key, expected): | ||
""" Validates the HMAC signature sent with the request, encoding the raw | ||
request body and comparing the signatures. | ||
Arguments | ||
--------- | ||
key : str | ||
The client private key | ||
expected : str | ||
The signature expected | ||
Returns | ||
------- | ||
bool | ||
If the signatures match | ||
""" | ||
|
||
# Generates a hex of the request digest based on the secret key | ||
sig = base64.b64encode(hmac.new(key, request.data, hashlib.sha256).digest()) | ||
|
||
# Return if they match or not | ||
return sig == expected |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
#!/usr/bin/env python | ||
# encoding: utf-8 | ||
|
||
""" | ||
tests.test_auth | ||
=============== | ||
Unittests for the fm.auth module. | ||
""" | ||
|
||
# Third Party Libs | ||
import mock | ||
|
||
# First Party Libs | ||
from fm.auth import authenticated, is_authenticated_request | ||
from fm.http import Unauthorized | ||
|
||
|
||
class TestAuthenticatedDecorator(object): | ||
|
||
@authenticated | ||
def i_am_protected(self): | ||
return True | ||
|
||
@mock.patch('fm.auth.is_authenticated_request') | ||
def test_returns_unauthorized(self, _is_authenticated_request): | ||
_is_authenticated_request.return_value = False | ||
|
||
response = self.i_am_protected() | ||
|
||
assert type(response) == Unauthorized | ||
|
||
@mock.patch('fm.auth.is_authenticated_request') | ||
def test_returns_resource(self, _is_authenticated_request): | ||
_is_authenticated_request.return_value = True | ||
|
||
assert self.i_am_protected() | ||
|
||
|
||
class TestIsAuthenticatedRequest(object): | ||
|
||
def test_returns_false_by_default(self): | ||
assert is_authenticated_request() == False | ||
|
||
@mock.patch('fm.auth.session.user_from_session') | ||
def test_valid_session(self, _user_from_session): | ||
_user_from_session.return_value = True | ||
|
||
assert is_authenticated_request() | ||
|
||
@mock.patch('fm.auth.clients.valid_request') | ||
def test_valid_client(self, _valid_request): | ||
_valid_request.return_value = True | ||
|
||
assert is_authenticated_request() |
Oops, something went wrong.