From 54c4f4018815056ee7ba14bab1942b2fbd927a63 Mon Sep 17 00:00:00 2001 From: ground7 Date: Fri, 27 Dec 2019 23:12:18 -0700 Subject: [PATCH 01/11] added LDAP import update defaults --- cps/admin.py | 19 +++--- cps/config_sql.py | 15 +++-- cps/services/simpleldap.py | 17 ++++- cps/templates/admin.html | 18 +++++- cps/templates/config_edit.html | 114 ++++++++++++++++++--------------- cps/web.py | 27 +++++++- 6 files changed, 142 insertions(+), 68 deletions(-) diff --git a/cps/admin.py b/cps/admin.py index 57796080..6ed48785 100644 --- a/cps/admin.py +++ b/cps/admin.py @@ -44,7 +44,7 @@ from .gdriveutils import is_gdrive_ready, gdrive_support from .web import admin_required, render_title_template, before_request, unconfigured, login_required_if_no_ano feature_support = { - 'ldap': False, # bool(services.ldap), + 'ldap': bool(services.ldap), 'goodreads': bool(services.goodreads_support) } @@ -326,13 +326,16 @@ def _configuration_update_helper(): return _configuration_result('Please enter a LDAP service account and password', gdriveError) config.set_from_dictionary(to_save, "config_ldap_serv_password", base64.b64encode) - _config_checkbox("config_ldap_use_ssl") - _config_checkbox("config_ldap_use_tls") - _config_checkbox("config_ldap_openldap") - _config_checkbox("config_ldap_require_cert") - _config_string("config_ldap_cert_path") - if config.config_ldap_cert_path and not os.path.isfile(config.config_ldap_cert_path): - return _configuration_result('LDAP Certfile location is not valid, please enter correct path', gdriveError) + _config_string("config_ldap_group_object_filter") + _config_string("config_ldap_group_members_field") + _config_string("config_ldap_group_name") + _config_checkbox("config_ldap_use_ssl") + _config_checkbox("config_ldap_use_tls") + _config_checkbox("config_ldap_openldap") + _config_checkbox("config_ldap_require_cert") + _config_string("config_ldap_cert_path") + if config.config_ldap_cert_path and not os.path.isfile(config.config_ldap_cert_path): + return _configuration_result('LDAP Certfile location is not valid, please enter correct path', gdriveError) # Remote login configuration _config_checkbox("config_remote_login") diff --git a/cps/config_sql.py b/cps/config_sql.py index 809e97d8..fcffc3bc 100644 --- a/cps/config_sql.py +++ b/cps/config_sql.py @@ -37,6 +37,8 @@ _Base = declarative_base() class _Settings(_Base): __tablename__ = 'settings' + config_is_initial = Column(Boolean, default=True) + id = Column(Integer, primary_key=True) mail_server = Column(String, default='mail.example.org') mail_port = Column(Integer, default=25) @@ -86,18 +88,21 @@ class _Settings(_Base): # config_oauth_provider = Column(Integer) - config_ldap_provider_url = Column(String, default='localhost') + config_ldap_provider_url = Column(String, default='example.org') config_ldap_port = Column(SmallInteger, default=389) config_ldap_schema = Column(String, default='ldap') - config_ldap_serv_username = Column(String) + config_ldap_serv_username = Column(String, default='cn=admin,dc=example,dc=org') config_ldap_serv_password = Column(String) config_ldap_use_ssl = Column(Boolean, default=False) config_ldap_use_tls = Column(Boolean, default=False) config_ldap_require_cert = Column(Boolean, default=False) config_ldap_cert_path = Column(String) - config_ldap_dn = Column(String) - config_ldap_user_object = Column(String) - config_ldap_openldap = Column(Boolean, default=False) + config_ldap_dn = Column(String, default='dc=example,dc=org') + config_ldap_user_object = Column(String, default='uid=%s') + config_ldap_openldap = Column(Boolean, default=True) + config_ldap_group_object_filter = Column(String, default='(&(objectclass=posixGroup)(cn=%s))') + config_ldap_group_members_field = Column(String, default='memberUid') + config_ldap_group_name = Column(String, default='calibreweb') config_ebookconverter = Column(Integer, default=0) config_converterpath = Column(String) diff --git a/cps/services/simpleldap.py b/cps/services/simpleldap.py index 42a9aacd..03f9704c 100644 --- a/cps/services/simpleldap.py +++ b/cps/services/simpleldap.py @@ -35,8 +35,7 @@ def init_app(app, config): 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_USERNAME'] = config.config_ldap_serv_username app.config['LDAP_PASSWORD'] = base64.b64decode(config.config_ldap_serv_password) app.config['LDAP_REQUIRE_CERT'] = bool(config.config_ldap_require_cert) if config.config_ldap_require_cert: @@ -46,17 +45,29 @@ def init_app(app, config): app.config['LDAP_USE_SSL'] = bool(config.config_ldap_use_ssl) app.config['LDAP_USE_TLS'] = bool(config.config_ldap_use_tls) app.config['LDAP_OPENLDAP'] = bool(config.config_ldap_openldap) + app.config['LDAP_GROUP_OBJECT_FILTER'] = config.config_ldap_group_object_filter + app.config['LDAP_GROUP_MEMBERS_FIELD'] = config.config_ldap_group_members_field _ldap.init_app(app) +def get_object_details(user=None, group=None, query_filter=None, dn_only=False): + return _ldap.get_object_details(user, group, query_filter, dn_only) + + +def bind(): + return _ldap.bind() + + +def get_group_members(group): + return _ldap.get_group_members(group) + def basic_auth_required(func): return _ldap.basic_auth_required(func) def bind_user(username, password): - # ulf= _ldap.get_object_details('admin') '''Attempts a LDAP login. :returns: True if login succeeded, False if login failed, None if server unavailable. diff --git a/cps/templates/admin.html b/cps/templates/admin.html index 17b84f34..ef60ab33 100644 --- a/cps/templates/admin.html +++ b/cps/templates/admin.html @@ -32,7 +32,11 @@ {% endif %} {% endfor %} -
{{_('Add new user')}}
+ {% if not (config.config_login_type == 1) %} +
{{_('Add new user')}}
+ {% else %} + + {% endif %} @@ -190,3 +194,15 @@ {% endblock %} +{% block js %} + +{% endblock %} diff --git a/cps/templates/config_edit.html b/cps/templates/config_edit.html index 85b9598e..88d2631d 100644 --- a/cps/templates/config_edit.html +++ b/cps/templates/config_edit.html @@ -186,6 +186,7 @@ {% endif %} + {% if not config.config_is_initial %} {% if feature_support['ldap'] or feature_support['oauth'] %}
@@ -199,59 +200,71 @@ {% endif %}
- {% if feature_support['ldap'] %} -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
+ {% if feature_support['ldap'] %} +
- - + + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
-
- - -
-
- - -
-
- - -
-
{% endif %} {% if feature_support['oauth'] %}
@@ -270,6 +283,7 @@ {% endfor %}
{% endif %} + {% endif %} {% endif %}
diff --git a/cps/web.py b/cps/web.py index 572ac969..e55cfc7d 100644 --- a/cps/web.py +++ b/cps/web.py @@ -54,7 +54,7 @@ from .pagination import Pagination from .redirect import redirect_back feature_support = { - 'ldap': False, # bool(services.ldap), + 'ldap': bool(services.ldap), 'goodreads': bool(services.goodreads_support) } @@ -253,6 +253,29 @@ def before_request(): return redirect(url_for('admin.basic_configuration')) +@app.route('/import_ldap_users') +def import_ldap_users(): + new_users = services.ldap.get_group_members(config.config_ldap_group_name) + for username in new_users: + user_data = services.ldap.get_object_details(user=username, group=None, query_filter=None, dn_only=False) + content = ub.User() + content.nickname = username + content.password = username # dummy password which will be replaced by ldap one + content.email = user_data['mail'][0] + if (len(user_data['mail']) > 1): + content.kindle_mail = user_data['mail'][1] + content.role = config.config_default_role + content.sidebar_view = config.config_default_show + content.mature_content = bool(config.config_default_show & constants.MATURE_CONTENT) + ub.session.add(content) + try: + ub.session.commit() + except Exception as e: + log.warning("Failed to create LDAP user: %s - %s", username, e) + ub.session.rollback() + return "" + + # ################################### data provider functions ######################################################### @@ -1155,10 +1178,12 @@ def login(): if user and check_password_hash(str(user.password), form['password']) and user.nickname != "Guest": login_user(user, remember=True) flash(_(u"You are now logged in as: '%(nickname)s'", nickname=user.nickname), category="success") + config.config_is_initial = False return redirect_back(url_for("web.index")) else: log.info('Login failed for user "%s" IP-adress: %s', form['username'], ipAdress) flash(_(u"Wrong Username or Password"), category="error") + settings = config.get_mail_settings() mail_configured = bool(settings.get("mail_server", "mail.example.org") != "mail.example.org") From 6555d5869faa6581f54a5cc4f90833c036c4eaee Mon Sep 17 00:00:00 2001 From: ground7 Date: Fri, 27 Dec 2019 23:45:42 -0700 Subject: [PATCH 02/11] attempt regular login if ldap login fails as fallback --- cps/web.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cps/web.py b/cps/web.py index e55cfc7d..f1351a55 100644 --- a/cps/web.py +++ b/cps/web.py @@ -1155,7 +1155,11 @@ def login(): flash(_(u"you are now logged in as: '%(nickname)s'", nickname=user.nickname), category="success") return redirect_back(url_for("web.index")) - if login_result is None: + elif user and check_password_hash(str(user.password), form['password']) and user.nickname != "Guest": + login_user(user, remember=True) + flash(_(u"You are now logged in as: '%(nickname)s'", nickname=user.nickname), category="success") + return redirect_back(url_for("web.index")) + elif login_result is None: flash(_(u"Could not login. LDAP server down, please contact your administrator"), category="error") else: ipAdress = request.headers.get('X-Forwarded-For', request.remote_addr) From b782489a8c9a4d946bbca3ec102d26bdb3305603 Mon Sep 17 00:00:00 2001 From: ground7 Date: Sat, 28 Dec 2019 21:52:26 -0700 Subject: [PATCH 03/11] ldap opds download bugged --- cps/opds.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cps/opds.py b/cps/opds.py index f5cc4673..ef83ad69 100644 --- a/cps/opds.py +++ b/cps/opds.py @@ -318,7 +318,6 @@ def feed_shelf(book_id): @opds.route("/opds/download///") @requires_basic_auth_if_no_ano -@download_required def opds_download_link(book_id, book_format): return get_download_link(book_id,book_format) From 587174b771e192f0efa56e0255526cdc8895dec8 Mon Sep 17 00:00:00 2001 From: Ozzieisaacs Date: Sat, 4 Apr 2020 08:53:22 +0200 Subject: [PATCH 04/11] Fix #1272 --- cps/static/js/get_meta.js | 254 ++++++++++++++++++++------------------ 1 file changed, 136 insertions(+), 118 deletions(-) diff --git a/cps/static/js/get_meta.js b/cps/static/js/get_meta.js index e10b83ed..ffaf0473 100644 --- a/cps/static/js/get_meta.js +++ b/cps/static/js/get_meta.js @@ -29,15 +29,15 @@ $(function () { var msg = i18nMsg; var douban = "https://api.douban.com"; var dbSearch = "/v2/book/search"; - var dbDone = true; + var dbDone = 0; var google = "https://www.googleapis.com"; var ggSearch = "/books/v1/volumes"; - var ggDone = false; + var ggDone = 0; var comicvine = "https://comicvine.gamespot.com"; var cvSearch = "/api/search/"; - var cvDone = false; + var cvDone = 0; var showFlag = 0; @@ -73,7 +73,9 @@ $(function () { if (showFlag === 1) { $("#meta-info").html("
    "); } - if (!ggDone && !dbDone) { + if ((ggDone == 3 || (ggDone == 1 && ggResults.length === 0)) && + (dbDone == 3 || (ggDone == 1 && dbResults.length === 0)) && + (cvDone == 3 || (ggDone == 1 && cvResults.length === 0))) { $("#meta-info").html("

    " + msg.no_result + "

    "); return; } @@ -93,128 +95,141 @@ $(function () { return [year, month, day].join("-"); } - if (ggDone && ggResults.length > 0) { - ggResults.forEach(function(result) { - var book = { - id: result.id, - title: result.volumeInfo.title, - authors: result.volumeInfo.authors || [], - description: result.volumeInfo.description || "", - publisher: result.volumeInfo.publisher || "", - publishedDate: result.volumeInfo.publishedDate || "", - tags: result.volumeInfo.categories || [], - rating: result.volumeInfo.averageRating || 0, - cover: result.volumeInfo.imageLinks ? - result.volumeInfo.imageLinks.thumbnail : "/static/generic_cover.jpg", - url: "https://books.google.com/books?id=" + result.id, - source: { - id: "google", - description: "Google Books", - url: "https://books.google.com/" - } - }; + if (ggResults.length > 0) { + if (ggDone < 2) { + ggResults.forEach(function(result) { + var book = { + id: result.id, + title: result.volumeInfo.title, + authors: result.volumeInfo.authors || [], + description: result.volumeInfo.description || "", + publisher: result.volumeInfo.publisher || "", + publishedDate: result.volumeInfo.publishedDate || "", + tags: result.volumeInfo.categories || [], + rating: result.volumeInfo.averageRating || 0, + cover: result.volumeInfo.imageLinks ? + result.volumeInfo.imageLinks.thumbnail : "/static/generic_cover.jpg", + url: "https://books.google.com/books?id=" + result.id, + source: { + id: "google", + description: "Google Books", + url: "https://books.google.com/" + } + }; - var $book = $(templates.bookResult(book)); - $book.find("img").on("click", function () { - populateForm(book); + var $book = $(templates.bookResult(book)); + $book.find("img").on("click", function () { + populateForm(book); + }); + + $("#book-list").append($book); }); - - $("#book-list").append($book); - }); - ggDone = false; + ggDone = 2; + } else { + ggDone = 3; + } } - if (dbDone && dbResults.length > 0) { - dbResults.forEach(function(result) { - var seriesTitle = ""; - if (result.series) { - seriesTitle = result.series.title; - } - var dateFomers = result.pubdate.split("-"); - var publishedYear = parseInt(dateFomers[0]); - var publishedMonth = parseInt(dateFomers[1]); - var publishedDate = new Date(publishedYear, publishedMonth - 1, 1); - publishedDate = formatDate(publishedDate); - - var book = { - id: result.id, - title: result.title, - authors: result.author || [], - description: result.summary, - publisher: result.publisher || "", - publishedDate: publishedDate || "", - tags: result.tags.map(function(tag) { - return tag.title.toLowerCase().replace(/,/g, "_"); - }), - rating: result.rating.average || 0, - series: seriesTitle || "", - cover: result.image, - url: "https://book.douban.com/subject/" + result.id, - source: { - id: "douban", - description: "Douban Books", - url: "https://book.douban.com/" + if (dbResults.length > 0) { + if (dbDone < 2) { + dbResults.forEach(function(result) { + var seriesTitle = ""; + if (result.series) { + seriesTitle = result.series.title; } - }; + var dateFomers = result.pubdate.split("-"); + var publishedYear = parseInt(dateFomers[0]); + var publishedMonth = parseInt(dateFomers[1]); + var publishedDate = new Date(publishedYear, publishedMonth - 1, 1); - if (book.rating > 0) { - book.rating /= 2; - } + publishedDate = formatDate(publishedDate); - var $book = $(templates.bookResult(book)); - $book.find("img").on("click", function () { - populateForm(book); + var book = { + id: result.id, + title: result.title, + authors: result.author || [], + description: result.summary, + publisher: result.publisher || "", + publishedDate: publishedDate || "", + tags: result.tags.map(function(tag) { + return tag.title.toLowerCase().replace(/,/g, "_"); + }), + rating: result.rating.average || 0, + series: seriesTitle || "", + cover: result.image, + url: "https://book.douban.com/subject/" + result.id, + source: { + id: "douban", + description: "Douban Books", + url: "https://book.douban.com/" + } + }; + + if (book.rating > 0) { + book.rating /= 2; + } + + var $book = $(templates.bookResult(book)); + $book.find("img").on("click", function () { + populateForm(book); + }); + + $("#book-list").append($book); }); - - $("#book-list").append($book); - }); - dbDone = false; + dbDone = 2; + } else { + dbDone = 3; + } } - if (cvDone && cvResults.length > 0) { - cvResults.forEach(function(result) { - var seriesTitle = ""; - if (result.volume.name) { - seriesTitle = result.volume.name; - } - var dateFomers = ""; - if (result.store_date) { - dateFomers = result.store_date.split("-"); - }else{ - dateFomers = result.date_added.split("-"); - } - var publishedYear = parseInt(dateFomers[0]); - var publishedMonth = parseInt(dateFomers[1]); - var publishedDate = new Date(publishedYear, publishedMonth - 1, 1); - - publishedDate = formatDate(publishedDate); - - var book = { - id: result.id, - title: seriesTitle + ' #' +('00' + result.issue_number).slice(-3) + ' - ' + result.name, - authors: result.author || [], - description: result.description, - publisher: "", - publishedDate: publishedDate || "", - tags: ['Comics', seriesTitle], - rating: 0, - series: seriesTitle || "", - cover: result.image.original_url, - url: result.site_detail_url, - source: { - id: "comicvine", - description: "ComicVine Books", - url: "https://comicvine.gamespot.com/" + if (cvResults.length > 0) { + if (cvDone < 2) { + cvResults.forEach(function(result) { + var seriesTitle = ""; + if (result.volume.name) { + seriesTitle = result.volume.name; } - }; + var dateFomers = ""; + if (result.store_date) { + dateFomers = result.store_date.split("-"); + }else{ + dateFomers = result.date_added.split("-"); + } + var publishedYear = parseInt(dateFomers[0]); + var publishedMonth = parseInt(dateFomers[1]); + var publishedDate = new Date(publishedYear, publishedMonth - 1, 1); - var $book = $(templates.bookResult(book)); - $book.find("img").on("click", function () { - populateForm(book); + publishedDate = formatDate(publishedDate); + + var book = { + id: result.id, + title: seriesTitle + ' #' +('00' + result.issue_number).slice(-3) + ' - ' + result.name, + authors: result.author || [], + description: result.description, + publisher: "", + publishedDate: publishedDate || "", + tags: ['Comics', seriesTitle], + rating: 0, + series: seriesTitle || "", + cover: result.image.original_url, + url: result.site_detail_url, + source: { + id: "comicvine", + description: "ComicVine Books", + url: "https://comicvine.gamespot.com/" + } + }; + + var $book = $(templates.bookResult(book)); + $book.find("img").on("click", function () { + populateForm(book); + }); + + $("#book-list").append($book); }); - - $("#book-list").append($book); - }); - cvDone = false; + cvDone = 2; + } else { + cvDone = 3; + } } } @@ -227,11 +242,10 @@ $(function () { success: function success(data) { if ("items" in data) { ggResults = data.items; - ggDone = true; } }, complete: function complete() { - ggDone = true; + ggDone = 1; showResult(); $("#show-google").trigger("change"); } @@ -252,7 +266,7 @@ $(function () { $("#meta-info").html("

    " + msg.search_error + "!

    " + $("#meta-info")[0].innerHTML); }, complete: function complete() { - dbDone = true; + dbDone = 1; showResult(); $("#show-douban").trigger("change"); } @@ -274,7 +288,7 @@ $(function () { $("#meta-info").html("

    " + msg.search_error + "!

    " + $("#meta-info")[0].innerHTML); }, complete: function complete() { - cvDone = true; + cvDone = 1; showResult(); $("#show-comics").trigger("change"); } @@ -283,6 +297,10 @@ $(function () { function doSearch (keyword) { showFlag = 0; + dbDone = ggDone = cvDone = 0; + dbResults = []; + ggResults = []; + cvResults = []; $("#meta-info").text(msg.loading); if (keyword) { dbSearchBook(keyword); From 25ab3cabfef7b4b443116c91a0b6a5b2b6ce333c Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Sat, 4 Apr 2020 18:28:43 +0200 Subject: [PATCH 05/11] Add question for logfile to Issue Template --- .github/ISSUE_TEMPLATE/bug_report.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index d31e3bcd..f59e5943 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -17,6 +17,9 @@ Steps to reproduce the behavior: 3. Scroll down to '....' 4. See error +**Logfile** +Add content of calibre-web.log file or the relevant error, try to reproduce your problem with "debug" log-level to get more output. + **Expected behavior** A clear and concise description of what you expected to happen. From 4749eccfa51dbdb1cc989565a0659f4c647e1f3b Mon Sep 17 00:00:00 2001 From: Ozzieisaacs Date: Mon, 13 Apr 2020 22:23:58 +0200 Subject: [PATCH 06/11] Added fix for python2 regex Fix for python2 attributeError instead of TypeError on login with wrong openLDAP setting Added default empty string on LDAPCertificate Fix ldap as scheme for tls connection Enabled add user on LDAP Authentication LDAP config port is now number input Added header for user import config Added python ldap version to about section Fix: It's no longer possible to login via fallback password as long as LDAP server is available Fix: TypeError on bind is now catched and transformed to error message Update Readme Fixes for ldap --- README.md | 6 +- cps/about.py | 1 + cps/admin.py | 72 ++- cps/config_sql.py | 10 +- cps/db.py | 7 +- cps/services/__init__.py | 5 +- cps/services/simpleldap.py | 35 +- cps/templates/admin.html | 3 +- cps/templates/config_edit.html | 49 +- cps/ub.py | 10 +- cps/web.py | 96 +++- test/Calibre-Web TestSummary.html | 910 +++++++++++++++++++++--------- 12 files changed, 815 insertions(+), 389 deletions(-) mode change 100644 => 100755 test/Calibre-Web TestSummary.html diff --git a/README.md b/README.md index 849cd1fd..88db2126 100644 --- a/README.md +++ b/README.md @@ -23,12 +23,12 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d - Send eBooks to Kindle devices with the click of a button - Sync your Kobo devices through Calibre-Web with your Calibre library - Support for reading eBooks directly in the browser (.txt, .epub, .pdf, .cbr, .cbt, .cbz) -- Upload new books in many formats -- Support for Calibre custom columns +- Upload new books in many formats, including audio formats (.mp3, .m4a, .m4b) +- Support for Calibre Custom Columns - Ability to hide content based on categories and Custom Column content per user - Self-update capability - "Magic Link" login to make it easy to log on eReaders -- Login via google/github oauth and via proxy authentication +- Login via LDAP, google/github oauth and via proxy authentication ## Quick start diff --git a/cps/about.py b/cps/about.py index fd52ca7b..dfc6c502 100644 --- a/cps/about.py +++ b/cps/about.py @@ -69,6 +69,7 @@ _VERSIONS = OrderedDict( pytz=pytz.__version__, Unidecode = unidecode_version, Flask_SimpleLDAP = u'installed' if bool(services.ldap) else u'not installed', + python_LDAP = services.ldapVersion if bool(services.ldapVersion) else u'not installed', Goodreads = u'installed' if bool(services.goodreads_support) else u'not installed', jsonschema = services.SyncToken.__version__ if bool(services.SyncToken) else u'not installed', ) diff --git a/cps/admin.py b/cps/admin.py index 726071d3..b2f740bc 100644 --- a/cps/admin.py +++ b/cps/admin.py @@ -504,7 +504,7 @@ def _configuration_update_helper(): with open(gdriveutils.CLIENT_SECRETS, 'r') as settings: gdrive_secrets = json.load(settings)['web'] if not gdrive_secrets: - return _configuration_result('client_secrets.json is not configured for web application') + return _configuration_result(_('client_secrets.json Is Not Configured For Web Application')) gdriveutils.update_settings( gdrive_secrets['client_id'], gdrive_secrets['client_secret'], @@ -520,11 +520,11 @@ def _configuration_update_helper(): reboot_required |= _config_string("config_keyfile") if config.config_keyfile and not os.path.isfile(config.config_keyfile): - return _configuration_result('Keyfile location is not valid, please enter correct path', gdriveError) + return _configuration_result(_('Keyfile Location is not Valid, Please Enter Correct Path'), gdriveError) reboot_required |= _config_string("config_certfile") if config.config_certfile and not os.path.isfile(config.config_certfile): - return _configuration_result('Certfile location is not valid, please enter correct path', gdriveError) + return _configuration_result(_('Certfile Location is not Valid, Please Enter Correct Path'), gdriveError) _config_checkbox_int("config_uploading") _config_checkbox_int("config_anonbrowse") @@ -542,45 +542,57 @@ def _configuration_update_helper(): #LDAP configurator, if config.config_login_type == constants.LOGIN_LDAP: - _config_string("config_ldap_provider_url") - _config_int("config_ldap_port") + reboot_required |= _config_string("config_ldap_provider_url") + reboot_required |= _config_int("config_ldap_port") # _config_string("config_ldap_schema") - _config_string("config_ldap_dn") - _config_string("config_ldap_user_object") + reboot_required |= _config_string("config_ldap_dn") + reboot_required |= _config_string("config_ldap_serv_username") + reboot_required |= _config_string("config_ldap_user_object") + reboot_required |= _config_string("config_ldap_group_object_filter") + reboot_required |= _config_string("config_ldap_group_members_field") + reboot_required |= _config_checkbox("config_ldap_openldap") + reboot_required |= _config_int("config_ldap_encryption") + reboot_required |= _config_string("config_ldap_cert_path") + _config_string("config_ldap_group_name") + if "config_ldap_serv_password" in to_save and to_save["config_ldap_serv_password"] != "": + reboot_required |= 1 + config.set_from_dictionary(to_save, "config_ldap_serv_password", base64.b64encode, encode='UTF-8') + config.save() + if not config.config_ldap_provider_url \ or not config.config_ldap_port \ or not config.config_ldap_dn \ or not config.config_ldap_user_object: - return _configuration_result('Please enter a LDAP provider, ' - 'port, DN and user object identifier', gdriveError) + return _configuration_result(_('Please Enter a LDAP Provider, ' + 'Port, DN and User Object Identifier'), gdriveError) - _config_string("config_ldap_serv_username") - if "config_ldap_serv_password" in to_save and to_save["config_ldap_serv_password"]: - config.set_from_dictionary(to_save, "config_ldap_serv_password", base64.b64encode, encode='UTF-8') - if not config.config_ldap_serv_username and not config.config_ldap_serv_password: - return _configuration_result('Please enter a LDAP service account and password', gdriveError) + if not config.config_ldap_serv_username or not bool(config.config_ldap_serv_password): + return _configuration_result('Please Enter a LDAP Service Account and Password', gdriveError) - _config_string("config_ldap_group_object_filter") - _config_string("config_ldap_group_members_field") - _config_string("config_ldap_group_name") #_config_checkbox("config_ldap_use_ssl") #_config_checkbox("config_ldap_use_tls") - _config_int("config_ldap_encryption") - _config_checkbox("config_ldap_openldap") + # reboot_required |= _config_checkbox("config_ldap_openldap") # _config_checkbox("config_ldap_require_cert") - _config_string("config_ldap_cert_path") - if config.config_ldap_group_object_filter.count("%s") != 1: - return _configuration_result('LDAP Group Object Filter Needs to Have One "%s" Format Identifier', - gdriveError) + if config.config_ldap_group_object_filter: + if config.config_ldap_group_object_filter.count("%s") != 1: + return _configuration_result(_('LDAP Group Object Filter Needs to Have One "%s" Format Identifier'), + gdriveError) + if config.config_ldap_group_object_filter.count("(") != config.config_ldap_group_object_filter.count(")"): + return _configuration_result(_('LDAP Group Object Filter Has Unmatched Parenthesis'), + gdriveError) if config.config_ldap_user_object.count("%s") != 1: - return _configuration_result('LDAP User Object Filter needs to Have One "%s" Format Identifier', + return _configuration_result(_('LDAP User Object Filter needs to Have One "%s" Format Identifier'), + gdriveError) + if config.config_ldap_user_object.count("(") != config.config_ldap_user_object.count(")"): + return _configuration_result(_('LDAP User Object Filter Has Unmatched Parenthesis'), gdriveError) - if config.config_ldap_cert_path and not os.path.isfile(config.config_ldap_cert_path): - return _configuration_result('LDAP Certfile location is not valid, please enter correct path', gdriveError) + if config.config_ldap_cert_path and not os.path.isdir(config.config_ldap_cert_path): + return _configuration_result(_('LDAP Certificate Location is not Valid, Please Enter Correct Path'), + gdriveError) # Remote login configuration _config_checkbox("config_remote_login") @@ -628,12 +640,12 @@ def _configuration_update_helper(): reboot_required |= _config_int("config_log_level") reboot_required |= _config_string("config_logfile") if not logger.is_valid_logfile(config.config_logfile): - return _configuration_result('Logfile location is not valid, please enter correct path', gdriveError) + return _configuration_result(_('Logfile Location is not Valid, Please Enter Correct Path'), gdriveError) reboot_required |= _config_checkbox_int("config_access_log") reboot_required |= _config_string("config_access_logfile") if not logger.is_valid_logfile(config.config_access_logfile): - return _configuration_result('Access Logfile location is not valid, please enter correct path', gdriveError) + return _configuration_result(_('Access Logfile Location is not Valid, Please Enter Correct Path'), gdriveError) # Rarfile Content configuration _config_string("config_rarfile_location") @@ -652,7 +664,7 @@ def _configuration_update_helper(): if db_change: # reload(db) if not db.setup_db(config): - return _configuration_result('DB location is not valid, please enter correct path', gdriveError) + return _configuration_result(_('DB Location is not Valid, Please Enter Correct Path'), gdriveError) config.save() flash(_(u"Calibre-Web configuration updated"), category="success") @@ -678,7 +690,7 @@ def _configuration_result(error_flash=None, gdriveError=None): show_login_button = config.db_configured and not current_user.is_authenticated if error_flash: config.load() - flash(_(error_flash), category="error") + flash(error_flash, category="error") show_login_button = False return render_title_template("config_edit.html", config=config, provider=oauthblueprints, diff --git a/cps/config_sql.py b/cps/config_sql.py index 98b30416..7fc99a91 100644 --- a/cps/config_sql.py +++ b/cps/config_sql.py @@ -73,7 +73,7 @@ class _Settings(_Base): config_kobo_sync = Column(Boolean, default=False) config_default_role = Column(SmallInteger, default=0) - config_default_show = Column(SmallInteger, default=6143) + config_default_show = Column(SmallInteger, default=constants.ADMIN_USER_SIDEBAR) config_columns_to_ignore = Column(String) config_denied_tags = Column(String, default="") @@ -99,11 +99,11 @@ class _Settings(_Base): config_ldap_port = Column(SmallInteger, default=389) # config_ldap_schema = Column(String, default='ldap') config_ldap_serv_username = Column(String, default='cn=admin,dc=example,dc=org') - config_ldap_serv_password = Column(String) + config_ldap_serv_password = Column(String, default="") config_ldap_encryption = Column(SmallInteger, default=0) # config_ldap_use_tls = Column(Boolean, default=False) # config_ldap_require_cert = Column(Boolean, default=False) - config_ldap_cert_path = Column(String) + config_ldap_cert_path = Column(String, default="") config_ldap_dn = Column(String, default='dc=example,dc=org') config_ldap_user_object = Column(String, default='uid=%s') config_ldap_openldap = Column(Boolean, default=True) @@ -285,7 +285,9 @@ class _ConfigSQL(object): self._session.commit() self.load() - def invalidate(self): + def invalidate(self, error=None): + if error: + log.error(error) log.warning("invalidating configuration") self.db_configured = False self.config_calibre_dir = None diff --git a/cps/db.py b/cps/db.py index f40e0cda..966adbe5 100755 --- a/cps/db.py +++ b/cps/db.py @@ -344,14 +344,13 @@ def setup_db(config): isolation_level="SERIALIZABLE", connect_args={'check_same_thread': False}) conn = engine.connect() - except: - config.invalidate() + # conn.text_factory = lambda b: b.decode(errors = 'ignore') possible fix for #1302 + except Exception as e: + config.invalidate(e) return False config.db_configured = True update_title_sort(config, conn.connection) - # conn.connection.create_function('lower', 1, lcase) - # conn.connection.create_function('upper', 1, ucase) if not cc_classes: cc = conn.execute("SELECT id, datatype FROM custom_columns") diff --git a/cps/services/__init__.py b/cps/services/__init__.py index 2eb82f0d..11ef4c65 100644 --- a/cps/services/__init__.py +++ b/cps/services/__init__.py @@ -30,10 +30,13 @@ except ImportError as err: goodreads_support = None -try: from . import simpleldap as ldap +try: + from . import simpleldap as ldap + from .simpleldap import ldapVersion except ImportError as err: log.debug("cannot import simpleldap, logging in with ldap will not work: %s", err) ldap = None + ldapVersion = None try: from . import SyncToken as SyncToken diff --git a/cps/services/simpleldap.py b/cps/services/simpleldap.py index 82690d87..841f61e1 100644 --- a/cps/services/simpleldap.py +++ b/cps/services/simpleldap.py @@ -23,6 +23,10 @@ from flask_simpleldap import LDAP, LDAPException from .. import constants, logger +try: + from ldap.pkginfo import __version__ as ldapVersion +except ImportError: + pass log = logger.create() _ldap = LDAP() @@ -34,14 +38,16 @@ def init_app(app, config): app.config['LDAP_HOST'] = config.config_ldap_provider_url app.config['LDAP_PORT'] = config.config_ldap_port - if config.config_ldap_encryption: + if config.config_ldap_encryption == 2: app.config['LDAP_SCHEMA'] = 'ldaps' else: app.config['LDAP_SCHEMA'] = 'ldap' # app.config['LDAP_SCHEMA'] = config.config_ldap_schema app.config['LDAP_USERNAME'] = config.config_ldap_serv_username + if config.config_ldap_serv_password is None: + config.config_ldap_serv_password = '' app.config['LDAP_PASSWORD'] = base64.b64decode(config.config_ldap_serv_password) - if config.config_ldap_cert_path: + if bool(config.config_ldap_cert_path): app.config['LDAP_REQUIRE_CERT'] = True app.config['LDAP_CERT_PATH'] = config.config_ldap_cert_path app.config['LDAP_BASE_DN'] = config.config_ldap_dn @@ -52,6 +58,7 @@ def init_app(app, config): app.config['LDAP_OPENLDAP'] = bool(config.config_ldap_openldap) app.config['LDAP_GROUP_OBJECT_FILTER'] = config.config_ldap_group_object_filter app.config['LDAP_GROUP_MEMBERS_FIELD'] = config.config_ldap_group_members_field + # app.config['LDAP_CUSTOM_OPTIONS'] = {'OPT_NETWORK_TIMEOUT': 10} _ldap.init_app(app) @@ -78,16 +85,22 @@ def bind_user(username, password): :returns: True if login succeeded, False if login failed, None if server unavailable. ''' try: - result = _ldap.bind_user(username, password) - log.debug("LDAP login '%s': %r", username, result) - return result is not None + if _ldap.get_object_details(username): + result = _ldap.bind_user(username, password) + log.debug("LDAP login '%s': %r", username, result) + return result is not None, None + return None, None # User not found + except (TypeError, AttributeError) as ex: + error = ("LDAP bind_user: %s" % ex) + return None, error except LDAPException as ex: if ex.message == 'Invalid credentials': - log.info("LDAP login '%s' failed: %s", username, ex) - return False + error = ("LDAP admin login failed") + return None, error if ex.message == "Can't contact LDAP server": - log.warning('LDAP Server down: %s', ex) - return None + # log.warning('LDAP Server down: %s', ex) + error = ('LDAP Server down: %s' % ex) + return None, error else: - log.warning('LDAP Server error: %s', ex.message) - return None + error = ('LDAP Server error: %s' % ex.message) + return None, error diff --git a/cps/templates/admin.html b/cps/templates/admin.html index b548f021..ab143c59 100644 --- a/cps/templates/admin.html +++ b/cps/templates/admin.html @@ -35,9 +35,8 @@ {% endif %} {% endfor %} - {% if not (config.config_login_type == 1) %} - {% else %} + {% if (config.config_login_type == 1) %}
    {{_('Import LDAP Users')}}
    {% endif %} diff --git a/cps/templates/config_edit.html b/cps/templates/config_edit.html index 5b114739..e9abdfde 100644 --- a/cps/templates/config_edit.html +++ b/cps/templates/config_edit.html @@ -230,18 +230,11 @@
    - +
    - - -
    -
    - - -
    -
    - + +
    + +
    + + +
    +
    + + +
    @@ -266,18 +268,19 @@
    -
    - - -
    -
    - - -
    -
    - - -
    +

    {{_('Following Settings are Needed For User Import')}}

    +
    + + +
    +
    + + +
    +
    + + +
    {% endif %} {% if feature_support['oauth'] %} diff --git a/cps/ub.py b/cps/ub.py index 21dc6e3c..13e996cc 100644 --- a/cps/ub.py +++ b/cps/ub.py @@ -232,7 +232,7 @@ class Anonymous(AnonymousUserMixin, UserBase): self.sidebar_view = data.sidebar_view self.default_language = data.default_language self.locale = data.locale - self.mature_content = data.mature_content + # self.mature_content = data.mature_content self.kindle_mail = data.kindle_mail self.denied_tags = data.denied_tags self.allowed_tags = data.allowed_tags @@ -441,14 +441,12 @@ def migrate_Database(session): "locale VARCHAR(2)," "sidebar_view INTEGER," "default_language VARCHAR(3)," - "mature_content BOOLEAN," "UNIQUE (nickname)," - "UNIQUE (email)," - "CHECK (mature_content IN (0, 1)))") + "UNIQUE (email))") conn.execute("INSERT INTO user_id(id, nickname, email, role, password, kindle_mail,locale," - "sidebar_view, default_language, mature_content) " + "sidebar_view, default_language) " "SELECT id, nickname, email, role, password, kindle_mail, locale," - "sidebar_view, default_language, mature_content FROM user") + "sidebar_view, default_language FROM user") # delete old user table and rename new user_id table to user: conn.execute("DROP TABLE user") conn.execute("ALTER TABLE user_id RENAME TO user") diff --git a/cps/web.py b/cps/web.py index e85b7c0e..84831bc2 100644 --- a/cps/web.py +++ b/cps/web.py @@ -28,6 +28,7 @@ import json import mimetypes import traceback import binascii +import re from babel import Locale as LC from babel.dates import format_date @@ -278,31 +279,66 @@ def import_ldap_users(): showtext = {} try: new_users = services.ldap.get_group_members(config.config_ldap_group_name) - except services.ldap.LDAPException as e: + except (services.ldap.LDAPException, TypeError, AttributeError) as e: log.debug(e) - showtext['text'] = _(u'Error : %(ldaperror)s', ldaperror=e) + showtext['text'] = _(u'Error: %(ldaperror)s', ldaperror=e) + return json.dumps(showtext) + if not new_users: + log.debug('LDAP empty response') + showtext['text'] = _(u'Error: No user returned in response of LDAP server') return json.dumps(showtext) for username in new_users: - user_data = services.ldap.get_object_details(user=username, group=None, query_filter=None, dn_only=False) - content = ub.User() - content.nickname = username - content.password = username # dummy password which will be replaced by ldap one - content.email = user_data['mail'][0] - if (len(user_data['mail']) > 1): - content.kindle_mail = user_data['mail'][1] - content.role = config.config_default_role - content.sidebar_view = config.config_default_show - content.mature_content = bool(config.config_default_show & constants.MATURE_CONTENT) - ub.session.add(content) - try: - ub.session.commit() - except Exception as e: - log.warning("Failed to create LDAP user: %s - %s", username, e) - ub.session.rollback() - showtext['text'] = _(u'Failed to create at least one LDAP user') + user = username.decode('utf-8') + if '=' in user: + match = re.search("([a-zA-Z0-9-]+)=%s", config.config_ldap_user_object, re.IGNORECASE | re.UNICODE) + if match: + match_filter = match.group(1) + match = re.search(match_filter + "=([[\d\w-]+)", user, re.IGNORECASE | re.UNICODE) + if match: + user = match.group(1) + else: + log.warning("Could Not Parse LDAP User: %s", user) + continue + else: + log.warning("Could Not Parse LDAP User: %s", user) + continue + if ub.session.query(ub.User).filter(ub.User.nickname == user.lower()).first(): + log.warning("LDAP User: %s Already in Database", user) + continue + user_data = services.ldap.get_object_details(user=user, + group=None, + query_filter=None, + dn_only=False) + if user_data: + content = ub.User() + content.nickname = user + content.password = '' # dummy password which will be replaced by ldap one + if 'mail' in user_data: + content.email = user_data['mail'][0].decode('utf-8') + if (len(user_data['mail']) > 1): + content.kindle_mail = user_data['mail'][1].decode('utf-8') + else: + log.debug('No Mail Field Found in LDAP Response') + content.email = user + '@email.com' + content.role = config.config_default_role + content.sidebar_view = config.config_default_show + content.allowed_tags = config.config_allowed_tags + content.denied_tags = config.config_denied_tags + content.allowed_column_value = config.config_allowed_column_value + content.denied_column_value = config.config_denied_column_value + ub.session.add(content) + try: + ub.session.commit() + except Exception as e: + log.warning("Failed to create LDAP user: %s - %s", user, e) + ub.session.rollback() + showtext['text'] = _(u'Failed to Create at Least One LDAP User') + else: + log.warning("LDAP User: %s Not Found", user) + showtext['text'] = _(u'At Least One LDAP User Not Found in Database') if not showtext: - showtext['text'] = _(u'User successfully imported') + showtext['text'] = _(u'User Successfully Imported') return json.dumps(showtext) @@ -844,8 +880,9 @@ def reconnect(): @web.route("/search", methods=["GET"]) @login_required_if_no_ano def search(): - term = request.args.get("query").strip().lower() + term = request.args.get("query") if term: + term.strip().lower() entries = get_search_results(term) ids = list() for element in entries: @@ -1175,24 +1212,27 @@ def login(): form = request.form.to_dict() user = ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == form['username'].strip().lower())\ .first() - if config.config_login_type == constants.LOGIN_LDAP and services.ldap and user: - login_result = services.ldap.bind_user(form['username'], form['password']) + if config.config_login_type == constants.LOGIN_LDAP and services.ldap and user and form['password'] != "": + login_result, error = services.ldap.bind_user(form['username'], form['password']) + # None if credentials are not matching + # -1 if LDAP Server error + # 0 if wrong passwort if login_result: login_user(user, remember=True) log.debug(u"You are now logged in as: '%s'", user.nickname) flash(_(u"you are now logged in as: '%(nickname)s'", nickname=user.nickname), category="success") return redirect_back(url_for("web.index")) - elif user and check_password_hash(str(user.password), form['password']) and user.nickname != "Guest": + elif login_result is None and user and check_password_hash(str(user.password), form['password']) and user.nickname != "Guest": login_user(user, remember=True) - log.info("LDAP Server Down, Fallback Login as: %(nickname)s", user.nickname) - flash(_(u"LDAP Server Down, Fallback Login as: '%(nickname)s'", + log.info("Local Fallback Login as: '%s'", user.nickname) + flash(_(u"Fallback Login as: '%(nickname)s', LDAP Server not reachable, or user not known", nickname=user.nickname), category="warning") return redirect_back(url_for("web.index")) elif login_result is None: - log.info("Could not login. LDAP server down") - flash(_(u"Could not login. LDAP server down, please contact your administrator"), category="error") + log.info(error) + flash(_(u"Could not login: %(message)s", message=error), category="error") else: ipAdress = request.headers.get('X-Forwarded-For', request.remote_addr) log.info('LDAP Login failed for user "%s" IP-adress: %s', form['username'], ipAdress) diff --git a/test/Calibre-Web TestSummary.html b/test/Calibre-Web TestSummary.html old mode 100644 new mode 100755 index d0c8094f..cb9772bf --- a/test/Calibre-Web TestSummary.html +++ b/test/Calibre-Web TestSummary.html @@ -36,17 +36,17 @@
    -

    Start Time: 2020-04-02 20:21:13

    +

    Start Time: 2020-04-13 20:58:27

    -

    Stop Time: 2020-04-02 20:57:57

    +

    Stop Time: 2020-04-13 21:36:17

    -

    Duration: 1878.33 s

    +

    Duration: 2002.47 s

    @@ -301,8 +301,8 @@ test_ebook_convert.test_ebook_convert 11 - 11 - 0 + 7 + 4 0 0 @@ -339,11 +339,33 @@ - +
    test_convert_email
    - PASS + +
    + FAIL +
    + + + + @@ -357,20 +379,64 @@ - +
    test_convert_only
    - PASS + +
    + FAIL +
    + + + + - +
    test_convert_parameter
    - PASS + +
    + FAIL +
    + + + + @@ -393,11 +459,33 @@ - +
    test_email_only
    - PASS + +
    + FAIL +
    + + + + @@ -414,13 +502,13 @@ test_edit_books.test_edit_books - 2 + 30 + 27 0 0 - 0 - 2 + 3 - Detail + Detail @@ -452,19 +540,109 @@ - + -
    test_rename_uppercase_lowercase
    +
    test_download_book
    + + PASS + + + + + + +
    test_edit_author
    + + PASS + + + + + + +
    test_edit_category
    + + PASS + + + + + + +
    test_edit_comments
    + + PASS + + + + + + +
    test_edit_custom_bool
    + + PASS + + + + + + +
    test_edit_custom_rating
    + + PASS + + + + + + +
    test_edit_custom_single_select
    + + PASS + + + + + + +
    test_edit_custom_text
    + + PASS + + + + + + +
    test_edit_language
    + + PASS + + + + + + +
    test_edit_publisher
    + + PASS + + + + + + +
    test_edit_publishing_date
    - SKIP + SKIP
    -