Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Modular auth backend #1162

Closed
wants to merge 12 commits into from
7 changes: 5 additions & 2 deletions ansible/roles/screenly/files/screenly.conf
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ verify_ssl = True
; Run Resin wifi connect if there is no connection
enable_offline_mode = False

[auth]
; If desired, fill in with appropriate username and password
; Set to 'auth_basic' to use HTTP Basic Authentication (see below)
auth_backend =

[auth_basic]
; Fill in with appropriate username and password
user=
password=
172 changes: 172 additions & 0 deletions auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from abc import ABCMeta, abstractmethod, abstractproperty
from functools import wraps
import hashlib

from flask import request, Response


class Auth(object):
__metaclass__ = ABCMeta

@abstractmethod
def authenticate(self):
"""
Let the user authenticate himself.
:return: a Response which initiates authentication.
"""
pass

@abstractproperty
def is_authorized(self):
"""
See if the user is authorized for the request.
:return: bool
"""
pass

def authorize(self):
"""
If the request is not authorized, let the user authenticate himself.
:return: a Response which initiates authentication or None if authorized.
"""
if not self.is_authorized:
return self.authenticate()

def update_settings(self, current_password):
pass

@property
def template(self):
pass


class NoAuth(Auth):
name = 'Disabled'
id = ''
config = {}

def is_authorized(self):
return True

def authenticate(self):
pass


class BasicAuth(Auth):
name = 'Basic'
id = 'auth_basic'

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usually, it is not considered a good practice to have anything named by builtins.

config = {
'auth_basic': {
'user': '',
'password': ''
}
}

def __init__(self, settings):
self.settings = settings

def _check(self, username, password):
"""
Check username/password combo against database.
:param username: str
:param password: str
:return: True if the check passes.
"""
return self.settings['user'] == username and self.check_password(password)

def check_password(self, password):
hashed_password = hashlib.sha256(password).hexdigest()
return self.settings['password'] == hashed_password

@property
def is_authorized(self):
auth = request.authorization
return auth and self._check(auth.username, auth.password)

@property
def template(self):
return 'auth_basic.html', {'user': self.settings['user']}

def authenticate(self):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not exactly doing what is written in the docstring.
Also, seems terms authorize and authenticated are used a bit confusing
http://cdn.differencebetween.net/wp-content/uploads/2017/10/Difference-between-Authentication-and-Authorization.png

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with your statement about terminology. I’ll come up with something better.
However the function does exactly what the docstring says: initiates authentication. Okay, the browser initiates it as a result of getting 401. But if we will have another auth backend which can generate its own login page that’s what this function will return. And that will truly initiate authentication.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But looking at the authorize function above it seems like authenticate is used to always return fail response.

if not self.is_authorized:
   return self.authenticate()

realm = "Screenly OSE {}".format(self.settings['player_name'])
return Response("Access denied", 401, {"WWW-Authenticate": 'Basic realm="{}"'.format(realm)})

def update_settings(self, current_pass):
current_pass_correct = self.check_password(current_pass)
new_user = request.form.get('user', '')
new_pass = request.form.get('password', '')
new_pass2 = request.form.get('password2', '')
new_pass = '' if new_pass == '' else hashlib.sha256(new_pass).hexdigest()
new_pass2 = '' if new_pass2 == '' else hashlib.sha256(new_pass2).hexdigest()
# Handle auth components
if self.settings['password'] != '': # if password currently set,
if new_user != self.settings['user']: # trying to change user
# should have current password set. Optionally may change password.
if not current_pass:
raise ValueError("Must supply current password to change username")
if not current_pass_correct:
raise ValueError("Incorrect current password.")

self.settings['user'] = new_user

if new_pass != '':
if not current_pass:
raise ValueError("Must supply current password to change password")
if not current_pass_correct:
raise ValueError("Incorrect current password.")

if new_pass2 != new_pass: # changing password
raise ValueError("New passwords do not match!")

self.settings['password'] = new_pass

else: # no current password
if new_user != '': # setting username and password
if new_pass != '' and new_pass != new_pass2:
raise ValueError("New passwords do not match!")
if new_pass == '':
raise ValueError("Must provide password")
self.settings['user'] = new_user
self.settings['password'] = new_pass
else:
raise ValueError("Must provide username")


class WoTTAuth(BasicAuth):
name = 'WoTT'
id = 'auth_wott'
config = {
'auth_wott': {
# TODO: return real settings
}
}

def __init__(self, settings):
super(WoTTAuth, self).__init__(settings)
# TODO: read credentials, store them into self.username and self.password

def _check(self, username, password):
# TODO: compare username and password with self.username and self.password
return super(WoTTAuth, self)._check(username, password)

@property
def template(self):
return None


def authorized(orig):
"""
Annotation which initiates authentication if the request is unauthorized.
:param orig: Flask function
:return: Response
"""
from settings import settings

@wraps(orig)
def decorated(*args, **kwargs):
if not settings.auth:
return orig(*args, **kwargs)
return settings.auth.authorize() or orig(*args, **kwargs)
return decorated
Loading