htauth 1.2.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,8 +2,7 @@ require 'htauth/algorithm'
2
2
  require 'digest/md5'
3
3
 
4
4
  module HTAuth
5
-
6
- # an implementation of the MD5 based encoding algorithm
5
+ # Internal: an implementation of the MD5 based encoding algorithm
7
6
  # as used in the apache htpasswd -m option
8
7
  class Md5 < Algorithm
9
8
 
@@ -1,18 +1,24 @@
1
- require 'htauth/errors'
1
+ require 'htauth/error'
2
2
  require 'htauth/entry'
3
3
  require 'htauth/algorithm'
4
4
 
5
5
  module HTAuth
6
- class InvalidPasswdEntry < StandardError ; end
7
-
8
- # A single record in an htdigest file.
6
+ # Internal: Object version of a single entry from a htpasswd file
9
7
  class PasswdEntry < Entry
10
8
 
9
+ # Internal: the user of this entry
11
10
  attr_accessor :user
11
+ # Internal: the password digest of this entry
12
12
  attr_accessor :digest
13
+ # Internal: the algorithm used to create the digest of this entry
13
14
  attr_reader :algorithm
14
15
 
15
16
  class << self
17
+ # Internal: Create an instance of this class from a line of text
18
+ #
19
+ # line - a line of text from a htpasswd file
20
+ #
21
+ # Returns an instance of PasswdEntry
16
22
  def from_line(line)
17
23
  parts = is_entry!(line)
18
24
  d = PasswdEntry.new(parts[0])
@@ -21,17 +27,26 @@ module HTAuth
21
27
  return d
22
28
  end
23
29
 
24
- # test if a line is an entry, raise InvalidPasswdEntry if it is not.
25
- # an entry must be composed of 2 parts, username:encrypted_password
26
- # where username, and password do not contain the ':' character
30
+ # Internal: test if the given line is valid for this Entry class
31
+ #
32
+ # A valid entry is a single line composed of two parts; a username and a
33
+ # password separated by a ':' character. Neither the username nor the
34
+ # password may contain a ':' character
35
+ #
36
+ # line - a line of text from a file
37
+ #
38
+ # Returns the individual parts of the line
39
+ # Raises InvalidPasswdEntry if it is not an valid entry
27
40
  def is_entry!(line)
28
41
  raise InvalidPasswdEntry, "line commented out" if line =~ /\A#/
29
42
  parts = line.strip.split(":")
30
- raise InvalidPasswdEntry, "line must be of the format username:pssword" if parts.size != 2
43
+ raise InvalidPasswdEntry, "line must be of the format username:password" if parts.size != 2
31
44
  return parts
32
45
  end
33
46
 
34
- # test if a line is an entry and return true or false
47
+ # Internal: Returns whether or not the line is a valid entry
48
+ #
49
+ # Returns true or false
35
50
  def is_entry?(line)
36
51
  begin
37
52
  is_entry!(line)
@@ -42,13 +57,15 @@ module HTAuth
42
57
  end
43
58
  end
44
59
 
60
+ # Internal: Create a new Entry with the given user, password, and algorithm
45
61
  def initialize(user, password = nil, alg = Algorithm::DEFAULT, alg_params = {} )
46
62
  @user = user
47
63
  alg = Algorithm::DEFAULT if alg == Algorithm::EXISTING
48
64
  @algorithm = Algorithm.algorithm_from_name(alg, alg_params)
49
- @digest = algorithm.encode(password) if password
65
+ @digest = calc_digest(password)
50
66
  end
51
67
 
68
+ # Internal: set the algorithm for the entry
52
69
  def algorithm=(alg)
53
70
  if alg.kind_of?(Array) then
54
71
  if alg.size == 1 then
@@ -62,35 +79,50 @@ module HTAuth
62
79
  return @algorithm
63
80
  end
64
81
 
82
+ # Internal: Update the password of the entry with its new value
83
+ #
84
+ # If we have an array of algorithms, then we set it to CRYPT
65
85
  def password=(new_password)
66
86
  if algorithm.kind_of?(Array) then
67
- @algorithm = Algorithm.algorithm_from_name("crypt")
87
+ @algorithm = Algorithm.algorithm_from_name(Algorithm::CRYPT)
68
88
  end
69
- @digest = algorithm.encode(new_password)
89
+ @digest = calc_digest(new_password)
90
+ end
91
+
92
+ # Internal: calculate the new digest of the given password
93
+ def calc_digest(password)
94
+ return nil unless password
95
+ algorithm.encode(password)
70
96
  end
71
97
 
98
+ # Public: Check if the given password is the password of this entry
72
99
  # check the password and make sure it works, in the case that the algorithm is unknown it
73
100
  # tries all of the ones that it thinks it could be, and marks the algorithm if it matches
101
+ # when looking for a matche, we always compare all of them, no short
102
+ # circuiting
74
103
  def authenticated?(check_password)
75
104
  authed = false
76
105
  if algorithm.kind_of?(Array) then
77
106
  algorithm.each do |alg|
78
- if alg.encode(check_password) == digest then
107
+ encoded = alg.encode(check_password)
108
+ if Algorithm.secure_compare(encoded, digest) then
79
109
  @algorithm = alg
80
110
  authed = true
81
- break
82
111
  end
83
112
  end
84
113
  else
85
- authed = digest == algorithm.encode(check_password)
114
+ encoded = algorithm.encode(check_password)
115
+ authed = Algorithm.secure_compare(encoded, digest)
86
116
  end
87
117
  return authed
88
118
  end
89
119
 
120
+ # Internal: Returns the key of this entry
90
121
  def key
91
122
  return "#{user}"
92
123
  end
93
124
 
125
+ # Internal: Returns the file line for this entry
94
126
  def to_s
95
127
  "#{user}:#{digest}"
96
128
  end
@@ -1,25 +1,52 @@
1
1
  require 'stringio'
2
2
  require 'tempfile'
3
3
 
4
- require 'htauth/errors'
4
+ require 'htauth/error'
5
5
  require 'htauth/file'
6
6
  require 'htauth/passwd_entry'
7
7
 
8
8
  module HTAuth
9
- class PasswdFileError < StandardError ; end
10
-
11
- # PasswdFile provides API style access to an +htpasswd+ produced file
9
+ # Public: An API for managing an 'htpasswd' file
10
+ #
11
+ # Examples
12
+ #
13
+ # ::HTAuth::PasswdFile.open("my.passwd") do |pf|
14
+ # pf.has_entry?('myuser', 'myrealm')
15
+ # pf.add_or_update('someuser', 'myrealm', 'a password')
16
+ # pf.delete('someolduser', 'myotherrealm')
17
+ # end
18
+ #
12
19
  class PasswdFile < HTAuth::File
13
20
 
21
+ # Private: The class implementing a single entry in the PasswdFile
14
22
  ENTRY_KLASS = HTAuth::PasswdEntry
15
23
 
16
- # does the entry the the specified username and realm exist in the file
24
+ # Public: Checks if the given username exists in the file
25
+ #
26
+ # username - the username to check
27
+ #
28
+ # Examples
29
+ #
30
+ # passwd_file.has_entry?("myuser")
31
+ # # => true
32
+ #
33
+ # Returns true or false if the username
17
34
  def has_entry?(username)
18
35
  test_entry = PasswdEntry.new(username)
19
36
  @entries.has_key?(test_entry.key)
20
37
  end
21
38
 
22
- # remove an entry from the file
39
+ # Public: remove the given username from the file
40
+ # The file is not written to disk until #save! is called.
41
+ #
42
+ # username - the username to remove
43
+ #
44
+ # Examples
45
+ #
46
+ # passwd_file.delete("myuser")
47
+ # passwd_file.save!
48
+ #
49
+ # Returns nothing
23
50
  def delete(username)
24
51
  if has_entry?(username) then
25
52
  ir = internal_record(username)
@@ -31,7 +58,27 @@ module HTAuth
31
58
  nil
32
59
  end
33
60
 
34
- # add or update an entry as appropriate
61
+ # Public: Add or update the username entry with the new password and
62
+ # algorithm. This will add a new entry if the username does not exist in
63
+ # the file. If the entry does exist in the file, then the password
64
+ # of the entry is updated to the new password / algorithm
65
+ #
66
+ # The file is not written to disk until #save! is called.
67
+ #
68
+ # username - the username of the entry
69
+ # password - the username of the entry
70
+ # algorithm - the algorithm to use (default: "md5"). Valid options are:
71
+ # "md5", "sha1", "plaintext", or "crypt"
72
+ #
73
+ # Examples
74
+ #
75
+ # passwd_file.add_or_update("newuser", "password", Algorithm::SHA1)
76
+ # passwd_file.save!
77
+ #
78
+ # passwd_file.add_or_update("newuser", "password")
79
+ # passwd_file.save!
80
+ #
81
+ # Returns nothing.
35
82
  def add_or_update(username, password, algorithm = Algorithm::DEFAULT)
36
83
  if has_entry?(username) then
37
84
  update(username, password, algorithm)
@@ -40,7 +87,23 @@ module HTAuth
40
87
  end
41
88
  end
42
89
 
43
- # add an new record. raises an error if the entry exists.
90
+ # Public: Add a new record to the file.
91
+ #
92
+ # username - the username of the entry
93
+ # password - the username of the entry
94
+ # algorithm - the algorithm to use (default: "md5"). Valid options are:
95
+ # "md5", "sha1", "plaintext", or "crypt"
96
+ #
97
+ # Examples
98
+ #
99
+ # passwd_file.add("newuser", "password")
100
+ # passwd_file.save!
101
+ #
102
+ # passwd_file.add("newuser", "password", "sha1")
103
+ # passwd_file.save!
104
+ #
105
+ # Returns nothing.
106
+ # Raises PasswdFileError if the give username already exists.
44
107
  def add(username, password, algorithm = Algorithm::DEFAULT)
45
108
  raise PasswdFileError, "Unable to add already existing user #{username}" if has_entry?(username)
46
109
  new_entry = PasswdEntry.new(username, password, algorithm)
@@ -51,7 +114,27 @@ module HTAuth
51
114
  return nil
52
115
  end
53
116
 
54
- # update an already existing entry with a new password. raises an error if the entry does not exist
117
+ # Public: Update an existing record in the file.
118
+ #
119
+ # By default, the same algorithm that already exists for the entry will be
120
+ # used with the new password. You may change the algorithm for an entry by
121
+ # setting the `algorithm` parameter.
122
+ #
123
+ # username - the username of the entry
124
+ # password - the username of the entry
125
+ # algorithm - the algorithm to use (default: "existing"). Valid options are:
126
+ # "existing", "md5", "sha1", "plaintext", or "crypt"
127
+ #
128
+ # Examples
129
+ #
130
+ # passwd_file.update("newuser", "password")
131
+ # passwd_file.save!
132
+ #
133
+ # passwd_file.update("newuser", "password", "sha1")
134
+ # passwd_file.save!
135
+ #
136
+ # Returns nothing.
137
+ # Raises PasswdFileError if the give username does not exist.
55
138
  def update(username, password, algorithm = Algorithm::EXISTING)
56
139
  raise PasswdFileError, "Unable to update non-existent user #{username}" unless has_entry?(username)
57
140
  ir = internal_record(username)
@@ -62,14 +145,28 @@ module HTAuth
62
145
  return nil
63
146
  end
64
147
 
65
- # fetches a copy of an entry from the file. Updateing the entry returned from fetch will NOT
66
- # propogate back to the file.
148
+ # Public: Returns a copy of then given PasswdEntry from the file.
149
+ #
150
+ # Updating the PasswdEntry instance returned by this method will NOT update
151
+ # the file. To update the file, use #update and #save!
152
+ #
153
+ # username - the username of the entry
154
+ #
155
+ # Examples
156
+ #
157
+ # entry = password_file.fetch("myuser")
158
+ #
159
+ # Returns a PasswdEntry if the entry is found
160
+ # Returns nil if the entry is not found
67
161
  def fetch(username)
68
162
  return nil unless has_entry?(username)
69
163
  ir = internal_record(username)
70
164
  return ir['entry'].dup
71
165
  end
72
166
 
167
+ # Internal: returns the class used for each entry
168
+ #
169
+ # Returns a Class
73
170
  def entry_klass
74
171
  ENTRY_KLASS
75
172
  end
@@ -1,8 +1,7 @@
1
1
  require 'htauth/algorithm'
2
2
 
3
3
  module HTAuth
4
-
5
- # the plaintext algorithm, which does absolutly nothing
4
+ # Internal: the plaintext algorithm, which does absolutly nothing
6
5
  class Plaintext < Algorithm
7
6
  # ignore parameters
8
7
  def initialize(params = {})
@@ -3,21 +3,21 @@ require 'digest/sha1'
3
3
  require 'base64'
4
4
 
5
5
  module HTAuth
6
+ # Internal: an implementation of the SHA based encoding algorithm
7
+ # as used in the apache htpasswd -s option
8
+ #
9
+ class Sha1 < Algorithm
6
10
 
7
- # an implementation of the SHA based encoding algorithm
8
- # as used in the apache htpasswd -s option
9
- class Sha1 < Algorithm
10
-
11
- # ignore the params
12
- def initialize(params = {})
13
- end
11
+ # ignore the params
12
+ def initialize(params = {})
13
+ end
14
14
 
15
- def prefix
16
- "{SHA}"
17
- end
15
+ def prefix
16
+ "{SHA}"
17
+ end
18
18
 
19
- def encode(password)
20
- "#{prefix}#{Base64.encode64(::Digest::SHA1.digest(password)).strip}"
21
- end
19
+ def encode(password)
20
+ "#{prefix}#{Base64.encode64(::Digest::SHA1.digest(password)).strip}"
22
21
  end
22
+ end
23
23
  end
@@ -1,21 +1,4 @@
1
1
  module HTAuth
2
- VERSION = "1.2.0"
3
- module Version
4
- STRING = HTAuth::VERSION
5
- def to_a
6
- STRING.split(".")
7
- end
8
-
9
- def to_s
10
- STRING
11
- end
12
-
13
- module_function :to_a
14
- module_function :to_s
15
-
16
- MAJOR = Version.to_a[0]
17
- MINOR = Version.to_a[1]
18
- BUILD = Version.to_a[2]
19
-
20
- end
2
+ # Public: The version of the htauth library
3
+ VERSION = "2.0.0"
21
4
  end
@@ -0,0 +1,150 @@
1
+ require 'spec_helper'
2
+ require 'htauth/cli/digest'
3
+ require 'tempfile'
4
+
5
+ describe HTAuth::CLI::Digest do
6
+
7
+ before(:each) do
8
+
9
+ # existing
10
+ @tf = Tempfile.new("rpasswrd-digest-test")
11
+ @tf.write(IO.read(DIGEST_ORIGINAL_TEST_FILE))
12
+ @tf.close
13
+ @rdigest = HTAuth::CLI::Digest.new
14
+
15
+ # new file
16
+ @new_file = File.join(File.dirname(@tf.path), "new-testfile")
17
+
18
+ # rework stdout and stderr
19
+ @stdout = ConsoleIO.new
20
+ @old_stdout = $stdout
21
+ $stdout = @stdout
22
+
23
+ @stderr = ConsoleIO.new
24
+ @old_stderr = $stderr
25
+ $stderr = @stderr
26
+
27
+ @stdin = ConsoleIO.new
28
+ @old_stdin = $stdin
29
+ $stdin = @stdin
30
+ end
31
+
32
+ after(:each) do
33
+ @tf.close(true)
34
+ $stderr = @old_stderr
35
+ $stdout = @old_stdout
36
+ $stdin = @old_stdin
37
+ File.unlink(@new_file) if File.exist?(@new_file)
38
+ end
39
+
40
+ it "displays help appropriately" do
41
+ begin
42
+ @rdigest.run([ "-h" ])
43
+ rescue SystemExit => se
44
+ se.status.must_equal 1
45
+ @stdout.string.must_match( /passwordfile realm username/m )
46
+ end
47
+ end
48
+
49
+ it "displays the version appropriately" do
50
+ begin
51
+ @rdigest.run([ "--version" ])
52
+ rescue SystemExit => se
53
+ se.status.must_equal 1
54
+ @stdout.string.must_match( /version #{HTAuth::VERSION}/ )
55
+ end
56
+ end
57
+
58
+ it "creates a new file with one entries" do
59
+ begin
60
+ @stdin.puts "b secret"
61
+ @stdin.puts "b secret"
62
+ @stdin.rewind
63
+ @rdigest.run([ "-c", @new_file, "htauth", "bob" ])
64
+ rescue SystemExit => se
65
+ se.status.must_equal 0
66
+ IO.read(@new_file).must_equal IO.readlines(DIGEST_ORIGINAL_TEST_FILE).first
67
+ end
68
+ end
69
+
70
+ it "truncates an exiting file if told to create a new file" do
71
+ begin
72
+ @stdin.puts "b secret"
73
+ @stdin.puts "b secret"
74
+ @stdin.rewind
75
+ @rdigest.run([ "-c", @tf.path, "htauth", "bob"])
76
+ rescue SystemExit => se
77
+ se.status.must_equal 0
78
+ IO.read(@tf.path).must_equal IO.read(DIGEST_DELETE_TEST_FILE)
79
+ end
80
+ end
81
+
82
+ it "adds an entry to an existing file" do
83
+ begin
84
+ @stdin.puts "c secret"
85
+ @stdin.puts "c secret"
86
+ @stdin.rewind
87
+ @rdigest.run([ @tf.path, "htauth-new", "charlie" ])
88
+ rescue SystemExit => se
89
+ se.status.must_equal 0
90
+ IO.read(@tf.path).must_equal IO.read(DIGEST_ADD_TEST_FILE)
91
+ end
92
+ end
93
+
94
+ it "updates an entry in an existing file" do
95
+ begin
96
+ @stdin.puts "a new secret"
97
+ @stdin.puts "a new secret"
98
+ @stdin.rewind
99
+ @rdigest.run([ @tf.path, "htauth", "alice" ])
100
+ rescue SystemExit => se
101
+ @stderr.string.must_equal ""
102
+ se.status.must_equal 0
103
+ IO.read(@tf.path).must_equal IO.read(DIGEST_UPDATE_TEST_FILE)
104
+ end
105
+ end
106
+
107
+ it "deletes an entry in an existing file" do
108
+ begin
109
+ @rdigest.run([ "-d", @tf.path, "htauth", "alice" ])
110
+ rescue SystemExit => se
111
+ @stderr.string.must_equal ""
112
+ se.status.must_equal 0
113
+ IO.read(@tf.path).must_equal IO.read(DIGEST_DELETE_TEST_FILE)
114
+ end
115
+ end
116
+
117
+ it "has an error if it does not have permissions on the file" do
118
+ begin
119
+ @stdin.puts "a secret"
120
+ @stdin.puts "a secret"
121
+ @stdin.rewind
122
+ @rdigest.run([ "-c", "/etc/you-cannot-create-me", "htauth", "alice"])
123
+ rescue SystemExit => se
124
+ @stderr.string.must_match( %r{Could not open password file /etc/you-cannot-create-me}m )
125
+ se.status.must_equal 1
126
+ end
127
+ end
128
+
129
+ it "has an error if the input passwords do not match" do
130
+ begin
131
+ @stdin.puts "a secret"
132
+ @stdin.puts "a bad secret"
133
+ @stdin.rewind
134
+ @rdigest.run([ @tf.path, "htauth", "alice"])
135
+ rescue SystemExit => se
136
+ @stderr.string.must_match( /They don't match, sorry./m )
137
+ se.status.must_equal 1
138
+ end
139
+ end
140
+
141
+ it "has an error if the options are incorrect" do
142
+ begin
143
+ @rdigest.run(["--blah"])
144
+ rescue SystemExit => se
145
+ @stderr.string.must_match( /ERROR:/m )
146
+ se.status.must_equal 1
147
+ end
148
+ end
149
+
150
+ end