Merge pull request #85 from SunilMohanAdapa/django-auth

Django auth
This commit is contained in:
Nick Daly 2014-07-07 00:37:28 +00:00
commit 9b1f2c6f0d
29 changed files with 123 additions and 547 deletions

2
.gitignore vendored
View File

@ -24,7 +24,7 @@ TODO
\#* \#*
.#* .#*
*~ *~
data/users.sqlite3 data/plinth.sqlite3
predepend predepend
build/ build/
*.pid *.pid

View File

@ -1,4 +1,4 @@
# License to Copy Plinth # License to Copy Plinth
Plinth is Copyright 2011-2013 James Vasile (<james@hackervisions.org>). It Plinth is Copyright 2011-2013 James Vasile (<james@hackervisions.org>). It
is distributed under the GNU Affero General Public License, Version 3 is distributed under the GNU Affero General Public License, Version 3
@ -16,10 +16,6 @@ The documentation to this software is also distributed under the [GNU
Free Documentation License](http://www.gnu.org/licenses/fdl.html), Free Documentation License](http://www.gnu.org/licenses/fdl.html),
version 1.3 or later. version 1.3 or later.
In default form, Plinth incorporates FileDict, a Python module
released under a "MIT/BSD/Python" license, as per [its blog
page](https://erezsh.wordpress.com/2009/05/31/filedict-bug-fixes-and-updates/).
## GNU Affero General Public License, Version 3 ## GNU Affero General Public License, Version 3
GNU AFFERO GENERAL PUBLIC LICENSE GNU AFFERO GENERAL PUBLIC LICENSE

View File

@ -9,18 +9,15 @@ specified and linked otherwise.
- COPYING :: N/A - COPYING :: N/A
- COPYRIGHTS :: N/A - COPYRIGHTS :: N/A
- fabfile.py :: - - fabfile.py :: -
- filedict.py :: [[http://erez.wikidot.com/filedict-0-1-code][CC-BY-SA 3.0]]
- INSTALL :: - - INSTALL :: -
- logger.py :: - - logger.py :: -
- Makefile :: - - Makefile :: -
- menu.py :: - - menu.py :: -
- model.py :: -
- NOTES :: - - NOTES :: -
- plinth :: - - plinth :: -
- plinth.config :: - - plinth.config :: -
- plinth.py :: [[file:plinth.py::__license__%20%3D%20"GPLv3%20or%20later"]["GPLv3 or later"]] - plinth.py :: [[file:plinth.py::__license__%20%3D%20"GPLv3%20or%20later"]["GPLv3 or later"]]
- plinth.sample.config :: - - plinth.sample.config :: -
- plugin_mount.py :: [[http://martyalchin.com/2008/jan/10/simple-plugin-framework/][CC-BY-SA 3.0]]
- README :: - - README :: -
- start.sh :: - - start.sh :: -
- test.sh :: - - test.sh :: -
@ -51,9 +48,7 @@ specified and linked otherwise.
- modules/expert_mode/expert_mode.py :: - - modules/expert_mode/expert_mode.py :: -
- modules/first_boot/first_boot.py :: - - modules/first_boot/first_boot.py :: -
- modules/help/help.py :: - - modules/help/help.py :: -
- modules/lib/auth_page.py :: -
- modules/lib/auth.py :: - - modules/lib/auth.py :: -
- modules/lib/user_store.py :: -
- modules/owncloud/owncloud.py :: - - modules/owncloud/owncloud.py :: -
- modules/packages/packages.py :: - - modules/packages/packages.py :: -
- modules/santiago/santiago.py :: - - modules/santiago/santiago.py :: -

2
cfg.py
View File

@ -19,7 +19,7 @@ host = None
port = None port = None
debug = False debug = False
no_daemon = False no_daemon = False
session_key = '_username' server_dir = ''
main_menu = Menu() main_menu = Menu()

View File

@ -1,152 +0,0 @@
"""filedict.py
a Persistent Dictionary in Python
Author: Erez Shinan
Date : 31-May-2009
"""
import json
import UserDict
import sqlite3
class DefaultArg(object):
pass
class Solutions(object):
Sqlite3 = 0
class FileDict(UserDict.DictMixin):
"A dictionary that stores its data persistantly in a file"
def __init__(self, solution=Solutions.Sqlite3, **options):
assert solution == Solutions.Sqlite3
try:
self.__conn = options.pop('connection')
except KeyError:
filename = options.pop('filename')
self.__conn = sqlite3.connect(filename)
self.__tablename = options.pop('table', 'dict')
self._nocommit = False
assert not options, "Unrecognized options: %s" % options
self.__conn.execute('create table if not exists %s (id integer primary key, hash integer, key blob, value blob);'%self.__tablename)
self.__conn.execute('create index if not exists %s_index ON %s(hash);' % (self.__tablename, self.__tablename))
self.__conn.commit()
def _commit(self):
if self._nocommit:
return
self.__conn.commit()
def __pack(self, value):
return sqlite3.Binary(json.dumps(value))
##return sqlite3.Binary(pickle.dumps(value, -1))
def __unpack(self, value):
return json.loads(str(value))
##return pickle.loads(str(value))
def __get_id(self, key):
cursor = self.__conn.execute('select key,id from %s where hash=?;'%self.__tablename, (hash(key),))
for k,id in cursor:
if self.__unpack(k) == key:
return id
raise KeyError(key)
def __getitem__(self, key):
cursor = self.__conn.execute('select key,value from %s where hash=?;'%self.__tablename, (hash(key),))
for k,v in cursor:
if self.__unpack(k) == key:
return self.__unpack(v)
raise KeyError(key)
def __setitem(self, key, value):
value_pickle = self.__pack(value)
try:
id = self.__get_id(key)
cursor = self.__conn.execute('update %s set value=? where id=?;'%self.__tablename, (value_pickle, id) )
except KeyError:
key_pickle = self.__pack(key)
cursor = self.__conn.execute('insert into %s (hash, key, value) values (?, ?, ?);'
%self.__tablename, (hash(key), key_pickle, value_pickle) )
assert cursor.rowcount == 1
def __setitem__(self, key, value):
self.__setitem(key, value)
self._commit()
def __delitem__(self, key):
id = self.__get_id(key)
cursor = self.__conn.execute('delete from %s where id=?;'%self.__tablename, (id,))
if cursor.rowcount <= 0:
raise KeyError(key)
self._commit()
def update(self, d):
for k,v in d.iteritems():
self.__setitem(k, v)
self._commit()
def __iter__(self):
return (self.__unpack(x[0]) for x in self.__conn.execute('select key from %s;'%self.__tablename) )
def keys(self):
return iter(self)
def values(self):
return (self.__unpack(x[0]) for x in self.__conn.execute('select value from %s;'%self.__tablename) )
def items(self):
return (map(self.__unpack, x) for x in self.__conn.execute('select key,value from %s;'%self.__tablename) )
def iterkeys(self):
return self.keys()
def itervalues(self):
return self.values()
def iteritems(self):
return self.items()
def __contains__(self, key):
try:
self.__get_id(key)
return True
except KeyError:
return False
def __len__(self):
return self.__conn.execute('select count(*) from %s;' % self.__tablename).fetchone()[0]
def __del__(self):
try:
self.__conn
except AttributeError:
pass
else:
self.__conn.commit()
@property
def batch(self):
return self._Batch(self)
class _Batch(object):
def __init__(self, d):
self.__d = d
def __enter__(self):
self.__old_nocommit = self.__d._nocommit
self.__d._nocommit = True
return self.__d
def __exit__(self, type, value, traceback):
self.__d._nocommit = self.__old_nocommit
self.__d._commit()
return True

View File

@ -14,11 +14,7 @@ def init():
class Logger(object): class Logger(object):
"""By convention, log levels are DEBUG, INFO, WARNING, ERROR and CRITICAL.""" """By convention, log levels are DEBUG, INFO, WARNING, ERROR and CRITICAL."""
def log(self, msg, level="DEBUG"): def log(self, msg, level="DEBUG"):
try: cherrypy.log.error("%s %s" % (level, msg), inspect.stack()[2][3], 20)
username = cherrypy.session.get(cfg.session_key)
except AttributeError:
username = ''
cherrypy.log.error("%s %s %s" % (username, level, msg), inspect.stack()[2][3], 20)
def __call__(self, *args): def __call__(self, *args):
self.log(*args) self.log(*args)

View File

@ -1,17 +0,0 @@
class User(dict):
""" Every user must have keys for a username, name, passphrase (this
is a bcrypt hash of the password), salt, groups, and an email address.
They can be blank or None, but the keys must exist. """
def __init__(self, dict=None):
super(User, self).__init__()
for key in ['username', 'name', 'passphrase', 'salt', 'email']:
self[key] = ''
for key in ['groups']:
self[key] = []
if dict:
for key in dict:
self[key] = dict[key]
def __getattr__(self, attr):
return None

View File

@ -21,6 +21,7 @@ Plinth module for configuring timezone, hostname etc.
from django import forms from django import forms
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.core import validators from django.core import validators
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from gettext import gettext as _ from gettext import gettext as _
@ -29,7 +30,6 @@ import socket
import actions import actions
import cfg import cfg
from ..lib.auth import login_required
import util import util
@ -102,7 +102,7 @@ def index(request):
form = None form = None
is_expert = cfg.users.expert(request=request) is_expert = request.user.groups.filter(name='Expert').exists()
if request.method == 'POST' and is_expert: if request.method == 'POST' and is_expert:
form = ConfigurationForm(request.POST, prefix='configuration') form = ConfigurationForm(request.POST, prefix='configuration')
# pylint: disable-msg=E1101 # pylint: disable-msg=E1101
@ -160,9 +160,6 @@ def set_hostname(hostname):
actions.superuser_run("xmpp-pre-hostname-change") actions.superuser_run("xmpp-pre-hostname-change")
actions.superuser_run("hostname-change", hostname) actions.superuser_run("hostname-change", hostname)
actions.superuser_run("xmpp-hostname-change", hostname, async=True) actions.superuser_run("xmpp-hostname-change", hostname, async=True)
# don't persist/cache change unless it was saved successfuly
sys_store = util.filedict_con(cfg.store_file, 'sys')
sys_store['hostname'] = hostname
except OSError: except OSError:
return False return False

View File

@ -19,12 +19,12 @@
Plinth module for running diagnostics Plinth module for running diagnostics
""" """
from gettext import gettext as _ from django.contrib.auth.decorators import login_required
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from gettext import gettext as _
import actions import actions
import cfg import cfg
from ..lib.auth import login_required
def init(): def init():

View File

@ -1,10 +1,11 @@
from django import forms from django import forms
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from gettext import gettext as _ from gettext import gettext as _
import cfg import cfg
from ..lib.auth import login_required from ..lib.auth import get_group
class ExpertsForm(forms.Form): # pylint: disable-msg=W0232 class ExpertsForm(forms.Form): # pylint: disable-msg=W0232
@ -43,23 +44,21 @@ def index(request):
def get_status(request): def get_status(request):
"""Return the current status""" """Return the current status"""
return {'expert_mode': cfg.users.expert(request=request)} return {'expert_mode': request.user.groups.filter(name='Expert').exists()}
def _apply_changes(request, new_status): def _apply_changes(request, new_status):
"""Apply expert mode configuration""" """Apply expert mode configuration"""
message = (messages.info, _('Settings unchanged')) message = (messages.info, _('Settings unchanged'))
user = cfg.users.current(request=request) expert_group = get_group('Expert')
if new_status['expert_mode']: if new_status['expert_mode']:
if not 'expert' in user['groups']: if not expert_group in request.user.groups.all():
user['groups'].append('expert') request.user.groups.add(expert_group)
message = (messages.success, _('Expert mode enabled')) message = (messages.success, _('Expert mode enabled'))
else: else:
if 'expert' in user['groups']: if expert_group in request.user.groups.all():
user['groups'].remove('expert') request.user.groups.remove(expert_group)
message = (messages.success, _('Expert mode disabled')) message = (messages.success, _('Expert mode disabled'))
cfg.users.set(user['username'], user)
message[0](request, message[1]) message[0](request, message[1])

View File

@ -19,12 +19,12 @@
Plinth module to configure a firewall Plinth module to configure a firewall
""" """
from django.contrib.auth.decorators import login_required
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from gettext import gettext as _ from gettext import gettext as _
import actions import actions
import cfg import cfg
from ..lib.auth import login_required
import service as service_module import service as service_module

View File

@ -20,9 +20,5 @@ Plinth library modules
""" """
from . import auth from . import auth
from . import auth_page
from . import user_store
__all__ = ['auth', __all__ = ['auth']
'auth_page',
'user_store']

View File

@ -1,98 +1,29 @@
from django.http.response import HttpResponseRedirect from django.contrib.auth.models import Group, User
import functools
from passlib.hash import bcrypt
from passlib.exc import PasswordSizeError
import cfg
from model import User
def add_user(username, passphrase, name='', email='', expert=False): def add_user(username, passphrase, name='', email='', expert=False):
"""Add a new user with specified username and passphrase. """Add a new user with specified username and passphrase"""
""" if not username:
error = None return 'Must specify a username!'
if not username: error = "Must specify a username!"
if not passphrase: error = "Must specify a passphrase!"
if error is None: if not passphrase:
if username in map(lambda x: x[0], cfg.users.get_all()): return 'Must specify a passphrase!'
error = "User already exists!"
else:
try:
pass_hash = bcrypt.encrypt(passphrase)
except PasswordSizeError:
error = "Password is too long."
if error is None: user = User.objects.create_user(username, email=email,
di = { password=passphrase)
'username':username, user.first_name = name
'name':name, user.save()
'email':email,
'expert':'on' if expert else 'off',
'groups':['expert'] if expert else [],
'passphrase':pass_hash,
'salt':pass_hash[7:29], # for bcrypt
}
new_user = User(di)
cfg.users.set(username,new_user)
if error: if expert:
cfg.log(error) user.groups.add(get_group('Expert'))
return error
def check_credentials(username, passphrase): def get_group(name):
"""Verifies credentials for username and passphrase. """Return an existing or newly created group with given name"""
Returns None on success or a string describing the error on failure.
Handles passwords up to 4096 bytes:
>>> len("A" * 4096)
4096
>>> len(u"u|2603" * 682)
4092
"""
if not username or not passphrase:
error = "No username or password."
cfg.log(error)
return error
bad_authentication = "Bad username or password."
hashed_password = None
if username not in cfg.users or 'passphrase' not in cfg.users[username]:
cfg.log(bad_authentication)
return bad_authentication
hashed_password = cfg.users[username]['passphrase']
try: try:
# time-dependent comparison when non-ASCII characters are used. group = Group.objects.get(name__exact=name)
if not bcrypt.verify(passphrase, hashed_password): except Group.DoesNotExist:
error = bad_authentication group = Group(name=name)
else: group.save()
error = None
except PasswordSizeError:
error = bad_authentication
if error: return group
cfg.log(error)
return error
# XXX: Only required until we start using Django authentication system properly
def login_required(func):
"""A decorator to ensure that user is logged in before accessing a view"""
@functools.wraps(func)
def wrapper(request, *args, **kwargs):
"""Check that user is logged in"""
if not request.session.get(cfg.session_key, None):
return HttpResponseRedirect(
cfg.server_dir + "/auth/login?from_page=%s" % request.path)
return func(request, *args, **kwargs)
return wrapper

View File

@ -1,67 +0,0 @@
"""
Controller to provide login and logout actions
"""
import cfg
from django import forms
from django.http.response import HttpResponseRedirect
from django.template.response import TemplateResponse
from gettext import gettext as _
from . import auth
class LoginForm(forms.Form): # pylint: disable-msg=W0232
"""Login form"""
username = forms.CharField(label=_('Username'))
password = forms.CharField(label=_('Passphrase'),
widget=forms.PasswordInput())
def clean(self):
"""Check for valid credentials"""
# pylint: disable-msg=E1101
if 'username' in self._errors or 'password' in self._errors:
return self.cleaned_data
error_msg = auth.check_credentials(self.cleaned_data['username'],
self.cleaned_data['password'])
if error_msg:
raise forms.ValidationError(error_msg, code='invalid_credentials')
return self.cleaned_data
def login(request):
"""Serve the login page"""
form = None
if request.method == 'POST':
form = LoginForm(request.POST, prefix='auth')
# pylint: disable-msg=E1101
if form.is_valid():
username = form.cleaned_data['username']
request.session[cfg.session_key] = username
return HttpResponseRedirect(_get_from_page(request))
else:
form = LoginForm(prefix='auth')
return TemplateResponse(request, 'form.html',
{'title': _('Login'),
'form': form,
'submit_text': _('Login')})
def logout(request):
"""Logout and redirect to origin page"""
try:
del request.session[cfg.session_key]
request.session.flush()
except KeyError:
pass
return HttpResponseRedirect(_get_from_page(request))
def _get_from_page(request):
"""Return the 'from page' of a request"""
return request.GET.get('from_page', cfg.server_dir + '/')

View File

@ -21,9 +21,13 @@ URLs for the Lib module
from django.conf.urls import patterns, url from django.conf.urls import patterns, url
import cfg
urlpatterns = patterns( # pylint: disable-msg=C0103 urlpatterns = patterns( # pylint: disable-msg=C0103
'modules.lib.auth_page', '',
url(r'^auth/login/$', 'login'), url(r'^accounts/login/$', 'django.contrib.auth.views.login',
url(r'^auth/logout/$', 'logout') {'template_name': 'login.html'}),
url(r'^accounts/logout/$', 'django.contrib.auth.views.logout',
{'next_page': cfg.server_dir})
) )

View File

@ -1,73 +0,0 @@
import cfg
from model import User
from plugin_mount import UserStoreModule
from withsqlite.withsqlite import sqlite_db
class UserStore(UserStoreModule, sqlite_db):
def __init__(self):
super(UserStore, self).__init__()
self.db_file = cfg.user_db
sqlite_db.__init__(self, self.db_file, autocommit=True, check_same_thread=False)
self.__enter__()
def close(self):
self.__exit__(None,None,None)
def current(self, request=None, name=False):
"""Return current user, if there is one, else None.
If name = True, return the username instead of the user."""
if not request:
return None
try:
username = request.session[cfg.session_key]
except KeyError:
return None
if name:
return username
return self.get(username)
def expert(self, username=None, request=None):
"""Return whether the current or provided user is an expert"""
if not username:
if not request:
return False
username = self.current(request=request, name=True)
groups = self.attr(username, 'groups')
if not groups:
return False
return 'expert' in groups
def attr(self, username=None, field=None):
return self.get(username)[field]
def get(self,username=None):
return User(sqlite_db.get(self,username))
def exists(self, username=None):
try:
user = self.get(username)
if not user:
return False
elif user["username"]=='':
return False
return True
except TypeError:
return False
def remove(self,username=None):
self.__delitem__(username)
def get_all(self):
return self.items()
def set(self,username=None,user=None):
sqlite_db.__setitem__(self,username, user)

View File

@ -1,11 +1,11 @@
from django import forms from django import forms
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from gettext import gettext as _ from gettext import gettext as _
import actions import actions
import cfg import cfg
from ..lib.auth import login_required
import service import service

View File

@ -1,11 +1,11 @@
from django import forms from django import forms
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from gettext import gettext as _ from gettext import gettext as _
import actions import actions
import cfg import cfg
from ..lib.auth import login_required
def get_modules_available(): def get_modules_available():

View File

@ -21,6 +21,7 @@ Plinth module for configuring PageKite service
from django import forms from django import forms
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.core import validators from django.core import validators
from django.template import RequestContext from django.template import RequestContext
from django.template.loader import render_to_string from django.template.loader import render_to_string
@ -29,7 +30,6 @@ from gettext import gettext as _
import actions import actions
import cfg import cfg
from ..lib.auth import login_required
def init(): def init():

View File

@ -19,12 +19,12 @@
Plinth module for configuring Tor Plinth module for configuring Tor
""" """
from django.contrib.auth.decorators import login_required
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from gettext import gettext as _ from gettext import gettext as _
import actions import actions
import cfg import cfg
from ..lib.auth import login_required
def init(): def init():

View File

@ -1,5 +1,7 @@
from django import forms from django import forms
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.core import validators from django.core import validators
from django.template import RequestContext from django.template import RequestContext
from django.template.loader import render_to_string from django.template.loader import render_to_string
@ -7,8 +9,7 @@ from django.template.response import TemplateResponse
from gettext import gettext as _ from gettext import gettext as _
import cfg import cfg
from ..lib.auth import add_user, login_required from ..lib.auth import add_user
from model import User
def init(): def init():
@ -72,7 +73,7 @@ def add(request):
def _add_user(request, data): def _add_user(request, data):
"""Add a user""" """Add a user"""
if cfg.users.exists(data['username']): if User.objects.filter(username=data['username']).exists():
messages.error(request, _('User "{username}" already exists').format( messages.error(request, _('User "{username}" already exists').format(
username=data['username'])) username=data['username']))
return return
@ -89,14 +90,11 @@ class UserEditForm(forms.Form): # pylint: disable-msg=W0232
# pylint: disable-msg=E1002 # pylint: disable-msg=E1002
super(forms.Form, self).__init__(*args, **kwargs) super(forms.Form, self).__init__(*args, **kwargs)
users = cfg.users.get_all() for user in User.objects.all():
for uname in users: label = '%s (%s)' % (user.first_name, user.username)
user = User(uname[1])
label = '%s (%s)' % (user['name'], user['username'])
field = forms.BooleanField(label=label, required=False) field = forms.BooleanField(label=label, required=False)
# pylint: disable-msg=E1101 # pylint: disable-msg=E1101
self.fields['delete_user_' + user['username']] = field self.fields['delete_user_' + user.username] = field
@login_required @login_required
@ -129,21 +127,21 @@ def _apply_edit_changes(request, data):
username = field.split('delete_user_')[1] username = field.split('delete_user_')[1]
requesting_user = request.session.get(cfg.session_key, None) requesting_user = request.user.username
cfg.log.info('%s asked to delete %s' % cfg.log.info('%s asked to delete %s' %
(requesting_user, username)) (requesting_user, username))
if username == cfg.users.current(request=request, name=True): if username == requesting_user:
messages.error( messages.error(
request, _('Can not delete current account - "%s"') % username) request, _('Can not delete current account - "%s"') % username)
continue continue
if not cfg.users.exists(username): if not User.objects.filter(username=username).exists():
messages.error(request, _('User "%s" does not exist') % username) messages.error(request, _('User "%s" does not exist') % username)
continue continue
try: try:
cfg.users.remove(username) User.objects.filter(username=username).delete()
messages.success(request, _('User "%s" deleted') % username) messages.success(request, _('User "%s" deleted') % username)
except IOError as exception: except IOError as exception:
messages.error(request, _('Error deleting "%s" - %s') % messages.error(request, _('Error deleting "%s" - %s') %

View File

@ -1,5 +1,6 @@
from django import forms from django import forms
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.template import RequestContext from django.template import RequestContext
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
@ -7,7 +8,6 @@ from gettext import gettext as _
import actions import actions
import cfg import cfg
from ..lib.auth import login_required
import service import service

View File

@ -1,10 +1,12 @@
#!/usr/bin/env python #!/usr/bin/env python
import argparse import argparse
import os
import sys
import django.conf import django.conf
import django.core.management
import django.core.wsgi import django.core.wsgi
import os
import stat
import sys
import cherrypy import cherrypy
from cherrypy import _cpserver from cherrypy import _cpserver
@ -12,7 +14,6 @@ from cherrypy.process.plugins import Daemonizer
import cfg import cfg
import module_loader import module_loader
import plugin_mount
import service import service
import logger import logger
@ -112,7 +113,6 @@ def context_processor(request):
'submenu': cfg.main_menu.active_item(request), 'submenu': cfg.main_menu.active_item(request),
'request_path': request.path, 'request_path': request.path,
'basehref': cfg.server_dir, 'basehref': cfg.server_dir,
'username': request.session.get(cfg.session_key, None),
'active_menu_urls': active_menu_urls 'active_menu_urls': active_menu_urls
} }
@ -129,19 +129,36 @@ def configure_django():
'django.contrib.messages.context_processors.messages', 'django.contrib.messages.context_processors.messages',
'plinth.context_processor'] 'plinth.context_processor']
data_file = os.path.join(cfg.data_dir, 'plinth.sqlite3')
template_directories = module_loader.get_template_directories() template_directories = module_loader.get_template_directories()
sessions_directory = os.path.join(cfg.data_dir, 'sessions') sessions_directory = os.path.join(cfg.data_dir, 'sessions')
django.conf.settings.configure( django.conf.settings.configure(
DEBUG=cfg.debug,
ALLOWED_HOSTS=['127.0.0.1', 'localhost'], ALLOWED_HOSTS=['127.0.0.1', 'localhost'],
TEMPLATE_DIRS=template_directories, CACHES={'default':
{'BACKEND': 'django.core.cache.backends.dummy.DummyCache'}},
DATABASES={'default':
{'ENGINE': 'django.db.backends.sqlite3',
'NAME': data_file}},
DEBUG=cfg.debug,
INSTALLED_APPS=['bootstrapform', INSTALLED_APPS=['bootstrapform',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.messages'], 'django.contrib.messages'],
LOGIN_URL=cfg.server_dir + '/accounts/login/',
LOGIN_REDIRECT_URL=cfg.server_dir + '/',
LOGOUT_URL=cfg.server_dir + '/accounts/logout/',
ROOT_URLCONF='urls', ROOT_URLCONF='urls',
SESSION_ENGINE='django.contrib.sessions.backends.file', SESSION_ENGINE='django.contrib.sessions.backends.file',
SESSION_FILE_PATH=sessions_directory, SESSION_FILE_PATH=sessions_directory,
STATIC_URL=cfg.server_dir + '/static/', STATIC_URL=cfg.server_dir + '/static/',
TEMPLATE_CONTEXT_PROCESSORS=context_processors) TEMPLATE_CONTEXT_PROCESSORS=context_processors,
TEMPLATE_DIRS=template_directories)
if not os.path.isfile(data_file):
cfg.log.info('Creating and initializing data file')
django.core.management.call_command('syncdb', interactive=False)
os.chmod(data_file, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP)
def main(): def main():
@ -160,8 +177,6 @@ def main():
module_loader.load_modules() module_loader.load_modules()
cfg.users = plugin_mount.UserStoreModule.get_plugins()[0]
setup_server() setup_server()
cherrypy.engine.start() cherrypy.engine.start()

View File

@ -1,54 +0,0 @@
import cfg
class PluginMount(type):
"""See http://martyalchin.com/2008/jan/10/simple-plugin-framework/ for documentation"""
def __init__(cls, name, bases, attrs):
if not hasattr(cls, 'plugins'):
cls.plugins = []
else:
cls.plugins.append(cls)
def init_plugins(cls, *args, **kwargs):
try:
cls.plugins = sorted(cls.plugins, key=lambda x: x.order, reverse=False)
except AttributeError:
pass
return [p(*args, **kwargs) for p in cls.plugins]
def get_plugins(cls, *args, **kwargs):
return cls.init_plugins(*args, **kwargs)
class MultiplePluginViolation(Exception):
"""Multiple plugins found for a type where only one is expected"""
pass
class PluginMountSingular(PluginMount):
"""Plugin mounter that allows only one plugin of this meta type"""
def __init__(cls, name, bases, attrs):
if not hasattr(cls, 'plugins'):
cls.plugins = []
else:
if len(cls.plugins) > 0:
raise MultiplePluginViolation
cls.plugins.append(cls)
class UserStoreModule(object):
"""
Mount Point for plugins that will manage the user backend storage,
where we keep a hash for each user.
Plugins implementing this reference should provide the following
methods, as described in the doc strings of the default
user_store.py: get, get_all, set, exists, remove, attr, expert.
See source code for doc strings.
This is designed as a plugin so mutiple types of user store can be
supported. But the project is moving towards LDAP for
compatibility with third party software. A future version of
Plinth is likely to require LDAP.
"""
# Singular because we can only use one user store at a time
__metaclass__ = PluginMountSingular

View File

@ -82,18 +82,18 @@
{% endfor %} {% endfor %}
</ul> </ul>
{% if username %} {% if user.is_authenticated %}
<p class="navbar-text pull-right"> <p class="navbar-text pull-right">
<i class="icon-user icon-white nav-icon"></i> <i class="icon-user icon-white nav-icon"></i>
Logged in as <a href="{{ username }}">{{ username }}</a>. Logged in as <a href="{{ user.username }}">{{ user.username }}</a>.
<a href="{{ basehref }}/auth/logout" title="Log out"> <a href="{{ basehref }}/accounts/logout" title="Log out">
Log out</a>. Log out</a>.
</p> </p>
{% else %} {% else %}
<p class="navbar-text pull-right"> <p class="navbar-text pull-right">
Not logged in. Not logged in.
<i class="icon-user icon-white nav-icon"></i> <i class="icon-user icon-white nav-icon"></i>
<a href="{{ basehref }}/auth/login" title="Log in"> <a href="{{ basehref }}/accounts/login" title="Log in">
Log in</a>. Log in</a>.
</p> </p>
{% endif %} {% endif %}

View File

@ -22,8 +22,6 @@
{% block main_block %} {% block main_block %}
{% include 'messages.html' %}
<form class="form" method="post"> <form class="form" method="post">
{% csrf_token %} {% csrf_token %}

34
templates/login.html Normal file
View File

@ -0,0 +1,34 @@
{% extends "base.html" %}
{% comment %}
#
# This file is part of Plinth.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
{% endcomment %}
{% load bootstrap %}
{% block main_block %}
<form class="form" method="post">
{% csrf_token %}
{{ form|bootstrap }}
<input type="submit" class="btn-primary" value="Login" />
<input type="hidden" name="next" value="{{ next }}" />
</form>
{% endblock %}

14
util.py
View File

@ -1,9 +1,4 @@
import os import os
import sys
import cfg
import sqlite3
from filedict import FileDict
def mkdir(newdir): def mkdir(newdir):
@ -34,12 +29,3 @@ def slurp(filespec):
def unslurp(filespec, msg): def unslurp(filespec, msg):
with open(filespec, 'w') as x: with open(filespec, 'w') as x:
x.write(msg) x.write(msg)
def filedict_con(filespec=None, table='dict'):
"""TODO: better error handling in filedict_con"""
try:
return FileDict(connection=sqlite3.connect(filespec), table=table)
except IOError as (errno, strerror):
cfg.log.critical("I/O error({0}): {1}".format(errno, strerror))
sys.exit(-1)

View File

@ -20,8 +20,6 @@ Main Plinth views
""" """
from django.http.response import HttpResponseRedirect from django.http.response import HttpResponseRedirect
import os
import stat
import cfg import cfg
from withsqlite.withsqlite import sqlite_db from withsqlite.withsqlite import sqlite_db
@ -32,10 +30,6 @@ def index(request):
# TODO: Move firstboot handling to firstboot module somehow # TODO: Move firstboot handling to firstboot module somehow
with sqlite_db(cfg.store_file, table='firstboot') as database: with sqlite_db(cfg.store_file, table='firstboot') as database:
if not 'state' in database: if not 'state' in database:
# If we created a new user db, make sure it can't be read by
# everyone
userdb_fname = '{}.sqlite3'.format(cfg.user_db)
os.chmod(userdb_fname, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP)
# Permanent redirect causes the browser to cache the redirect, # Permanent redirect causes the browser to cache the redirect,
# preventing the user from navigating to /plinth until the # preventing the user from navigating to /plinth until the
# browser is restarted. # browser is restarted.
@ -46,7 +40,7 @@ def index(request):
return HttpResponseRedirect( return HttpResponseRedirect(
cfg.server_dir + '/firstboot/state%d' % database['state']) cfg.server_dir + '/firstboot/state%d' % database['state'])
if request.session.get(cfg.session_key, None): if request.user.is_authenticated():
return HttpResponseRedirect(cfg.server_dir + '/apps') return HttpResponseRedirect(cfg.server_dir + '/apps')
return HttpResponseRedirect(cfg.server_dir + '/help/about') return HttpResponseRedirect(cfg.server_dir + '/help/about')