diff --git a/Gemfile b/Gemfile index 633a219..1de8b75 100644 --- a/Gemfile +++ b/Gemfile @@ -44,6 +44,8 @@ group :development do # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring gem 'spring' gem 'spring-watcher-listen', '~> 2.0.0' + gem 'letter_opener' + gem 'letter_opener_web' end group :test do diff --git a/Gemfile.lock b/Gemfile.lock index 266dfba..9dcd176 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -97,6 +97,14 @@ GEM concurrent-ruby (~> 1.0) jbuilder (2.10.1) activesupport (>= 5.0.0) + launchy (2.4.3) + addressable (~> 2.3) + letter_opener (1.7.0) + launchy (~> 2.2) + letter_opener_web (1.3.4) + actionmailer (>= 3.2) + letter_opener (~> 1.0) + railties (>= 3.2) listen (3.2.1) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) @@ -237,6 +245,8 @@ DEPENDENCIES devise_ldap_authenticatable dotenv-rails jbuilder (~> 2.7) + letter_opener + letter_opener_web listen (~> 3.2) net-ldap puma (~> 4.1) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 09705d1..19434cd 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,2 +1,5 @@ class ApplicationController < ActionController::Base + rescue_from DeviseLdapAuthenticatable::LdapException do |exception| + render :text => exception, :status => 500 + end end diff --git a/app/controllers/devise/passwords_controller.rb b/app/controllers/devise/passwords_controller.rb new file mode 100644 index 0000000..adf163e --- /dev/null +++ b/app/controllers/devise/passwords_controller.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +class Devise::PasswordsController < DeviseController + prepend_before_action :require_no_authentication + # Render the #edit only if coming from a reset password email link + append_before_action :assert_reset_token_passed, only: :edit + + # GET /resource/password/new + def new + self.resource = resource_class.new + end + + # POST /resource/password + def create + self.resource = resource_class.send_reset_password_instructions(resource_params) + yield resource if block_given? + + if successfully_sent?(resource) + respond_with({}, location: after_sending_reset_password_instructions_path_for(resource_name)) + else + respond_with(resource) + end + end + + # GET /resource/password/edit?reset_password_token=abcdef + def edit + self.resource = resource_class.new + set_minimum_password_length + resource.reset_password_token = params[:reset_password_token] + end + + # PUT /resource/password + def update + self.resource = resource_class.reset_password_by_token(resource_params) + yield resource if block_given? + + if resource.errors.empty? + resource.unlock_access! if unlockable?(resource) + if Devise.sign_in_after_reset_password + flash_message = resource.active_for_authentication? ? :updated : :updated_not_active + set_flash_message!(:notice, flash_message) + resource.after_ldap_authentication + sign_in(resource_name, resource) + else + set_flash_message!(:notice, :updated_not_active) + end + respond_with resource, location: after_resetting_password_path_for(resource) + else + set_minimum_password_length + respond_with resource + end + end + + protected + def after_resetting_password_path_for(resource) + Devise.sign_in_after_reset_password ? after_sign_in_path_for(resource) : new_session_path(resource_name) + end + + # The path used after sending reset password instructions + def after_sending_reset_password_instructions_path_for(resource_name) + new_session_path(resource_name) if is_navigational_format? + end + + # Check if a reset_password_token is provided in the request + def assert_reset_token_passed + if params[:reset_password_token].blank? + set_flash_message(:alert, :no_token) + redirect_to new_session_path(resource_name) + end + end + + # Check if proper Lockable module methods are present & unlock strategy + # allows to unlock resource on password reset + def unlockable?(resource) + resource.respond_to?(:unlock_access!) && + resource.respond_to?(:unlock_strategy_enabled?) && + resource.unlock_strategy_enabled?(:email) + end + + def translation_scope + 'devise.passwords' + end +end diff --git a/app/controllers/ldap_users_controller.rb b/app/controllers/ldap_users_controller.rb index b7f4f6d..4124b3e 100644 --- a/app/controllers/ldap_users_controller.rb +++ b/app/controllers/ldap_users_controller.rb @@ -16,6 +16,7 @@ class LdapUsersController < ApplicationController uid: e.uid.first, mail: e.try(:mail) ? e.mail.first : nil, admin: e.try(:admin) ? 'admin' : nil + # password: e.userpassword.first } end # ldap_client.get_operation_result @@ -25,12 +26,16 @@ class LdapUsersController < ApplicationController def ldap_client ldap_client ||= Net::LDAP.new host: ENV['LDAP_HOST'], - port: ENV['LDAP_PORT'], - encryption: ENV['LDAP_USE_TLS'] ? :simple_tls : nil, + port: ldap_config['port'], + encryption: ldap_config['ssl'], auth: { method: :simple, - username: Rails.application.credentials.ldap[:username], - password: Rails.application.credentials.ldap[:password] + username: ldap_config['admin_user'], + password: ldap_config['admin_password'] } end + + def ldap_config + ldap_config ||= YAML.load(ERB.new(File.read("#{Rails.root}/config/ldap.yml")).result)[Rails.env] + end end diff --git a/app/controllers/settings_controller.rb b/app/controllers/settings_controller.rb new file mode 100644 index 0000000..b9adfb9 --- /dev/null +++ b/app/controllers/settings_controller.rb @@ -0,0 +1,4 @@ +class SettingsController < ApplicationController + def index + end +end diff --git a/app/controllers/welcome_controller.rb b/app/controllers/welcome_controller.rb index f9b859b..5aaad05 100644 --- a/app/controllers/welcome_controller.rb +++ b/app/controllers/welcome_controller.rb @@ -1,4 +1,7 @@ class WelcomeController < ApplicationController def index + if user_signed_in? + redirect_to settings_path and return + end end end diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb new file mode 100644 index 0000000..ffbedba --- /dev/null +++ b/app/helpers/settings_helper.rb @@ -0,0 +1,2 @@ +module SettingsHelper +end diff --git a/app/models/user.rb b/app/models/user.rb new file mode 100644 index 0000000..2682230 --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,23 @@ +class User < ApplicationRecord + # Include default devise modules. Others available are: + # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable + devise :ldap_authenticatable, + :confirmable, + :registerable, + :recoverable, + :validatable + + def ldap_before_save + self.email = Devise::LDAP::Adapter.get_ldap_param(self.cn, "mail").first + dn = Devise::LDAP::Adapter.get_ldap_param(self.cn, "dn") + self.ou = dn.split(',').select{|e| e[0..1] == "ou"}.first.delete_prefix("ou=") + end + + def reset_password(new_password, new_password_confirmation) + if new_password == new_password_confirmation && ::Devise.ldap_update_password + Devise::LDAP::Adapter.update_password(login_with, new_password) + end + clear_reset_password_token if valid? + save + end +end diff --git a/app/views/devise/confirmations/new.html.erb b/app/views/devise/confirmations/new.html.erb new file mode 100644 index 0000000..b12dd0c --- /dev/null +++ b/app/views/devise/confirmations/new.html.erb @@ -0,0 +1,16 @@ +

Resend confirmation instructions

+ +<%= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| %> + <%= render "devise/shared/error_messages", resource: resource %> + +
+ <%= f.label :email %>
+ <%= f.email_field :email, autofocus: true, autocomplete: "email", value: (resource.pending_reconfirmation? ? resource.unconfirmed_email : resource.email) %> +
+ +
+ <%= f.submit "Resend confirmation instructions" %> +
+<% end %> + +<%= render "devise/shared/links" %> diff --git a/app/views/devise/mailer/confirmation_instructions.html.erb b/app/views/devise/mailer/confirmation_instructions.html.erb new file mode 100644 index 0000000..dc55f64 --- /dev/null +++ b/app/views/devise/mailer/confirmation_instructions.html.erb @@ -0,0 +1,5 @@ +

Welcome <%= @email %>!

+ +

You can confirm your account email through the link below:

+ +

<%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %>

diff --git a/app/views/devise/mailer/email_changed.html.erb b/app/views/devise/mailer/email_changed.html.erb new file mode 100644 index 0000000..32f4ba8 --- /dev/null +++ b/app/views/devise/mailer/email_changed.html.erb @@ -0,0 +1,7 @@ +

Hello <%= @email %>!

+ +<% if @resource.try(:unconfirmed_email?) %> +

We're contacting you to notify you that your email is being changed to <%= @resource.unconfirmed_email %>.

+<% else %> +

We're contacting you to notify you that your email has been changed to <%= @resource.email %>.

+<% end %> diff --git a/app/views/devise/mailer/password_change.html.erb b/app/views/devise/mailer/password_change.html.erb new file mode 100644 index 0000000..b41daf4 --- /dev/null +++ b/app/views/devise/mailer/password_change.html.erb @@ -0,0 +1,3 @@ +

Hello <%= @resource.email %>!

+ +

We're contacting you to notify you that your password has been changed.

diff --git a/app/views/devise/mailer/reset_password_instructions.html.erb b/app/views/devise/mailer/reset_password_instructions.html.erb new file mode 100644 index 0000000..f667dc1 --- /dev/null +++ b/app/views/devise/mailer/reset_password_instructions.html.erb @@ -0,0 +1,8 @@ +

Hello <%= @resource.email %>!

+ +

Someone has requested a link to change your password. You can do this through the link below.

+ +

<%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %>

+ +

If you didn't request this, please ignore this email.

+

Your password won't change until you access the link above and create a new one.

diff --git a/app/views/devise/mailer/unlock_instructions.html.erb b/app/views/devise/mailer/unlock_instructions.html.erb new file mode 100644 index 0000000..41e148b --- /dev/null +++ b/app/views/devise/mailer/unlock_instructions.html.erb @@ -0,0 +1,7 @@ +

Hello <%= @resource.email %>!

+ +

Your account has been locked due to an excessive number of unsuccessful sign in attempts.

+ +

Click the link below to unlock your account:

+ +

<%= link_to 'Unlock my account', unlock_url(@resource, unlock_token: @token) %>

diff --git a/app/views/devise/passwords/edit.html.erb b/app/views/devise/passwords/edit.html.erb new file mode 100644 index 0000000..5fbb9ff --- /dev/null +++ b/app/views/devise/passwords/edit.html.erb @@ -0,0 +1,25 @@ +

Change your password

+ +<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f| %> + <%= render "devise/shared/error_messages", resource: resource %> + <%= f.hidden_field :reset_password_token %> + +
+ <%= f.label :password, "New password" %>
+ <% if @minimum_password_length %> + (<%= @minimum_password_length %> characters minimum)
+ <% end %> + <%= f.password_field :password, autofocus: true, autocomplete: "new-password" %> +
+ +
+ <%= f.label :password_confirmation, "Confirm new password" %>
+ <%= f.password_field :password_confirmation, autocomplete: "new-password" %> +
+ +
+ <%= f.submit "Change my password" %> +
+<% end %> + +<%= render "devise/shared/links" %> diff --git a/app/views/devise/passwords/new.html.erb b/app/views/devise/passwords/new.html.erb new file mode 100644 index 0000000..9b486b8 --- /dev/null +++ b/app/views/devise/passwords/new.html.erb @@ -0,0 +1,16 @@ +

Forgot your password?

+ +<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| %> + <%= render "devise/shared/error_messages", resource: resource %> + +
+ <%= f.label :email %>
+ <%= f.email_field :email, autofocus: true, autocomplete: "email" %> +
+ +
+ <%= f.submit "Send me reset password instructions" %> +
+<% end %> + +<%= render "devise/shared/links" %> diff --git a/app/views/devise/registrations/edit.html.erb b/app/views/devise/registrations/edit.html.erb new file mode 100644 index 0000000..38d95b8 --- /dev/null +++ b/app/views/devise/registrations/edit.html.erb @@ -0,0 +1,43 @@ +

Edit <%= resource_name.to_s.humanize %>

+ +<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %> + <%= render "devise/shared/error_messages", resource: resource %> + +
+ <%= f.label :email %>
+ <%= f.email_field :email, autofocus: true, autocomplete: "email" %> +
+ + <% if devise_mapping.confirmable? && resource.pending_reconfirmation? %> +
Currently waiting confirmation for: <%= resource.unconfirmed_email %>
+ <% end %> + +
+ <%= f.label :password %> (leave blank if you don't want to change it)
+ <%= f.password_field :password, autocomplete: "new-password" %> + <% if @minimum_password_length %> +
+ <%= @minimum_password_length %> characters minimum + <% end %> +
+ +
+ <%= f.label :password_confirmation %>
+ <%= f.password_field :password_confirmation, autocomplete: "new-password" %> +
+ +
+ <%= f.label :current_password %> (we need your current password to confirm your changes)
+ <%= f.password_field :current_password, autocomplete: "current-password" %> +
+ +
+ <%= f.submit "Update" %> +
+<% end %> + +

Cancel my account

+ +

Unhappy? <%= button_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?" }, method: :delete %>

+ +<%= link_to "Back", :back %> diff --git a/app/views/devise/registrations/new.html.erb b/app/views/devise/registrations/new.html.erb new file mode 100644 index 0000000..d655b66 --- /dev/null +++ b/app/views/devise/registrations/new.html.erb @@ -0,0 +1,29 @@ +

Sign up

+ +<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %> + <%= render "devise/shared/error_messages", resource: resource %> + +
+ <%= f.label :email %>
+ <%= f.email_field :email, autofocus: true, autocomplete: "email" %> +
+ +
+ <%= f.label :password %> + <% if @minimum_password_length %> + (<%= @minimum_password_length %> characters minimum) + <% end %>
+ <%= f.password_field :password, autocomplete: "new-password" %> +
+ +
+ <%= f.label :password_confirmation %>
+ <%= f.password_field :password_confirmation, autocomplete: "new-password" %> +
+ +
+ <%= f.submit "Sign up" %> +
+<% end %> + +<%= render "devise/shared/links" %> diff --git a/app/views/devise/sessions/new.html.erb b/app/views/devise/sessions/new.html.erb new file mode 100644 index 0000000..d3a87b0 --- /dev/null +++ b/app/views/devise/sessions/new.html.erb @@ -0,0 +1,21 @@ +

Log in

+ +<%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %> + <%= render "devise/shared/error_messages", resource: resource %> + +
+ <%= f.label :cn, 'User' %>
+ <%= f.text_field :cn, autofocus: true, autocomplete: "username" %> @ kosmos.org +
+ +
+ <%= f.label :password %>
+ <%= f.password_field :password, autocomplete: "current-password" %> +
+ +
+ <%= f.submit "Log in" %> +
+<% end %> + +<%= render "devise/shared/links" %> diff --git a/app/views/devise/shared/_error_messages.html.erb b/app/views/devise/shared/_error_messages.html.erb new file mode 100644 index 0000000..ba7ab88 --- /dev/null +++ b/app/views/devise/shared/_error_messages.html.erb @@ -0,0 +1,15 @@ +<% if resource.errors.any? %> +
+

+ <%= I18n.t("errors.messages.not_saved", + count: resource.errors.count, + resource: resource.class.model_name.human.downcase) + %> +

+ +
+<% end %> diff --git a/app/views/devise/shared/_links.html.erb b/app/views/devise/shared/_links.html.erb new file mode 100644 index 0000000..084af70 --- /dev/null +++ b/app/views/devise/shared/_links.html.erb @@ -0,0 +1,25 @@ +<%- if controller_name != 'sessions' %> + <%= link_to "Log in", new_session_path(resource_name) %>
+<% end %> + +<%- if devise_mapping.registerable? && controller_name != 'registrations' %> + <%= link_to "Sign up", new_registration_path(resource_name) %>
+<% end %> + +<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %> + <%= link_to "Forgot your password?", new_password_path(resource_name) %>
+<% end %> + +<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %> + <%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %>
+<% end %> + +<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %> + <%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %>
+<% end %> + +<%- if devise_mapping.omniauthable? %> + <%- resource_class.omniauth_providers.each do |provider| %> + <%= link_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider) %>
+ <% end %> +<% end %> diff --git a/app/views/devise/unlocks/new.html.erb b/app/views/devise/unlocks/new.html.erb new file mode 100644 index 0000000..ffc34de --- /dev/null +++ b/app/views/devise/unlocks/new.html.erb @@ -0,0 +1,16 @@ +

Resend unlock instructions

+ +<%= form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f| %> + <%= render "devise/shared/error_messages", resource: resource %> + +
+ <%= f.label :email %>
+ <%= f.email_field :email, autofocus: true, autocomplete: "email" %> +
+ +
+ <%= f.submit "Resend unlock instructions" %> +
+<% end %> + +<%= render "devise/shared/links" %> diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 6ca95fa..9b8f091 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -10,6 +10,17 @@ +

Kosmos Accounts

+ <% if user_signed_in? %> +

+ Signed in as <%= current_user.cn %>@kosmos.org. + <%= link_to "Log out", destroy_user_session_path, method: :delete %> +

+ <% flash.each do |type, msg| %> +

<%= msg %>

+ <% end %> + <% end %> + <%= yield %> diff --git a/app/views/ldap_users/index.html.erb b/app/views/ldap_users/index.html.erb index 30ec77d..452a877 100644 --- a/app/views/ldap_users/index.html.erb +++ b/app/views/ldap_users/index.html.erb @@ -1,4 +1,4 @@ -

LDAP users

+

LDAP users