htauth 1.2.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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