diff --git a/cps/__init__.py b/cps/__init__.py index c6abbdd1..1d3d598d 100755 --- a/cps/__init__.py +++ b/cps/__init__.py @@ -87,7 +87,11 @@ global_WorkerThread = WorkerThread() from .server import server Server = server() +from .ldap import Ldap +ldap = Ldap() + babel = Babel() + log = logger.create() @@ -102,6 +106,7 @@ def create_app(): Server.init_app(app) db.setup_db() babel.init_app(app) + ldap.init_app(app) global_WorkerThread.start() return app diff --git a/cps/admin.py b/cps/admin.py index 4d220fd0..3712420a 100644 --- a/cps/admin.py +++ b/cps/admin.py @@ -397,17 +397,53 @@ def configuration_helper(origin): #LDAP configurator, if "config_login_type" in to_save and to_save["config_login_type"] == "1": - if to_save["config_ldap_provider_url"] == u'' or to_save["config_ldap_dn"] == u'': + if not to_save["config_ldap_provider_url"] or not to_save["config_ldap_port"] or not to_save["config_ldap_dn"] or not to_save["config_ldap_user_object"]: ub.session.commit() - flash(_(u'Please enter a LDAP provider and a DN'), category="error") - return render_title_template("config_edit.html", config=config, origin=origin, - gdriveError=gdriveError, feature_support=feature_support, - title=_(u"Basic Configuration"), page="config") + flash(_(u'Please enter a LDAP provider, port, DN and user object identifier'), category="error") + return render_title_template("config_edit.html", content=config, origin=origin, + gdrive=gdriveutils.gdrive_support, gdriveError=gdriveError, + goodreads=goodreads_support, title=_(u"Basic Configuration"), + page="config") + elif not to_save["config_ldap_serv_username"] or not to_save["config_ldap_serv_password"]: + ub.session.commit() + flash(_(u'Please enter a LDAP service account and password'), category="error") + return render_title_template("config_edit.html", content=config, origin=origin, + gdrive=gdriveutils.gdrive_support, gdriveError=gdriveError, + goodreads=goodreads_support, title=_(u"Basic Configuration"), + page="config") else: - content.config_login_type = constants.LOGIN_LDAP + content.config_use_ldap = 1 content.config_ldap_provider_url = to_save["config_ldap_provider_url"] + content.config_ldap_port = to_save["config_ldap_port"] + content.config_ldap_schema = to_save["config_ldap_schema"] + content.config_ldap_serv_username = to_save["config_ldap_serv_username"] + content.config_ldap_serv_password = base64.b64encode(to_save["config_ldap_serv_password"]) content.config_ldap_dn = to_save["config_ldap_dn"] - db_change = True + content.config_ldap_user_object = to_save["config_ldap_user_object"] + reboot_required = True + content.config_ldap_use_ssl = 0 + content.config_ldap_use_tls = 0 + content.config_ldap_require_cert = 0 + content.config_ldap_openldap = 0 + if "config_ldap_use_ssl" in to_save and to_save["config_ldap_use_ssl"] == "on": + content.config_ldap_use_ssl = 1 + if "config_ldap_use_tls" in to_save and to_save["config_ldap_use_tls"] == "on": + content.config_ldap_use_tls = 1 + if "config_ldap_require_cert" in to_save and to_save["config_ldap_require_cert"] == "on": + content.config_ldap_require_cert = 1 + if "config_ldap_openldap" in to_save and to_save["config_ldap_openldap"] == "on": + content.config_ldap_openldap = 1 + if "config_ldap_cert_path " in to_save: + if content.config_ldap_cert_path != to_save["config_ldap_cert_path "]: + if os.path.isfile(to_save["config_ldap_cert_path "]) or to_save["config_ldap_cert_path "] is u"": + content.config_certfile = to_save["config_ldap_cert_path "] + else: + ub.session.commit() + flash(_(u'Certfile location is not valid, please enter correct path'), category="error") + return render_title_template("config_edit.html", content=config, origin=origin, + gdrive=gdriveutils.gdrive_support, gdriveError=gdriveError, + goodreads=goodreads_support, title=_(u"Basic Configuration"), + page="config") # Remote login configuration content.config_remote_login = ("config_remote_login" in to_save and to_save["config_remote_login"] == "on") diff --git a/cps/ldap.py b/cps/ldap.py new file mode 100644 index 00000000..43a8fcba --- /dev/null +++ b/cps/ldap.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- + +# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) +# Copyright (C) 2019 Krakinou +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import base64 + +try: + from flask_simpleldap import LDAP, LDAPException + ldap_support = True +except ImportError: + ldap_support = False + +from . import config, logger + +log = logger.create() + +class Ldap(): + + def __init__(self): + return + + def init_app(self, app): + if ldap_support and config.config_login_type == 1: + app.config['LDAP_HOST'] = config.config_ldap_provider_url + app.config['LDAP_PORT'] = config.config_ldap_port + app.config['LDAP_SCHEMA'] = config.config_ldap_schema + app.config['LDAP_USERNAME'] = config.config_ldap_user_object.replace('%s', config.config_ldap_serv_username)\ + + ',' + config.config_ldap_dn + app.config['LDAP_PASSWORD'] = base64.b64decode(config.config_ldap_serv_password) + if config.config_ldap_use_ssl: + app.config['LDAP_USE_SSL'] = True + if config.config_ldap_use_tls: + app.config['LDAP_USE_TLS'] = True + app.config['LDAP_REQUIRE_CERT'] = config.config_ldap_require_cert + if config.config_ldap_require_cert: + app.config['LDAP_CERT_PATH'] = config.config_ldap_cert_path + app.config['LDAP_BASE_DN'] = config.config_ldap_dn + app.config['LDAP_USER_OBJECT_FILTER'] = config.config_ldap_user_object + if config.config_ldap_openldap: + app.config['LDAP_OPENLDAP'] = True + + # app.config['LDAP_BASE_DN'] = 'ou=users,dc=yunohost,dc=org' + # app.config['LDAP_USER_OBJECT_FILTER'] = '(uid=%s)' + # ldap = LDAP(app) + + elif config.config_login_type == 1 and not ldap_support: + log.error('Cannot activate ldap support, did you run \'pip install --target vendor -r optional-requirements.txt\'?') diff --git a/cps/opds.py b/cps/opds.py index dc5aea02..d72d1163 100644 --- a/cps/opds.py +++ b/cps/opds.py @@ -36,6 +36,11 @@ from .helper import fill_indexpage, get_download_link, get_book_cover from .pagination import Pagination from .web import common_filters, get_search_results, render_read_books, download_required +try: + import ldap + ldap_support = True +except ImportError: + ldap_support = False opds = Blueprint('opds', __name__) log = logger.create() @@ -52,33 +57,39 @@ def requires_basic_auth_if_no_ano(f): return decorated +def basic_auth_required_check(condition): + def decorator(f): + if condition and ldap_support: + return ldap.basic_auth_required(f) + return requires_basic_auth_if_no_ano(f) + return decorator @opds.route("/opds/") -@requires_basic_auth_if_no_ano +@basic_auth_required_check(config.config_login_type) def feed_index(): return render_xml_template('index.xml') @opds.route("/opds/osd") -@requires_basic_auth_if_no_ano +@basic_auth_required_check(config.config_login_type) def feed_osd(): return render_xml_template('osd.xml', lang='en-EN') @opds.route("/opds/search", defaults={'query': ""}) @opds.route("/opds/search/") -@requires_basic_auth_if_no_ano +@basic_auth_required_check(config.config_login_type) def feed_cc_search(query): return feed_search(query.strip()) @opds.route("/opds/search", methods=["GET"]) -@requires_basic_auth_if_no_ano +@basic_auth_required_check(config.config_login_type) def feed_normal_search(): return feed_search(request.args.get("query").strip()) @opds.route("/opds/new") -@requires_basic_auth_if_no_ano +@basic_auth_required_check(config.config_login_type) def feed_new(): off = request.args.get("offset") or 0 entries, __, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), @@ -87,7 +98,7 @@ def feed_new(): @opds.route("/opds/discover") -@requires_basic_auth_if_no_ano +@basic_auth_required_check(config.config_login_type) def feed_discover(): entries = db.session.query(db.Books).filter(common_filters()).order_by(func.random())\ .limit(config.config_books_per_page) @@ -96,7 +107,7 @@ def feed_discover(): @opds.route("/opds/rated") -@requires_basic_auth_if_no_ano +@basic_auth_required_check(config.config_login_type) def feed_best_rated(): off = request.args.get("offset") or 0 entries, __, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), @@ -105,7 +116,7 @@ def feed_best_rated(): @opds.route("/opds/hot") -@requires_basic_auth_if_no_ano +@basic_auth_required_check(config.config_login_type) def feed_hot(): off = request.args.get("offset") or 0 all_books = ub.session.query(ub.Downloads, func.count(ub.Downloads.book_id)).order_by( @@ -130,7 +141,7 @@ def feed_hot(): @opds.route("/opds/author") -@requires_basic_auth_if_no_ano +@basic_auth_required_check(config.config_login_type) def feed_authorindex(): off = request.args.get("offset") or 0 entries = db.session.query(db.Authors).join(db.books_authors_link).join(db.Books).filter(common_filters())\ @@ -141,7 +152,7 @@ def feed_authorindex(): @opds.route("/opds/author/") -@requires_basic_auth_if_no_ano +@basic_auth_required_check(config.config_login_type) def feed_author(book_id): off = request.args.get("offset") or 0 entries, __, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), @@ -150,7 +161,7 @@ def feed_author(book_id): @opds.route("/opds/publisher") -@requires_basic_auth_if_no_ano +@basic_auth_required_check(config.config_login_type) def feed_publisherindex(): off = request.args.get("offset") or 0 entries = db.session.query(db.Publishers).join(db.books_publishers_link).join(db.Books).filter(common_filters())\ @@ -161,7 +172,7 @@ def feed_publisherindex(): @opds.route("/opds/publisher/") -@requires_basic_auth_if_no_ano +@basic_auth_required_check(config.config_login_type) def feed_publisher(book_id): off = request.args.get("offset") or 0 entries, __, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), @@ -171,7 +182,7 @@ def feed_publisher(book_id): @opds.route("/opds/category") -@requires_basic_auth_if_no_ano +@basic_auth_required_check(config.config_login_type) def feed_categoryindex(): off = request.args.get("offset") or 0 entries = db.session.query(db.Tags).join(db.books_tags_link).join(db.Books).filter(common_filters())\ @@ -182,7 +193,7 @@ def feed_categoryindex(): @opds.route("/opds/category/") -@requires_basic_auth_if_no_ano +@basic_auth_required_check(config.config_login_type) def feed_category(book_id): off = request.args.get("offset") or 0 entries, __, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), @@ -191,7 +202,7 @@ def feed_category(book_id): @opds.route("/opds/series") -@requires_basic_auth_if_no_ano +@basic_auth_required_check(config.config_login_type) def feed_seriesindex(): off = request.args.get("offset") or 0 entries = db.session.query(db.Series).join(db.books_series_link).join(db.Books).filter(common_filters())\ @@ -202,7 +213,7 @@ def feed_seriesindex(): @opds.route("/opds/series/") -@requires_basic_auth_if_no_ano +@basic_auth_required_check(config.config_login_type) def feed_series(book_id): off = request.args.get("offset") or 0 entries, __, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), @@ -212,7 +223,7 @@ def feed_series(book_id): @opds.route("/opds/shelfindex/", defaults={'public': 0}) @opds.route("/opds/shelfindex/") -@requires_basic_auth_if_no_ano +@basic_auth_required_check(config.config_login_type) def feed_shelfindex(public): off = request.args.get("offset") or 0 if public is not 0: @@ -227,7 +238,7 @@ def feed_shelfindex(public): @opds.route("/opds/shelf/") -@requires_basic_auth_if_no_ano +@basic_auth_required_check(config.config_login_type) def feed_shelf(book_id): off = request.args.get("offset") or 0 if current_user.is_anonymous: @@ -251,14 +262,14 @@ def feed_shelf(book_id): @opds.route("/opds/download///") -@requires_basic_auth_if_no_ano +@basic_auth_required_check(config.config_login_type) @download_required def opds_download_link(book_id, book_format): return get_download_link(book_id,book_format) @opds.route("/ajax/book/") -@requires_basic_auth_if_no_ano +@basic_auth_required_check(config.config_login_type) def get_metadata_calibre_companion(uuid): entry = db.session.query(db.Books).filter(db.Books.uuid.like("%" + uuid + "%")).first() if entry is not None: @@ -307,19 +318,19 @@ def render_xml_template(*args, **kwargs): @opds.route("/opds/cover_240_240/") @opds.route("/opds/cover_90_90/") @opds.route("/opds/cover/") -@requires_basic_auth_if_no_ano +@basic_auth_required_check(config.config_login_type) def feed_get_cover(book_id): return get_book_cover(book_id) @opds.route("/opds/readbooks/") -@requires_basic_auth_if_no_ano +@basic_auth_required_check(config.config_login_type) def feed_read_books(): off = request.args.get("offset") or 0 return render_read_books(int(off) / (int(config.config_books_per_page)) + 1, True, True) @opds.route("/opds/unreadbooks/") -@requires_basic_auth_if_no_ano +@basic_auth_required_check(config.config_login_type) def feed_unread_books(): off = request.args.get("offset") or 0 return render_read_books(int(off) / (int(config.config_books_per_page)) + 1, False, True) diff --git a/cps/templates/config_edit.html b/cps/templates/config_edit.html index 3872ad5d..353bdc5c 100644 --- a/cps/templates/config_edit.html +++ b/cps/templates/config_edit.html @@ -197,18 +197,59 @@ {% endif %} - - {% if feature_support['ldap'] %} -
-
- - -
+ {% if feature_support['ldap'] %} +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
- - + +
+
+ + +
+
+ + +
+
+ + +
+
{% endif %} {% if feature_support['oauth'] %}
diff --git a/cps/templates/read.html b/cps/templates/read.html index d65d9f9b..8cb9dd62 100644 --- a/cps/templates/read.html +++ b/cps/templates/read.html @@ -79,6 +79,17 @@
+ +