mroch-authpipe 0.1.1

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.
@@ -0,0 +1,4 @@
1
+ ---
2
+ :minor: 1
3
+ :patch: 1
4
+ :major: 0
@@ -0,0 +1,9 @@
1
+ module Authpipe
2
+ class AuthpipeException < Exception; end
3
+ end
4
+
5
+ require 'authpipe/account_data'
6
+ require 'authpipe/pre'
7
+ require 'authpipe/auth'
8
+ require 'authpipe/enumerate'
9
+ require 'authpipe/passwd'
@@ -0,0 +1,49 @@
1
+ # Extends a Hash to construct the authpipe response.
2
+ #
3
+ # Returns the account data as a series of ATTR=value newline-terminated lines,
4
+ # followed by a period on a line of its own. Valid attributes are:
5
+ #
6
+ # USERNAME=username -- system account which owns mailbox (name)
7
+ # UID=uid -- system account which owns mailbox (numeric uid)
8
+ # GID=gid -- numeric groupid
9
+ # HOME=homedir -- home directory
10
+ # ADDRESS=addr -- e-mail address
11
+ # NAME=name -- full name
12
+ # MAILDIR=maildir -- Maildir relative to home directory
13
+ # QUOTA=quota -- quota string: maxbytesS,maxfilesC
14
+ # PASSWD=cryptpasswd -- encrypted password
15
+ # PASSWD2=plainpasswd -- plain text password
16
+ # OPTIONS=acctoptions -- option1=val1,option2=val2,...
17
+ # .
18
+ #
19
+ # Of these, it is mandatory to return ADDRESS, HOME, GID, and either UID or
20
+ # USERNAME; the others are optional.
21
+ #
22
+
23
+ module Authpipe
24
+ class InvalidAccountData < AuthpipeException ; end
25
+
26
+ class AccountData < Hash
27
+ def to_authpipe
28
+ validate!
29
+ result = self.inject([]) do |result, (key, value)|
30
+ (result << key.to_s.upcase + "=" + value.to_s) unless value.nil?
31
+ result
32
+ end
33
+ result.sort!
34
+ result << ".\n"
35
+ return result.join("\n")
36
+ end
37
+
38
+ def to_enumerate
39
+ [self[:username], self[:uid], self[:gid], self[:home], self[:maildir], self[:options]].join("\t")
40
+ end
41
+
42
+ def validate!
43
+ raise InvalidAccountData, 'ADDRESS is required' unless self[:address]
44
+ raise InvalidAccountData, 'HOME is required' unless self[:home]
45
+ raise InvalidAccountData, 'GID is required' unless self[:gid]
46
+ raise InvalidAccountData, 'Either UID or USERNAME is required' unless self[:uid] || self[:username]
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,80 @@
1
+ # AUTH <len>\n<len-bytes>
2
+ #
3
+ # Validate a login attempt. The AUTH line is followed by len-bytes of
4
+ # authentication data, which does not necessarily end with a newline. The
5
+ # currently defined authentication requests are:
6
+ #
7
+ # login \n username \n password [\n] -- plaintext login
8
+ # cram-md5 \n challenge \n response [\n] -- base-64 encoded challenge and response
9
+ # cram-sha1 \n challenge \n response [\n] -- ditto
10
+ # cram-sha256 \n challenge \n response [\n] -- ditto
11
+ #
12
+ # In the case of success, return the complete set of account parameters in the
13
+ # same format as PRE, ending with a period on a line of its own. In the case of
14
+ # failure (e.g. username does not exist, password wrong, unsupported
15
+ # authentication type), return FAIL<newline>. If there is a temporary failure,
16
+ # such as a database being down, authProg should terminate without sending any
17
+ # response.
18
+ #
19
+ # Note: if the user provides a plaintext password and authenticates
20
+ # successfully, then you can return it as PASSWD2 (plain text password) even if
21
+ # the database contains an encrypted password. This is useful when using the
22
+ # POP3/IMAP proxy functions of courier-imap.
23
+ #
24
+
25
+ require 'readbytes'
26
+
27
+ module Authpipe
28
+ class Auth
29
+
30
+ def self.process(request)
31
+ Auth.new.process(request)
32
+ end
33
+
34
+ def process(request)
35
+ params = parse_request(request)
36
+ if (account = get_account_data(params))
37
+ return account.to_authpipe
38
+ else
39
+ raise AuthpipeException, "AUTH failed"
40
+ end
41
+ end
42
+
43
+ protected
44
+ # Authenticates the user and returns its account data in an
45
+ # Authpipe::AccountData object, or returns nil if the user isn't found
46
+ # or authentication fails.
47
+ def get_account_data(params)
48
+ raise NotImplementedError, 'get_account_data must be overridden by subclass'
49
+ end
50
+
51
+ def parse_request(request)
52
+ data = STDIN.readbytes(request.to_i)
53
+ authservice, authtype, field1, field2 = data.split(/\n/)
54
+ case authtype
55
+ when 'login':
56
+ { :authservice => authservice, :authtype => 'login', :username => field1, :password => field2 }
57
+ when 'cram-md5', 'cram-sha1', 'cram-sha256':
58
+ { :authservice => authservice, :authtype => authtype, :challenge => field1, :response => field2 }
59
+ else
60
+ raise UnsupportedAuthenticationType.new(authtype)
61
+ end
62
+ end
63
+
64
+ end
65
+
66
+ # Exception raised when an unsupported authentication mechanism is requested.
67
+ class UnsupportedAuthenticationType < AuthpipeException
68
+ def initialize(authtype)
69
+ @authtype = authtype
70
+ end
71
+
72
+ def message
73
+ if @authtype
74
+ "'#{@authtype}' is not a supported authentication type. login, cram-md5, cram-sha1 and cram-sha256 are supported."
75
+ else
76
+ "Unsupported authentication type. login, cram-md5, cram-sha1 and cram-sha256 are supported."
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,39 @@
1
+ # ENUMERATE \n
2
+ #
3
+ # Return a list of all accounts, one per line in the following format, ending
4
+ # with a period on a line of its own:
5
+ #
6
+ # username \t uid \t gid \t homedir \t maildir \t options \n
7
+ # .
8
+ #
9
+ # If your module does not support the ENUMERATE command then return just a
10
+ # period on a line of its own (which will still allow enumeration data from
11
+ # other modules to be returned). In the case of a temporary failure, such as a
12
+ # database being down or an error occuring mid-way through returning account
13
+ # data, authProg should terminate before sending the terminating period.
14
+ #
15
+
16
+ module Authpipe
17
+ class Enumerate
18
+
19
+ def self.process(request = nil)
20
+ Pre.new.process(request)
21
+ end
22
+
23
+ def process(request = nil)
24
+ if (accounts = get_account_data) && !accounts.empty?
25
+ return accounts.collect { |a| a.to_enumerate }.join("\n") + "\n."
26
+ else
27
+ return '.'
28
+ end
29
+ end
30
+
31
+ protected
32
+ # Retrieves account data for the username and authservice given in +params+.
33
+ # Returns an Authpipe::AccountData object or nil if the user isn't found.
34
+ def get_account_data
35
+ raise NotImplementedError, 'get_account_data must be overridden by subclass'
36
+ end
37
+
38
+ end
39
+ end
@@ -0,0 +1,45 @@
1
+ # PASSWD service<tab> username<tab> oldpasswd<tab> newpasswd<tab> <newline>
2
+ #
3
+ # Request a password change for the given account: validate that the
4
+ # oldpassword is correct, and if so, change it to the newpassword.
5
+ #
6
+ # Reply: the string for success, or FAIL<newline> for a data error (e.g. no
7
+ # such account, old password wrong, new password not acceptable). In the case
8
+ # of a temporary failure, such as a database being down, authProg should
9
+ # terminate without sending any response.
10
+ #
11
+
12
+ module Authpipe
13
+ class Passwd
14
+
15
+ def self.process(request = nil)
16
+ Pre.new.process(request)
17
+ end
18
+
19
+ def process(request)
20
+ params = parse_request(request)
21
+ if (account = update_account_password(params))
22
+ return account.to_authpipe
23
+ else
24
+ raise AuthpipeException, 'Unable to update password'
25
+ end
26
+ end
27
+
28
+ protected
29
+ # Looks up the account by :service and :username, then confirms that
30
+ # :oldpasswd is correct, then changes the password to :newpasswd and
31
+ # returns the updated Authpipe::AccountData object.
32
+ def update_account_password(params)
33
+ raise NotImplementedError, 'update_account_password must be overridden by subclass'
34
+ end
35
+
36
+ def parse_request(request)
37
+ if request.strip =~ /^(\w+)\t(\w+)\t(\w+)\t(\w+)$/
38
+ { :authservice => $1, :username => $2, :oldpasswd => $3, :newpasswd => $4 }
39
+ else
40
+ raise ArgumentError, "Invalid PASSWD request: #{request}"
41
+ end
42
+ end
43
+
44
+ end
45
+ end
@@ -0,0 +1,46 @@
1
+ # PRE . <authservice> <username> \n
2
+ #
3
+ # Look up data for an account. authservice identifies the service the user is
4
+ # trying to use - e.g. pop3, imap, webmail etc.
5
+ #
6
+ #
7
+ # If the account is not known, return FAIL<newline>. If there is a temporary
8
+ # failure, such as a database being down, authProg should terminate (thereby
9
+ # closing stdin/stdout) without sending any response. authdaemon will restart
10
+ # the pipe module for the next request, thus ensuring it is properly
11
+ # reinitialized.
12
+ #
13
+
14
+ module Authpipe
15
+ class Pre
16
+
17
+ def self.process(request)
18
+ Pre.new.process(request)
19
+ end
20
+
21
+ def process(request)
22
+ params = parse_request(request)
23
+ if (account = get_account_data(params))
24
+ return account.to_authpipe
25
+ else
26
+ raise AuthpipeException, "No account found for service '#{params[:authservice]}' and username '#{params[:username]}'"
27
+ end
28
+ end
29
+
30
+ protected
31
+ # Retrieves account data for the username and authservice given in +params+.
32
+ # Returns an Authpipe::AccountData object or nil if the user isn't found.
33
+ def get_account_data(params)
34
+ raise NotImplementedError, 'get_account_data must be overridden by subclass'
35
+ end
36
+
37
+ def parse_request(request)
38
+ if request.strip =~ /^\. (\w+) (\w+)$/
39
+ { :authservice => $1, :username => $2 }
40
+ else
41
+ raise ArgumentError, "Invalid PRE request: #{request}"
42
+ end
43
+ end
44
+
45
+ end
46
+ end
@@ -0,0 +1,98 @@
1
+ require File.dirname(__FILE__) + '/../test_helper'
2
+
3
+ class AccountDataTest < Test::Unit::TestCase
4
+
5
+ def setup
6
+ @account1 = Authpipe::AccountData[:username => 'foo', :address => 'foo@example.com', :home => '/home/foo', :gid => 1024, :uid => 1024]
7
+ @account2 = Authpipe::AccountData[:username => 'bar', :address => 'bar@example.com', :home => '/home/bar', :gid => 1025, :uid => 1025]
8
+ end
9
+
10
+ def test_validate
11
+ acct = create_account_data
12
+ assert_nothing_raised { acct.validate! }
13
+
14
+ acct = create_account_data(:address => nil)
15
+ assert_raise(Authpipe::InvalidAccountData) { acct.validate! }
16
+
17
+ acct = create_account_data(:home => nil)
18
+ assert_raise(Authpipe::InvalidAccountData) { acct.validate! }
19
+
20
+ acct = create_account_data(:gid => nil)
21
+ assert_raise(Authpipe::InvalidAccountData) { acct.validate! }
22
+
23
+ acct = create_account_data(:uid => nil, :username => nil)
24
+ assert_raise(Authpipe::InvalidAccountData) { acct.validate! }
25
+
26
+ acct = create_account_data(:uid => nil) # OK since username still set
27
+ assert_nothing_raised { acct.validate! }
28
+
29
+ acct = create_account_data(:username => nil) # OK since uid still set
30
+ assert_nothing_raised { acct.validate! }
31
+
32
+ acct = Authpipe::AccountData.new
33
+ assert_raise(Authpipe::InvalidAccountData) { acct.validate! }
34
+ end
35
+
36
+ def test_to_authpipe
37
+ assert_equal <<-EOF, create_account_data.to_authpipe
38
+ ADDRESS=foo@example.com
39
+ GID=1024
40
+ HOME=/home/foo
41
+ MAILDIR=.maildir
42
+ NAME=Test User
43
+ OPTIONS=option1=val1,option2=val2
44
+ PASSWD2=abcdef
45
+ PASSWD=cryptedpassword
46
+ QUOTA=1024S,1000C
47
+ UID=1024
48
+ USERNAME=foo
49
+ .
50
+ EOF
51
+
52
+ assert_equal <<-EOF, create_account_data(:maildir => nil).to_authpipe
53
+ ADDRESS=foo@example.com
54
+ GID=1024
55
+ HOME=/home/foo
56
+ NAME=Test User
57
+ OPTIONS=option1=val1,option2=val2
58
+ PASSWD2=abcdef
59
+ PASSWD=cryptedpassword
60
+ QUOTA=1024S,1000C
61
+ UID=1024
62
+ USERNAME=foo
63
+ .
64
+ EOF
65
+ end
66
+
67
+ def test_to_enumerate
68
+ assert_equal \
69
+ "foo\t1024\t1024\t/home/foo\t.maildir\toption1=val1,option2=val2",
70
+ create_account_data.to_enumerate
71
+
72
+ assert_equal \
73
+ "\t1024\t1024\t/home/foo\t.maildir\toption1=val1,option2=val2",
74
+ create_account_data(:username => nil).to_enumerate
75
+
76
+ assert_equal \
77
+ "foo\t\t1024\t/home/foo\t.maildir\toption1=val1,option2=val2",
78
+ create_account_data(:uid => nil).to_enumerate
79
+ end
80
+
81
+ private
82
+ def create_account_data(params = {})
83
+ Authpipe::AccountData[{
84
+ :username => 'foo',
85
+ :address => 'foo@example.com',
86
+ :home => '/home/foo',
87
+ :uid => 1024,
88
+ :gid => 1024,
89
+ :name => 'Test User',
90
+ :maildir => '.maildir',
91
+ :quota => '1024S,1000C',
92
+ :passwd => 'cryptedpassword',
93
+ :passwd2 => 'abcdef',
94
+ :options => 'option1=val1,option2=val2'
95
+ }.merge!(params)]
96
+ end
97
+
98
+ end
@@ -0,0 +1,68 @@
1
+ require File.dirname(__FILE__) + '/../test_helper'
2
+
3
+ class AuthTest < Test::Unit::TestCase
4
+
5
+ def setup
6
+ @handler = Authpipe::Auth.new
7
+ @account = Authpipe::AccountData[:address => 'foo@example.com', :home => '/home/foo', :gid => 1024, :uid => 1024]
8
+ end
9
+
10
+ def test_process_validates_auth_type
11
+ auth = "unsupported\nfoo\nbar"
12
+ STDIN.expects(:readbytes).returns(auth)
13
+ @handler.expects(:get_account_data).never
14
+ assert_raises(Authpipe::UnsupportedAuthenticationType) do
15
+ @handler.process(auth.length)
16
+ end
17
+
18
+ ['login', 'cram-md5', 'cram-sha1', 'cram-sha256'].each do |authtype|
19
+ auth = "#{authtype}\nfoo\nbar"
20
+ STDIN.expects(:readbytes).returns(auth)
21
+ @handler.expects(:get_account_data).returns(@account)
22
+ assert_nothing_raised do
23
+ @handler.process(auth.length)
24
+ end
25
+ end
26
+ end
27
+
28
+ def test_process_fails_auth
29
+ auth = "login\nfoo\nbar"
30
+ STDIN.expects(:readbytes).returns(auth)
31
+
32
+ @handler.expects(:get_account_data).returns(nil)
33
+ assert_raises(Authpipe::AuthpipeException) do
34
+ @handler.process(auth.length)
35
+ end
36
+ end
37
+
38
+ def test_process_passes_auth
39
+ auth = "login\nfoo\nbar"
40
+ STDIN.expects(:readbytes).returns(auth)
41
+
42
+ @handler.expects(:get_account_data).returns(@account)
43
+ assert_nothing_raised do
44
+ assert_equal @account.to_authpipe, @handler.process(auth.length)
45
+ end
46
+ end
47
+
48
+ def test_process_calls_get_account_data
49
+ auth = "login\nfoo\nbar"
50
+ STDIN.expects(:readbytes).returns(auth)
51
+ @handler.expects(:get_account_data).with(
52
+ :authtype => 'login', :username => 'foo', :password => 'bar'
53
+ ).returns(@account)
54
+ assert_nothing_raised do
55
+ @handler.process(auth.length)
56
+ end
57
+
58
+ auth = "login\nabcdef\nqazwsx"
59
+ STDIN.expects(:readbytes).returns(auth)
60
+ @handler.expects(:get_account_data).with(
61
+ :authtype => 'login', :username => 'abcdef', :password => 'qazwsx'
62
+ ).returns(@account)
63
+ assert_nothing_raised do
64
+ @handler.process(auth.length)
65
+ end
66
+ end
67
+
68
+ end
@@ -0,0 +1,29 @@
1
+ require File.dirname(__FILE__) + '/../test_helper'
2
+
3
+ class EnumerateTest < Test::Unit::TestCase
4
+
5
+ def setup
6
+ @handler = Authpipe::Enumerate.new
7
+ @account1 = Authpipe::AccountData[:username => 'foo', :address => 'foo@example.com', :home => '/home/foo', :gid => 1024, :uid => 1024]
8
+ @account2 = Authpipe::AccountData[:username => 'bar', :address => 'bar@example.com', :home => '/home/bar', :gid => 1025, :uid => 1025]
9
+ end
10
+
11
+ def test_process_calls_get_account_data
12
+ @handler.expects(:get_account_data).with()
13
+ @handler.process
14
+ end
15
+
16
+ def test_process_fails_pre
17
+ @handler.expects(:get_account_data).returns(nil)
18
+ assert_equal '.', @handler.process
19
+
20
+ @handler.expects(:get_account_data).returns([])
21
+ assert_equal '.', @handler.process
22
+ end
23
+
24
+ def test_process_passes_pre
25
+ @handler.expects(:get_account_data).returns([@account1, @account2])
26
+ assert_equal "#{@account1.to_enumerate}\n#{@account2.to_enumerate}\n.", @handler.process
27
+ end
28
+
29
+ end
@@ -0,0 +1,31 @@
1
+ require File.dirname(__FILE__) + '/../test_helper'
2
+
3
+ class PasswdTest < Test::Unit::TestCase
4
+
5
+ def setup
6
+ @handler = Authpipe::Passwd.new
7
+ @account = Authpipe::AccountData[
8
+ :username => 'foo',
9
+ :address => 'foo@example.com',
10
+ :home => '/home/foo',
11
+ :gid => 1024,
12
+ :uid => 1024,
13
+ :passwd2 => 'def'
14
+ ]
15
+ end
16
+
17
+ def test_process_fails_pre
18
+ @handler.expects(:update_account_password).returns(nil)
19
+ assert_raise(Authpipe::AuthpipeException) do
20
+ @handler.process("imap\tfoo\tabc\tdef")
21
+ end
22
+ end
23
+
24
+ def test_process_passes_pre
25
+ @handler.expects(:update_account_password).with(
26
+ { :authservice => 'imap', :username => 'foo', :oldpasswd => 'abc', :newpasswd => 'def' }
27
+ ).returns(@account)
28
+ assert_equal @account.to_authpipe, @handler.process("imap\tfoo\tabc\tdef")
29
+ end
30
+
31
+ end
@@ -0,0 +1,32 @@
1
+ require File.dirname(__FILE__) + '/../test_helper'
2
+
3
+ class PreTest < Test::Unit::TestCase
4
+
5
+ def setup
6
+ @handler = Authpipe::Pre.new
7
+ @account = Authpipe::AccountData[:address => 'foo@example.com', :home => '/home/foo', :gid => 1024, :uid => 1024]
8
+ end
9
+
10
+ def test_process_calls_get_account_data
11
+ pre = ". imap foo"
12
+ @handler.expects(:get_account_data).with(
13
+ :authservice => 'imap', :username => 'foo'
14
+ ).returns(@account)
15
+ @handler.process(pre)
16
+ end
17
+
18
+ def test_process_fails_pre
19
+ @handler.expects(:get_account_data).returns(nil)
20
+ assert_raises(Authpipe::AuthpipeException) do
21
+ @handler.process(". imap foo")
22
+ end
23
+ end
24
+
25
+ def test_process_passes_pre
26
+ @handler.expects(:get_account_data).returns(@account)
27
+ assert_nothing_raised do
28
+ assert_equal @account.to_authpipe, @handler.process(". imap foo")
29
+ end
30
+ end
31
+
32
+ end
@@ -0,0 +1,6 @@
1
+ $:.unshift(File.dirname(__FILE__) + '/../lib')
2
+ require 'test/unit'
3
+ require 'rubygems'
4
+ require 'mocha'
5
+
6
+ require 'authpipe'
metadata ADDED
@@ -0,0 +1,68 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mroch-authpipe
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Marshall Roch
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-03-11 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description:
17
+ email: mroch@cmu.edu
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files: []
23
+
24
+ files:
25
+ - VERSION.yml
26
+ - lib/authpipe
27
+ - lib/authpipe/account_data.rb
28
+ - lib/authpipe/auth.rb
29
+ - lib/authpipe/enumerate.rb
30
+ - lib/authpipe/passwd.rb
31
+ - lib/authpipe/pre.rb
32
+ - lib/authpipe.rb
33
+ - test/authpipe
34
+ - test/authpipe/account_data_test.rb
35
+ - test/authpipe/auth_test.rb
36
+ - test/authpipe/enumerate_test.rb
37
+ - test/authpipe/passwd_test.rb
38
+ - test/authpipe/pre_test.rb
39
+ - test/test_helper.rb
40
+ has_rdoc: true
41
+ homepage: http://github.com/mroch/authpipe
42
+ post_install_message:
43
+ rdoc_options:
44
+ - --inline-source
45
+ - --charset=UTF-8
46
+ require_paths:
47
+ - lib
48
+ required_ruby_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: "0"
53
+ version:
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: "0"
59
+ version:
60
+ requirements: []
61
+
62
+ rubyforge_project:
63
+ rubygems_version: 1.2.0
64
+ signing_key:
65
+ specification_version: 2
66
+ summary: TODO
67
+ test_files: []
68
+