diff --git a/Gemfile b/Gemfile index 07cb31f..93ef9bf 100644 --- a/Gemfile +++ b/Gemfile @@ -1,12 +1,13 @@ source 'https://rubygems.org' -ruby '1.9.3' +ruby '2.4.0' -gem 'sinatra','~> 1.4.2' -gem 'redis','~> 3.0.4' -gem 'hiredis', '~> 0.4.5' -gem 'json', '~> 1.7.7' -gem 'ruby-hmac', '~> 0.4.0' +gem 'sinatra' +gem 'redis' +gem 'hiredis' +gem 'json', '~> 1.8.1' +gem 'ruby-hmac' +gem 'net-ldap' group :development, :test do gem 'rake' diff --git a/Gemfile.lock b/Gemfile.lock index 65b455b..196711b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,7 +3,8 @@ GEM specs: diff-lcs (1.2.5) hiredis (0.4.5) - json (1.7.7) + json (1.8.6) + net-ldap (0.16.1) rack (1.5.2) rack-protection (1.5.1) rack @@ -30,11 +31,18 @@ PLATFORMS ruby DEPENDENCIES - hiredis (~> 0.4.5) - json (~> 1.7.7) + hiredis + json (~> 1.8.1) + net-ldap rack-test rake - redis (~> 3.0.4) + redis rspec - ruby-hmac (~> 0.4.0) - sinatra (~> 1.4.2) + ruby-hmac + sinatra + +RUBY VERSION + ruby 2.4.0p0 + +BUNDLED WITH + 1.14.6 diff --git a/README.md b/README.md index 4db4531..ab4e3ab 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,15 @@ The representation is a simple Redis key in the form: `auth:` -> User ID +LDAP Authentication +--- + +By configuring in `app_config.rb` how to connect with an ldap server, +username/password are checked against that directory. +registration is blocked as well the forget password link. +Users are still stored the same way and are created the first time a user logged succesful through ldap. + + News --- diff --git a/app.rb b/app.rb index cd37114..3fdbf14 100644 --- a/app.rb +++ b/app.rb @@ -40,6 +40,7 @@ require_relative 'about' require 'openssl' if UseOpenSSL require 'uri' +require 'net/ldap' Version = "0.11.0" @@ -222,13 +223,14 @@ def setup_redis(uri=RedisURL) H.inputtext(:id => "username", :name => "username")+ H.label(:for => "password") {"password"}+ H.inputpass(:id => "password", :name => "password")+H.br+ - H.checkbox(:name => "register", :value => "1")+ - "create account"+H.br+ + ((!UseLDAP)? H.checkbox(:name => "register", :value => "1") + "create account": "")+ + H.br+ H.submit(:name => "do_login", :value => "Login") } }+ H.div(:id => "errormsg"){}+ - H.a(:href=>"/reset-password") {"reset password"}+ + ((!UseLDAP)? H.a(:href=>"/reset-password") {"reset password"} : "")+ + H.script() {' $(function() { $("form[name=f]").submit(login); @@ -661,7 +663,7 @@ def render_comment_subthread(comment,sep="") end end -get '/api/login' do +post '/api/login' do content_type 'application/json' if (!check_params "username","password") return { @@ -687,6 +689,12 @@ def render_comment_subthread(comment,sep="") get '/api/reset-password' do content_type 'application/json' + if(UseLDAP) + return { + :status => "err", + :error => "Cannot reset password when using LDAP as auth" + }.to_json + end if (!check_params "username","email") return { :status => "err", @@ -728,6 +736,12 @@ def render_comment_subthread(comment,sep="") post '/api/create_account' do content_type 'application/json' + if(UseLDAP) + return { + :status => "err", + :error => "Cannot create an account when using LDAP as auth" + }.to_json + end if (!check_params "username","password") return { :status => "err", @@ -1054,7 +1068,7 @@ def application_header "logout" } else - H.a(:href => "/login") {"login / register"} + H.a(:href => "/login") {(UseLDAP)? "login": "login / register"} end } menu_mobile = H.a(:href => "#", :id => "link-menu-mobile"){"<~>"} @@ -1133,6 +1147,15 @@ def application_footer # # Return value: none, the function works by side effect. def auth_user(auth) + remote_user = request.env[HttpAuthenticationHeader] + if remote_user + user = get_user_by_username(remote_user) + if user + auth = user['auth'] + else + auth,apisecret,errmsg = create_user(remote_user,get_rand) + end + end return if !auth id = $r.get("auth:#{auth}") return if !id @@ -1259,13 +1282,49 @@ def get_user_by_username(username) get_user_by_id(id) end +# check given credentials aaginst the configured ldap server +# if the user isn't already in redis it's created with given password +def check_ldap_credentials(username, password) + ldap = Net::LDAP.new + ldap.host = LDAPHost + ldap.auth LDAPAdminUserDn, LDAPAdminUserPassword + begin + result = ldap.bind_as(:base => LDAPAdminUserBase, + :filter =>"(uid=#{username})", + :password => password) + rescue Net::LDAP::Error + # catch any connection error on ldap + return nil + end + + return nil if !result + + username = result.first.uid.first + user = get_user_by_username(username) + + if !user + auth,apisecret,errmsg = create_user(username,password) + if errmsg + puts errmsg + return nil + end + return auth,apisecret + end + + [user['auth'], user['apisecret']] +end + # Check if the username/password pair identifies an user. # If so the auth token and form secret are returned, otherwise nil is returned. def check_user_credentials(username,password) - user = get_user_by_username(username) - return nil if !user - hp = hash_password(password,user['salt']) - (user['password'] == hp) ? [user['auth'],user['apisecret']] : nil + if UseLDAP + check_ldap_credentials(username, password) + else + user = get_user_by_username(username) + return nil if !user + hp = hash_password(password,user['salt']) + (user['password'] == hp) ? [user['auth'],user['apisecret']] : nil + end end # Has the user submitted a news story in the last `NewsSubmissionBreak` seconds? diff --git a/app_config.rb b/app_config.rb index 7c0babb..6ed6cde 100644 --- a/app_config.rb +++ b/app_config.rb @@ -2,9 +2,23 @@ SiteName = "Lamer News" SiteUrl = "http://lamernews.com" SiteDescription = "Programming News" +GoogleAnalytics = "XXX" # Redis config -RedisURL = "redis://127.0.0.1:10000" +RedisURL = "redis://127.0.0.1:6379" + +# LDAP Config +# Authentication with LDAP backend +# registration is blocked +UseLDAP = false +LDAPHost = "ldap.domain.com" +LDAPAdminUserDn = "cn=JonDoeAdmin,ou=people,dc=domain,dc=com" +LDAPAdminUserBase = "ou=people,dc=domain,dc=com" +LDAPAdminUserPassword = "XXX" + +# Remote authentication +# Header with username to login/register coming from a remote Proxyed auth system, like mod_ldap on apache +HttpAuthenticationHeader = "HTTP_REMOTE_USER" # Security PBKDF2Iterations = 1000 # Set this to 5000 to improve security. But it is slow. diff --git a/page.rb b/page.rb index 31c9e84..9f53015 100644 --- a/page.rb +++ b/page.rb @@ -138,6 +138,15 @@ def page() self.body { self.div(:class => "container") { _header+H.div(:id => "content"){yield}+_footer + }+ + self.script { + "(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ + (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), + m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) + })(window,document,'script','https://www.google-analytics.com/analytics.js','ga'); + + ga('create', '#{GoogleAnalytics}', 'auto'); + ga('send', 'pageview');" } } } diff --git a/public/js/app.js b/public/js/app.js index eb92b10..7e30046 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -5,7 +5,7 @@ function login() { }; var register = $("input[name=register]").attr("checked"); $.ajax({ - type: register ? "POST" : "GET", + type: "POST" , url: register ? "/api/create_account" : "/api/login", data: data, success: function(r) {