diff --git a/Gemfile b/Gemfile index beb2d9f3..61e0cc42 100644 --- a/Gemfile +++ b/Gemfile @@ -69,7 +69,9 @@ gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] gem 'active_job_status', '~> 1.2.1' gem 'devise' gem 'devise-guests', '~> 0.6' +gem 'devise_ldap_authenticatable' gem 'handle-system', '0.1.1' +gem 'ladle' gem 'mysql2' gem 'react-rails' gem 'redis-activesupport', '~> 5.0.4' diff --git a/Gemfile.lock b/Gemfile.lock index cf79ee7d..71556506 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -229,6 +229,9 @@ GEM warden (~> 1.2.3) devise-guests (0.6.0) devise + devise_ldap_authenticatable (0.8.5) + devise (>= 3.4.1) + net-ldap (>= 0.6.0, <= 0.11) diff-lcs (1.3) docile (1.1.5) dotenv (2.2.1) @@ -459,6 +462,8 @@ GEM kaminari-core (1.0.1) kaminari_route_prefix (0.1.1) kaminari (~> 1.0) + ladle (1.0.1) + open4 (~> 1.0) launchy (2.4.3) addressable (~> 2.3) ld-patch (0.3.2) @@ -533,6 +538,7 @@ GEM nest (2.1.0) redic net-http-persistent (2.9.4) + net-ldap (0.11) net-scp (1.2.1) net-ssh (>= 2.6.5) net-ssh (4.1.0) @@ -555,6 +561,7 @@ GEM activesupport nokogiri (>= 1.4.2) solrizer (~> 3.3) + open4 (1.3.4) openseadragon (0.4.0) rails (> 3.2.0) orm_adapter (0.5.0) @@ -884,6 +891,7 @@ DEPENDENCIES database_cleaner devise devise-guests (~> 0.6) + devise_ldap_authenticatable dotenv-rails factory_girl_rails fcrepo_wrapper @@ -894,6 +902,7 @@ DEPENDENCIES hyrax (= 2.0.0) jbuilder (~> 2.5) jquery-rails + ladle listen (~> 3.0.5) mysql2 nokogiri (>= 1.8.1) diff --git a/README.md b/README.md index dc235b03..370d6b57 100644 --- a/README.md +++ b/README.md @@ -32,15 +32,35 @@ bundle exec rails db:setup bundle exec rake spec ``` +### running the development environment LDAP server +You need to start up the LDAP server using the `ladle` task and Solr, Fedora, and a webserver using the `hydra:server` task. +It's usually best to run each service in it's own terminal session. +```sh +# if you checked out new code, run the next two commands +# bundle install +# bundle exec rake db:migrate +bundle exec rake ladle #start an LDAP server in a new window +bundle exec hydra:server #start the development server, fedora, and solr in a new window +# visit http://localhost:3000 +``` + +The application is configured to use LDAP for authentication. The development and test +environments use the [ladle](https://github.com/NUBIC/ladle) gem to launch a self-contained LDAP server. +LDAP users are seeded from the file at `config/ldap_seed_users.ldif`, so you can login +using either `user` or `admin` with the password 'password'. Note that the system is configured to expect +a username, not an email address. + ### making an admin user -First, you'll need to start your development server and create a new user. +First, you'll need to start your development server and login as one of the LDAP users. +We'll assume you logged in as `admin` ```sh bundle exec rails c -> u = User.create(email: 'admin@example.org', display_name: 'Admin, Example', password: 'password') +> u = User.find_by_user_key('admin') > u.add_role('admin') > exit ``` -Now you should be able to login as `admin@example.org` with access to the administator dashboard. +If you go back and refresh your browser where `admin` is logged in, you +should now have access to the administrator dashboard. ### seeding deposit types MIRA supports a number of configurable deposit types. A seed configuration is checked into the repository at @@ -53,6 +73,7 @@ If you wish to make changes to the seeds, use the "Manage Self Deposit Types" op Make any changes you want, export the configuration using the "Export Deposit Type Data" link at the bottom of the "Manage Deposit Types" view, and then check the updated deposit type configuration CSV file into the repository. + ## Re-create derivatives If you need to re-create derivatives, use these rake tasks: 1. One at a time, by id: `RAILS_ENV=production bundle exec rake derivatives:recreate_by_id[2801pg32c]` @@ -121,4 +142,3 @@ Notifications are defined in `app/services/hyrax/workflow`. There are three kind The `/contribute` forms deposit works into specific collections. In order to ensure that the expected collections exist, they are created at application deploy time and (if necessary) at deposit time via the `Tufts::ContributeCollections` class. To change the names or identifiers of these Collection objects, edit the `app/lib/tufts/contribute_collections.rb` file. To create the collections explicitly, run `rake tufts:create_contribute_collections`. - diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 4163086e..1e296861 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,4 +1,7 @@ class ApplicationController < ActionController::Base + rescue_from DeviseLdapAuthenticatable::LdapException do |exception| + render text: exception, status: 500 + end helper Openseadragon::OpenseadragonHelper # Adds a few additional behaviors into the application controller include Blacklight::Controller diff --git a/app/models/forms/contribution.rb b/app/models/forms/contribution.rb index f11d9e9b..f6a2164e 100644 --- a/app/models/forms/contribution.rb +++ b/app/models/forms/contribution.rb @@ -41,7 +41,7 @@ def tufts_pdf ) copy_attributes add_to_collection - user = User.find_by(email: @depositor) + user = ::User.find_by_user_key(@depositor) current_ability = ::Ability.new(user) uploaded_file = Hyrax::UploadedFile.create(user: user, file: @attachment) attributes = { uploaded_files: [uploaded_file.id] } diff --git a/app/models/forms/generic_tisch_deposit.rb b/app/models/forms/generic_tisch_deposit.rb index 555baa90..4b9b8b9d 100644 --- a/app/models/forms/generic_tisch_deposit.rb +++ b/app/models/forms/generic_tisch_deposit.rb @@ -20,7 +20,7 @@ def tufts_pdf ) copy_attributes add_to_collection - user = User.find_by(email: @depositor) + user = ::User.find_by_user_key(@depositor) current_ability = ::Ability.new(user) uploaded_file = Hyrax::UploadedFile.create(user: user, file: @attachment) attributes = { uploaded_files: [uploaded_file.id] } diff --git a/app/models/user.rb b/app/models/user.rb index 28a20226..7c97a192 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -15,7 +15,7 @@ class User < ApplicationRecord include Blacklight::User # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable and :omniauthable - devise :database_authenticatable, :registerable, + devise :ldap_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable ## @@ -55,4 +55,23 @@ def mailboxer_email(_object) def preferred_locale 'en' end + + def ldap_before_save + self.email = Devise::LDAP::Adapter.get_ldap_param(username, "mail").first + self.display_name = Devise::LDAP::Adapter.get_ldap_param(username, "tuftsEduDisplayNameLF").first + end +end + +# Override a Hyrax class that expects to create system users with passwords +module Hyrax::User + module ClassMethods + def find_or_create_system_user(user_key) + u = ::User.find_or_create_by(username: user_key) + u.display_name = user_key + u.email = "#{user_key}@example.com" + u.password = ('a'..'z').to_a.shuffle(random: Random.new).join + u.save + u + end + end end diff --git a/app/services/hyrax/workflow/mira_workflow_notification.rb b/app/services/hyrax/workflow/mira_workflow_notification.rb index 570c32a8..7279515f 100644 --- a/app/services/hyrax/workflow/mira_workflow_notification.rb +++ b/app/services/hyrax/workflow/mira_workflow_notification.rb @@ -20,7 +20,7 @@ def admins # The Hyrax::User who desposited the work # @return [Hyrax::User] def depositor - ::User.find_by(email: document.depositor) + ::User.find_by_user_key(document.depositor) end ## diff --git a/app/views/devise/sessions/new.html.erb b/app/views/devise/sessions/new.html.erb index 9f2265f6..969720f8 100644 --- a/app/views/devise/sessions/new.html.erb +++ b/app/views/devise/sessions/new.html.erb @@ -3,7 +3,7 @@ <%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>
<%= f.label 'Tufts Username' %>
- <%= f.email_field :email, autofocus: true %> + <%= f.text_field :username, autofocus: true %>
diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 86e57ef4..c2669655 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -1,6 +1,17 @@ # Use this hook to configure devise mailer, warden hooks and so forth. # Many of these configuration options can be set straight in your model. Devise.setup do |config| + # ==> LDAP Configuration + config.ldap_logger = true + config.ldap_create_user = true + # config.ldap_update_password = true + # config.ldap_config = "#{Rails.root}/config/ldap.yml" + # config.ldap_check_group_membership = false + # config.ldap_check_group_membership_without_admin = false + # config.ldap_check_attributes = false + # config.ldap_use_admin_to_bind = false + # config.ldap_ad_group_check = false + # The secret key used by Devise. Devise uses this key to generate # random tokens. Changing this key will render invalid all existing # confirmation, reset password and unlock tokens in the database. @@ -34,7 +45,7 @@ # session. If you need permissions, you should implement that in a before filter. # You can also supply a hash where the value is a boolean determining whether # or not authentication should be aborted when the value is not present. - # config.authentication_keys = [:email] + config.authentication_keys = [:username] # Configure parameters from the request object used for authentication. Each entry # given should be a request method and it will automatically be passed to the @@ -46,12 +57,12 @@ # Configure which authentication keys should be case-insensitive. # These keys will be downcased upon creating or modifying a user and when used # to authenticate or find a user. Default is :email. - config.case_insensitive_keys = [:email] + config.case_insensitive_keys = [:username] # Configure which authentication keys should have whitespace stripped. # These keys will have whitespace before and after removed upon creating or # modifying a user and when used to authenticate or find a user. Default is :email. - config.strip_whitespace_keys = [:email] + config.strip_whitespace_keys = [:username] # Tell if authentication through request.params is enabled. True by default. # It can be set to an array that will enable params authentication only for the diff --git a/config/initializers/hyrax.rb b/config/initializers/hyrax.rb index f9ee27d4..f4957079 100644 --- a/config/initializers/hyrax.rb +++ b/config/initializers/hyrax.rb @@ -115,10 +115,10 @@ # config.display_share_button_when_not_logged_in = true # The user who runs batch jobs. Update this if you aren't using emails - # config.batch_user_key = 'batchuser@example.com' + config.batch_user_key = 'batchuser' # The user who runs fixity check jobs. Update this if you aren't using emails - # config.audit_user_key = 'audituser@example.com' + config.audit_user_key = 'audituser' # # The banner image. Should be 5000px wide by 1000px tall # config.banner_image = 'https://cloud.githubusercontent.com/assets/92044/18370978/88ecac20-75f6-11e6-8399-6536640ef695.jpg' diff --git a/config/ldap.yml b/config/ldap.yml new file mode 100644 index 00000000..bc733047 --- /dev/null +++ b/config/ldap.yml @@ -0,0 +1,47 @@ +## Authorizations +# Uncomment out the merging for each environment that you'd like to include. +# You can also just copy and paste the tree (do not include the "authorizations") to each +# environment if you need something different per enviornment. +authorizations: &AUTHORIZATIONS + allow_unauthenticated_bind: false + group_base: ou=groups,dc=example,dc=org + ## Requires config.ldap_check_group_membership in devise.rb be true + # Can have multiple values, must match all to be authorized + required_groups: + # If only a group name is given, membership will be checked against "uniqueMember" + - cn=admins,ou=groups,dc=example,dc=org + - cn=users,ou=groups,dc=example,dc=org + # If an array is given, the first element will be the attribute to check against, the second the group name + - ["moreMembers", "cn=users,ou=groups,dc=example,dc=org"] + ## Requires config.ldap_check_attributes in devise.rb to be true + ## Can have multiple attributes and values, must match all to be authorized + require_attribute: + objectClass: inetOrgPerson + +## Environment + +development: + host: localhost + port: 3389 + attribute: cn + base: ou=people,dc=example,dc=org + ssl: false + # <<: *AUTHORIZATIONS + +test: + host: localhost + port: 3389 + attribute: cn + base: ou=people,dc=example,dc=org + ssl: false + # <<: *AUTHORIZATIONS + +production: + host: localhost + port: 689 + attribute: cn + base: ou=people,dc=example,dc=org + admin_user: cn=admin,ou=people,dc=my_domain,dc=com + admin_password: admin_password + ssl: start_tls + # <<: *AUTHORIZATIONS diff --git a/config/ldap_seed_users.ldif b/config/ldap_seed_users.ldif new file mode 100644 index 00000000..8a6bb17c --- /dev/null +++ b/config/ldap_seed_users.ldif @@ -0,0 +1,38 @@ + +version: 1 + +# people.example.org +dn: ou=people,dc=example,dc=org +objectClass: top +objectClass: organizationalUnit +ou: people + +# user.people.examle.org +dn: cn=user,ou=people,dc=example,dc=org +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +objectClass: tuftsEduPerson +tuftsEduDisplayNameLF: Ffrind, Rhyw +sn: Ffrind +givenName: Rhyw +uid: example_user +mail: user@example.org +cn: user +userPassword: password + +# admin.people.examle.org +dn: cn=admin,ou=people,dc=example,dc=org +objectClass: top +objectClass: person +objectClass: organizationalPerson +objectClass: inetOrgPerson +objectClass: tuftsEduPerson +tuftsEduDisplayNameLF: Arall, Rhywun +sn: Arall +givenName: Rhywun +uid: admin_user +mail: admin@example.org +cn: admin +userPassword: password diff --git a/config/tufts_schema.ldif b/config/tufts_schema.ldif new file mode 100644 index 00000000..d1b42a68 --- /dev/null +++ b/config/tufts_schema.ldif @@ -0,0 +1,23 @@ +version: 1 + +dn: m-oid=1.3.6.1.4.1.6940.1.1.1.1.2.1.1.47,ou=attributeTypes,cn=other,ou=schema +objectClass: metaAttributeType +objectClass: metaTop +objectClass: top +m-collective: FALSE +m-description: 'displayName in last, first form' +m-name: tuftsEduDisplayNameLF +m-syntax: 1.3.6.1.4.1.1466.115.121.1.15 +m-usage: USER_APPLICATIONS +m-oid: 1.3.6.1.4.1.6940.1.1.1.1.2.1.1.47 + +dn: m-oid=1.3.6.1.4.1.6940.1.1.1.1.2.1.2.1,ou=objectClasses,cn=other,ou=schema +objectClass: metaObjectClass +objectClass: metaTop +objectClass: top +m-description: Tufts schema extensions for people directory entries +m-may: tuftsEduDisplayNameLF +m-supobjectclass: top +m-name: tuftsEduPerson +m-oid: 1.3.6.1.4.1.6940.1.1.1.1.2.1.2.1 +m-typeobjectclass: ABSTRACT diff --git a/db/migrate/20171215144758_add_username_to_users.rb b/db/migrate/20171215144758_add_username_to_users.rb new file mode 100644 index 00000000..7ba31180 --- /dev/null +++ b/db/migrate/20171215144758_add_username_to_users.rb @@ -0,0 +1,6 @@ +class AddUsernameToUsers < ActiveRecord::Migration[5.0] + def change + add_column :users, :username, :string + add_index :users, :username, unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index feb368c0..ab115222 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20171009170516) do +ActiveRecord::Schema.define(version: 20171215144758) do create_table "batch_tasks", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8" do |t| t.string "batch_type" @@ -546,8 +546,10 @@ t.string "arkivo_subscription" t.binary "zotero_token", limit: 65535 t.string "zotero_userid" + t.string "username" t.index ["email"], name: "index_users_on_email", unique: true, using: :btree t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree + t.index ["username"], name: "index_users_on_username", unique: true, using: :btree end create_table "version_committers", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8" do |t| diff --git a/lib/tasks/ladle.rake b/lib/tasks/ladle.rake new file mode 100644 index 00000000..20030522 --- /dev/null +++ b/lib/tasks/ladle.rake @@ -0,0 +1,23 @@ +require 'ladle' + +desc 'Start a ladle server' +task :ladle do + conf_path = Rails.root.join('config') + ldap_port = Rails.application.config_for(:ldap)['port'] + + server = Ladle::Server.new( + port: ldap_port, + quiet: false, + custom_schemas: conf_path.join('tufts_schema.ldif').to_s, + ldif: conf_path.join('ldap_seed_users.ldif').to_s + ) + + begin + server.start + sleep + rescue Interrupt + puts ' Stopping server' + ensure + server.stop + end +end diff --git a/spec/factories/users.rb b/spec/factories/users.rb index 9a57e322..1feece07 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -3,7 +3,10 @@ FactoryGirl.define do factory :user do sequence :email do |n| - "person#{User.count}_#{n}@example.com" + "#{n}#{FFaker::Internet.email}" + end + sequence :username do |n| + "#{FFaker::Internet.user_name}#{n}" end password 'password' display_name FFaker::Name.name diff --git a/spec/features/login_out_spec.rb b/spec/features/login_out_spec.rb index 41590a35..ee01c171 100644 --- a/spec/features/login_out_spec.rb +++ b/spec/features/login_out_spec.rb @@ -6,14 +6,17 @@ context 'logging in' do scenario 'provide a login and password' do - u = User.create(email: 'admin@example.org', display_name: 'Admin, Example', password: 'password') + skip "This test will only work if Ladle (or other LDAP) is running. Disabled for CI." + # It's a good way to test that LDAP authentication is working as expected, + # but we don't need to load LDAP auth every time we run our CI tests. + u = User.create(email: 'admin@example.org', username: 'admin', display_name: 'Admin, Example', password: 'password') u.add_role('admin') u.save visit '/dashboard' expect(current_path).to eq "/contribute" find('a.btn-primary').click expect(current_path).to eq "/users/sign_in" - fill_in 'user_email', with: u.user_key + fill_in 'user_username', with: u.user_key fill_in 'user_password', with: u.password click_on 'Log in' expect(current_path).to eq "/dashboard" diff --git a/spec/features/publication_workflow_spec.rb b/spec/features/publication_workflow_spec.rb index d1ebefaa..1628a799 100644 --- a/spec/features/publication_workflow_spec.rb +++ b/spec/features/publication_workflow_spec.rb @@ -69,12 +69,6 @@ visit("/notifications") expect(page).to have_content "Comment about #{work.title.first}" - # Check notifications for depositor again - logout - login_as depositing_user - visit("/notifications") - expect(page).to have_content "#{work.title.first} (#{work.id}) has been published by #{publishing_user.display_name} (#{publishing_user.user_key}). Published in publication_workflow_spec.rb" - # After publication, an admin can unpublish a work. Tufts::WorkflowStatus.unpublish(work: work, current_user: publishing_user, comment: "Unpublished in publication_workflow_spec.rb") expect(work.to_sipity_entity.reload.workflow_state_name).to eq "unpublished" diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 38d07063..c45b50d1 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -73,4 +73,10 @@ expect(admin_user).to be_admin end end + + describe '#username' do + it "has a username" do + expect(user.username).not_to be_blank + end + end end diff --git a/spec/services/hyrax/workflow/comment_notification_spec.rb b/spec/services/hyrax/workflow/comment_notification_spec.rb index f74329c5..ce44112c 100644 --- a/spec/services/hyrax/workflow/comment_notification_spec.rb +++ b/spec/services/hyrax/workflow/comment_notification_spec.rb @@ -22,13 +22,13 @@ end it "can find depositor" do expect(notification.depositor).to be_instance_of(::User) - expect(notification.depositor.email).to eq depositor.user_key + expect(notification.depositor.user_key).to eq depositor.user_key end it "can find admins" do expect(notification.admins).to be_instance_of(Array) expect(notification.admins.pluck(:id)).to contain_exactly(admin.id) end it "sends notifications to the depositor, application admins and no one else" do - expect(notification.recipients["to"].pluck(:email)).to contain_exactly(depositor.user_key, admin.user_key) + expect(notification.recipients["to"].pluck(Hydra.config.user_key_field)).to contain_exactly(depositor.user_key, admin.user_key) end end diff --git a/spec/services/hyrax/workflow/published_notification_spec.rb b/spec/services/hyrax/workflow/published_notification_spec.rb index 386302c4..d784af02 100644 --- a/spec/services/hyrax/workflow/published_notification_spec.rb +++ b/spec/services/hyrax/workflow/published_notification_spec.rb @@ -22,13 +22,13 @@ end it "can find depositor" do expect(notification.depositor).to be_instance_of(::User) - expect(notification.depositor.email).to eq depositor.user_key + expect(notification.depositor.user_key).to eq depositor.user_key end it "can find admins" do expect(notification.admins).to be_instance_of(Array) expect(notification.admins.pluck(:id)).to include(admin.id) end it "sends notifications to the depositor, application admins and no one else" do - expect(notification.recipients["to"].pluck(:email)).to contain_exactly(depositor.user_key, admin.user_key) + expect(notification.recipients["to"].pluck(Hydra.config.user_key_field)).to contain_exactly(depositor.user_key, admin.user_key) end end diff --git a/spec/services/hyrax/workflow/unpublished_notification_spec.rb b/spec/services/hyrax/workflow/unpublished_notification_spec.rb index 70692f3f..b8686e31 100644 --- a/spec/services/hyrax/workflow/unpublished_notification_spec.rb +++ b/spec/services/hyrax/workflow/unpublished_notification_spec.rb @@ -22,13 +22,13 @@ end it "can find depositor" do expect(notification.depositor).to be_instance_of(::User) - expect(notification.depositor.email).to eq depositor.user_key + expect(notification.depositor.user_key).to eq depositor.user_key end it "can find admins" do expect(notification.admins).to be_instance_of(Array) expect(notification.admins.pluck(:id)).to contain_exactly(admin.id) end it "sends notifications to application admins and no one else" do - expect(notification.recipients["to"].pluck(:email)).to contain_exactly(admin.user_key) + expect(notification.recipients["to"].pluck(Hydra.config.user_key_field)).to contain_exactly(admin.user_key) end end