Skip to content

Commit

Permalink
allow role grants to other databases for mongodb_user
Browse files Browse the repository at this point in the history
Adopt PR #308 by @sharon-tickell
to support assigning user roles to other database using syntax role@database.

With support of #547

mongodb_spec:
Supply explicit JSON document incl. db name to mongodb command when changing roles;
see syntax https://docs.mongodb.com/manual/reference/method/db.grantRolesToUser/
Compatibility level is mongodb >= 2.6
  • Loading branch information
pecharmin committed Nov 26, 2019
1 parent 37dd6f0 commit 6bdc562
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 25 deletions.
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -649,7 +649,10 @@ For more information please refer to [MongoDB Authentication Process](http://doc
Plain-text user password (will be hashed)

##### `roles`
Array with user roles. Default: ['dbAdmin']
Array with user roles as string.
Roles will be granted to user's database if no alternative database is explicitly defined.
Example: ['dbAdmin', 'readWrite@other_database']
Default: ['dbAdmin']

### Providers

Expand Down Expand Up @@ -697,7 +700,10 @@ Plaintext password of the user.
Name of database. It will be created, if not exists.

##### `roles`
Array with user roles. Default: ['dbAdmin']
Array with user roles as string.
Roles will be granted to user's database if no alternative database is explicitly defined.
Example: ['dbAdmin', 'readWrite@other_database']
Default: ['dbAdmin']

##### `tries`
The maximum amount of two second tries to wait MongoDB startup. Default: 10
Expand Down
38 changes: 32 additions & 6 deletions lib/puppet/provider/mongodb_user/mongodb.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def create
customData: {
createdBy: "Puppet Mongodb_user['#{@resource[:name]}']"
},
roles: @resource[:roles],
roles: role_hashes(@resource[:roles], @resource[:database]),
digestPassword: false
}

Expand Down Expand Up @@ -110,14 +110,14 @@ def password=(value)

def roles=(roles)
if db_ismaster
grant = roles - @property_hash[:roles]
grant = to_roles(roles, @resource[:database]) - to_roles(@property_hash[:roles], @resource[:database])
unless grant.empty?
mongo_eval("db.getSiblingDB(#{@resource[:database].to_json}).grantRolesToUser(#{@resource[:username].to_json}, #{grant.to_json})")
mongo_eval("db.getSiblingDB(#{@resource[:database].to_json}).grantRolesToUser(#{@resource[:username].to_json}, #{role_hashes(grant, @resource[:database]).to_json})")
end

revoke = @property_hash[:roles] - roles
revoke = to_roles(@property_hash[:roles], @resource[:database]) - to_roles(roles, @resource[:database])
unless revoke.empty?
mongo_eval("db.getSiblingDB(#{@resource[:database].to_json}).revokeRolesFromUser(#{@resource[:username].to_json}, #{revoke.to_json})")
mongo_eval("db.getSiblingDB(#{@resource[:database].to_json}).revokeRolesFromUser(#{@resource[:username].to_json}, #{role_hashes(revoke, @resource[:database]).to_json})")
end
else
Puppet.warning 'User roles operations are available only from master host'
Expand All @@ -128,11 +128,37 @@ def roles=(roles)

def self.from_roles(roles, db)
roles.map do |entry|
if entry['db'] == db
if entry['db'].empty? || entry['db'] == db
entry['role']
else
"#{entry['role']}@#{entry['db']}"
end
end.sort
end

def to_roles(roles, db)
roles.map do |entry|
if entry.include? '@'
entry
else
"#{entry}@#{db}"
end
end
end

def role_hashes(roles, db)
roles.sort.map do |entry|
if entry.include? '@'
{
'role' => entry.gsub(%r{^(.*)@.*$}, '\1'),
'db' => entry.gsub(%r{^.*@(.*)$}, '\1')
}
else
{
'role' => entry,
'db' => db
}
end
end
end
end
2 changes: 1 addition & 1 deletion lib/puppet/type/mongodb_user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def initialize(*args)
newproperty(:roles, array_matching: :all) do
desc "The user's roles."
defaultto ['dbAdmin']
newvalue(%r{^\w+$})
newvalue(%r{^\w+(@\w+)?$})

# Pretty output for arrays.
def should_to_s(value)
Expand Down
66 changes: 66 additions & 0 deletions spec/acceptance/user_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,70 @@ class { 'mongodb::server': port => 27018 }
end
end
end

context 'with the basic roles syntax' do
it 'compiles with no errors' do
pp = <<-EOS
class { 'mongodb::server': }
-> class { 'mongodb::client': }
-> mongodb_database { 'testdb': ensure => present }
->
mongodb_user {'testuser':
ensure => present,
password_hash => mongodb_password('testuser', 'passw0rd'),
database => 'testdb',
roles => ['readWrite', 'dbAdmin'],
}
EOS

apply_manifest(pp, catch_failures: true)
apply_manifest(pp, catch_changes: true)
end

it 'creates the user' do
shell("mongo testdb --quiet --eval 'db.auth(\"testuser\",\"passw0rd\")'") do |r|
expect(r.stdout.chomp).to eq('1')
end
end
end

context 'with the new multidb role syntax' do
it 'compiles with no errors' do
pp = <<-EOS
class { 'mongodb::server': }
-> class { 'mongodb::client': }
-> mongodb_database { 'testdb': ensure => present }
-> mongodb_database { 'testdb2': ensure => present }
->
mongodb_user {'testuser':
ensure => present,
password_hash => mongodb_password('testuser', 'passw0rd'),
database => 'testdb',
roles => ['readWrite', 'dbAdmin'],
}
->
mongodb_user {'testuser2':
ensure => present,
password_hash => mongodb_password('testuser2', 'passw0rd'),
database => 'testdb2',
roles => ['readWrite', 'dbAdmin', 'readWrite@testdb', 'dbAdmin@testdb'],
}
EOS

apply_manifest(pp, catch_failures: true)
apply_manifest(pp, catch_changes: true)
end

it 'allows the testuser' do
shell("mongo testdb --quiet --eval 'db.auth(\"testuser\",\"passw0rd\")'") do |r|
expect(r.stdout.chomp).to eq('1')
end
end

it 'allows the second user to connect to its default database' do
shell("mongo testdb2 --quiet --eval 'db.auth(\"testuser2\",\"passw0rd\")'") do |r|
expect(r.stdout.chomp).to eq('1')
end
end
end
end
32 changes: 16 additions & 16 deletions spec/unit/puppet/provider/mongodb_user/mongodb_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
describe Puppet::Type.type(:mongodb_user).provider(:mongodb) do
let(:raw_users) do
[
{ '_id' => 'admin.root', 'user' => 'root', 'db' => 'admin', 'credentials' => { 'MONGODB-CR' => 'pass', 'SCRAM-SHA-1' => { 'iterationCount' => 10_000, 'salt' => 'salt', 'storedKey' => 'storedKey', 'serverKey' => 'serverKey' } }, 'roles' => [{ 'role' => 'role2', 'db' => 'admin' }, { 'role' => 'role1', 'db' => 'admin' }] }
{ '_id' => 'admin.root', 'user' => 'root', 'db' => 'admin', 'credentials' => { 'MONGODB-CR' => 'pass', 'SCRAM-SHA-1' => { 'iterationCount' => 10_000, 'salt' => 'salt', 'storedKey' => 'storedKey', 'serverKey' => 'serverKey' } }, 'roles' => [{ 'role' => 'role2', 'db' => 'admin' }, { 'role' => 'role3', 'db' => 'user_database' }, { 'role' => 'role1', 'db' => 'admin' }] }
].to_json
end

Expand All @@ -17,7 +17,7 @@
name: 'new_user',
database: 'new_database',
password_hash: 'pass',
roles: %w[role1 role2],
roles: %w[role1 role2@other_database],
provider: described_class.name
)
end
Expand Down Expand Up @@ -56,7 +56,7 @@
"createUser":"new_user",
"pwd":"pass",
"customData":{"createdBy":"Puppet Mongodb_user['new_user']"},
"roles":["role1","role2"],
"roles":[{"role":"role1","db":"new_database"},{"role":"role2","db":"other_database"}],
"digestPassword":false
}
EOS
Expand Down Expand Up @@ -114,40 +114,40 @@

describe 'roles' do
it 'returns a sorted roles' do
expect(instance.roles).to eq(%w[role1 role2])
expect(instance.roles).to eq(%w[role1 role2 role3@user_database])
end
end

describe 'roles=' do
it 'changes nothing' do
resource.provider.set(name: 'new_user', ensure: :present, roles: %w[role1 role2])
resource.provider.set(name: 'new_user', ensure: :present, roles: %w[role1 role2@other_database])
expect(provider).not_to receive(:mongo_eval)
provider.roles = %w[role1 role2]
provider.roles = %w[role1 role2@other_database]
end

it 'grant a role' do
resource.provider.set(name: 'new_user', ensure: :present, roles: %w[role1 role2])
resource.provider.set(name: 'new_user', ensure: :present, roles: %w[role1 role2@other_database])
expect(provider).to receive(:mongo_eval).
with('db.getSiblingDB("new_database").grantRolesToUser("new_user", ["role3"])')
provider.roles = %w[role1 role2 role3]
with('db.getSiblingDB("new_database").grantRolesToUser("new_user", [{"role":"role3","db":"new_database"}])')
provider.roles = %w[role1 role2@other_database role3]
end

it 'revokes a role' do
resource.provider.set(name: 'new_user', ensure: :present, roles: %w[role1 role2])
resource.provider.set(name: 'new_user', ensure: :present, roles: %w[role1 role2@other_database])
expect(provider).to receive(:mongo_eval).
with('db.getSiblingDB("new_database").revokeRolesFromUser("new_user", ["role1"])')
provider.roles = ['role2']
with('db.getSiblingDB("new_database").revokeRolesFromUser("new_user", [{"role":"role1","db":"new_database"}])')
provider.roles = ['role2@other_database']
end

# rubocop:disable RSpec/MultipleExpectations
it 'exchanges a role' do
resource.provider.set(name: 'new_user', ensure: :present, roles: %w[role1 role2])
resource.provider.set(name: 'new_user', ensure: :present, roles: %w[role1 role2@other_database])
expect(provider).to receive(:mongo_eval).
with('db.getSiblingDB("new_database").revokeRolesFromUser("new_user", ["role1"])')
with('db.getSiblingDB("new_database").revokeRolesFromUser("new_user", [{"role":"role1","db":"new_database"}])')
expect(provider).to receive(:mongo_eval).
with('db.getSiblingDB("new_database").grantRolesToUser("new_user", ["role3"])')
with('db.getSiblingDB("new_database").grantRolesToUser("new_user", [{"role":"role3","db":"new_database"}])')

provider.roles = %w[role2 role3]
provider.roles = %w[role2@other_database role3]
end
# rubocop:enable RSpec/MultipleExpectations
end
Expand Down

0 comments on commit 6bdc562

Please sign in to comment.