Skip to content
This repository has been archived by the owner on Apr 17, 2023. It is now read-only.

Commit

Permalink
Merge pull request #301 from mssola/ldap
Browse files Browse the repository at this point in the history
Introducing LDAP support
  • Loading branch information
mssola committed Sep 4, 2015
2 parents 7309057 + 16da5e3 commit 2da3f91
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 2da3f91

Please sign in to comment.