Skip to content

Commit

Permalink
Introducing LDAP support
Browse files Browse the repository at this point in the history
This is an introduction to LDAP support. There are some questions that need to
be addressed, but at least the basic structure is in place now.

Fixes SUSE#150

Signed-off-by: Miquel Sabaté Solà <msabate@suse.com>
  • Loading branch information
mssola committed Sep 4, 2015
1 parent 7309057 commit 16da5e3
Show file tree
Hide file tree
Showing 27 changed files with 606 additions and 66 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## Upcoming Version

- Introduced LDAP support. See PR [#301](https://github.com/SUSE/Portus/pull/301).
- Users will not be able to create namespaces without a Registry currently
existing.
- PhantomJS is now being used in the testing infrastructure. See the following
Expand Down
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ gem "mysql2"
gem "search_cop"
gem "kaminari"
gem "crono"
gem "net-ldap"

# Assets group.
#
Expand Down
2 changes: 2 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ GEM
multi_json (1.11.1)
multipart-post (2.0.0)
mysql2 (0.3.18)
net-ldap (0.11)
nokogiri (1.6.6.2)
mini_portile (~> 0.6.0)
octokit (2.0.0)
Expand Down Expand Up @@ -339,6 +340,7 @@ DEPENDENCIES
jwt
kaminari
mysql2
net-ldap
poltergeist
pry-rails
public_activity
Expand Down
17 changes: 16 additions & 1 deletion app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,20 @@ class ApplicationController < ActionController::Base
before_action :check_requirements
helper_method :fixes
before_action :authenticate_user!
before_action :force_update_profile!
protect_from_forgery with: :exception

include Pundit
rescue_from Pundit::NotAuthorizedError, with: :deny_access

respond_to :html

# Two things can happen when signing in.
# 1. The current user has no email: this happens on LDAP registration. In
# this case, the user will be asked to submit an email.
# 2. Everything is fine, go to the root url.
def after_sign_in_path_for(_resource)
root_url
current_user.email.empty? ? edit_user_registration_url : root_url
end

def after_sign_out_path_for(_resource)
Expand Down Expand Up @@ -39,6 +44,16 @@ def check_requirements
redirect_to "/errors/500"
end

# Redirect users to their profile page if they haven't set up their email
# account (this happens when signing up through LDAP suppor).
def force_update_profile!
return unless current_user && current_user.email.empty?

controller = params[:controller]
return if controller == "auth/registrations" || controller == "auth/sessions"
redirect_to edit_user_registration_url
end

def deny_access
render text: "Access Denied", status: :unauthorized
end
Expand Down
6 changes: 6 additions & 0 deletions app/controllers/auth/registrations_controller.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
class Auth::RegistrationsController < Devise::RegistrationsController
layout "authentication", except: :edit

before_action :check_ldap, only: [:new, :create]
before_action :check_admin, only: [:new, :create]
before_action :configure_sign_up_params, only: [:create]
before_action :authenticate_user!, only: [:disable]
Expand Down Expand Up @@ -86,6 +87,11 @@ def configure_sign_up_params

protected

# Redirect to the login page if LDAP is enabled.
def check_ldap
redirect_to new_user_session_path if Portus::LDAP.enabled?
end

# Returns true if the contents of the `params` hash contains the needed keys
# to update the password of the user.
def password_update?
Expand Down
7 changes: 4 additions & 3 deletions app/controllers/auth/sessions_controller.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
class Auth::SessionsController < Devise::SessionsController
layout "authentication"

# Re-implementing. The logic is: if there is already a user that can log in,
# work as usual. Otherwise, redirect always to the signup page.
# Re-implementing. The logic is: if there is already a user that can log in
# or LDAP support is enabled, work as usual. Otherwise, redirect always to
# the signup page.
def new
if User.not_portus.any?
if User.not_portus.any? || Portus::LDAP.enabled?
super
else
# For some reason if we get here from the root path, we'll get a flashy
Expand Down
16 changes: 13 additions & 3 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ class User < ActiveRecord::Base
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable, authentication_keys: [:username]

validates :username, presence: true, uniqueness: true,
format: { with: /\A[a-z0-9]{4,30}\Z/,
message: 'Accepted format: "\A[a-z0-9]{4,30}\Z"' }
USERNAME_CHARS = "a-z0-9"
USERNAME_FORMAT = /\A[#{USERNAME_CHARS}]{4,30}\Z/

validates :username, presence: true, uniqueness: true,
format: {
with: USERNAME_FORMAT,
message: "Only alphanumeric characters are allowed. Minimum 4 characters, maximum 30."
}
validate :private_namespace_available, on: :create

has_many :team_users
Expand All @@ -16,6 +20,12 @@ class User < ActiveRecord::Base
scope :enabled, -> { not_portus.where enabled: true }
scope :admins, -> { not_portus.where enabled: true, admin: true }

# Special method used by Devise to require an email on signup. This is always
# true except for LDAP.
def email_required?
!(Portus::LDAP.enabled? && !ldap_name.nil?)
end

def private_namespace_available
return unless Namespace.exists?(name: username)
errors.add(:username, "cannot be used as name for private namespace")
Expand Down
86 changes: 47 additions & 39 deletions app/views/devise/registrations/edit.html.slim
Original file line number Diff line number Diff line change
Expand Up @@ -7,48 +7,56 @@
' Public Profile
.panel-body
= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put, class: 'profile' }) do |f|
.form-group
- if current_user.email.empty?
p
| Your profile is not complete. Please, submit an email to be used in this Portus instance.
.form-group class=(current_user.email.empty? ? "has-error" : "")
.field
= f.label :email
= f.text_field(:email, class: 'form-control', required: true)
- if current_user.email.empty?
= f.label :email, "Email", class: "control-label", title: "This profile is not complete. You need to provide an email first"
- else
= f.label :email
= f.text_field(:email, class: 'form-control', required: true, autofocus: true)
.form-group
.actions
= f.submit('Update', class: 'btn btn-primary', disabled: true)

.panel.panel-default
.panel-heading
h5
' Change Password
.panel-body
= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put, class: 'password' }) do |f|
- if devise_mapping.confirmable? && resource.pending_reconfirmation?
div
Currently waiting confirmation for: #{resource.unconfirmed_email}
.form-group
.field
= f.label :current_password, class: 'control-label'
= f.password_field :current_password, autocomplete: 'off', class: 'form-control'
br
.field
= f.label :password, class: 'control-label'
= f.password_field :password, autocomplete: 'off', class: 'form-control'
br
.field
= f.label :password_confirmation, class: 'control-label'
= f.password_field :password_confirmation, autocomplete: 'off', class: 'form-control'
.form-group
.actions
= f.submit('Change', class: 'btn btn-primary', disabled: true)

- unless current_user.admin? && @admin_count == 1
.panel.panel-default
.panel-heading
h5
' Disable account
.panel-body
= form_tag(toggle_enabled_path(current_user), method: :put, remote: true, id: 'disable-form') do
- unless current_user.email.empty?
- if current_user.ldap_name.nil?
.panel.panel-default
.panel-heading
h5
' Change Password
.panel-body
= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put, class: 'password' }) do |f|
- if devise_mapping.confirmable? && resource.pending_reconfirmation?
div
Currently waiting confirmation for: #{resource.unconfirmed_email}
.form-group
.field
= f.label :current_password, class: 'control-label'
= f.password_field :current_password, autocomplete: 'off', class: 'form-control'
br
.field
= f.label :password, class: 'control-label'
= f.password_field :password, autocomplete: 'off', class: 'form-control'
br
.field
= f.label :password_confirmation, class: 'control-label'
= f.password_field :password_confirmation, autocomplete: 'off', class: 'form-control'
.form-group
p
| By disabling the account, you won't be able to access Portus with it, and
any affiliations with any team will be lost.
= submit_tag('Disable', class: 'btn btn-primary btn-danger')
.actions
= f.submit('Change', class: 'btn btn-primary', disabled: true)

- unless current_user.admin? && @admin_count == 1
.panel.panel-default
.panel-heading
h5
' Disable account
.panel-body
= form_tag(toggle_enabled_path(current_user), method: :put, remote: true, id: 'disable-form') do
.form-group
p
| By disabling the account, you won't be able to access Portus with it, and
any affiliations with any team will be lost.
= submit_tag('Disable', class: 'btn btn-primary btn-danger')
12 changes: 10 additions & 2 deletions app/views/devise/sessions/new.html.slim
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ section.row-0
= f.text_field :username, class: 'input form-control input-lg first', placeholder: 'Username', autofocus: true, required: true
= f.password_field :password, class: 'input form-control input-lg last', placeholder: 'Password', autocomplete: false, required: true
= f.button class: 'classbutton btn btn-primary btn-block btn-lg' do
i.fa.fa-check Login
- if Portus::LDAP.enabled?
i.fa.fa-check LDAP Login
- else
i.fa.fa-check Login

.text-center = link_to 'or, Create a new account', new_user_registration_url
- if Portus::LDAP.enabled?
- unless User.not_portus.any?
p
| <b>NOTE</b>: The first user to be created will have admin permissions !
- else
.text-center = link_to 'or, Create a new account', new_user_registration_url
2 changes: 1 addition & 1 deletion app/views/shared/_header.html.slim
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
i.fa.fa-search
.username-logout
.hidden-xs
- if APP_CONFIG['gravatar']
- if APP_CONFIG.enabled?("gravatar")
= gravatar_image_tag(current_user.email)
- else
i.fa.fa-user.fa-1x
Expand Down
13 changes: 11 additions & 2 deletions config/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,14 @@
# application. In order to change them, write your own config-local.yml file
# (it will be ignored by git).

settings:
gravatar: true
gravatar:
enabled: true

ldap:
enabled: false

hostname: "ldap_hostname"
port: 389

# The base where users are located (e.g. "ou=users,dc=example,dc=com").
base: ""
21 changes: 19 additions & 2 deletions config/initializers/config.rb
Original file line number Diff line number Diff line change
@@ -1,17 +1,34 @@

# TODO: (mssola) move this into its own file in the `lib` directory.
# TODO: (mssola) take advantage of YAML syntax for inheriting values. This way
# we could define different values for different environments (useful for
# testing).

config = File.join(Rails.root, "config", "config.yml")
local = File.join(Rails.root, "config", "config-local.yml")

app_config = YAML.load_file(config)["settings"] || {}
app_config = YAML.load_file(config) || {}

if File.exist?(local)
# Check for bad user input in the local config.yml file.
local_config = YAML.load_file(local)["settings"]
local_config = YAML.load_file(local)
if local_config.nil? || !local_config.is_a?(Hash)
raise StandardError, "Wrong format for the config-local file!"
end

app_config = app_config.merge(local_config)
end

class << app_config
# The `enabled?` method is a convenient method that checks whether a specific
# feature is enabled or not. This method takes advantage of the convention
# that each feature has the "enabled" key inside of it. If this key exists in
# the checked feature, and it's set to true, then this method will return
# true. It returns false otherwise.
def enabled?(feature)
return false if !self[feature] || self[feature].empty?
self[feature]["enabled"].eql?(true)
end
end

APP_CONFIG = app_config
6 changes: 6 additions & 0 deletions config/initializers/devise.rb
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,12 @@
# manager.intercept_401 = false
# manager.default_strategies(scope: :user).unshift :some_external_strategy
# end
if Portus::LDAP.enabled? && !Rails.env.test?
config.warden do |manager|
# Let's put LDAP in front of every other strategy.
manager.default_strategies(scope: :user).unshift :ldap_authenticatable
end
end

# ==> Mountable engine configurations
# When using Devise inside an engine, let's call it `MyEngine`, and this engine
Expand Down
2 changes: 2 additions & 0 deletions config/initializers/ldap_authenticatable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
require "portus/ldap"
Warden::Strategies.add(:ldap_authenticatable, Portus::LDAP)
2 changes: 2 additions & 0 deletions config/locales/devise.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ en:
timeout: "Your session expired. Please sign in again to continue."
unauthenticated: "You need to sign in or sign up before continuing."
unconfirmed: "You have to confirm your email address before continuing."
user:
invalid_login: "Invalid %{authentication_keys} or password."
mailer:
confirmation_instructions:
subject: "Confirmation instructions"
Expand Down
6 changes: 6 additions & 0 deletions db/migrate/20150831131727_add_ldap_name_to_users.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class AddLdapNameToUsers < ActiveRecord::Migration
def change
add_column :users, :ldap_name, :string, default: nil
add_index :users, :ldap_name, unique: true
end
end
4 changes: 3 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 20150805130722) do
ActiveRecord::Schema.define(version: 20150831131727) do

create_table "activities", force: :cascade do |t|
t.integer "trackable_id", limit: 4
Expand Down Expand Up @@ -136,9 +136,11 @@
t.datetime "updated_at"
t.boolean "admin", limit: 1, default: false
t.boolean "enabled", limit: 1, default: true
t.string "ldap_name", limit: 255
end

add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree
add_index "users", ["ldap_name"], name: "index_users_on_ldap_name", unique: true, using: :btree
add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree
add_index "users", ["username"], name: "index_users_on_username", unique: true, using: :btree

Expand Down
Empty file removed lib/assets/.keep
Empty file.
Loading

0 comments on commit 16da5e3

Please sign in to comment.