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

Commit 7c534db

Browse files
committed
Introducing LDAP support
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 #150 Signed-off-by: Miquel Sabaté Solà <[email protected]>
2 parents 7718aec + 10f5ae7 commit 7c534db

File tree

21 files changed

+420
-55
lines changed

21 files changed

+420
-55
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
## Upcoming Version
22

3+
- Introduced LDAP support. See PR [#301](https://github.com/SUSE/Portus/pull/301).
34
- Users will not be able to create namespaces without a Registry currently
45
existing.
56
- PhantomJS is now being used in the testing infrastructure. See the following

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ gem "mysql2"
1818
gem "search_cop"
1919
gem "kaminari"
2020
gem "crono"
21+
gem "net-ldap"
2122

2223
# Assets group.
2324
#

Gemfile.lock

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ GEM
146146
multi_json (1.11.1)
147147
multipart-post (2.0.0)
148148
mysql2 (0.3.18)
149+
net-ldap (0.11)
149150
nokogiri (1.6.6.2)
150151
mini_portile (~> 0.6.0)
151152
octokit (2.0.0)
@@ -339,6 +340,7 @@ DEPENDENCIES
339340
jwt
340341
kaminari
341342
mysql2
343+
net-ldap
342344
poltergeist
343345
pry-rails
344346
public_activity

app/controllers/application_controller.rb

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
class ApplicationController < ActionController::Base
22
before_action :check_requirements
33
before_action :authenticate_user!
4+
before_action :force_update_profile!
45
protect_from_forgery with: :exception
56

67
include Pundit
78
rescue_from Pundit::NotAuthorizedError, with: :deny_access
89

910
respond_to :html
1011

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

1520
def after_sign_out_path_for(_resource)
@@ -39,6 +44,16 @@ def fixes
3944
[fix_secrets, fix_ssl]
4045
end
4146

47+
# Redirect users to their profile page if they haven't set up their email
48+
# account (this happens when signing up through LDAP suppor).
49+
def force_update_profile!
50+
return unless current_user && current_user.email.empty?
51+
52+
controller = params[:controller]
53+
return if controller == "auth/registrations" || controller == "auth/sessions"
54+
redirect_to edit_user_registration_url
55+
end
56+
4257
def deny_access
4358
render text: "Access Denied", status: :unauthorized
4459
end

app/models/user.rb

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@ class User < ActiveRecord::Base
22
devise :database_authenticatable, :registerable,
33
:recoverable, :rememberable, :trackable, :validatable, authentication_keys: [:username]
44

5+
USERNAME_CHARS = "a-z0-9"
6+
USERNAME_FORMAT = /\A[#{USERNAME_CHARS}]{4,30}\Z/
7+
58
validates :username, presence: true, uniqueness: true,
6-
format: { with: /\A[a-z0-9]{4,30}\Z/,
7-
message: 'Accepted format: "\A[a-z0-9]{4,30}\Z"' }
9+
format: { with: USERNAME_FORMAT,
10+
message: "Only alphanumeric characters are allowed" }
811

912
validate :private_namespace_available, on: :create
1013

@@ -16,6 +19,12 @@ class User < ActiveRecord::Base
1619
scope :enabled, -> { not_portus.where enabled: true }
1720
scope :admins, -> { not_portus.where enabled: true, admin: true }
1821

22+
# Special method used by Devise to require an email on signup. This is always
23+
# true except for LDAP.
24+
def email_required?
25+
!(Portus::LDAP.enabled? && !ldap_name.empty?)
26+
end
27+
1928
def private_namespace_available
2029
return unless Namespace.exists?(name: username)
2130
errors.add(:username, "cannot be used as name for private namespace")

app/views/devise/registrations/edit.html.slim

Lines changed: 43 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -7,48 +7,55 @@
77
' Public Profile
88
.panel-body
99
= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put, class: 'profile' }) do |f|
10-
.form-group
10+
- if current_user.email.empty?
11+
p
12+
| Your profile is not complete. Please, submit an email to be used in this Portus instance.
13+
.form-group class=(current_user.email.empty? ? "has-error" : "")
1114
.field
12-
= f.label :email
13-
= f.text_field(:email, class: 'form-control', required: true)
15+
- if current_user.email.empty?
16+
= f.label :email, "Email", class: "control-label", title: "This profile is not complete. You need to provide an email first"
17+
- else
18+
= f.label :email
19+
= f.text_field(:email, class: 'form-control', required: true, autofocus: true)
1420
.form-group
1521
.actions
1622
= f.submit('Update', class: 'btn btn-primary', disabled: true)
1723

18-
.panel.panel-default
19-
.panel-heading
20-
h5
21-
' Change Password
22-
.panel-body
23-
= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put, class: 'password' }) do |f|
24-
- if devise_mapping.confirmable? && resource.pending_reconfirmation?
25-
div
26-
Currently waiting confirmation for: #{resource.unconfirmed_email}
27-
.form-group
28-
.field
29-
= f.label :current_password, class: 'control-label'
30-
= f.password_field :current_password, autocomplete: 'off', class: 'form-control'
31-
br
32-
.field
33-
= f.label :password, class: 'control-label'
34-
= f.password_field :password, autocomplete: 'off', class: 'form-control'
35-
br
36-
.field
37-
= f.label :password_confirmation, class: 'control-label'
38-
= f.password_field :password_confirmation, autocomplete: 'off', class: 'form-control'
39-
.form-group
40-
.actions
41-
= f.submit('Change', class: 'btn btn-primary', disabled: true)
42-
43-
- unless current_user.admin? && @admin_count == 1
24+
- unless current_user.email.empty?
4425
.panel.panel-default
4526
.panel-heading
4627
h5
47-
' Disable account
28+
' Change Password
4829
.panel-body
49-
= form_tag(toggle_enabled_path(current_user), method: :put, remote: true, id: 'disable-form') do
50-
.form-group
51-
p
52-
| By disabling the account, you won't be able to access Portus with it, and
53-
any affiliations with any team will be lost.
54-
= submit_tag('Disable', class: 'btn btn-primary btn-danger')
30+
= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put, class: 'password' }) do |f|
31+
- if devise_mapping.confirmable? && resource.pending_reconfirmation?
32+
div
33+
Currently waiting confirmation for: #{resource.unconfirmed_email}
34+
.form-group
35+
.field
36+
= f.label :current_password, class: 'control-label'
37+
= f.password_field :current_password, autocomplete: 'off', class: 'form-control'
38+
br
39+
.field
40+
= f.label :password, class: 'control-label'
41+
= f.password_field :password, autocomplete: 'off', class: 'form-control'
42+
br
43+
.field
44+
= f.label :password_confirmation, class: 'control-label'
45+
= f.password_field :password_confirmation, autocomplete: 'off', class: 'form-control'
46+
.form-group
47+
.actions
48+
= f.submit('Change', class: 'btn btn-primary', disabled: true)
49+
50+
- unless current_user.admin? && @admin_count == 1
51+
.panel.panel-default
52+
.panel-heading
53+
h5
54+
' Disable account
55+
.panel-body
56+
= form_tag(toggle_enabled_path(current_user), method: :put, remote: true, id: 'disable-form') do
57+
.form-group
58+
p
59+
| By disabling the account, you won't be able to access Portus with it, and
60+
any affiliations with any team will be lost.
61+
= submit_tag('Disable', class: 'btn btn-primary btn-danger')

app/views/shared/_header.html.slim

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
i.fa.fa-search
1414
.username-logout
1515
.hidden-xs
16-
- if APP_CONFIG['gravatar']
16+
- if APP_CONFIG.enabled?("gravatar")
1717
= gravatar_image_tag(current_user.email)
1818
- else
1919
i.fa.fa-user.fa-1x

config/config.yml

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,14 @@
22
# application. In order to change them, write your own config-local.yml file
33
# (it will be ignored by git).
44

5-
settings:
6-
gravatar: true
5+
gravatar:
6+
enabled: true
7+
8+
ldap:
9+
enabled: false
10+
11+
hostname: "ldap_hostname"
12+
port: 389
13+
14+
# The base where users are located (e.g. "ou=users,dc=example,dc=com").
15+
base: ""

config/initializers/config.rb

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,34 @@
11

2+
# TODO: (mssola) move this into its own file in the `lib` directory.
3+
# TODO: (mssola) take advantage of YAML syntax for inheriting values. This way
4+
# we could define different values for different environments (useful for
5+
# testing).
6+
27
config = File.join(Rails.root, "config", "config.yml")
38
local = File.join(Rails.root, "config", "config-local.yml")
49

5-
app_config = YAML.load_file(config)["settings"] || {}
10+
app_config = YAML.load_file(config) || {}
611

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

1419
app_config = app_config.merge(local_config)
1520
end
1621

22+
class << app_config
23+
# The `enabled?` method is a convenient method that checks whether a specific
24+
# feature is enabled or not. This method takes advantage of the convention
25+
# that each feature has the "enabled" key inside of it. If this key exists in
26+
# the checked feature, and it's set to true, then this method will return
27+
# true. It returns false otherwise.
28+
def enabled?(feature)
29+
return false if !self[feature] || self[feature].empty?
30+
self[feature]["enabled"].eql?(true)
31+
end
32+
end
33+
1734
APP_CONFIG = app_config

config/initializers/devise.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,11 @@
243243
# manager.intercept_401 = false
244244
# manager.default_strategies(scope: :user).unshift :some_external_strategy
245245
# end
246+
if Portus::LDAP.enabled? && !Rails.env.test?
247+
config.warden do |manager|
248+
manager.default_strategies(scope: :user).unshift :ldap_authenticatable
249+
end
250+
end
246251

247252
# ==> Mountable engine configurations
248253
# When using Devise inside an engine, let's call it `MyEngine`, and this engine

0 commit comments

Comments
 (0)