Add LDAP authentication (with uid lookup)
authorRowan Thorpe <rowan@rowanthorpe.com>
Tue, 6 May 2014 15:30:53 +0000 (15:30 +0000)
committerLunar <lunar@anargeek.net>
Wed, 7 May 2014 14:35:07 +0000 (14:35 +0000)
14 files changed:
Gemfile.lock
INSTALL
README
conf/settings-default.yml
conf/settings-ldap.yml [new file with mode: 0644]
coquelicot.gemspec
lib/coquelicot/auth/ldap.rb [new file with mode: 0644]
po/coquelicot.pot
po/de/coquelicot.po
po/es/coquelicot.po
po/fr/coquelicot.po
public/javascripts/coquelicot.auth.ldap.js [new file with mode: 0644]
spec/coquelicot_spec.rb
views/auth/ldap.haml [new file with mode: 0644]

index 4f8a783..4ca7b0a 100644 (file)
@@ -54,6 +54,7 @@ GEM
     moneta (0.7.20)
     multi_json (1.10.0)
     multipart-parser (0.1.1)
+    net-ldap (0.6.1)
     nokogiri (1.6.1)
       mini_portile (~> 0.5.0)
     rack (1.5.2)
@@ -112,6 +113,7 @@ DEPENDENCIES
   coquelicot!
   gettext
   hpricot
+  net-ldap
   rack-test
   rake
   rspec (~> 2.11)
diff --git a/INSTALL b/INSTALL
index 2e571b3..8ff43c1 100644 (file)
--- a/INSTALL
+++ b/INSTALL
@@ -134,9 +134,15 @@ Further settings example:
  * `conf/settings-imap.yml`: necessary configuration for the "imap"
    authentication mechanism.
 
+ * `conf/settings-ldap.yml`: necessary configuration for the "ldap"
+   authentication mechanism.
+
 You can copy one of these examples to `conf/settings.yml` and adjust
 them according to your environment.
 
+Using the LDAP authentication method requires the `net-ldap` gem
+to be installed manually.
+
 A different location for the configuration file can be specified using
 the `-c` option when running `bin/coquelicot`.
 
diff --git a/README b/README
index 7571d3b..503f013 100644 (file)
--- a/README
+++ b/README
@@ -21,12 +21,14 @@ Features
 
    In order to prevent random Internet users to eat bandwidth and
    disk space, Coquelicot limits upload to authenticated users.
-   It currently ships with two authentication mechanisms:
+   It currently ships with three authentication mechanisms:
 
     - "simplepass": uploading users need to provide a global,
       pre-shared, password;
     - "imap": users will need to provide a login and a password,
       that are used to authenticate against an existing IMAP server.
+    - "ldap": users will need to provide a uid and a password,
+      that are used to authenticate against an existing LDAP server.
 
    It is possible to integrate more authentication mechanisms by
    implementing a single method, some JavaScript, and a partial template
@@ -110,6 +112,8 @@ Background image (`public/images/background.jpg`) derived from:
 [“coquelicot” picture] © 2008 Jean-Louis Zimmermann  
 Licensed under [Creative Commons Attributions 2.0 Generic]  
 
+LDAP authentication code initially provided by Rowan Thorpe.
+
 *jQuery* is © 2011 John Resig. Licensed under the [MIT license].  
 *jquery.uploadProgress* is © 2008 Piotr Sarnacki. Licensed under the
 [MIT license].  
index 8077fa9..ebb2012 100644 (file)
@@ -110,8 +110,8 @@ show_exceptions: false
 
 # Authentication method
 #
-#   Please have look at `conf/settings-simplepass.yml` and
-#   `conf/settings-imap.yml` for more details.
+#   Please have a look at `conf/settings-simplepass.yml`,
+#   `conf/settings-imap.yml` and `conf/settings-ldap.yml` for more details.
 #
 # The default password is 'test'.
 authentication_method:
diff --git a/conf/settings-ldap.yml b/conf/settings-ldap.yml
new file mode 100644 (file)
index 0000000..6151bf7
--- /dev/null
@@ -0,0 +1,20 @@
+# Settings for the LDAP authentication method
+# -------------------------------------------
+#
+# When using the LDAP authentication method users will be
+# asked for a login and a password. Those credentials will
+# be tested against the given LDAP server.
+#
+# Connections to the LDAP server are made using SSL/TLS.
+
+authentication_method:
+  name: ldap
+
+  # Hostname of the authenticating LDAP server
+  ldap_server: "ldap.example.com"
+
+  # Port of the authenticating LDAP server
+  ldap_port: 636
+
+  # Search base of the authenticating LDAP server
+  ldap_base: "dc=example,dc=com"
index 7701e50..bfed1f1 100644 (file)
@@ -52,6 +52,7 @@ Gem::Specification.new do |s|
   s.add_development_dependency 'activesupport'
   s.add_development_dependency 'tzinfo'
   s.add_development_dependency 'gettext'
+  s.add_development_dependency 'net-ldap'
 
   s.add_runtime_dependency 'sinatra', '~>1.3'
   s.add_runtime_dependency 'sinatra-contrib', '~>1.3'
diff --git a/lib/coquelicot/auth/ldap.rb b/lib/coquelicot/auth/ldap.rb
new file mode 100644 (file)
index 0000000..cd67dc0
--- /dev/null
@@ -0,0 +1,68 @@
+# -*- coding: UTF-8 -*-
+# Coquelicot: "one-click" file sharing with a focus on users' privacy.
+# Copyright © 2012-2014 potager.org <jardiniers@potager.org>
+#           ©      2014 Rowan Thorpe <rowan@rowanthorpe.com>
+#
+# 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/>.
+
+# TODO: set array of multiple ldap servers in settings and loop over them to
+#       find first matching UID to connect as
+
+# TODO: add commented code showing how to direct login by full username,
+#       without lookup
+
+# TODO: add commented code showing how to use starttls as an option instead of
+#       dedicated SSL port, too
+
+# NB:   :simple_tls ensures all communication is encrypted, but it doesn't
+#       verify the server-certificate. A method which does *both* doesn't
+#       seem to exist in Net::LDAP yet...
+
+require 'net/ldap'
+
+module Coquelicot
+  module Auth
+    class LdapAuthenticator < AbstractAuthenticator
+      def authenticate(params)
+        if params[:ldap_user].empty? || params[:ldap_password].empty?
+          raise Coquelicot::Auth::Error.new('Empty username or password.')
+        end
+        # connect anonymously & lookup user to do authenticated bind_as() next
+        ldap = Net::LDAP.new(:host => settings.ldap_server,
+                             :port => settings.ldap_port,
+                             :base => settings.ldap_base,
+                             :encryption => :simple_tls,
+                             :auth => { :method => :anonymous })
+        result = ldap.bind_as(:base => settings.ldap_base,
+                              :filter => "(uid=#{Net::LDAP::Filter.escape(params[:ldap_user])})",
+                              :password => params[:ldap_password])
+        unless result
+          raise Coquelicot::Auth::Error.new(
+                    'Failed authentication to LDAP server')
+        end
+        true
+      rescue Errno::ECONNREFUSED
+        raise Coquelicot::Auth::Error.new(
+                  'Unable to connect to LDAP server')
+      rescue NoMethodError => ex
+        if [:ldap_server, :ldap_port, :ldap_base].include? ex.name
+          raise Coquelicot::Auth::Error.new(
+                    "Missing '#{ex.name}' attribute in configuration.")
+        else
+          raise
+        end
+      end
+    end
+  end
+end
index 503dd26..cf1fadc 100644 (file)
@@ -49,7 +49,11 @@ msgstr ""
 msgid "E-mail User:"
 msgstr ""
 
-#: views/auth/imap.haml:23 views/enter_file_key.haml:22
+#: views/auth/ldap.haml:20
+msgid "LDAP User:"
+msgstr ""
+
+#: views/auth/imap.haml:23 views/auth/ldap.haml:23 views/enter_file_key.haml:22
 msgid "Password:"
 msgstr ""
 
index c0b45ed..ce9e739 100644 (file)
@@ -48,7 +48,11 @@ msgstr "Upload Passwort:"
 msgid "E-mail User:"
 msgstr "Email User:"
 
-#: views/auth/imap.haml:23 views/enter_file_key.haml:22
+#: views/auth/ldap.haml:20
+msgid "LDAP User:"
+msgstr "LDAP User:"
+
+#: views/auth/imap.haml:23 views/auth/ldap.haml:23 views/enter_file_key.haml:22
 msgid "Password:"
 msgstr "Passwort:"
 
index 0a721e9..f2954aa 100644 (file)
@@ -48,7 +48,11 @@ msgstr "Contraseña para el envío:"
 msgid "E-mail User:"
 msgstr "Correo electrónico:"
 
-#: views/auth/imap.haml:23 views/enter_file_key.haml:22
+#: views/auth/ldap.haml:20
+msgid "LDAP User:"
+msgstr ""
+
+#: views/auth/imap.haml:23 views/auth/ldap.haml:23 views/enter_file_key.haml:22
 msgid "Password:"
 msgstr "Contraseña:"
 
index c5230c3..9e8e9e0 100644 (file)
@@ -48,7 +48,11 @@ msgstr "Mot de passe pour envoyer :"
 msgid "E-mail User:"
 msgstr "Compte email :"
 
-#: views/auth/imap.haml:23 views/enter_file_key.haml:22
+#: views/auth/ldap.haml:20
+msgid "LDAP User:"
+msgstr "Compte LDAP :"
+
+#: views/auth/imap.haml:23 views/auth/ldap.haml:23 views/enter_file_key.haml:22
 msgid "Password:"
 msgstr "Mot de passe :"
 
diff --git a/public/javascripts/coquelicot.auth.ldap.js b/public/javascripts/coquelicot.auth.ldap.js
new file mode 100644 (file)
index 0000000..833f0dd
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * Coquelicot: "one-click" file sharing with a focus on users' privacy.
+ * Copyright © 2012-2014 potager.org <jardiniers@potager.org>
+ *           ©      2014 Rowan Thorpe <rowan@rowanthorpe.com>
+ *
+ * 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/>.
+ */
+
+var authentication = {
+  getData: function() {
+    return {
+      ldap_user: $('#ldap_user').val(),
+      ldap_password: $('#ldap_password').val()
+    };
+  },
+  focus: function() {
+    $('#ldap_user').focus();
+  },
+  handleReject: function() {
+    $('#ldap_user').val('');
+    $('#ldap_password').val('');
+  },
+};
+
+$(document).ready(function() {
+  $('#ldap-auth-submit').remove();
+  var submit = $('<input type="submit" />');
+  submit.attr('value', 'Login');
+  submit.attr('id', 'ldap-auth-submit');
+  $('#upload-authentication').append(
+    $('<div class="field" />').append(
+      $('<div class="submit" />').append(
+        submit)));
+});
index c45b846..5a6c9a3 100644 (file)
@@ -341,4 +341,46 @@ PART
       expect(last_response).to be_ok
     end
   end
+
+  context "when using 'ldap' authentication mechanism" do
+    before(:each) do
+      app.set :authentication_method, :name => 'ldap',
+                                      :ldap_server => 'example.org',
+                                      :ldap_port => 636,
+                                      :ldap_base => 'dc=example,dc=com'
+    end
+
+    it "should try to login to the LDAP server when using AJAX" do
+      ldap = double('Net::LDAP').as_null_object
+      expect(ldap).to receive(:bind_as).with(
+            :base => 'dc=example,dc=com',
+            :filter => '(uid=user)',
+            :password => 'password').
+        and_return(double('Net::LDAP::PDU'))
+      expect(Net::LDAP).to receive(:new).with(
+          :host => 'example.org',
+          :port => 636,
+          :base => 'dc=example,dc=com',
+          :encryption => :simple_tls,
+          :auth => { :method => :anonymous }).
+        and_return(ldap)
+      request '/authenticate', :method => 'POST', :xhr => true,
+                               :params => { :ldap_user     => 'user',
+                                            :ldap_password => 'password' }
+      expect(last_response).not_to be_nil
+    end
+
+    it "should properly escape the given username" do
+      ldap = double('Net::LDAP').as_null_object
+      expect(ldap).to receive(:bind_as).with(
+            :base => 'dc=example,dc=com',
+            :filter => '(uid=us\\29er)',
+            :password => 'password').
+        and_return(double('Net::LDAP::PDU'))
+      expect(Net::LDAP).to receive(:new).and_return(ldap)
+      request '/authenticate', :method => 'POST', :xhr => true,
+                               :params => { :ldap_user     => 'us)er',
+                                            :ldap_password => 'password' }
+    end
+  end
 end
diff --git a/views/auth/ldap.haml b/views/auth/ldap.haml
new file mode 100644 (file)
index 0000000..74e6690
--- /dev/null
@@ -0,0 +1,24 @@
+-# -*- coding: UTF-8 -*-
+-# Coquelicot: "one-click" file sharing with a focus on users' privacy.
+-# Copyright © 2012-2014 potager.org <jardiniers@potager.org>
+-#           ©      2014 Rowan Thorpe <rowan@rowanthorpe.com>
+-#
+-# 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/>.
+
+.field
+  %label{ :for => 'ldap_user' } LDAP User:
+  %input.input{ :type => 'text', :id => 'ldap_user', :name => 'ldap_user' }
+.field
+  %label{ :for => 'ldap_password' } Password:
+  %input.input{ :type => 'password', :id => 'ldap_password', :name => 'ldap_password' }