diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..021454b --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.pyc +*~ +*.swp diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..0d4e84b --- /dev/null +++ b/.travis.yml @@ -0,0 +1,14 @@ +language: python + +python: + - 2.7 + +install: + - pip install . + +env: + global: + - LDAPASS_CONFIG=tests/ldapass.conf + +script: + - py.test diff --git a/README.md b/README.md index 1fd3405..8dc628b 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,35 @@ -ldapass +ldapass [![Build Status](https://travis-ci.org/bartekrutkowski/ldapass.svg?branch=master)](https://travis-ci.org/bartekrutkowski/ldapass) ======= **Web application for setting/changing LDAP user passwords.** -LDAPass is a Python/Flask simple web application that aims to make Unix/Linux LDAP user account passwords manageable by their users without involvement of DevOps/SysAdmin resources in a simple, non complicated application not requiring extensive setup nor maintenance. +LDAPass is a Python/Flask simple web application that aims to make Unix/Linux user account LDAP passwords manageable by their owners without involvement of DevOps/SysAdmin resources in a simple application not requiring extensive setup or maintenance. ## Requirements -To host LDAPass on a Unix/Linux system you need the following software installed: +To run LDAPass on a Unix/Linux system you need the following software installed: - Python 2.7.x with following modules installed: - Flask - - python-ldap + - Python-LDAP - WTForms + - SQLite3 - A web server (Nginx, Apache etc., example Nginx configuration is provided) -- UWSGI 2.x +- Application Server Container (example UWSGI 2.x configuration is provided) ## Installation The following examples are assuming you are deploying LDAPass on a FreeBSD system, you should adjust your paths accordingly to your OS. -Clone the repository into proper location: +Clone the repository to the chosen location: ```sh -$ git clone git@github.com:bartekrutkowski/ldapass.git /var/www/ldapass +$ git clone https://github.com/bartekrutkowski/ldapass.git /var/www/ldapass ``` -Copy the config ldapass.conf configuration file and edit it with appropriate values: +Copy the example ldapass.conf configuration file to main app directory and edit it with appropriate values: ```sh -$ cp /var/www/ldapass/examples/ldapass.conf /var/www/ldapass/ldapass/ +$ cp /var/www/ldapass/examples/ldapass.conf /var/www/ldapass/ldapass/ldapass.conf $ vi /var/www/ldapass/ldapass/ldapass.conf ``` @@ -39,7 +40,7 @@ $ cp /var/www/ldapass/examples/nginx_ldapass.example.com.conf /usr/local/etc/ngi $ vi /usr/local/etc/nginx/conf.d/ldapass.example.com.conf ``` -Copy the UWSGI uwsgi_ldapass.ini configuration file into your OR UWSGI config directory and edit it with appropriate values: +Copy the UWSGI uwsgi_ldapass.ini configuration file into your OS UWSGI config directory and edit it with appropriate values: ```sh $ cp /var/www/ldapass/examples/uwsgi_ldapass.ini /usr/local/etc/uwsgi_ldapass.ini diff --git a/examples/ldapass.conf b/examples/ldapass.conf index edf864d..5aa9c64 100644 --- a/examples/ldapass.conf +++ b/examples/ldapass.conf @@ -4,11 +4,13 @@ listen_port = 8080 smtp_addr = smtp-server.example.com hostname = ldapass-hostname.example.com database = ldapass.sql -ldap_debug = 0 +debug = False [ldap] addr = ldap-server.example.com port = 389 +starttls = True user = cn=Manager,dc=example,dc=com pass = very-complicated-ldap-password basedn = ou=People,dc=example,dc=com +debug = 0 \ No newline at end of file diff --git a/examples/nginx_ldapass.example.com.conf b/examples/nginx_ldapass.example.com.conf index 913e7de..1ab5e94 100644 --- a/examples/nginx_ldapass.example.com.conf +++ b/examples/nginx_ldapass.example.com.conf @@ -6,14 +6,14 @@ server { error_log /var/log/nginx/ldapass.example.com-error.log; location /static { - alias /var/www/ldapass/app/static; + alias /var/www/ldapass/ldapass/static; } location / { try_files $uri @ldapass.example.com; } location @ldapass.example.com { - root /var/www/ldapass/app; + root /var/www/ldapass/ldapass; include uwsgi_params; uwsgi_pass 127.0.0.1:8005; } -} +} \ No newline at end of file diff --git a/examples/uwsgi_ldapass.ini b/examples/uwsgi_ldapass.ini index 7045434..90ef430 100644 --- a/examples/uwsgi_ldapass.ini +++ b/examples/uwsgi_ldapass.ini @@ -1,14 +1,14 @@ [uwsgi] -chdir = /var/www/ldapass/app -venv = /var/www/ldapass/ldapass_venv +chdir = /var/www/ldapass/ldapass module = ldapass callable = app +env = LDAPASS_CONFIG=/var/www/ldapass/ldapass/ldapass.conf master = true -processes = 8 -chmod-socket = 666 +processes = 2 +chmod-socket = 127.0.0.1:8005 vacuum = true -logto = /var/log/uwsgi/ldapass.log -logto2 = /var/log/uwsgi/ldapass2.log +logger = syslog log-micros = true +log-format = %(addr) - %(user) [%(ltime)] "%(method) %(uri) %(proto)" %(status) %(size) "%(referer)" "%(uagent)" diff --git a/ldapass/__init__.py b/ldapass/__init__.py new file mode 100644 index 0000000..16fb447 --- /dev/null +++ b/ldapass/__init__.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +__version__ = "0.1" diff --git a/ldapass/ldapass.py b/ldapass/ldapass.py index e95cf44..b5ec34e 100644 --- a/ldapass/ldapass.py +++ b/ldapass/ldapass.py @@ -13,7 +13,7 @@ from wtforms import Form, TextField, PasswordField, validators -app = Flask('__name__') +app = Flask(__name__) app.secret_key = os.urandom(128) conf = RawConfigParser() conf.read(os.environ['LDAPASS_CONFIG']) @@ -69,22 +69,18 @@ def index(): if form.validate(): ldap_uri = 'ldap://{addr}:{port}'.format( addr=conf.get('ldap', 'addr'), port=conf.get('ldap', 'port')) + ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER) + ldap_conn = ldap.initialize(ldap_uri, trace_level=conf.get('ldap', 'debug')) + search_filter = 'mail={mail}'.format(mail=form.mail.data) + try: - ldap.set_option( - ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER) - l = ldap.initialize( - ldap_uri, trace_level=conf.get('app', 'ldap_debug')) - l.start_tls_s() - except ldap.LDAPError as error: - return render_template('index.html', error=error, form=form) - try: - search_filter = 'mail={mail}'.format(mail=form.mail.data) - ldap_result_id = l.search( - conf.get('ldap', 'basedn'), ldap.SCOPE_SUBTREE, - search_filter, None) + if conf.getboolean('ldap', 'starttls'): + ldap_conn.start_tls_s() + ldap_result_id = ldap_conn.search(conf.get('ldap', 'basedn'), ldap.SCOPE_SUBTREE, search_filter, None) + result_type, result_data = ldap_conn.result(ldap_result_id, 0) except ldap.LDAPError as error: return render_template('index.html', error=error, form=form) - result_type, result_data = l.result(ldap_result_id, 0) + if len(result_data) == 1: link_id = '{uuid}-{account}'.format( uuid=str(uuid.uuid4()), @@ -101,10 +97,10 @@ def index(): db_curs.execute( "INSERT INTO mails (mail, link_id, created) VALUES \ ('{mail}', '{link_id}', '{created}')".format( - mail=form.mail.data, - link_id=link_id, - created=datetime.datetime.now() - )) + mail=form.mail.data, + link_id=link_id, + created=datetime.datetime.now() + )) flash('Email containing password reset url has been sent \ to {mail}'.format(mail=form.mail.data)) else: @@ -114,10 +110,10 @@ def index(): db_curs.execute( "REPLACE INTO mails (mail, link_id, created) VALUES \ ('{mail}', '{link_id}', '{created}')".format( - mail=form.mail.data, - link_id=link_id, - created=datetime.datetime.now() - )) + mail=form.mail.data, + link_id=link_id, + created=datetime.datetime.now() + )) flash('Email containing password reset url has been sent \ to {mail}. Previous reset urls have been \ invalidated.'.format(mail=form.mail.data)) @@ -173,25 +169,19 @@ def reset(link_id): addr=conf.get('ldap', 'addr'), port=conf.get('ldap', 'port') ) + ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER) + ldap_conn = ldap.initialize(ldap_uri, trace_level=conf.get('ldap', 'debug')) + search_filter = 'mail={mail}'.format(mail=db_data[0][1]) + try: - ldap.set_option( - ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER) - l = ldap.initialize( - ldap_uri, trace_level=conf.get('app', 'ldap_debug')) - l.start_tls_s() - except ldap.LDAPError as error: - return render_template('error.html', error=error) - try: - search_filter = 'mail={mail}'.format(mail=db_data[0][1]) - ldap_result_id = l.search( - conf.get('ldap', 'basedn'), - ldap.SCOPE_SUBTREE, - search_filter, - None) - result_type, result_data = l.result(ldap_result_id, 0) - l.simple_bind_s( + if conf.getboolean('ldap', 'starttls'): + ldap_conn.start_tls_s() + ldap_result_id = ldap_conn.search(conf.get('ldap', 'basedn'), + ldap.SCOPE_SUBTREE, search_filter, None) + result_type, result_data = ldap_conn.result(ldap_result_id, 0) + ldap_conn.simple_bind_s( conf.get('ldap', 'user'), conf.get('ldap', 'pass')) - l.passwd_s( + ldap_conn.passwd_s( 'uid={uid},{basedn}'.format( uid=result_data[0][1]['uid'][0], basedn=conf.get('ldap', 'basedn')), @@ -200,11 +190,8 @@ def reset(link_id): except ldap.LDAPError as error: error = 'LDAP error: {error}, please get in touch with \ LDAP administration.'.format(error=error) - return render_template( - 'reset.html', - error=error, - form=form - ) + return render_template('reset.html', error=error, form=form) + flash('Password for account {mail} has been changed.'.format( mail=db_data[0][1])) db_curs.execute( @@ -232,7 +219,7 @@ def reset(link_id): db_curs = db_conn.cursor() db_curs.execute( "SELECT name FROM sqlite_master WHERE type='table' AND name='mails'") - if len(db_curs.fetchall()) == 0: + if not db_curs.fetchall(): print('WARNING: the SQLite file {database} doesnt exist! Sleeping for \ 10 seconds and creating the database file. KILL ME if this is an \ error!').format(database=conf.get('app', 'database')) @@ -241,7 +228,7 @@ def reset(link_id): '''create table mails ( id INTEGER PRIMARY KEY, mail VARCHAR(255) NOT NULL COLLATE NOCASE, - link_id VARCHAR(512) NOT NULL COLLATE NOCASE, + link_id VARCHAR(512) NOT NULL COLLATE NOCASE, created INTEGER DEFAULT NULL); ''') db_conn.commit() @@ -252,4 +239,5 @@ def reset(link_id): db_conn.close() app.run(host=conf.get('app', 'listen_addr'), - port=conf.getint('app', 'listen_port'), debug=True) + port=conf.getint('app', 'listen_port'), + debug=conf.getboolean('app', 'debug')) diff --git a/ldapass/templates/base.html b/ldapass/templates/base.html index 687ecf5..f4d8fbb 100644 --- a/ldapass/templates/base.html +++ b/ldapass/templates/base.html @@ -22,6 +22,6 @@ - + \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 3d637e3..1dfb14b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ Flask -WTForms python-ldap +sqlite3 +WTForms \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..2a3d044 --- /dev/null +++ b/setup.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python + +from setuptools import setup, find_packages + +setup( + name='ldapass', + version='0.1', + description='Web application for setting/changing LDAP user passwords.', + author='Bartek Rutkowski', + author_email='contact+ldapass@robakdesign.com', + license='BSD3', + url='https://github.com/bartekrutkowski/ldapass', + packages=find_packages(), + include_package_data=True, + package_data={ + 'ldapass': ['static/css/*', + 'static/fonts/*', + 'static/js/*', + 'templates/*.html' + ] + }, + entry_points={'console_scripts': ['ldapass = ldapass:main']}, + install_requires=['Flask', 'WTForms', 'python-ldap'] +) diff --git a/tests/ldapass.conf b/tests/ldapass.conf new file mode 100644 index 0000000..77620f1 --- /dev/null +++ b/tests/ldapass.conf @@ -0,0 +1,14 @@ +[app] +listen_addr = 0.0.0.0 +listen_port = 80 +smtp_addr = smtp-server.example.com +hostname = ldapass-hostname.example.com +database = ldapass.sql +ldap_debug = 0 + +[ldap] +addr = ldap-server.example.com +port = 389 +user = cn=Manager,dc=example,dc=com +pass = very-complicated-ldap-password +basedn = ou=People,dc=example,dc=com diff --git a/tests/ldapass_test.py b/tests/ldapass_test.py new file mode 100644 index 0000000..1bd9ef8 --- /dev/null +++ b/tests/ldapass_test.py @@ -0,0 +1,35 @@ +import pytest + +from ldapass import ldapass + + +@pytest.yield_fixture(autouse=True) +def flask_app(): + ldapass.app.testing = True + ldapass.app.debug = True + with ldapass.app.app_context(): + yield ldapass.app + + +@pytest.fixture +def test_client(flask_app): + return flask_app.test_client() + + +def test_get_response_code(test_client): + '''GET / reques should return 200 HTTP code.''' + resp = test_client.get('/') + assert resp.status_code == 200 + + +def test_get_html_text(test_client): + '''GET / request should return html with proper text.''' + resp = test_client.get('/') + assert b'Setup/Reset LDAP Password' in resp.data + + +def test_get_html_form(test_client): + '''GET / request should return html with proper form.''' + resp = test_client.get('/') + assert b'
' in resp.data + assert b'' in resp.data