Skip to content

Commit 3906d21

Browse files
committed
(MODULES-1737) Add pw_hash() function
1 parent afc83ea commit 3906d21

File tree

4 files changed

+167
-0
lines changed

4 files changed

+167
-0
lines changed

README.markdown

+16
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,22 @@ Calling the class or definition from outside the current module will fail. For e
377377

378378
*Type*: statement
379379

380+
* `pw_hash`: Hashes a password using the crypt function. Provides a hash usable on most POSIX systems.
381+
382+
The first argument to this function is the password to hash. If it is undef or an empty string, this function returns undef.
383+
384+
The second argument to this function is a specifier of which hash type to use. Valid hash types are:
385+
386+
|Specifier|Hash type |
387+
|---------|---------------------|
388+
|1 |MD5 |
389+
|5 |SHA-256 |
390+
|6 |SHA-512 (recommended)|
391+
392+
The third argument to this function is the sale to use.
393+
394+
Note: this uses the Puppet Master's implementation of crypt(3). If your environment contains several different operating systems, ensure that they are compatible before using this function.
395+
380396
* `range`: When given range in the form of '(start, stop)', `range` extrapolates a range as an array. For example, `range("0", "9")` returns [0,1,2,3,4,5,6,7,8,9]. Zero-padded strings are converted to integers automatically, so `range("00", "09")` returns [0,1,2,3,4,5,6,7,8,9].
381397

382398
Non-integer strings are accepted; `range("a", "c")` returns ["a","b","c"], and `range("host01", "host10")` returns ["host01", "host02", ..., "host09", "host10"].
+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
Puppet::Parser::Functions::newfunction(
2+
:pw_hash,
3+
:type => :rvalue,
4+
:arity => 3,
5+
:doc => "Hashes a password using the crypt function. Provides a hash
6+
usable on most POSIX systems.
7+
8+
The first argument to this function is the password to hash. If it is
9+
undef or an empty string, this function returns undef.
10+
11+
The second argument to this function is a specifier of which hash type
12+
to use. Valid hash types are:
13+
14+
|Specifier|Hash type |
15+
|---------|---------------------|
16+
|1 |MD5 |
17+
|5 |SHA-256 |
18+
|6 |SHA-512 (recommended)|
19+
20+
The third argument to this function is the salt to use.
21+
22+
Note: this uses the Puppet Master's implementation of crypt(3). If your
23+
environment contains several different operating systems, ensure that they
24+
are compatible before using this function.") do |args|
25+
raise ArgumentError, "pw_hash(): wrong number of arguments (#{args.size} for 3)" if args.size != 3
26+
raise ArgumentError, "pw_hash(): first argument must be a string" unless args[0].is_a? String
27+
raise ArgumentError, "pw_hash(): #{args[1]} is not a valid hash type" unless ['1', '5', '6'].include? args[1].to_s
28+
raise ArgumentError, "pw_hash(): third argument must be a string" unless args[2].is_a? String
29+
raise ArgumentError, "pw_hash(): third argument must not be empty" if args[2].empty?
30+
raise ArgumentError, "pw_hash(): characters in salt must be in the set [a-zA-Z0-9./]" unless args[2].match(/\A[a-zA-Z0-9.\/]+\z/)
31+
32+
password = args[0].to_s
33+
return nil if password.empty?
34+
35+
# work around JRuby bug in String#crypt for JRuby < 1.7.17
36+
if RUBY_PLATFORM == 'java' and 'test'.crypt('$1$1') != '$1$1$Bp8CU9Oujr9SSEw53WV6G.'
37+
def password.crypt(salt)
38+
# puppetserver bundles Apache Commons Codec
39+
org.apache.commons.codec.digest.Crypt.crypt(self.to_java_bytes, salt)
40+
end
41+
end
42+
password.crypt("$#{args[1]}$#{args[2]}")
43+
end

spec/acceptance/pw_hash_spec.rb

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#! /usr/bin/env ruby -S rspec
2+
require 'spec_helper_acceptance'
3+
4+
# Windows and OS X do not have useful implementations of crypt(3)
5+
describe 'pw_hash function', :unless => (UNSUPPORTED_PLATFORMS + ['windows', 'Darwin']).include?(fact('operatingsystem')) do
6+
describe 'success' do
7+
it 'hashes passwords' do
8+
pp = <<-EOS
9+
$o = pw_hash('password', 6, 'salt')
10+
notice(inline_template('pw_hash is <%= @o.inspect %>'))
11+
EOS
12+
13+
apply_manifest(pp, :catch_failures => true) do |r|
14+
expect(r.stdout).to match(/pw_hash is "\$6\$salt\$IxDD3jeSOb5eB1CX5LBsqZFVkJdido3OUILO5Ifz5iwMuTS4XMS130MTSuDDl3aCI6WouIL9AjRbLCelDCy\.g\."/)
15+
end
16+
end
17+
18+
it 'returns nil if no password is provided' do
19+
pp = <<-EOS
20+
$o = pw_hash('', 6, 'salt')
21+
notice(inline_template('pw_hash is <%= @o.inspect %>'))
22+
EOS
23+
24+
apply_manifest(pp, :catch_failures => true) do |r|
25+
expect(r.stdout).to match(/pw_hash is ""/)
26+
end
27+
end
28+
end
29+
describe 'failure' do
30+
it 'handles less than three arguments'
31+
it 'handles more than three arguments'
32+
it 'handles non strings'
33+
end
34+
end

spec/functions/pw_hash_spec.rb

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#! /usr/bin/env ruby -S rspec
2+
require 'spec_helper'
3+
4+
describe "the pw_hash function" do
5+
let(:scope) { PuppetlabsSpec::PuppetInternals.scope }
6+
7+
it "should exist" do
8+
expect(Puppet::Parser::Functions.function("pw_hash")).to eq("function_pw_hash")
9+
end
10+
11+
it "should raise an ArgumentError if there are less than 3 arguments" do
12+
expect { scope.function_pw_hash([]) }.to( raise_error(ArgumentError, /[Ww]rong number of arguments/) )
13+
expect { scope.function_pw_hash(['password']) }.to( raise_error(ArgumentError, /[Ww]rong number of arguments/) )
14+
expect { scope.function_pw_hash(['password', 6]) }.to( raise_error(ArgumentError, /[Ww]rong number of arguments/) )
15+
end
16+
17+
it "should raise an ArgumentError if there are more than 3 arguments" do
18+
expect { scope.function_pw_hash(['password', 6, 'salt', 5]) }.to( raise_error(ArgumentError, /[Ww]rong number of arguments/) )
19+
end
20+
21+
it "should raise an ArgumentError if the first argument is not a string" do
22+
expect { scope.function_pw_hash([['password'], 6, 'salt']) }.to( raise_error(ArgumentError, /first argument must be a string/) )
23+
# in Puppet 3, numbers are passed as strings, so we can't test that
24+
end
25+
26+
it "should return nil if the first argument is empty" do
27+
expect(scope.function_pw_hash(['', 6, 'salt'])).to eq(nil)
28+
end
29+
30+
it "should raise an ArgumentError if the second argument is an invalid hash type" do
31+
expect { scope.function_pw_hash(['', 3, 'salt']) }.to( raise_error(ArgumentError, /not a valid hash type/) )
32+
end
33+
34+
it "should raise an ArgumentError if the third argument is not a string" do
35+
expect { scope.function_pw_hash(['password', 6, ['salt']]) }.to( raise_error(ArgumentError, /third argument must be a string/) )
36+
# in Puppet 3, numbers are passed as strings, so we can't test that
37+
end
38+
39+
it "should raise an ArgumentError if the third argument is empty" do
40+
expect { scope.function_pw_hash(['password', 6, '']) }.to( raise_error(ArgumentError, /third argument must not be empty/) )
41+
end
42+
43+
it "should raise an ArgumentError if the third argument has invalid characters" do
44+
expect { scope.function_pw_hash(['password', 6, '%']) }.to( raise_error(ArgumentError, /characters in salt must be in the set/) )
45+
end
46+
47+
it "should return a hashed password" do
48+
result = scope.function_pw_hash(['password', 6, 'salt'])
49+
expect(result).to eql('$6$salt$IxDD3jeSOb5eB1CX5LBsqZFVkJdido3OUILO5Ifz5iwMuTS4XMS130MTSuDDl3aCI6WouIL9AjRbLCelDCy.g.')
50+
end
51+
52+
it "should use the specified salt" do
53+
result = scope.function_pw_hash(['password', 6, 'salt'])
54+
expect(result).to match('salt')
55+
end
56+
57+
it "should use the specified hash type" do
58+
result1 = scope.function_pw_hash(['password', 1, 'salt'])
59+
result5 = scope.function_pw_hash(['password', 5, 'salt'])
60+
result6 = scope.function_pw_hash(['password', 6, 'salt'])
61+
62+
expect(result1).to eql('$1$salt$qJH7.N4xYta3aEG/dfqo/0')
63+
expect(result5).to eql('$5$salt$Gcm6FsVtF/Qa77ZKD.iwsJlCVPY0XSMgLJL0Hnww/c1')
64+
expect(result6).to eql('$6$salt$IxDD3jeSOb5eB1CX5LBsqZFVkJdido3OUILO5Ifz5iwMuTS4XMS130MTSuDDl3aCI6WouIL9AjRbLCelDCy.g.')
65+
end
66+
67+
it "should generate a valid hash" do
68+
password_hash = scope.function_pw_hash(['password', 6, 'salt'])
69+
70+
hash_parts = password_hash.match(%r{\A\$(.*)\$([a-zA-Z0-9./]+)\$([a-zA-Z0-9./]+)\z})
71+
72+
expect(hash_parts).not_to eql(nil)
73+
end
74+
end

0 commit comments

Comments
 (0)