Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Add a login form.

  • Loading branch information...
commit a6db9cd82cdfe237937185b1bae7fed0f1101825 1 parent 169f91c
@dnouri dnouri authored
View
34 README.rst
@@ -76,17 +76,32 @@ Kotti includes two `Paste Deploy`_ configuration files in
*kotti.authentication_policy_factory* and *kotti.authorization_policy_factory*
------------------------------------------------------------------------------
-The ``development.ini`` configuration disables security by setting two
-configuration variables in the ``[app:Kotti]`` part::
+You can override the authentication and authorization policy that
+Kotti uses. By default, Kotti uses these factories::
- kotti.authentication_policy_factory = kotti.none_factory
- kotti.authorization_policy_factory = kotti.none_factory
+ kotti.authentication_policy_factory = kotti.authtkt_factory
+ kotti.authorization_policy_factory = kotti.acl_factory
-The ``production.ini`` does not set these configuration variables,
-which results in the default authentication and authorization policy
-factories to be used, which use
+These settings correspond to
`pyramid.authentication.AuthTktAuthenticationPolicy`_ and
-`pyramid.authorization.ACLAuthorizationPolicy`_ respectively.
+`pyramid.authorization.ACLAuthorizationPolicy`_ being used.
+
+*kotti.secret*
+--------------
+
+``kotti.secret`` and ``kotti.secret2`` (optional) are used as salts
+for various hashing functions. Also, ``kotti.secret`` is the password
+of the default admin user. (Which you should change immediately.)
+
+An example::
+
+ kotti.secret = myhiddensecret
+ kotti.secret2 = myothersecret
+
+To log in as admin, you would log in as ``admin`` with the password
+``myhiddensecret``. ``kotti.secret`` is used as a salt to the
+passwords in the default user database. Changing it will result in
+the user database's passwords becoming invalid.
*kotti.session_factory*
-----------------------
@@ -130,7 +145,8 @@ templates. The defaults are::
The default configuration here is::
- kotti.includes = kotti.views.view kotti.views.edit kotti.events
+ kotti.includes =
+ kotti.events kotti.views.view kotti.views.edit kotti.views.login
These point to modules that contain an ``includeme`` function. An
``includeme`` function that registers an edit view for an ``Event``
View
4 development.ini
@@ -8,8 +8,8 @@ debug_templates = true
default_locale_name = en
sqlalchemy.url = sqlite:///%(here)s/Kotti.db
kotti.secret = qwerty
-kotti.authentication_policy_factory = kotti.none_factory
-kotti.authorization_policy_factory = kotti.none_factory
+#kotti.authentication_policy_factory = kotti.none_factory
+#kotti.authorization_policy_factory = kotti.none_factory
[pipeline:main]
pipeline =
View
13 kotti/__init__.py
@@ -18,10 +18,7 @@ def __getitem__(self, key):
if key in self.dotted_names and isinstance(value, basestring):
values = []
for dottedname in value.split():
- try:
- values.append(DottedNameResolver(None).resolve(dottedname))
- except ImportError: # pragma: no coverage
- raise ValueError("Could not resolve %r." % dottedname)
+ values.append(DottedNameResolver(None).resolve(dottedname))
super(Configuration, self).__setitem__(key, values)
return values
else:
@@ -53,7 +50,7 @@ def none_factory(**kwargs):
'kotti.templates.view_css': 'kotti:static/view.css',
'kotti.templates.edit_css': 'kotti:static/edit.css',
'kotti.configurators': '',
- 'kotti.includes': 'kotti.views.view kotti.views.edit kotti.events',
+ 'kotti.includes': 'kotti.events kotti.views.view kotti.views.edit kotti.views.login',
'kotti.available_types': 'kotti.resources.Document',
'kotti.authentication_policy_factory': 'kotti.authtkt_factory',
'kotti.authorization_policy_factory': 'kotti.acl_factory',
@@ -82,8 +79,10 @@ def main(global_config, **settings):
for func in configuration['kotti.configurators']:
func(configuration) # XXX testme
- secret1 = settings.pop("kotti.secret")
- secret2 = settings.pop("kotti.secret2", secret1)
+ secret1 = settings['kotti.secret']
+ secret2 = settings.get('kotti.secret2', secret1)
+ configuration.secret = secret1
+ configuration.secret2 = secret2
from kotti.resources import appmaker
engine = engine_from_config(settings, 'sqlalchemy.')
View
40 kotti/browser.txt
@@ -13,6 +13,46 @@ Setup
>>> session = DBSession()
>>> root = session.query(Node).get(1)
+Login
+-----
+
+Editing is locked down to authenticated users:
+
+ >>> browser.open(tests.BASE_URL + '/edit')
+ >>> "Log in" in browser.contents
+ True
+ >>> browser.getControl("Username or email").value = "admin"
+ >>> browser.getControl("Password").value = "secret"
+ >>> browser.getControl(name="submitted").click()
+ >>> "Welcome, Administrator" in browser.contents
+ True
+ >>> browser.url == tests.BASE_URL + '/edit'
+ True
+
+Logging out redirects us to the URL we came from and presents us with
+the login form:
+
+ >>> browser.getLink("Logout").click()
+ >>> "You have been logged out" in browser.contents
+ True
+
+Log in again, this time force an error:
+
+ >>> browser.getControl("Username or email").value = "admin"
+ >>> browser.getControl("Password").value = "funny"
+ >>> browser.getControl(name="submitted").click()
+ >>> "Welcome, Adminstrator" in browser.contents
+ False
+ >>> "Login failed" in browser.contents
+ True
+ >>> browser.getControl("Username or email").value = "admin"
+ >>> browser.getControl("Password").value = "secret"
+ >>> browser.getControl(name="submitted").click()
+ >>> "Welcome, Administrator" in browser.contents
+ True
+ >>> browser.url == tests.BASE_URL + '/edit'
+ True
+
Add a document
--------------
View
15 kotti/resources.py
@@ -31,7 +31,6 @@
from kotti.util import JsonType
from kotti.security import PersistentACL
from kotti.security import get_principals
-from kotti.security import Principal
class Container(object, DictMixin):
"""Containers form the API of a Node that's used for subitem
@@ -187,7 +186,7 @@ def __init__(self, body=u"", mime_type='text/html', **kwargs):
mapper(Document, documents, inherits=Node, polymorphic_identity='document')
-def default_get_root(request):
+def get_root(request):
session = DBSession()
return session.query(Node).filter(Node.parent_id==None).first()
@@ -208,11 +207,11 @@ def populate():
principals = get_principals()
if u'admin' not in principals:
- principals[u'admin'] = Principal(
- u'admin',
- password=configuration.get('secret'),
- title=u"Administrator",
- )
+ principals[u'admin'] = {
+ 'id': u'admin',
+ 'password': configuration.secret,
+ 'title': u"Administrator",
+ }
session.flush()
transaction.commit()
@@ -231,4 +230,4 @@ def initialize_sql(engine):
def appmaker(engine):
initialize_sql(engine)
- return default_get_root
+ return get_root
View
16 kotti/security.py
@@ -100,9 +100,15 @@ def set_groups(id, context, groups_to_set):
def list_groups_callback(id, request):
if not is_user(id):
- return None
+ return None # Disallow logging in with groups
if id in get_principals():
- return list_groups(id, request.context)
+ context = getattr(request, 'context', None)
+ if context is None:
+ # XXX This stems from an issue with SA events; they don't
+ # have request.context available:
+ from kotti.resources import get_root
+ context = get_root(request)
+ return list_groups(id, context)
def get_principals():
return configuration['kotti.principals'][0]
@@ -183,8 +189,8 @@ def search(self, term):
return query
def hash_password(self, password):
- salt = configuration['kotti.secret']
- return hashlib.sha224(salt + password).hexdigest()
+ salt = configuration.secret
+ return unicode(hashlib.sha224(salt + password).hexdigest())
principals = Principals()
@@ -192,7 +198,7 @@ def hash_password(self, password):
Column('id', Unicode(100), primary_key=True),
Column('password', Unicode(100)),
Column('title', Unicode(100), nullable=False),
- Column('email', Unicode(100)),
+ Column('email', Unicode(100), unique=True),
Column('groups', JsonType(), nullable=False),
Column('creation_date', DateTime(), nullable=False),
)
View
37 kotti/templates/edit/login.pt
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xml:lang="en"
+ xmlns:tal="http://xml.zope.org/namespaces/tal"
+ metal:use-macro="api['master_edit.main']">
+
+ <div metal:fill-slot="content" class="document-view">
+ <h1>Login</h1>
+
+ <div class="form">
+ <div metal:use-macro="api['master_edit.messages']">
+ </div>
+
+ <form action="${url}" method="post">
+ <input type="hidden" name="came_from" value="${came_from}"/>
+ <ul>
+ <li>
+ <label for="form-login">Username or email</label><br/>
+ <input type="text" name="login" id="form-login"
+ value="${login}"/>
+ </li>
+ <li>
+ <label for="form-password">Password</label><br/>
+ <input type="password" name="password" id="form-password"
+ value="${password}"/>
+ </li>
+ <li>
+ <button type="submit" name="submitted" class="submit">
+ Log in
+ </button>
+ </li>
+ </ul>
+ </form>
+ </div>
+
+ </div>
+</html>
View
6 kotti/templates/edit/master.pt
@@ -19,7 +19,8 @@
</head>
<body class="edit-view">
- <div id="edit-box" class="vertical-text round-corners-top">
+ <div id="edit-box" class="vertical-text round-corners-top"
+ tal:condition="api.has_permission('view')">
<a href="${api.url()}">View</a>
</div>
<div id="header">
@@ -31,6 +32,9 @@
${link['name'].title()}
</a>
</li>
+ <li tal:condition="api.user">
+ <a href="${request.application_url}/logout?came_from=${request.url}">Logout</a>
+ </li>
</ul>
</div>
<div class="clear"></div>
View
17 kotti/tests.py
@@ -314,15 +314,20 @@ def test_list_groups_callback_with_groups(self):
class TestUser(UnitTestBase):
def _make_bob(self):
users = get_principals()
- users['bob'] = dict(
+ users[u'bob'] = dict(
id=u'bob', title=u'Bob Dabolina', groups=[u'group:bobsgroup'])
- return users['bob']
+ return users[u'bob']
def _assert_is_bob(self, bob):
self.assertEqual(bob.id, u'bob')
self.assertEqual(bob.title, u'Bob Dabolina')
self.assertEqual(bob.groups, [u'group:bobsgroup'])
+ def test_default_admin(self):
+ admin = get_principals()[u'admin']
+ hashed = get_principals().hash_password(u'secret')
+ self.assertEqual(admin.password, hashed)
+
def test_users_empty(self):
users = get_principals()
self.assertRaises(KeyError, users.__getitem__, u'bob')
@@ -382,12 +387,12 @@ def test_hash_password(self):
hash_password = get_principals().hash_password
# For 'hash_password' to work, we need to set a secret:
- configuration['kotti.secret'] = 'there is no secret'
+ configuration.secret = 'there is no secret'
hashed = hash_password(password)
self.assertEqual(hashed, hash_password(password))
- configuration['kotti.secret'] = 'different'
+ configuration.secret = 'different'
self.assertNotEqual(hashed, hash_password(password))
- configuration.pop('kotti.secret')
+ del configuration.secret
class TestEvents(UnitTestBase):
def setUp(self):
@@ -693,8 +698,6 @@ def setUpFunctional(global_config=None, **settings):
configuration = {
'sqlalchemy.url': 'sqlite://',
- 'kotti.authentication_policy_factory': 'kotti.none_factory',
- 'kotti.authorization_policy_factory': 'kotti.none_factory',
'kotti.secret': 'secret',
}
View
9 kotti/views/edit.py
@@ -136,13 +136,12 @@ def move_node(context, request):
if 'delete' in P and 'delete-confirm' in P:
parent = context.__parent__
- parent.children.remove(context)
-
- elements = []
+ redirect_elements = []
if view_execution_permitted(parent, request, 'edit'):
- elements.append('edit')
- location = resource_url(parent, request, *elements)
+ redirect_elements.append('edit')
+ location = resource_url(parent, request, *redirect_elements)
request.session.flash(u'%s deleted.' % context.title, 'success')
+ parent.children.remove(context)
return HTTPFound(location=location)
if 'rename' in P:
View
75 kotti/views/login.py
@@ -0,0 +1,75 @@
+from formencode.validators import Email
+from pyramid.httpexceptions import HTTPFound
+from pyramid.security import remember
+from pyramid.security import forget
+
+from kotti.security import get_principals
+from kotti.resources import get_root
+from kotti.resources import Node
+
+def login(context, request):
+ from kotti.views.util import TemplateAPIEdit
+ root = get_root(request)
+ api = TemplateAPIEdit(root, request)
+ came_from = request.params.get('came_from', request.url)
+ login, password = u'', u''
+ if 'submitted' in request.POST:
+ login = request.params['login']
+ password = request.params['password']
+ principals = get_principals()
+ principal = principals.get(login)
+
+ if principal is None:
+ # Maybe an e-mail address, XXX there should be a nicer way
+ # of searching 'principals', maybe add kwargs search back.
+ try:
+ Email().to_python(login)
+ except Exception:
+ pass
+ else:
+ for p in principals.search(login):
+ if p.email == login:
+ principal = p
+ break
+
+ if (principal is not None and
+ principal.password == principals.hash_password(password)):
+ headers = remember(request, login)
+ request.session.flash(
+ u"Welcome, %s!" % principal.title or principal.id, 'success')
+ return HTTPFound(location=came_from, headers=headers)
+
+ request.session.flash(u"Login failed.", 'error')
+
+ return {
+ 'api': api,
+ 'url': request.application_url + '/login',
+ 'came_from': came_from,
+ 'login': login,
+ 'password': password,
+ }
+
+def logout(context, request):
+ headers = forget(request)
+ request.session.flash(u"You have been logged out.")
+ location = request.params.get('came_from', request.application_url)
+ return HTTPFound(location=location, headers=headers)
+
+def includeme(config):
+ config.add_view(
+ login,
+ context='pyramid.exceptions.Forbidden',
+ renderer='../templates/edit/login.pt',
+ )
+
+ config.add_view(
+ login,
+ name='login',
+ context=Node,
+ renderer='../templates/edit/login.pt',
+ )
+
+ config.add_view(
+ logout,
+ name='logout',
+ )
View
7 kotti/views/util.py
@@ -5,6 +5,7 @@
from pyramid.location import inside
from pyramid.location import lineage
from pyramid.renderers import get_renderer
+from pyramid.security import authenticated_userid
from pyramid.security import has_permission
from pyramid.security import view_execution_permitted
from pyramid.url import resource_url
@@ -13,6 +14,7 @@
from kotti import configuration
from kotti.resources import DBSession
from kotti.resources import Node
+from kotti.security import get_principals
class TemplateAPI(object):
"""This implements the 'api' object that's passed to all
@@ -64,6 +66,11 @@ def root(self):
def lineage(self):
return list(lineage(self.context))
+ @reify
+ def user(self):
+ userid = authenticated_userid(self.request)
+ return get_principals().get(userid)
+
def has_permission(self, permission, context=None):
if context is None:
context = self.context
View
2  production.ini
@@ -7,7 +7,7 @@ debug_routematch = false
debug_templates = false
default_locale_name = en
sqlalchemy.url = sqlite:///%(here)s/Kotti.db
-;kotti.secret = qwerty
+#kotti.secret = qwerty
[filter:weberror]
use = egg:WebError#error_catcher
View
1  setup.py
@@ -12,6 +12,7 @@
'repoze.tm2>=1.0b1', # default_commit_veto
'sqlalchemy>=0.7b1dev',
'zope.sqlalchemy',
+ 'formencode',
'deform',
'WebError',
]
Please sign in to comment.
Something went wrong with that request. Please try again.