mroch-authpipe 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+