Skip to content

Commit

Permalink
fixed #26. Use redis database for high availability of the pivportal …
Browse files Browse the repository at this point in the history
…server.
  • Loading branch information
thedavidwhiteside committed Jan 7, 2017
1 parent 3e107d3 commit 9539462
Show file tree
Hide file tree
Showing 9 changed files with 70 additions and 35 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ before_install:
# - sudo apt-get install libcurl4-gnutls-dev

install:
- pip install nose flask coveralls PyYAML PyJWT
- pip install nose flask flask-redis mockredispy coveralls PyYAML PyJWT

# Run Tests
script:
Expand Down
1 change: 1 addition & 0 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ RUN apt-get update && apt-get install -y python \
openssl \
libssl-dev \
apache2 \
redis-server \
python-pip
RUN pip install -U pip
RUN pip install -U setuptools
Expand Down
1 change: 1 addition & 0 deletions docker/docker-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

/sbin/setup-initial-client-certs.sh

service redis-server start
service apache2 start

su - pivportal -c "pivportal"
Expand Down
1 change: 1 addition & 0 deletions pivportal/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ port - TCP Port to run service on
---
authorized_users: {"dn1": "user1", "dn2": "user2"}
listen_address: 127.0.0.1
redis: "redis://localhost:6379/0"
port: 8088
register_ticket_timeout: 60
```
11 changes: 11 additions & 0 deletions pivportal/lib/pivportal/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,27 @@ def __init__(self):
help="Listen IP Address",
metavar="LISTEN",
default=None)
parser.add_option("-r", "--redis", dest="redis",
help="redis URL",
metavar="REDIS",
default=None)
parser.add_option("-d", "--debug", dest="debug",
help="debug",
metavar="DEBUG",
default=None)
(options, args) = parser.parse_args()
self.port = options.port
self.listen = options.listen
self.redis_url = options.redis
self.is_debug = options.debug

# Set Defaults If Not Set On CLI
if self.port is None:
self.port = "8088"
if self.listen is None:
self.listen = "127.0.0.1"
if self.redis_url is None:
self.redis_url = "redis://localhost:6379/0"
if self.is_debug is None:
self.is_debug = False

Expand All @@ -49,10 +56,14 @@ def __init__(self):
if "listen_address" in pivportal_conf:
if self.listen is None:
self.listen = str(pivportal_conf["listen_address"])
if "redis" in pivportal_conf:
if self.redis_url is None:
self.redis_url = str(pivportal_conf["redis"])
if "register_ticket_timeout" in pivportal_conf:
pivportal.security.register_ticket_timeout = int(pivportal_conf["register_ticket_timeout"])


def run(self):
""" EntryPoint Of Application """
pivportal.rest.app.config["REDIS_URL"] = self.redis_url
pivportal.rest.app.run(threaded=True, host=self.listen, port=self.port, debug=self.is_debug)
60 changes: 35 additions & 25 deletions pivportal/lib/pivportal/rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,17 @@
import time
import pivportal.security

from flask_redis import FlaskRedis

app = Flask(__name__)

def create_app():
app = Flask(__name__)
redis_store = FlaskRedis()
redis_store.init_app(app)
return (app, redis_store)


(app, redis_store) = create_app()
app.secret_key = os.urandom(24)


Expand Down Expand Up @@ -36,17 +45,17 @@ def user_info():
@pivportal.security.token_required(app.secret_key)
@pivportal.security.valid_client_cert_required
def request_list():

request_list = []
count = 0
for item in pivportal.security.auth_requests:
if item["username"] == username:
if time.time() < item["time"]+pivportal.security.register_ticket_timeout:
request_list.append(item)
auth_requests = redis_store.hgetall("requests")
for requestid in auth_requests:
this_request = json.loads(auth_requests[requestid])
if this_request["username"] == username:
if time.time() < this_request["time"]+pivportal.security.register_ticket_timeout:
this_request["requestid"] = requestid # requestid needed for request_list
request_list.append(this_request)
else:
# Request Expired
del pivportal.security.auth_requests[count]
count += 1
redis_store.hdel("requests", requestid)

return Response(response=json.dumps(request_list), status=200, mimetype="application/json")

Expand All @@ -69,12 +78,13 @@ def request_auth():
return Response(response=json.dumps({"response": " invalid request"}), status=400, mimetype="application/json")

# Authenticate Request
count = 0
for item in pivportal.security.auth_requests:
if item["username"] == username and item["requestid"] == requestid and item["client_ip"] == client_ip:
if item["authorized"] == False and authorized == True and time.time() < item["time"]+pivportal.security.register_ticket_timeout:
pivportal.security.auth_requests[count]["authorized"] = True
count += 1
auth_requests = redis_store.hgetall("requests")
if requestid in auth_requests:
this_request = json.loads(auth_requests[requestid])
if this_request["username"] == username and this_request["client_ip"] == client_ip:
if this_request["authorized"] == False and authorized == True and time.time() < this_request["time"]+pivportal.security.register_ticket_timeout:
this_request["authorized"] = True
redis_store.hmset("requests", {requestid: json.dumps(this_request)})

return Response(response=json.dumps({"response": "success"}), status=200, mimetype="application/json")

Expand All @@ -89,10 +99,11 @@ def request_register():
# client_ip is None when testing, so its ok
return Response(response=json.dumps({"response": " invalid request"}), status=400, mimetype="application/json")

if pivportal.security.is_duplicate_register(username, requestid, pivportal.security.auth_requests):
if pivportal.security.is_duplicate_register(username, requestid, redis_store.hgetall("requests")):
return Response(response=json.dumps({"response": " invalid request"}), status=400, mimetype="application/json")

pivportal.security.auth_requests.append({"username": username, "requestid": requestid, "client_ip": client_ip, "authorized": False, "time": time.time()})
this_request = {"username": username, "client_ip": client_ip, "authorized": False, "time": time.time()}
redis_store.hmset("requests", {requestid: json.dumps(this_request)})

return Response(response=json.dumps({"response": "success"}), status=200, mimetype="application/json")

Expand All @@ -106,17 +117,16 @@ def request_status():
if not pivportal.security.username_is_valid(username) or not pivportal.security.requestid_is_valid(requestid) or not pivportal.security.ip_is_valid(client_ip):
return Response(response=json.dumps({"response": " invalid request"}), status=400, mimetype="application/json")

count = 0
for item in pivportal.security.auth_requests:
if item["username"] == username and item["requestid"] == requestid and item["client_ip"] == client_ip:
if item["authorized"] == True:
auth_requests = redis_store.hgetall("requests")
if requestid in auth_requests:
this_request = json.loads(auth_requests[requestid])
if this_request["username"] == username and this_request["client_ip"] == client_ip:
if this_request["authorized"] == True:
# Success
del pivportal.security.auth_requests[count]
redis_store.hdel("requests", requestid)
return Response(response=json.dumps({"response": "success"}), status=200, mimetype="application/json")
else:
# Delete auth_request, it failed anyway
del pivportal.security.auth_requests[count]
break
count += 1
redis_store.hdel("requests", requestid)

return Response(response=json.dumps({"response": "Authentication Failure"}), status=401, mimetype="application/json")
10 changes: 5 additions & 5 deletions pivportal/lib/pivportal/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@
import datetime
from functools import wraps


# [{ "username": X, "requestid": X, "client_ip": X, "authorized": False, "time": time.time()},]
auth_requests = []
# Redis "requests" hash
# {"12345678": { "username": X, "client_ip": X, "authorized": False, "time": time.time()}}

# {"dn1": "user1", "dn2": "user2"}
dn_to_username = {}
Expand Down Expand Up @@ -40,8 +39,9 @@ def ip_is_valid(ip):


def is_duplicate_register(username, requestid, auth_requests):
for item in auth_requests:
if item["username"] == username and item["requestid"] == requestid:
if requestid in auth_requests:
this_request = json.loads(auth_requests[requestid])
if this_request["username"] == username:
# Request Is Already Registered
return True
return False
Expand Down
2 changes: 1 addition & 1 deletion pivportal/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
author_email='[email protected]',
url='https://github.com/starboarder2001/pivportal/pivportal',
license='MIT',
install_requires=["flask", "PyYAML", "PyJWT", "setuptools"],
install_requires=["flask", "flask-redis", "PyYAML", "PyJWT", "setuptools"],
package_dir={ '': 'lib' },
packages=find_packages('lib'),
classifiers=[
Expand Down
17 changes: 14 additions & 3 deletions pivportal/test/test_pivportal.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@

class TestCli(unittest.TestCase):

def setUp(self):
from flask_redis import FlaskRedis
from mockredis import MockRedis
class MockRedisWrapper(MockRedis):
'''A wrapper to add the `from_url` classmethod'''
@classmethod
def from_url(cls, *args, **kwargs):
return cls()
pivportal.rest.redis_store = FlaskRedis.from_custom_provider(MockRedisWrapper)
pivportal.rest.redis_store.init_app(pivportal.rest.app)

def test_dn_is_valid_withinvalidchars(self):
assert pivportal.security.dn_is_valid("%#DW;$%&*") == False

Expand Down Expand Up @@ -38,21 +49,21 @@ def test_is_duplicate_register(self):
username = "user1"
requestid = "1234567890123456"
authorized = False
auth_requests = [{ "username": username, "requestid": "2234567890123456", "authorized": authorized},]
auth_requests = {"2234567890123456": json.dumps({ "username": username, "authorized": authorized})}
assert pivportal.security.is_duplicate_register(username, requestid, auth_requests) == False

def test_is_duplicate_register_samerequestid(self):
username = "user1"
requestid = "1234567890123456"
authorized = False
auth_requests = [{ "username": username, "requestid": requestid, "authorized": authorized},]
auth_requests = {requestid: json.dumps({ "username": username, "authorized": authorized})}
assert pivportal.security.is_duplicate_register(username, requestid, auth_requests) == True

def test_is_duplicate_register_exact(self):
username = "user1"
requestid = "1234567890123456"
authorized = False
auth_requests = [{ "username": username, "requestid": requestid, "authorized": authorized},]
auth_requests = {requestid: json.dumps({ "username": username, "authorized": authorized})}
assert pivportal.security.is_duplicate_register(username, requestid, auth_requests) == True

def test_user_auth(self):
Expand Down

0 comments on commit 9539462

Please sign in to comment.