First commit

This commit is contained in:
Léo 2019-01-17 19:20:13 +01:00
commit 6dc68ff8ac
11 changed files with 896 additions and 0 deletions

26
adduser.py Executable file
View File

@ -0,0 +1,26 @@
#!/usr/bin/env python3
# -*-coding:utf-8 -*
import sauth
import getpass
import argparse
parser = argparse.ArgumentParser(description="""
Add a user with its password
""", formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument('username', help="Username to add")
parser.add_argument('--password', '-p', help="Password. If not provided will be asked", type=str, dest='password')
args = parser.parse_args()
if args.password is None :
password = getpass.getpass('Password : ')
else:
password = args.password
sa = sauth.SAuth()
if sa.add_user(args.username, password):
print("User added !")
else:
print("An error occured (user already present for instance)")

165
example.py Executable file
View File

@ -0,0 +1,165 @@
#!/usr/bin/env python3
# -*-coding:utf-8 -*
import bottle
import beaker.middleware
import sauth
app = bottle.app()
session_opts = {
'session.type': 'file',
'session.data_dir': './sessions/',
'session.auto': True,
}
app = beaker.middleware.SessionMiddleware(app, session_opts)
sa = sauth.SAuth()
auth = sa.make_auth_decorator(fail_redirect="/login")
def post_get(name, default=''):
return bottle.request.POST.get(name, default).strip()
def get_get(name, default=''):
return bottle.request.GET.get(name, default).strip()
@bottle.route('/')
@bottle.view('home')
def hello():
usn = sauth.get_username()
usr = sauth.get_roles()
if usn=='':
usn=None
return {'username':usn, 'roles':usr}
@bottle.route('/login')
@bottle.view('login')
def login_form():
"""Serve login form"""
if get_get('error')=='':
return {'error':False}
else:
return {'error':True}
@bottle.post('/login')
def login():
"""Authenticate users"""
username = post_get('username')
password = post_get('password')
sa.login(username, password, success_redirect='/private', fail_redirect='/login?error=1')
@bottle.route('/logout')
def logout():
sa.logout()
return "LOGOUT DONE <a href=\"/\">Back to home</a>"
@bottle.route('/password')
@bottle.view('password')
@auth()
def password_form():
return {'username':sauth.get_username()}
@bottle.post('/password')
@auth()
def password():
username = sauth.get_username()
password = post_get('password')
sa.set_password(username, password)
bottle.redirect('/')
@bottle.route('/private')
@auth()
def private():
return "Page for Log-in users only. <a href=\"/\">Back to home</a>"
################################################################
# Admin section
################################################################
@bottle.route('/admin')
@bottle.view('admin')
@auth(role='admin')
def admin():
sa.refresh()
luser = []
for u in sa.get_all_usernames():
us = sa.get_user(u)
luser.append([u, us])
return {'user':sauth.get_username(), 'luser':luser, 'lroles':sa.get_all_roles()}
@bottle.post('/admin/user/add')
@auth(role='admin')
def admin_user_add():
username = post_get('username')
password = post_get('password')
sa.add_user(username, password)
bottle.redirect('/admin')
return
@bottle.route('/admin/user/<user>/del_role/<role>')
@auth(role='admin')
def admin_user_delrole(user, role):
sa.rm_user_role(user, role)
bottle.redirect('/admin')
return
@bottle.post('/admin/user/<user>/add_role')
@auth(role='admin')
def admin_user_addrole(user):
role = post_get('role')
if not role=="":
sa.add_user_role(user, role)
bottle.redirect('/admin')
return
@bottle.post('/admin/user/<user>/password')
@auth(role='admin')
def admin_user_password(user):
password = post_get('password')
sa.set_password(user, password)
bottle.redirect('/admin')
return
@bottle.route('/admin/user/<user>/rm')
@auth(role='admin')
def admin_user_rm(user):
sa.rm_user(user)
bottle.redirect('/admin')
return
@bottle.post('/admin/role/add')
@auth(role='admin')
def admin_role_add():
role = post_get('role')
sa.add_role(role)
bottle.redirect('/admin')
return
@bottle.route('/admin/role/<role>/del_sub/<subrole>')
@auth(role='admin')
def admin_role_delsub(role, subrole):
sa.rm_role(role, subrole=subrole)
bottle.redirect('/admin')
return
@bottle.post('/admin/role/<role>/add_sub/')
@auth(role='admin')
def admin_role_addsub(role):
subrole = post_get('addrole')
print("1::subrole::{}".format(subrole))
if not subrole=='':
sa.add_role(role, subrole=subrole)
bottle.redirect('/admin')
return
@bottle.route('/admin/role/<role>/rm/')
@auth(role='admin')
def admin_role_rm(role):
sa.rm_role(role)
bottle.redirect('/admin')
return
bottle.run(app, host='localhost', port=8080)

260
sauth/__init__.py Normal file
View File

@ -0,0 +1,260 @@
# -*-coding:utf-8 -*
import logging
logger = logging.getLogger()
import bottle
import hashlib
import os
def get_session():
return bottle.request.environ['beaker.session']
def get_username():
session = get_session()
if 'username' in session:
username = get_session()['username']
if username is None or username=='' :
return None
else:
return username
return None
def get_roles():
session = get_session()
if 'roles' in session:
roles = get_session()['roles']
if roles is None or roles=='' :
return None
else:
return roles
return None
def _redirect(location):
if location is None :
bottle.abort(401, "Sorry, access denied.")
else:
bottle.redirect(location)
class SAuth(object):
"""
Simple Auth class with decorators for bottle
"""
def __init__(self, backend=None, backend_infos={}, hashing_algorithm=None):
"""Auth/Authorization/Accounting class
backend: backend to be used (str)
backend_infos: arguments passed to backend constructor
hashing_algorithm: hashing algorith to use, sha256 by default
"""
if hashing_algorithm in hashlib.algorithms_available :
self.hashing_algorithm = hashing_algorithm
elif 'sha256' in hashlib.algorithms_available :
self.hashing_algorithm = 'sha256'
# Setup JsonBackend by default
# Currently no other backend available
if backend is None or backend == 'json':
from sauth import json_backend
logger.info("Selected backend : json")
self._backend = json_backend.JsonBackend(**backend_infos)
else:
raise ValueError("Selected backend not found")
def make_auth_decorator(self, username=None, role=None, fail_redirect='/login'):
'''
Create a decorator to be used for authentication and authorization
:param username: A resource can be protected for a specific user
:param role: Minimum role level required for authorization
:param fail_redirect: The URL to redirect to if a login is required.
'''
session_manager = self
def auth_require(username=username, role=role, fail_redirect=fail_redirect):
def decorator(func):
import functools
@functools.wraps(func)
def wrapper(*a, **ka):
session_manager._require(username=username, role=role, fail_redirect=fail_redirect)
return func(*a, **ka)
return wrapper
return decorator
return(auth_require)
def get_user(self, username=None):
"""
return a User object for the current user or a specified username
return None if user nout logged in or not found
"""
if username is None :
user = get_username()
if(user is None):
return None
else:
return self.get_user(username=user)
else:
return self._backend.get_user(username)
def get_user_roles(self, username=None):
"""
return a list of the roles of user (main roles and herited)
return None if not found
"""
if username is None :
user = get_username()
if(user is None):
return None
else:
return self.get_user_roles(user)
else:
return self._backend.get_user_roles(username)
def _require(self, username=None, role=None, fail_redirect=None):
"""
Teste authentification for decorator
"""
# Authentication
user = get_username()
if user is None:
_redirect(fail_redirect)
if(username is not None):
if not (user==username):
_redirect(fail_redirect)
if(role is not None):
roles = get_roles()
if not (role in roles):
_redirect(fail_redirect)
return # success
def login(self, username, password, success_redirect=None, fail_redirect=None):
"""
Login function
username : str
password : str
success_redirect and fail_redirect : locations on which to redirect if
login is successful or not
"""
user = self.get_user(username)
ip = bottle.request.environ.get('REMOTE_ADDR')
if(user is not None):
pw=user.password
if pw is None or not len(pw.split('/'))==3 :
logger.warn("Login FAIL user {} from {}".format(user, ip))
_redirect(fail_redirect)
return False
pw=pw.split('/')
hashing_algorithm = pw[0]
salt = bytes.fromhex(pw[1])
dk = hashlib.pbkdf2_hmac(hashing_algorithm, password.encode('utf-8'), salt, 100000)
pwtest = dk.hex()
if(pw[2]==pwtest):
get_session()['username']=username
get_session()['roles']=self.get_user_roles(username)
logger.info("Login OK user {} from {}".format(user, ip))
if(success_redirect is not None):
_redirect(success_redirect)
return True
logger.warn("Login FAIL user {} from {}".format(user, ip))
_redirect(fail_redirect)
return False
def logout(self, redirect=None):
"""
Logout function. Destroys session.
"""
usn = get_username()
ip = bottle.request.environ.get('REMOTE_ADDR')
session = get_session()
session['username']=None
session.delete()
logger.info("Logout user {} from {}".format(usn, ip))
if(redirect is not None):
_redirect(redirect)
return
def add_user(self, username, password):
"""
Add a new user
username: str (return False if alsready exists)
password: str
"""
if self.get_user(username) is not None:
return False
logger.info("Adduser {}".format(username))
self._backend.add_user(username)
self.set_password(username, password)
return True
def add_role(self, role, subrole=None):
"""
Add a role or a subrole
(role given by another)
"""
self._backend.add_role(role, subrole=subrole)
return
def add_user_role(self, username, role):
"""
Add a role to user
"""
return self._backend.add_user_role(username, role)
def rm_user(self, username):
"""
Delete a user by name
"""
return self._backend.rm_user(username)
def rm_role(self, role, subrole=None):
"""
Delete a role.
If subrole is not None, delete the subrole of role
"""
return self._backend.rm_role(role, subrole=subrole)
def rm_user_role(self, username, role):
"""
Remove a role to user
"""
return self._backend.rm_user_role(username, role)
def update_user(self, username=None, **kwargs):
if username is None :
username = get_username()
self._backend.update_user(username, **kwargs)
return
def set_password(self, username, password):
"""
Set a new password to user
username: username of the user (str)
password: new password (str)
"""
logger.info("Change password {}".format(username))
salt = os.urandom(16)
dk = hashlib.pbkdf2_hmac(self.hashing_algorithm, password.encode('utf-8'), salt, 100000)
ssalt = salt.hex()
spassword = dk.hex()
s = self.hashing_algorithm + "/" + ssalt + "/" + spassword
return self._backend.update_user(username, password=s)
def get_all_usernames(self):
return self._backend.get_users()
def get_all_roles(self):
return self._backend.get_roles()
def refresh(self):
if 'refresh' in dir(self._backend):
self._backend.refresh()
return
return

133
sauth/json_backend.py Normal file
View File

@ -0,0 +1,133 @@
# -*-coding:utf-8 -*
import json
import collections
from sauth import user
class JsonBackend:
def __init__(self, directory='users', users_fname='users.json', roles_fname='roles.json'):
"""
directory: directory in which configuration files are (str)
users_fname: filename for users (str)
roles_fname: filename for roles (str)
"""
if not (directory[-1]=='/'):
directory = directory + "/"
self.users_f = directory + users_fname
self.roles_f = directory + roles_fname
self.refresh()
def get_user(self, username):
if username in self._json_users :
data = self._json_users[username]
return user.User(username, **data)
else:
return None
def get_user_roles(self, username):
if username in self._json_users :
data = self._json_users[username]
us = user.User(username, **data)
for r in us.roles:
if(r in self._json_roles):
for sr in self._json_roles[r]:
if not sr in us.roles:
us.roles.append(sr)
return us.roles
else:
return None
def get_roles(self):
return self._json_roles
def get_users(self):
return list(self._json_users.keys())
def add_role(self, role, subrole=None):
print("Role :{} subrole:{}".format(role, subrole))
if role not in self._json_roles:
print("Role added")
self._json_roles[role]=[]
if(subrole is not None):
print("Subrole added")
self._json_roles[role].append(subrole)
self._update_json_roles()
return
def add_user_role(self, username, role):
if 'roles' in self._json_users[username]:
if not role in self._json_users[username]['roles']:
self._json_users[username]['roles'].append(role)
self._update_json()
return True
else:
self._json_users[username]['roles'] = [role]
self._update_json()
return True
return False
def rm_user(self, username):
if username in self._json_users :
del self._json_users[username]
self._update_json_roles()
return True
return False
def rm_role(self, role, subrole=None):
if role in self._json_roles:
if subrole is None:
del self._json_roles[role]
self._update_json_roles()
return True
else:
for i in range(len(self._json_roles[role])):
if self._json_roles[role][i]==subrole :
del self._json_roles[role][i]
self._update_json_roles()
return True
break
else:
return False
def rm_user_role(self, username, role):
if 'roles' in self._json_users[username] and role in self._json_users[username]['roles']:
for i in range(len(self._json_users[username]['roles'])):
if self._json_users[username]['roles'][i]==role:
del self._json_users[username]['roles'][i]
self._update_json()
return True
break
return False
def add_user(self, username, **kwargs):
if (username in self._json_users):
return False
self._json_users[username] = {}
self._update_json()
return True
def update_user(self, username, **kwargs):
if(username in self._json_users):
for e in kwargs:
self._json_users[username][e] = kwargs[e]
self._update_json()
return True
else:
return False
def _update_json(self):
with open(self.users_f, 'w') as f:
json.dump(self._json_users, f, indent=2, ensure_ascii=False)
return
def _update_json_roles(self):
with open(self.roles_f, 'w') as f:
json.dump(self._json_roles, f, ensure_ascii=False)
return
def refresh(self):
with open(self.users_f, 'r') as f:
self._json_users = json.load(f, object_pairs_hook=collections.OrderedDict)
with open(self.roles_f, 'r') as f:
self._json_roles = json.load(f, object_pairs_hook=collections.OrderedDict)

19
sauth/user.py Normal file
View File

@ -0,0 +1,19 @@
# -*-coding:utf-8 -*
class User:
def __init__(self, username, password=None, roles=[], **kw):
self.username = username
self.password = password
self.roles = roles
self.info = {}
for e in kw:
self.info[e] = kw[e]
def __str__(self):
s = "User {}".format(self.username)
for e in self.info:
s+= " {} : {}".format(e, self.info[e])
return s
def __repr__(self):
return self.__str__()

1
users/roles.json Normal file
View File

@ -0,0 +1 @@
{"admin": ["user"], "user": []}

15
users/users.json Normal file
View File

@ -0,0 +1,15 @@
{
"admin": {
"password": "sha256/34467647c75727d973bcef4ca739388d/9c6ee4cfe5091789035f162cf479fe2b853b679c381ebfc5b4174959b1a280e3",
"roles": [
"admin",
"user"
]
},
"user": {
"password": "sha256/13332f080564518570773fdd24b7602d/9b88dba299cc37fe8ab171066c791e4343a0e71853dfbcbcc76e4df43d57f923",
"roles": [
"user"
]
}
}

160
views/admin.tpl Normal file
View File

@ -0,0 +1,160 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta content="text/html; charset=utf-8" http-equiv="content-type">
<div id='main'>
<h2>Administration page</h2>
<p>Welcome {{user}}</p>
<div id="users">
<h4> Users </h4>
<p>Create new user:</p>
<form action="/admin/user/add" method="post">
<p><label>Username</label> <input type="text" name="username" /></p>
<p><label>Password</label> <input type="password" name="password" /></p>
<button type="submit" style="margin-left: 13em;"> OK </button>
</form>
%for u in luser:
<fieldset>
<legend><b>{{u[0]}}</b></legend>
<b>Roles : </b>
%if u[1].roles:
%for r in u[1].roles:
{{r}} (<a href="/admin/user/{{u[0]}}/del_role/{{r}}">X</a>),
%end
%end
<br/>
<a href="javascript:showhide('user_{{u[0]}}')">Details +/-</a>
<div id="user_{{u[0]}}" style="display:none;">
<!-- Add Role -->
<p>
<form action="/admin/user/{{u[0]}}/add_role" method="post">
<b>Add role : </b>
<select name="role">
<option></option>
%for r in lroles:
<option>{{r}}</option>
%end
</select>
<button type="submit">OK</button>
</form>
</p>
<!-- Change Password -->
<p>
<form action="/admin/user/{{u[0]}}/password" method="post">
<b>Change password : </b>
<input type="password" name="password" />
<button type="submit">OK</button>
</form>
</p>
<!-- Other data -->
%for i in u[1].info :
%if i not in ['password', 'roles']:
<p>
<b>{{i}} :</b> {{u[1].info[i]}}
</p>
%end
%end
<p><a href="/admin/user/{{u[0]}}/rm">Delete user</a></p>
</div>
</fieldset>
%end
<br/>
</div>
<div id='roles'>
<h4> Roles </h4>
<p>Create new role:</p>
<form action="/admin/role/add" method="post">
<p><label>Role</label> <input type="text" name="role" /></p>
<button type="submit" style="margin-left: 13em;"> OK </button>
</form>
<br />
%for r in lroles:
<fieldset>
<legend><b>{{r}}</b></legend>
%for sr in lroles[r]:
{{sr}} (<a href="/admin/role/{{r}}/del_sub/{{sr}}">X</a>),
%end
<br/>
<a href="javascript:showhide('role_{{r}}')">Details +/-</a>
<div id="role_{{r}}" style="display:none;">
<!-- Add Role -->
<p>
<form action="/admin/role/{{r}}/add_sub/" method="post">
<b>Add provided : </b>
<select name="addrole">
<option></option>
%for ri in lroles:
<option>{{ri}}</option>
%end
</select>
<button type="submit">OK</button>
</form>
</p>
<p><a href="/admin/role/{{r}}/rm/">Delete role</a></p>
</div>
</fieldset>
%end
</div>
<div class="clear"></div>
<div id="urls">
<a href="/">index</a> <a href="/logout">logout</a>
</div>
</div>
<script>
function showhide (id) {
element = document.getElementById(id)
if(element.style.display=='block'){
element.style.display = 'none';
}
else{
element.style.display = 'block';
}
}
</script>
<style>
div#roles { width: 45%; float: right}
div#users { width: 45%; float: left}
div#main {
color: #777;
margin: auto;
margin-left: 5em;
font-size: 80%;
}
input {
background: #f8f8f8;
border: 1px solid #777;
margin: auto;
}
input:hover { background: #fefefe}
label {
width: 8em;
float: left;
text-align: right;
margin-right: 0.5em;
display: block
}
div#status {
border: 1px solid #999;
padding: .5em;
margin: 2em;
width: 15em;
-moz-border-radius: 10px;
border-radius: 10px;
}
.clear { clear: both;}
div#urls {
position:absolute;
top:0;
right:1em;
}
</style>

46
views/home.tpl Normal file
View File

@ -0,0 +1,46 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta content="text/html; charset=utf-8" http-equiv="content-type">
<style>
div {
color: #777;
margin: auto;
width: 20em;
text-align: center;
}
div#hbox {width: 100%;}
div#hbox div.box {float: center; width: 33%;}
</style>
</head>
<body>
<div id="hbox">
<div class="box">
<h2>Simple Auth for Bottle</h2>
<h3>Demonstration site</h3>
%if username is None:
<p>You are not loged in</p>
%else:
<p>You are loged as {{username}}</p>
<p>You have these roles :
%for r in roles:
{{r}},
%end
</p>
<p><a href="/logout">Logout</a></p>
<p><a href="/password">Change password</a></p>
%end
<h4>Pages of the test site :</h4>
<p>
<ul>
<li>This home page</a>
<li><a href="/login">Login page</a>
<li><a href="/private">A restricted access page</a>
<li><a href="/admin">The admin page</a>
</ul>
</p>
</div>
</div>
</body>
</html>

44
views/login.tpl Normal file
View File

@ -0,0 +1,44 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta content="text/html; charset=utf-8" http-equiv="content-type">
<style>
div {
color: #777;
margin: auto;
width: 20em;
text-align: center;
}
div#hbox {width: 100%;}
div#hbox div.box {float: center; width: 33%;}
p.error {width:100%; background-color:#ffb3b3;}
input {
background: #f8f8f8;
border: 1px solid #777;
margin: auto;
}
input:hover { background: #fefefe}
</style>
</head>
<body>
<div id="hbox">
<div class="box">
<h2>Login</h2>
<p>Please insert your credentials:</p>
%if error:
<p class="error">Incorrect username/password.</p>
%end
<form action="login" method="post" name="login">
<p>Username : <input type="text" name="username" /></p>
<p>Password : <input type="password" name="password" /></p>
<br/><br/>
<button type="submit"> OK </button>
</form>
<br />
</div>
<br style="clear: left;" />
</div>
</body>
</html>

27
views/password.tpl Normal file
View File

@ -0,0 +1,27 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta content="text/html; charset=utf-8" http-equiv="content-type">
<div class="box">
<h2>Password change</h2>
<p>Please insert your new password:</p>
<form action="/password" method="post">
<p><input type="password" name="password" /></p>
<p><button type="submit" > OK </button></p>
</form>
<br />
</div>
<style>
div {
color: #777;
margin: auto;
width: 20em;
text-align: center;
}
input {
background: #f8f8f8;
border: 1px solid #777;
margin: auto;
}
input:hover { background: #fefefe}
</style>