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